import * as plugins from './plugins.js';
import * as interfaces from '../ts_interfaces/index.js';
import { DevToolsController } from './controllers/controller.devtools.js';
import { TypedRequestController } from './controllers/controller.typedrequest.js';
import { BuiltInRoutesController } from './controllers/controller.builtin.js';

/**
 * Content Security Policy configuration
 * Each directive can be a string or array of sources
 */
export interface IContentSecurityPolicy {
  /** Fallback for other directives */
  defaultSrc?: string | string[];
  /** Valid sources for scripts */
  scriptSrc?: string | string[];
  /** Valid sources for stylesheets */
  styleSrc?: string | string[];
  /** Valid sources for images */
  imgSrc?: string | string[];
  /** Valid sources for fonts */
  fontSrc?: string | string[];
  /** Valid sources for AJAX, WebSockets, etc. */
  connectSrc?: string | string[];
  /** Valid sources for media (audio/video) */
  mediaSrc?: string | string[];
  /** Valid sources for frames */
  frameSrc?: string | string[];
  /** Valid sources for <object>, <embed>, <applet> */
  objectSrc?: string | string[];
  /** Valid sources for web workers */
  workerSrc?: string | string[];
  /** Valid sources for form actions */
  formAction?: string | string[];
  /** Controls which URLs can embed the page */
  frameAncestors?: string | string[];
  /** Restricts URLs for <base> element */
  baseUri?: string | string[];
  /** Report violations to this URL */
  reportUri?: string;
  /** Report violations to this endpoint */
  reportTo?: string;
  /** Upgrade insecure requests to HTTPS */
  upgradeInsecureRequests?: boolean;
  /** Block all mixed content */
  blockAllMixedContent?: boolean;
}

/**
 * Security headers configuration
 */
export interface ISecurityHeaders {
  /** Content Security Policy */
  csp?: IContentSecurityPolicy;
  /** X-Frame-Options: DENY, SAMEORIGIN, or ALLOW-FROM uri */
  xFrameOptions?: 'DENY' | 'SAMEORIGIN' | string;
  /** X-Content-Type-Options: nosniff */
  xContentTypeOptions?: boolean;
  /** X-XSS-Protection header (legacy, but still useful) */
  xXssProtection?: boolean | string;
  /** Referrer-Policy header */
  referrerPolicy?: 'no-referrer' | 'no-referrer-when-downgrade' | 'origin' | 'origin-when-cross-origin' | 'same-origin' | 'strict-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url';
  /** Strict-Transport-Security (HSTS) max-age in seconds */
  hstsMaxAge?: number;
  /** Include subdomains in HSTS */
  hstsIncludeSubDomains?: boolean;
  /** HSTS preload flag */
  hstsPreload?: boolean;
  /** Permissions-Policy (formerly Feature-Policy) */
  permissionsPolicy?: Record<string, string[]>;
  /** Cross-Origin-Opener-Policy */
  crossOriginOpenerPolicy?: 'unsafe-none' | 'same-origin-allow-popups' | 'same-origin';
  /** Cross-Origin-Embedder-Policy */
  crossOriginEmbedderPolicy?: 'unsafe-none' | 'require-corp' | 'credentialless';
  /** Cross-Origin-Resource-Policy */
  crossOriginResourcePolicy?: 'same-site' | 'same-origin' | 'cross-origin';
}

/**
 * Bundled content item - matches tsbundle output format
 */
export interface IBundledContentItem {
  path: string;
  contentBase64: string;
}

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

  /**
   * Bundled content to serve from memory (higher priority than filesystem)
   * Accepts array format from tsbundle: { path: string; contentBase64: string }[]
   */
  bundledContent?: IBundledContentItem[];

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

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

  cors: boolean;

  /**
   * a default answer given in case there is no other handler.
   */
  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
   */
  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;

  /**
   * SPA fallback - serve index.html for non-file routes (e.g., /login, /dashboard)
   * Useful for single-page applications with client-side routing
   */
  spaFallback?: boolean;

  /**
   * Security headers configuration (CSP, HSTS, X-Frame-Options, etc.)
   */
  securityHeaders?: ISecurityHeaders;

  /**
   * Response compression configuration
   * Set to true for defaults (brotli + gzip), false to disable, or provide detailed config
   */
  compression?: plugins.smartserve.ICompressionConfig | boolean;

  /**
   * Disable all client-side caching by setting appropriate headers
   * Useful for development or when content changes frequently
   */
  noCache?: boolean;
}

export type THttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'ALL';

export interface IRouteHandler {
  (ctx: plugins.smartserve.IRequestContext): Promise<Response | null>;
}

export class TypedServer {
  // instance
  public options: IServerOptions;
  public smartServe: plugins.smartserve.SmartServe;
  public smartwatchInstance: plugins.smartwatch.Smartwatch;
  public serveDirHashSubject = new plugins.smartrx.rxjs.ReplaySubject<string>(1);
  public serveHash: string = '000000';
  public typedsocket: plugins.typedsocket.TypedSocket;
  public typedrouter = new plugins.typedrequest.TypedRouter();

  // Sitemap helper
  private sitemapHelper: SitemapHelper;
  private smartmanifestInstance: plugins.smartmanifest.SmartManifest;

  // Decorated controllers
  private devToolsController: DevToolsController;
  private typedRequestController: TypedRequestController;
  private builtInRoutesController: BuiltInRoutesController;

  // File server for static files
  private fileServer: plugins.smartserve.FileServer;

  // Bundled content map for O(1) lookup
  private bundledContentMap: Map<string, {
    content: Uint8Array;
    etag: string;
    mimeType: string;
    size: number;
  }> = new Map();
  private bundledContentHash: string = '';

  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,
    };
  }

  /**
   * Access sitemap URLs (for adding/replacing)
   */
  public get sitemap() {
    return this.sitemapHelper;
  }

  /**
   * Add a custom route handler
   * Supports Express-style path patterns like '/path/:param' and '/path/*splat'
   * @param path - The route path pattern
   * @param method - HTTP method (GET, POST, PUT, DELETE, PATCH, ALL)
   * @param handler - Async function that receives IRequestContext and returns Response or null
   */
  public addRoute(path: string, method: THttpMethod, handler: IRouteHandler): void {
    // Delegate to smartserve's ControllerRegistry
    plugins.smartserve.ControllerRegistry.addRoute(path, method, handler);
  }

  /**
   * Initialize bundled content from base64-encoded files
   */
  private async initializeBundledContent(): Promise<void> {
    if (!this.options.bundledContent?.length) return;

    const hashParts: string[] = [];

    for (const item of this.options.bundledContent) {
      let path = item.path.replace(/^\/+/, '') || 'index.html';

      // Decode base64 to Uint8Array
      const binary = atob(item.contentBase64);
      const bytes = new Uint8Array(binary.length);
      for (let i = 0; i < binary.length; i++) {
        bytes[i] = binary.charCodeAt(i);
      }

      // Generate ETag from content hash
      const hashBuffer = await crypto.subtle.digest('SHA-256', bytes);
      const hashArray = Array.from(new Uint8Array(hashBuffer));
      const etag = '"' + hashArray.slice(0, 16).map(b => b.toString(16).padStart(2, '0')).join('') + '"';

      // Get MIME type
      const mimeType = this.getMimeType(path);

      this.bundledContentMap.set(path, { content: bytes, etag, mimeType, size: bytes.length });
      hashParts.push(etag);
    }

    // Combined hash for cache busting
    this.bundledContentHash = hashParts.join('').slice(0, 12);
    if (!this.options.serveDir) this.serveHash = this.bundledContentHash;

    console.log(`Initialized ${this.bundledContentMap.size} bundled files (hash: ${this.bundledContentHash})`);
  }

  /**
   * Get MIME type for a file path
   */
  private getMimeType(path: string): string {
    const ext = path.split('.').pop()?.toLowerCase() || '';
    const mimeTypes: Record<string, string> = {
      'html': 'text/html; charset=utf-8',
      'js': 'application/javascript; charset=utf-8',
      'mjs': 'application/javascript; charset=utf-8',
      'css': 'text/css; charset=utf-8',
      'json': 'application/json; charset=utf-8',
      'png': 'image/png',
      'jpg': 'image/jpeg',
      'jpeg': 'image/jpeg',
      'gif': 'image/gif',
      'svg': 'image/svg+xml',
      'ico': 'image/x-icon',
      'woff': 'font/woff',
      'woff2': 'font/woff2',
      'ttf': 'font/ttf',
      'eot': 'application/vnd.ms-fontobject',
    };
    return mimeTypes[ext] || 'application/octet-stream';
  }

  /**
   * Serve bundled content from memory
   */
  private async serveBundledContent(request: Request, pathname: string): Promise<Response | null> {
    if (this.bundledContentMap.size === 0) return null;

    let path = pathname.replace(/^\/+/, '');
    if (!path || path.endsWith('/')) path = (path || '') + 'index.html';

    const entry = this.bundledContentMap.get(path);
    if (!entry) return null;

    const headers = new Headers({
      'Content-Type': entry.mimeType,
      'ETag': entry.etag,
      'Cache-Control': 'public, max-age=31536000, immutable',
    });
    if (this.bundledContentHash) headers.set('appHash', this.bundledContentHash);

    // Conditional request
    const ifNoneMatch = request.headers.get('If-None-Match');
    if (ifNoneMatch === entry.etag) {
      return new Response(null, { status: 304, headers });
    }

    if (request.method === 'HEAD') {
      headers.set('Content-Length', entry.size.toString());
      return new Response(null, { status: 200, headers });
    }

    // HTML injection for reload
    if (this.options.injectReload && entry.mimeType.includes('text/html')) {
      return this.serveBundledHtmlWithInjection(entry, headers);
    }

    headers.set('Content-Length', entry.size.toString());
    return new Response(Buffer.from(entry.content), { status: 200, headers });
  }

  /**
   * Serve bundled HTML with reload script injection
   */
  private serveBundledHtmlWithInjection(
    entry: { content: Uint8Array; mimeType: string },
    headers: Headers
  ): Response {
    let html = new TextDecoder().decode(entry.content);

    if (html.includes('<head>')) {
      html = html.replace('<head>', `<head>
      <!-- injected by @apiglobal/typedserver -->
      <script async defer type="module" src="/typedserver/devtools"></script>
      <script>globalThis.typedserver = { lastReload: ${this.lastReload} }</script>`);
    }

    headers.set('Cache-Control', 'no-cache, no-store, must-revalidate');
    headers.delete('ETag');

    const content = new TextEncoder().encode(html);
    headers.set('Content-Length', content.length.toString());
    return new Response(content, { status: 200, headers });
  }

  /**
   * inits and starts the server
   */
  public async start() {
    // Validate essential configuration before starting
    if (this.options.injectReload && !this.options.serveDir && !this.options.bundledContent) {
      throw new Error(
        'injectReload requires serveDir or bundledContent'
      );
    }

    const port =
      typeof this.options.port === 'string'
        ? parseInt(this.options.port, 10)
        : this.options.port || 3000;

    // Initialize optional helpers
    if (this.options.sitemap) {
      this.sitemapHelper = new SitemapHelper(this.options.domain);
    }
    if (this.options.manifest) {
      this.smartmanifestInstance = new plugins.smartmanifest.SmartManifest(this.options.manifest);
    }

    // Initialize file server for static files
    if (this.options.serveDir) {
      this.fileServer = new plugins.smartserve.FileServer({
        root: this.options.serveDir,
        index: ['index.html'],
        etag: true,
      });
    }

    // Initialize bundled content from memory
    await this.initializeBundledContent();

    // Initialize decorated controllers
    if (this.options.injectReload) {
      this.devToolsController = new DevToolsController({
        getLastReload: () => this.lastReload,
        getEnded: () => this.ended,
      });
    }

    this.typedRequestController = new TypedRequestController(this.typedrouter);

    this.builtInRoutesController = new BuiltInRoutesController({
      domain: this.options.domain,
      robots: this.options.robots,
      manifest: this.smartmanifestInstance,
      sitemap: this.options.sitemap,
      feed: this.options.feed,
      appVersion: this.options.appVersion,
      feedMetadata: this.options.feedMetadata,
      articleGetterFunction: this.options.articleGetterFunction,
      blockWaybackMachine: this.options.blockWaybackMachine,
      getSitemapUrls: () => this.sitemapHelper?.urls || [],
    });

    // Register controllers with SmartServe's ControllerRegistry
    // Note: @Route decorators auto-register classes at import time.
    // Controllers with constructor args (like DevToolsController) use default no-op
    // constructors to handle auto-instantiation gracefully.
    if (this.options.injectReload) {
      plugins.smartserve.ControllerRegistry.registerInstance(this.devToolsController);
    }
    plugins.smartserve.ControllerRegistry.registerInstance(this.typedRequestController);
    plugins.smartserve.ControllerRegistry.registerInstance(this.builtInRoutesController);

    // Compile routes for fast matching
    plugins.smartserve.ControllerRegistry.compileRoutes();

    // Build SmartServe options
    const smartServeOptions: plugins.smartserve.ISmartServeOptions = {
      port,
      hostname: '0.0.0.0',
      compression: this.options.compression,
      tls:
        this.options.privateKey && this.options.publicKey
          ? {
              key: this.options.privateKey,
              cert: this.options.publicKey,
            }
          : undefined,
      websocket: {
        typedRouter: this.typedrouter,
        onConnectionOpen: (peer) => {
          peer.tags.add('allClients');
          console.log(`WebSocket connected: ${peer.id}`);
        },
        onConnectionClose: (peer) => {
          console.log(`WebSocket disconnected: ${peer.id}`);
        },
      },
    };

    this.smartServe = new plugins.smartserve.SmartServe(smartServeOptions);

    // Set up custom request handler that integrates with ControllerRegistry
    this.smartServe.setHandler(async (request: Request): Promise<Response> => {
      return this.handleRequest(request);
    });

    // Setup file watching
    if (this.options.watch && this.options.serveDir) {
      try {
        // Use glob pattern to match all files recursively in serveDir
        const watchGlob = this.options.serveDir.endsWith('/')
          ? `${this.options.serveDir}**/*`
          : `${this.options.serveDir}/**/*`;
        this.smartwatchInstance = new plugins.smartwatch.Smartwatch([watchGlob]);
        await this.smartwatchInstance.start();
        (await this.smartwatchInstance.getObservableFor('change')).subscribe(async () => {
          await this.createServeDirHash();
          this.reload();
        });
        await this.createServeDirHash();
      } catch (error) {
        console.error('Failed to initialize file watching:', error);
      }
    }

    // Start the server
    await this.smartServe.start();
    console.log(`TypedServer listening on port ${port}`);

    // Setup TypedSocket using SmartServe integration
    try {
      this.typedsocket = plugins.typedsocket.TypedSocket.fromSmartServe(
        this.smartServe,
        this.typedrouter
      );

      // Setup typedrouter handlers
      this.typedrouter.addTypedHandler<interfaces.IReq_GetLatestServerChangeTime>(
        new plugins.typedrequest.TypedHandler('getLatestServerChangeTime', async () => {
          return {
            time: this.lastReload,
          };
        })
      );

      // Speedtest handler for service worker dashboard
      // Client calls this in a loop for the test duration to get accurate time-based measurements
      this.typedrouter.addTypedHandler<interfaces.serviceworker.IRequest_Serviceworker_Speedtest>(
        new plugins.typedrequest.TypedHandler('serviceworker_speedtest', async (reqArg) => {
          const chunkSizeKB = reqArg.chunkSizeKB || 64;
          const sizeBytes = chunkSizeKB * 1024;
          let payload: string | undefined;
          let bytesTransferred = 0;

          switch (reqArg.type) {
            case 'download_chunk':
              // Generate chunk data for download test
              payload = 'x'.repeat(sizeBytes);
              bytesTransferred = sizeBytes;
              break;
            case 'upload_chunk':
              // Acknowledge received upload data
              bytesTransferred = reqArg.payload?.length || 0;
              break;
            case 'latency':
              // Simple ping - minimal data
              bytesTransferred = 0;
              break;
          }

          return { bytesTransferred, timestamp: Date.now(), payload };
        })
      );
    } catch (error) {
      console.error('Failed to initialize TypedSocket:', error);
    }
  }

  /**
   * Create an IRequestContext from a Request
   */
  private createContext(
    request: Request,
    params: Record<string, string>
  ): plugins.smartserve.IRequestContext {
    const url = new URL(request.url);
    const method = request.method.toUpperCase() as THttpMethod;

    // Parse query params
    const query: Record<string, string> = {};
    url.searchParams.forEach((value, key) => {
      query[key] = value;
    });

    // Cached body parsers (lazy evaluation)
    let jsonCache: unknown;
    let textCache: string;
    let arrayBufferCache: ArrayBuffer;
    let formDataCache: FormData;

    return {
      request,
      params,
      query,
      headers: request.headers,
      path: url.pathname,
      method,
      url,
      runtime: 'node' as const,
      state: {},
      async json<T = unknown>(): Promise<T> {
        if (jsonCache === undefined) {
          jsonCache = await request.clone().json();
        }
        return jsonCache as T;
      },
      async text(): Promise<string> {
        if (textCache === undefined) {
          textCache = await request.clone().text();
        }
        return textCache;
      },
      async arrayBuffer(): Promise<ArrayBuffer> {
        if (arrayBufferCache === undefined) {
          arrayBufferCache = await request.clone().arrayBuffer();
        }
        return arrayBufferCache;
      },
      async formData(): Promise<FormData> {
        if (formDataCache === undefined) {
          formDataCache = await request.clone().formData();
        }
        return formDataCache;
      },
    };
  }

  /**
   * Build CSP header string from configuration
   */
  private buildCspHeader(csp: IContentSecurityPolicy): string {
    const directives: string[] = [];

    const addDirective = (name: string, value: string | string[] | undefined) => {
      if (value) {
        const sources = Array.isArray(value) ? value.join(' ') : value;
        directives.push(`${name} ${sources}`);
      }
    };

    addDirective('default-src', csp.defaultSrc);
    addDirective('script-src', csp.scriptSrc);
    addDirective('style-src', csp.styleSrc);
    addDirective('img-src', csp.imgSrc);
    addDirective('font-src', csp.fontSrc);
    addDirective('connect-src', csp.connectSrc);
    addDirective('media-src', csp.mediaSrc);
    addDirective('frame-src', csp.frameSrc);
    addDirective('object-src', csp.objectSrc);
    addDirective('worker-src', csp.workerSrc);
    addDirective('form-action', csp.formAction);
    addDirective('frame-ancestors', csp.frameAncestors);
    addDirective('base-uri', csp.baseUri);

    if (csp.reportUri) {
      directives.push(`report-uri ${csp.reportUri}`);
    }
    if (csp.reportTo) {
      directives.push(`report-to ${csp.reportTo}`);
    }
    if (csp.upgradeInsecureRequests) {
      directives.push('upgrade-insecure-requests');
    }
    if (csp.blockAllMixedContent) {
      directives.push('block-all-mixed-content');
    }

    return directives.join('; ');
  }

  /**
   * Apply all configured headers (CORS, security, cache control) to a response
   */
  private applyResponseHeaders(response: Response): Response {
    const headers = new Headers(response.headers);

    // No-cache headers
    if (this.options.noCache) {
      headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0');
      headers.set('Pragma', 'no-cache');
      headers.set('Expires', '0');
    }

    // CORS headers
    if (this.options.cors) {
      headers.set('Access-Control-Allow-Origin', '*');
      headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS, PATCH');
      headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
      headers.set('Access-Control-Max-Age', '86400');
    }

    // Security headers
    const security = this.options.securityHeaders;
    if (security) {
      // Content Security Policy
      if (security.csp) {
        const cspHeader = this.buildCspHeader(security.csp);
        if (cspHeader) {
          headers.set('Content-Security-Policy', cspHeader);
        }
      }

      // X-Frame-Options
      if (security.xFrameOptions) {
        headers.set('X-Frame-Options', security.xFrameOptions);
      }

      // X-Content-Type-Options
      if (security.xContentTypeOptions) {
        headers.set('X-Content-Type-Options', 'nosniff');
      }

      // X-XSS-Protection
      if (security.xXssProtection) {
        const value = typeof security.xXssProtection === 'string'
          ? security.xXssProtection
          : '1; mode=block';
        headers.set('X-XSS-Protection', value);
      }

      // Referrer-Policy
      if (security.referrerPolicy) {
        headers.set('Referrer-Policy', security.referrerPolicy);
      }

      // Strict-Transport-Security (HSTS)
      if (security.hstsMaxAge !== undefined) {
        let hsts = `max-age=${security.hstsMaxAge}`;
        if (security.hstsIncludeSubDomains) {
          hsts += '; includeSubDomains';
        }
        if (security.hstsPreload) {
          hsts += '; preload';
        }
        headers.set('Strict-Transport-Security', hsts);
      }

      // Permissions-Policy
      if (security.permissionsPolicy) {
        const policies = Object.entries(security.permissionsPolicy)
          .map(([feature, allowlist]) => `${feature}=(${allowlist.join(' ')})`)
          .join(', ');
        if (policies) {
          headers.set('Permissions-Policy', policies);
        }
      }

      // Cross-Origin-Opener-Policy
      if (security.crossOriginOpenerPolicy) {
        headers.set('Cross-Origin-Opener-Policy', security.crossOriginOpenerPolicy);
      }

      // Cross-Origin-Embedder-Policy
      if (security.crossOriginEmbedderPolicy) {
        headers.set('Cross-Origin-Embedder-Policy', security.crossOriginEmbedderPolicy);
      }

      // Cross-Origin-Resource-Policy
      if (security.crossOriginResourcePolicy) {
        headers.set('Cross-Origin-Resource-Policy', security.crossOriginResourcePolicy);
      }
    }

    return new Response(response.body, {
      status: response.status,
      statusText: response.statusText,
      headers
    });
  }

  /**
   * Main request handler - routes to appropriate sub-handlers
   */
  private async handleRequest(request: Request): Promise<Response> {
    const url = new URL(request.url);
    const path = url.pathname;
    const method = request.method.toUpperCase() as THttpMethod;

    // Handle OPTIONS preflight for CORS
    if (method === 'OPTIONS' && this.options.cors) {
      return this.applyResponseHeaders(new Response(null, { status: 204 }));
    }

    // Process the request and wrap response with all configured headers
    const response = await this.handleRequestInternal(request, path, method);
    return this.applyResponseHeaders(response);
  }

  /**
   * Internal request handler - routes to appropriate sub-handlers
   */
  private async handleRequestInternal(
    request: Request,
    path: string,
    method: THttpMethod
  ): Promise<Response> {
    // First, try to match via ControllerRegistry (decorated routes)
    const match = plugins.smartserve.ControllerRegistry.matchRoute(path, method);
    if (match) {
      try {
        const context = this.createContext(request, match.params);
        const result = await match.route.handler(context);

        // Handle Response or convert to Response
        if (result instanceof Response) {
          return result;
        }
        return new Response(JSON.stringify(result), {
          status: 200,
          headers: { 'Content-Type': 'application/json' },
        });
      } catch (error) {
        if (error instanceof plugins.smartserve.RouteNotFoundError) {
          // Route explicitly threw "not found", continue to other handlers
        } else {
          console.error('Controller error:', error);
          return new Response('Internal Server Error', { status: 500 });
        }
      }
    }

    // Try bundled content first (in-memory, faster)
    if (this.bundledContentMap.size > 0 && (method === 'GET' || method === 'HEAD')) {
      const bundledResponse = await this.serveBundledContent(request, path);
      if (bundledResponse) return bundledResponse;
    }

    // HTML injection for reload (if enabled)
    if (this.options.injectReload && this.options.serveDir) {
      const response = await this.handleHtmlWithInjection(request);
      if (response) return response;
    }

    // Try static file serving
    if (this.fileServer && (method === 'GET' || method === 'HEAD')) {
      try {
        const staticResponse = await this.fileServer.serve(request);
        if (staticResponse) {
          return staticResponse;
        }
      } catch (error) {
        // Fall through to 404
      }
    }

    // Default answer for root
    if (path === '/' && method === 'GET' && this.options.defaultAnswer) {
      const html = await this.options.defaultAnswer();
      return new Response(html, {
        status: 200,
        headers: { 'Content-Type': 'text/html' },
      });
    }

    // SPA fallback - serve index.html for non-file routes
    if (this.options.spaFallback && method === 'GET' && !path.includes('.')) {
      // Try bundled index.html first
      if (this.bundledContentMap.has('index.html')) {
        const response = await this.serveBundledContent(request, '/index.html');
        if (response) return response;
      }

      // Fall back to filesystem
      if (this.options.serveDir) {
        try {
          const indexPath = plugins.path.join(this.options.serveDir, 'index.html');
          let html = await plugins.fsInstance.file(indexPath).encoding('utf8').read() as string;

          // Inject reload script if enabled
          if (this.options.injectReload && html.includes('<head>')) {
            const injection = `<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 -->
              `;
            html = html.replace('<head>', injection);
          }

          return new Response(html, {
            status: 200,
            headers: {
              'Content-Type': 'text/html; charset=utf-8',
              'Cache-Control': 'no-cache, no-store, must-revalidate',
              appHash: this.serveHash,
            },
          });
        } catch {
          // Fall through to 404
        }
      }
    }

    // Not found
    return new Response('Not Found', { status: 404 });
  }

  /**
   * Handle HTML files with reload script injection
   */
  private async handleHtmlWithInjection(request: Request): Promise<Response | null> {
    const url = new URL(request.url);
    const requestPath = url.pathname;

    // Check if this is a request for an HTML file or root
    if (requestPath === '/' || requestPath.endsWith('.html') || !requestPath.includes('.')) {
      try {
        let filePath = requestPath === '/' ? 'index.html' : requestPath.slice(1);
        if (!filePath.endsWith('.html') && !filePath.includes('.')) {
          filePath = plugins.path.join(filePath, 'index.html');
        }

        const fullPath = plugins.path.join(this.options.serveDir, filePath);

        // Security check
        if (!fullPath.startsWith(this.options.serveDir)) {
          return new Response('Forbidden', { status: 403 });
        }

        let fileContent = (await plugins.fsInstance
          .file(fullPath)
          .encoding('utf8')
          .read()) as string;

        // Inject reload script
        if (fileContent.includes('<head>')) {
          const injection = `<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 -->
              `;
          fileContent = fileContent.replace('<head>', injection);
          console.log('injected typedserver script.');
        }

        return new Response(fileContent, {
          status: 200,
          headers: {
            'Content-Type': 'text/html; charset=utf-8',
            'Cache-Control': 'no-cache, no-store, must-revalidate',
            Pragma: 'no-cache',
            Expires: '0',
            appHash: this.serveHash,
          },
        });
      } catch (error) {
        // Fall through to default handling
      }
    }

    return null;
  }

  /**
   * reloads the page
   */
  public async reload() {
    this.lastReload = Date.now();
    if (!this.typedsocket) {
      console.warn('TypedSocket not initialized, skipping client notifications');
      return;
    }

    // Push cache invalidation to service workers first
    try {
      const swConnections = await this.typedsocket.findAllTargetConnectionsByTag('serviceworker');
      for (const connection of swConnections) {
        const pushCacheInvalidate =
          this.typedsocket.createTypedRequest<interfaces.serviceworker.IRequest_Serviceworker_CacheInvalidate>(
            'serviceworker_cacheInvalidate',
            connection
          );
        pushCacheInvalidate.fire({
          reason: 'File change detected',
          timestamp: this.lastReload,
        }).catch(err => {
          console.warn('Failed to push cache invalidation to service worker:', err);
        });
      }
      if (swConnections.length > 0) {
        console.log(`Pushed cache invalidation to ${swConnections.length} service worker(s)`);
      }
    } catch (error) {
      console.warn('Failed to notify service workers:', error);
    }

    // Notify frontend clients
    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(err => {
          console.warn('Failed to push latest server change time to client:', err);
        });
      }
    } 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 SmartServe
    if (this.smartServe) {
      tasks.push(stopWithErrorHandling(() => this.smartServe.stop(), 'SmartServe'));
    }

    // Stop TypedSocket (in SmartServe mode, this is a no-op but good for cleanup)
    if (this.typedsocket) {
      tasks.push(stopWithErrorHandling(() => this.typedsocket.stop(), 'TypedSocket'));
    }

    // Stop file watcher
    if (this.smartwatchInstance) {
      tasks.push(stopWithErrorHandling(() => this.smartwatchInstance.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.fsInstance
        .directory(this.options.serveDir)
        .recursive()
        .treeHash();
      this.serveHash = serveDirHash.slice(0, 12);
      console.log('Current ServeDir hash: ' + this.serveHash);
      this.serveDirHashSubject.next(this.serveHash);
    } catch (error) {
      console.error('Failed to create serve directory hash:', error);
      const fallbackHash = Date.now().toString(16).slice(-6);
      this.serveHash = fallbackHash;
      console.log('Using fallback hash: ' + fallbackHash);
      this.serveDirHashSubject.next(fallbackHash);
    }
  }
}

// ============================================================================
// Helper Classes
// ============================================================================

/**
 * Sitemap helper class
 */
class SitemapHelper {
  public urls: plugins.smartsitemap.ISitemapUrl[] = [];

  constructor(domain?: string) {
    if (domain) {
      this.urls.push({
        loc: `https://${domain}/`,
        lastmod: new Date(),
        changefreq: 'daily',
      });
    }
  }

  async createSitemap(): Promise<string> {
    return plugins.smartsitemap.SmartSitemap.create().addUrls(this.urls).toXml();
  }

  async createSitemapNews(articles: plugins.tsclass.content.IArticle[]): Promise<string> {
    return plugins.smartsitemap.SmartSitemap.fromArticles(articles, { publicationName: 'News' }).toXml();
  }

  replaceUrls(urlsArg: plugins.smartsitemap.ISitemapUrl[]) {
    this.urls = urlsArg;
  }

  addUrls(urlsArg: plugins.smartsitemap.ISitemapUrl[]) {
    this.urls = this.urls.concat(urlsArg);
  }
}
