import express from 'express';
import http from 'http';
import * as SocketIO from 'socket.io';
import morgan from 'morgan';
import session from 'express-session';
import glob from "glob";
import path from 'path';
import { developer_admin } from "../lib/api.js";
import logging  from '../lib/logging.js';
import assert from 'assert';
import * as error_handlers from '../api/_errors.js';
import devtools from '../lib/devtools.js';
import * as acorn from "acorn";
import fs from "fs";
import cors from "cors";
import { base_host } from "../client/config.js";
import { createHttpTerminator } from 'http-terminator';

const app = express();
const http_server = http.createServer(app);
const terminator = createHttpTerminator({ server: http_server});
const io = new SocketIO.Server(http_server);
const log = logging.create("/services/api.js");
const API_BASE = path.resolve("./api");
const SOCKET_BASE = path.resolve("./socket");
const socket_routes = {};

export const description = "The API service for bando.";

export const options = [
  ["--host <ip_addr>", "IP address to listen on.", "0.0.0.0"],
  ["--port <int>", "port to listen on.", "5001"],
  ["--DANGER_ADMIN", "Enable DANGEROUS development mode.", developer_admin ? "1" : undefined],
  ["--debug-headers", "Print out HTTP request and response headers"],
  ["--cookies-suck", "Check various headers/settings that ruin headers"],
  ["--log-http", "Enable HTTP request logging.", false],
];

const report_code_error = (error, file_name) => {
  // svelte uses filename so use that too
  let error_info = {
    message: error.message,
    error_name: error.name,
    filename: file_name,
    stack: error.stack,
    code: fs.readFileSync(file_name).toString()
  }


  try {
    let parse_info = acorn.parse(error_info.code, {ecmaVersion: 2020, sourceType: "module"});
  } catch(parse_error) {
    error_info.line = parse_error.loc.line;
    error_info.column = parse_error.loc.column;
  }

  log.error(error, `Failed to load ${file_name}`);
  devtools.errors.push(error_info);
  console.log("ERRORS", devtools.errors);
}

const dynamic_load = async (pattern, loader) => {
  const files = glob.sync(pattern);

  for(let file_name of files) {
    try {
      log.info(`Loading module ${file_name}`);
      let route = await import('../' + file_name);

      for(let func_name in route) {
        await loader(file_name, route, func_name);
      }
    } catch(error) {
      report_code_error(error, file_name);
    }
  }
}


const shorten_path = (base, file_name) => {
  const long = path.resolve(file_name).replace(/\\/gi, "/");
  // convert all the windows crap \\ to / for the web

  return long.slice(base.length).slice(0, -3); // the -3 strips .js
}

const shutdown = async () => {
  console.warn("SHUTTING DOWN...");
  await terminator.terminate();
  console.warn("finished, now exiting.");
  process.exit(0);
}

export const main = async (opts) => {
  const auth = await import("../lib/auth.js");

  if(opts.DANGER_ADMIN === "1") {
    const { media_servers } = await import("../lib/config.js");
    app.use(cors({origin: media_servers}));

    // this is necessary to deal with Node 18 defaulting to IPv6 for "localhost"
    app.use((req, res, next) => {
      const full_host = `${req.protocol}://${req.get('host')}`;

      if(full_host !== base_host) {
        const redirect_to = new URL(req.url, base_host);

        log.error(`Your client/config.js:base_host is ${base_host} but you are accessing the site at ${full_host}. Redirecting to ${redirect_to} so cookies will function!`);

        res.redirect(redirect_to);
      } else {
        next();
      }
    })
  }

  if(opts.debugHeaders) {
    morgan.token('headers', (req, res) => {
      const req_h = JSON.stringify(req.headers, null, 4);
      const res_h = JSON.stringify(res.getHeaders(), null, 4);
      return `--- REQUEST\n${req_h}\n--- RESPONSE\n${res_h}`;
    });
    app.use(morgan('\n#### :date[web] :method :status :url\n:headers'));
  } else if(opts.logHttp) {
    app.use(morgan('combined'));
  }

  // this is needed because the json parser doesn't
  // keep the original body but it's needed for signatures
  const raw_body = (req, res, buf, encoding) => {
    if(buf && buf.length) {
      req.raw_body = buf.toString(encoding || 'utf8');
    }
  }

  // parse application/x-www-form-urlencoded
  app.use(express.urlencoded({ extended: false, verify: raw_body }));

  // parse application/json
  app.use(express.json({verify: raw_body}));

  assert(auth.cookie_domain !== undefined, "The cookie domain is not set. It's required now.");

  // you have to tell express to trust the proxy twice
  app.set('trust proxy', true);

  const session_config = {
    cookie: {
      maxAge: 86400000,
      domain: auth.cookie_domain,
      httpOnly: false,
      sameSite: "strict",
      secure: opts.DANGER_ADMIN !== "1",
    },
    saveUninitialized: false,
    store: auth.sessionStore,
    resave: false,
    secret: auth.cookie_secret,
    proxy: opts.DANGER_ADMIN !== "1"
  }

  const session_handler = session(session_config);

  const cookie_debugger = (req, res, next) => {
    session_handler(req, res, () => {
      const forward_proto = req.headers['x-forwarded-proto'];
      const secure_required = req.session && req.session.cookie.secure;
      const trust_proxy = session_config.proxy;
      const request_secure = req.secure;
      const encrypted = req.connection && req.connection.encrypted;

      if(!req.session) {
        log.warn("No session found, results may be different than if there is a session.");
      }

      // this replicates the logic in express-session
      if(secure_required) {
        if(encrypted) {
          log.info("cookie.secure=true, and connection is encrypted, so cookies should work");
        } else if(trust_proxy === false) {
          log.error("cookie.secure=true, but proxy=false, and connection NOT encrypted, so express-session will refuese to set cookies");
        } else if(trust_proxy !== true) {
          // remember, this weirdo logic is from expresss-session
          if(request_secure === true) {
            log.info("no explicit trust set (no proxy: set), but req.secure is set by express, so cookies should work");
          } else {
            log.error("trust proxy not explicitly true or false (no proxy: set), but express says the req.secure=false, cookies will NOT BE SET");
          }
        } else {
          // read the proto from x-forwarded-proto header
          let header = forward_proto || '';
          let index = header.indexOf(',');
          let proto = index !== -1
            ? header.substr(0, index).toLowerCase().trim()
            : header.toLowerCase().trim()
          if(proto === "https") {
            log.info("cookie.secure=true, the connection is not encrypted, and x-forwarded-proto is https, so cookies should be set");
          } else {
            log.error("cookie.secure=true, the connection is not encrypted, and x-forwarded-proto is NOT https! Your cookies will fail.");
          }
        }
      } else {
        log.warn("cookie.secure is false so cookies should work, but are not secure");
      }

      next();
    });
  }

  // and also say trust the proxy here too
  if(opts.cookiesSuck) {
    app.use(cookie_debugger)
  } else {
    app.use(session_handler);
  }

  auth.init(app);
  app.use(express.static('public'));
  app.use("/media", express.static('media'));

  if(opts.DANGER_ADMIN === "1") {
    log.warn("!!!!!! Exposing client/bando/demos to the network because you set DANGER_ADMIN.");
    app.use("/bando/demos/", express.static("admin/bando/demos"));
  }

  await dynamic_load("./api/**/[A-Za-z]*.js", (file_name, route, func_name) => {
    try {
      // don't load non-js files
      if(path.extname(file_name) !== ".js") {
        log.warn(`File ${file_name} not loaded since it doesn't end in .js`);
        return;
      }

      const short = shorten_path(API_BASE, file_name);

      let route_path = `/api${short}`;
      log.debug(`Loading ${file_name}:${func_name} into route ${route_path}`);

      // TODO: maybe wrap this with a try/catch for better error reporting, but
      // the problem is we have to copy the authenticated and login setting over
      // to the wrapping function, and in production it might not be very helpful
      const func = route[func_name];

      if(func_name == 'del') {
        log.debug(`Renaming del method to delete to compensate for the JavaScript delete keyword in ${file_name}`);
        func_name = 'delete';
      }

      let func_info = {
        name: func_name,
        code: func.toString(),
        authenticated: func.authenticated === true,
        login: func.login === true
      };

      if(opts.DANGER_ADMIN === "1") {
        if(devtools.api[route_path] == undefined) {
          // new thing so set up its data initially
          devtools.api[route_path] = { name: route_path, functions: [func_info] };
        } else {
          // seen this so just add to the functions list
          devtools.api[route_path].functions.push(func_info);
        }
      }

      if(func.authenticated) {
        app[func_name](route_path, auth.required(), func);
      } else if(func.login) {
        app[func_name](route_path, auth.login(), func);
      } else {
        app[func_name](route_path, func);
      }
    } catch(error) {
      log.error(error, `Failed to run ${file_name}:${func_name}`);
    }
  })


  await dynamic_load("./socket/**/[A-Za-z]*.js", (file_name, route, func_name) => {
    // don't load non-js files
    if(path.extname(file_name) !== ".js") {
      log.warn(`File ${file_name} not loaded since it doesn't end in .js`);
      return;
    }

    const route_path = shorten_path(SOCKET_BASE, file_name);
    const func = route[func_name];
    const target_name = `${route_path}/${func_name}`;

    log.debug(`Adding route ${target_name} from file ${file_name} which is ${route_path}.`);

    socket_routes[target_name] = func

    if(opts.DANGER_ADMIN === "1") {
      devtools.sockets[target_name] = {
        route_path, target_name, file_name, code: func.toString()
      }
    }
  })

  app.use(error_handlers.exception);
  app.use(error_handlers.missing);

  io.on('connection', (socket) => {
    log.debug('a user connected');

    socket.on('disconnect', () => {
      log.debug('user disconnected');
    })

    for(let target_name in socket_routes) {
      const func = socket_routes[target_name];
      log.debug(`Adding ${target_name} to socket. Authenticated is ${func.authenticated}.`);
      if(func.authenticated) {
        socket.on(target_name, async (data) => {
          if(await auth.socket(socket)) {
            return func(io, socket, data);
          } else {
            log.error(`Authentication failure for ${target_name}`, socket.user, data);
          }
        });
      } else {
        socket.on(target_name, async (data) => {
          return func(io, socket, data);
        });
      }
    }
  })

  http_server.listen(opts.port, opts.host, () => {
    log.info(`listening on ${opts.host}:${opts.port}`);
  })
}

process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
