import * as plugins from './plugins.js';
import * as paths from './paths.js';
import * as interfaces from '../dist_ts_interfaces/index.js';
import * as servertools from './servertools/index.js';
import { type TCompressionMethod } from './servertools/classes.compressor.js';

export interface IServerOptions {
  /**
   * serve a particular directory
   */
  serveDir?: string;

  /**
   * inject a reload script that takes care of live reloading
   */
  injectReload?: boolean;

  /**
   * enable compression
   */
  enableCompression?: boolean;

  /**
   * choose a preferred compression method
   */
  preferredCompressionMethod?: TCompressionMethod;

  /**
   * watch the serve directory?
   */
  watch?: boolean;

  cors: boolean;

  /**
   * a default answer given in case there is no other handler.
   * @returns
   */
  defaultAnswer?: () => Promise<string>;

  /**
   * will try to reroute traffic to an ssl connection using headers
   */
  forceSsl?: boolean;
  /**
   * allows serving manifests
   */
  manifest?: plugins.smartmanifest.ISmartManifestConstructorOptions;
  /**
   * the port to listen on
   * can be overwritten when actually starting the server
   */
  port?: number | string;
  publicKey?: string;
  privateKey?: string;
  sitemap?: boolean;
  feed?: boolean;
  robots?: boolean;
  domain?: string;
  /**
   * convey information about the app being served
   */
  appVersion?: string;
  feedMetadata?: plugins.smartfeed.IFeedOptions;
  articleGetterFunction?: () => Promise<plugins.tsclass.content.IArticle[]>;
  blockWaybackMachine?: boolean;
}

export class TypedServer {
  // static
  // nothing here yet

  // instance
  public options: IServerOptions;
  public server: servertools.Server;
  public smartchokInstance: plugins.smartchok.Smartchok;
  public serveDirHashSubject = new plugins.smartrx.rxjs.ReplaySubject<string>(1);
  public serveHash: string = '000000';
  public typedsocket: plugins.typedsocket.TypedSocket;
  public typedrouter = new plugins.typedrequest.TypedRouter();

  public lastReload: number = Date.now();
  public ended = false;
  
  constructor(optionsArg: IServerOptions) {
    const standardOptions: IServerOptions = {
      port: 3000,
      injectReload: false,
      serveDir: null,
      watch: false,
      cors: true,
    };
    this.options = {
      ...standardOptions,
      ...optionsArg,
    };

    this.server = new servertools.Server(this.options);
    // add routes to the smartexpress instance
    this.server.addRoute(
      '/typedserver/:request',
      new servertools.Handler('ALL', async (req, res) => {
        switch (req.params.request) {
          case 'devtools':
            res.setHeader('Content-Type', 'text/javascript');
            res.status(200);
            res.write(plugins.smartfile.fs.toStringSync(paths.injectBundlePath));
            res.end();
            break;
          case 'reloadcheck':
            console.log('got request for reloadcheck');
            res.setHeader('Content-Type', 'text/plain');
            res.status(200);
            if (this.ended) {
              res.write('end');
              res.end();
              return;
            }
            res.write(this.lastReload.toString());
            res.end();
            break;
          default:
            res.status(404);
            res.write('Unknown request type');
            res.end();
            break;
        }
      })
    );
    this.server.addRoute(
      '/typedrequest',
      new servertools.HandlerTypedRouter(this.typedrouter)
    );
  }

  /**
   * inits and starts the server
   */
  public async start() {
    // Validate essential configuration before starting
    if (this.options.injectReload && !this.options.serveDir) {
      throw new Error(
        'You set to inject the reload script without a serve dir. This is not supported at the moment.'
      );
    }

    if (this.options.serveDir) {
      this.server.addRoute(
        '/*',
        new servertools.HandlerStatic(this.options.serveDir, {
          responseModifier: async (responseArg) => {
            if (plugins.path.parse(responseArg.path).ext === '.html') {
              let fileString = responseArg.responseContent.toString();
              const fileStringArray = fileString.split('<head>');
              if (this.options.injectReload && fileStringArray.length === 2) {
                fileStringArray[0] = `${fileStringArray[0]}<head>
                  <!-- injected by @apiglobal/typedserver start -->
                  <script async defer type="module" src="/typedserver/devtools"></script>
                  <script>
                    globalThis.typedserver = {
                      lastReload: ${this.lastReload},
                      versionInfo: ${JSON.stringify({}, null, 2)},
                    }
                  </script>
                  <!-- injected by @apiglobal/typedserver stop -->
                `;
                fileString = fileStringArray.join('');
                console.log('injected typedserver script.');
                responseArg.responseContent = Buffer.from(fileString);
              } else if (this.options.injectReload) {
                console.log('Could not insert typedserver script - no <head> tag found');
              }
            }
            const headers = responseArg.headers;
            headers.appHash = this.serveHash;
            headers['Cache-Control'] = 'no-cache, no-store, must-revalidate';
            headers['Pragma'] = 'no-cache';
            headers['Expires'] = '0';
            return {
              headers,
              path: responseArg.path,
              responseContent: responseArg.responseContent,
              travelData: responseArg.travelData,
            };
          },
          serveIndexHtmlDefault: true,
          enableCompression: this.options.enableCompression,
          preferredCompressionMethod: this.options.preferredCompressionMethod,
        })
      );
    }
    
    if (this.options.watch && this.options.serveDir) {
      try {
        this.smartchokInstance = new plugins.smartchok.Smartchok([this.options.serveDir]);
        await this.smartchokInstance.start();
        (await this.smartchokInstance.getObservableFor('change')).subscribe(async () => {
          await this.createServeDirHash();
          this.reload();
        });
        await this.createServeDirHash();
      } catch (error) {
        console.error('Failed to initialize file watching:', error);
        // Continue without file watching rather than crashing
      }
    }

    // lets start the server
    await this.server.start();

    try {
      this.typedsocket = await plugins.typedsocket.TypedSocket.createServer(
        this.typedrouter,
        this.server
      );

      // lets setup typedrouter
      this.typedrouter.addTypedHandler<interfaces.IReq_GetLatestServerChangeTime>(
        new plugins.typedrequest.TypedHandler('getLatestServerChangeTime', async () => {
          return {
            time: this.lastReload,
          };
        })
      );
    } catch (error) {
      console.error('Failed to initialize TypedSocket:', error);
      // Continue without WebSocket support rather than crashing
    }
  }

  /**
   * reloads the page
   */
  public async reload() {
    this.lastReload = Date.now();
    if (!this.typedsocket) {
      console.warn('TypedSocket not initialized, skipping client notifications');
      return;
    }
    
    try {
      const connections = await this.typedsocket.findAllTargetConnectionsByTag('typedserver_frontend');
      for (const connection of connections) {
        const pushTime = this.typedsocket.createTypedRequest<interfaces.IReq_PushLatestServerChangeTime>(
          'pushLatestServerChangeTime',
          connection
        );
        pushTime.fire({
          time: this.lastReload,
        });
      }
    } catch (error) {
      console.error('Failed to notify clients about reload:', error);
    }
  }

  /**
   * Stops the server and cleans up resources
   */
  public async stop(): Promise<void> {
    this.ended = true;
    
    const stopWithErrorHandling = async (
      stopFn: () => Promise<unknown>,
      componentName: string
    ): Promise<void> => {
      try {
        await stopFn();
      } catch (err) {
        console.error(`Error stopping ${componentName}:`, err);
      }
    };
    
    const tasks: Promise<void>[] = [];
    
    // Stop server
    if (this.server) {
      tasks.push(stopWithErrorHandling(() => this.server.stop(), 'server'));
    }
    
    // Stop TypedSocket
    if (this.typedsocket) {
      tasks.push(stopWithErrorHandling(() => this.typedsocket.stop(), 'TypedSocket'));
    }
    
    // Stop file watcher
    if (this.smartchokInstance) {
      tasks.push(stopWithErrorHandling(() => this.smartchokInstance.stop(), 'file watcher'));
    }
    
    await Promise.all(tasks);
  }

  /**
   * Calculates a hash of the served directory for cache busting
   */
  public async createServeDirHash() {
    try {
      const serveDirHash = await plugins.smartfile.fs.fileTreeToHash(this.options.serveDir, '**/*');
      this.serveHash = serveDirHash;
      console.log('Current ServeDir hash: ' + serveDirHash);
      this.serveDirHashSubject.next(serveDirHash);
    } catch (error) {
      console.error('Failed to create serve directory hash:', error);
      // Use a timestamp-based hash as fallback
      const fallbackHash = Date.now().toString(16).slice(-6);
      this.serveHash = fallbackHash;
      console.log('Using fallback hash: ' + fallbackHash);
      this.serveDirHashSubject.next(fallbackHash);
    }
  }
}