import * as plugins from '../plugins.js';

import { Route } from './classes.route.js';
import { Handler } from './classes.handler.js';
import { HandlerTypedRouter } from './classes.handlertypedrouter.js';

// export types
import { setupRobots } from './tools.robots.js';
import { setupManifest } from './tools.manifest.js';
import { Sitemap } from './classes.sitemap.js';
import { Feed } from './classes.feed.js';
import { type IServerOptions } from '../classes.typedserver.js';
export type TServerStatus = 'initiated' | 'running' | 'stopped';

/**
 * can be used to spawn a server to answer http/https calls
 * for constructor options see [[IServerOptions]]
 */
export class Server {
  public httpServer: plugins.http.Server | plugins.https.Server;
  public expressAppInstance: plugins.express.Application;
  public routeObjectMap = new Array<Route>();
  public options: IServerOptions;
  public serverStatus: TServerStatus = 'initiated';

  public feed: Feed;
  public sitemap: Sitemap;

  public executeAfterStartFunctions: (() => Promise<void>)[] = [];

  // do stuff when server is ready
  private startedDeferred = plugins.smartpromise.defer();
  // tslint:disable-next-line:member-ordering
  public startedPromise = this.startedDeferred.promise;

  private socketMap = new plugins.lik.ObjectMap<plugins.net.Socket>();

  constructor(optionsArg: IServerOptions) {
    this.options = {
      ...optionsArg,
    };
  }

  /**
   * allows updating of server options
   * @param optionsArg
   */
  public updateServerOptions(optionsArg: IServerOptions) {
    Object.assign(this.options, optionsArg);
  }

  public addTypedRequest(typedrouter: plugins.typedrequest.TypedRouter) {
    this.addRoute('/typedrequest', new HandlerTypedRouter(typedrouter));
  }

  public addTypedSocket(typedrouter: plugins.typedrequest.TypedRouter): void {
    this.executeAfterStartFunctions.push(async () => {
      plugins.typedsocket.TypedSocket.createServer(typedrouter, this);
    });
  }

  public addRoute(routeStringArg: string, handlerArg?: Handler) {
    const route = new Route(this, routeStringArg);
    if (handlerArg) {
      route.addHandler(handlerArg);
    }
    this.routeObjectMap.push(route);
    return route;
  }

  public addRouteBefore(routeStringArg: string, handlerArg?: Handler) {
    const route = new Route(this, routeStringArg);
    if (handlerArg) {
      route.addHandler(handlerArg);
    }
    this.routeObjectMap.unshift(route);
    return route;
  }

  public async start(portArg: number | string = this.options.port, doListen = true) {
    const done = plugins.smartpromise.defer();

    if (typeof portArg === 'string') {
      portArg = parseInt(portArg);
    }

    this.expressAppInstance = plugins.express();
    if (!this.httpServer && (!this.options.privateKey || !this.options.publicKey)) {
      console.log('Got no SSL certificates. Please ensure encryption using e.g. a reverse proxy');
      this.httpServer = plugins.http.createServer(this.expressAppInstance);
    } else if (!this.httpServer) {
      console.log('Got SSL certificate. Using it for the http server');
      this.httpServer = plugins.https.createServer(
        {
          key: this.options.privateKey,
          cert: this.options.publicKey,
        },
        this.expressAppInstance
      );
    } else {
      console.log('Using externally supplied http server');
    }
    this.httpServer.keepAliveTimeout = 600 * 1000;
    this.httpServer.headersTimeout = 20 * 1000;

    // general request handlling
    this.expressAppInstance.use((req, res, next) => {
      next();
    });

    // forceSsl
    if (this.options.forceSsl) {
      this.expressAppInstance.set('forceSSLOptions', {
        enable301Redirects: true,
        trustXFPHeader: true,
        sslRequiredMessage: 'SSL Required.',
      });
      this.expressAppInstance.use(plugins.expressForceSsl);
    }

    // cors
    if (this.options.cors) {
      const cors = plugins.cors({
        allowedHeaders: '*',
        methods: '*',
        origin: '*',
      });

      this.expressAppInstance.use(cors);
      this.expressAppInstance.options('/*', cors);
    }

    this.expressAppInstance.use((req, res, next) => {
      res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
      res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
      res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
      res.setHeader('Access-Control-Allow-Origin', '*');
      res.setHeader('SERVEZONE_ROUTE', 'LOSSLESS_ORIGIN_CONTAINER');
      res.setHeader('Cache-Control', 'no-cache');
      res.setHeader('Expires', new Date(Date.now()).toUTCString());
      next();
    });

    // body parsing
    this.expressAppInstance.use(async (req, res, next) => {
      if (req.headers['content-type'] === 'application/json') {
        let data = '';
        req.on('data', chunk => {
          data += chunk;
        });
        req.on('end', () => {
          try {
            req.body = plugins.smartjson.parse(data);
            next();
          } catch (error) {
            res.status(400).send('Invalid JSON');
          }
        });
      } else {
        next();
      }
    });
    this.expressAppInstance.use(plugins.bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded

    // robots
    if (this.options.robots && this.options.domain) {
      await setupRobots(this, this.options.domain);
    }

    // manifest.json
    if (this.options.manifest) {
      await setupManifest(this.expressAppInstance, this.options.manifest);
    }

    // sitemaps
    if (this.options.sitemap) {
      this.sitemap = new Sitemap(this);
    }

    if (this.options.feed) {
      // feed
      this.feed = new Feed(this);
    }

    // appVersion
    if (this.options.appVersion) {
      this.expressAppInstance.use((req, res, next) => {
        res.set('appversion', this.options.appVersion);
        next();
      });
      this.addRoute(
        '/appversion',
        new Handler('GET', async (req, res) => {
          res.write(this.options.appVersion);
          res.end();
        })
      );
    }

    // set up routes in for express
    await this.routeObjectMap.forEach(async (routeArg) => {
      console.log(
        `"${routeArg.routeString}" maps to ${routeArg.handlerObjectMap.getArray().length} handlers`
      );
      const expressRoute = this.expressAppInstance.route(routeArg.routeString);
      routeArg.handlerObjectMap.forEach(async (handler) => {
        console.log(` -> ${handler.httpMethod}`);
        switch (handler.httpMethod) {
          case 'GET':
            expressRoute.get(handler.handlerFunction);
            return;
          case 'POST':
            expressRoute.post(handler.handlerFunction);
            return;
          case 'PUT':
            expressRoute.put(handler.handlerFunction);
            return;
          case 'ALL':
            expressRoute.all(handler.handlerFunction);
            return;
          case 'DELETE':
            expressRoute.delete(handler.handlerFunction);
            return;
          default:
            return;
        }
      });
    });

    if (this.options.defaultAnswer) {
      this.expressAppInstance.get('/', async (request, response) => {
        response.send(await this.options.defaultAnswer());
      });
    }

    this.httpServer.on('connection', (connection: plugins.net.Socket) => {
      this.socketMap.add(connection);
      console.log(`added connection. now ${this.socketMap.getArray().length} sockets connected.`);
      
      const closeListener = () => {
        console.log('connection closed');
        cleanupConnection();
      };
      
      const errorListener = () => {
        console.log('connection errored');
        cleanupConnection();
      };
      
      const endListener = () => {
        console.log('connection ended');
        cleanupConnection();
      };
      
      const timeoutListener = () => {
        console.log('connection timed out');
        cleanupConnection();
      };
      
      connection.addListener('close', closeListener);
      connection.addListener('error', errorListener);
      connection.addListener('end', endListener);
      connection.addListener('timeout', timeoutListener);
      
      const cleanupConnection = async () => {
        connection.removeListener('close', closeListener);
        connection.removeListener('error', errorListener);
        connection.removeListener('end', endListener);
        connection.removeListener('timeout', timeoutListener);
        
        if (this.socketMap.checkForObject(connection)) {
          this.socketMap.remove(connection);
          console.log(`removed connection. ${this.socketMap.getArray().length} sockets remaining.`);
          await plugins.smartdelay.delayFor(0);
          if (connection.destroyed === false) {
            connection.destroy();
          }
        }
      };
    });

    // finally listen on a port
    if (doListen) {
      this.httpServer.listen(portArg, '0.0.0.0', () => {
        console.log(`now listening on ${portArg}!`);
        this.startedDeferred.resolve();
        this.serverStatus = 'running';
        done.resolve();
      });
    } else {
      console.log(
        'The server does not listen on a network stack and instead expects to get handed requests by other mechanics'
      );
    }
    await done.promise;
    for (const executeAfterStartFunction of this.executeAfterStartFunctions) {
      await executeAfterStartFunction();
    }
  }

  public getHttpServer() {
    return this.httpServer;
  }

  public getExpressAppInstance() {
    return this.expressAppInstance;
  }

  public async stop() {
    const done = plugins.smartpromise.defer();
    if (this.httpServer) {
      this.httpServer.close(async () => {
        this.serverStatus = 'stopped';
        done.resolve();
      });
      await this.socketMap.forEach(async (socket) => {
        socket.destroy();
      });
    } else {
      throw new Error('There is no Server to be stopped. Have you started it?');
    }
    return await done.promise;
  }

  /**
   * allows handling requests and responses that come from other
   * @param req
   * @param res
   */
  public async handleReqRes(req: plugins.express.Request, res: plugins.express.Response) {
    this.expressAppInstance(req, res);
  }
}