import * as interfaces from './interfaces.js';
import * as plugins from './plugins.js';
import * as paths from './paths.js';
import { getEmbeddedFile, hasEmbeddedFile, listEmbeddedFiles } from './embedded-ui.generated.js';

export interface IPackageConfig {
  baseDirectory?: string;  // Override base directory for this package
  registry?: string;       // Override registry URL for this package
}

export interface IPublicServerOptions {
  packageBaseDirectory?: string;  // Default base directory (default: './')
  npmRegistryUrl?: string;        // Default registry URL
  allowedPackages?: string[];     // List of allowed package names
  packageConfigs?: Record<string, IPackageConfig>;  // Per-package configuration
  port?: number;
  mode: 'dev' | 'prod';
  log?: boolean;
}

/**
 * the main public server instance
 */
export class UiPublicServer {
  public projectinfo = new plugins.projectinfo.ProjectinfoNpm(paths.packageDir);
  public startedAt: string;
  private server: plugins.http.Server;
  private npmRegistry: plugins.smartnpm.NpmRegistry;

  private corsDev = '*';
  private corsProd = ''; // TODO: Define allowed URLs

  public defaultOptions: IPublicServerOptions = {
    packageBaseDirectory: './',
    npmRegistryUrl: 'https://registry.npmjs.org',
    allowedPackages: [],
    packageConfigs: {},
    port: 8080,
    mode: 'dev',
    log: true,
  };
  public options: IPublicServerOptions;

  /**
   * Get the base directory for a specific package
   */
  private getPackageBaseDirectory(packageName: string): string {
    const packageConfig = this.options.packageConfigs?.[packageName];
    let baseDir = packageConfig?.baseDirectory || this.options.packageBaseDirectory;
    if (!baseDir.endsWith('/')) {
      baseDir += '/';
    }
    return baseDir;
  }

  /**
   * Get the registry for a specific package
   */
  private getPackageRegistry(packageName: string): plugins.smartnpm.NpmRegistry {
    const packageConfig = this.options.packageConfigs?.[packageName];
    if (packageConfig?.registry && packageConfig.registry !== this.options.npmRegistryUrl) {
      return new plugins.smartnpm.NpmRegistry({
        npmRegistryUrl: packageConfig.registry,
      });
    }
    return this.npmRegistry;
  }

  constructor(optionsArg?: IPublicServerOptions) {
    // lets create an npm instance for the registry that we are talking to
    this.options = {
      ...this.defaultOptions,
      ...optionsArg,
    };

    if (!this.options.packageBaseDirectory.endsWith('/')) {
      this.options.packageBaseDirectory += '/';
    }

    this.npmRegistry = new plugins.smartnpm.NpmRegistry({
      npmRegistryUrl: this.options.npmRegistryUrl,
    });
  }

  /**
   * starts the server
   */
  public async startServer() {
    console.log('starting the uipublicserver');

    const done = plugins.smartpromise.defer();
    const expressApplication = plugins.express();

    const shouldCompress = (req: plugins.express.Request, res: plugins.express.Response) => {
      if (req.headers['x-no-compression']) {
        // don't compress responses with this request header
        return false;
      }

      // fallback to standard filter function
      return plugins.compression.filter(req, res);
    };

    expressApplication.use(plugins.compression({ filter: shouldCompress }));

    // Serve embedded UI files (index.html, bundle.js, etc.)
    expressApplication.get('/', async (req, res) => {
      await this.serveIndexHtml(req, res);
    });

    expressApplication.get('/index.html', async (req, res) => {
      await this.serveIndexHtml(req, res);
    });

    // Serve other embedded static files (bundle.js, css, etc.)
    expressApplication.get('/bundle.js', async (req, res) => {
      res.setHeader('cache-control', 'no-cache, no-store, must-revalidate');
      await this.serveEmbeddedFile('/bundle.js', res);
    });

    expressApplication.get('/bundle.js.map', async (req, res) => {
      res.setHeader('cache-control', 'no-cache, no-store, must-revalidate');
      await this.serveEmbeddedFile('/bundle.js.map', res);
    });

    // Serve any other embedded files
    expressApplication.use('/assets', async (req, res, next) => {
      const filePath = '/assets' + req.path;
      if (hasEmbeddedFile(filePath)) {
        await this.serveEmbeddedFile(filePath, res);
      } else {
        next();
      }
    });

    // API endpoint for file listing
    expressApplication.get('/api/files/:org/:package', async (req, res) => {
      const packageName = `${req.params.org}/${req.params.package}`;
      const version = (req.query.version as string) || '';
      const distTag = (req.query.disttag as string) || '';

      if (!this.options.allowedPackages.includes(packageName)) {
        res.status(403).json({ error: 'Package not allowed' });
        return;
      }

      const baseDirectory = this.getPackageBaseDirectory(packageName);
      const registry = this.getPackageRegistry(packageName);

      try {
        const result = await registry.getFilesFromPackage(
          packageName,
          plugins.path.join(baseDirectory),
          { version, distTag }
        );

        const files = result.map(smartfile => {
          return smartfile.path.replace(plugins.path.join('package', baseDirectory), '');
        });

        // Get package info for versions
        const packageInfo = await registry.getPackageInfo(packageName).catch(() => null);
        const versions = packageInfo?.allVersions?.slice(-20).reverse().map(v => v.version) || [];

        res.json({ files, versions });
      } catch (err) {
        res.status(404).json({ error: 'Package not found' });
      }
    });

    // Peek route serves the SPA
    expressApplication.get('/peek/*', async (req, res) => {
      await this.serveIndexHtml(req, res);
    });

    expressApplication.get('/peek', async (req, res) => {
      await this.serveIndexHtml(req, res);
    });

    // Main package serving route
    expressApplication.use('/', async (req, res) => {
      const host = req.headers.host || 'localhost';
      const parsedUrl = new plugins.url.URL(`http://${host}${req.url}`);
      const simpleResponse: interfaces.ISimpleResponse = await this.render({
        headers: req.headers,
        parsedUrl,
      });
      for (const header of Object.keys(simpleResponse.headers)) {
        res.setHeader(header, simpleResponse.headers[header]);
      }
      // CORS
      res.setHeader('ui-public-server-version', this.projectinfo.version);
      res.setHeader('access-control-allow-origin', true ? this.corsDev : this.corsProd); // TODO: replace true with check for env
      res.status(simpleResponse.status);
      res.write(simpleResponse.body);
      res.end();
    });

    // note: We assume that ssl termination is done in reverse proxy in kubernetes. so simple http should suffice here.
    this.server = expressApplication.listen(this.options.port, () => {
      console.log(`listening on port ${this.options.port}`);
      this.startedAt = new Date().toISOString();
      done.resolve();
    });
    await done.promise;
  }

  /**
   * Serve the index.html with configuration injected
   */
  private async serveIndexHtml(req: plugins.express.Request, res: plugins.express.Response) {
    const embeddedFile = getEmbeddedFile('/index.html');
    if (!embeddedFile) {
      res.status(500).send('UI not bundled. Run pnpm bundleUI first.');
      return;
    }

    // Inject configuration into the HTML
    let html = embeddedFile.data.toString('utf-8');
    const config = {
      version: this.projectinfo.version,
      mode: this.options.mode,
      allowedPackages: this.options.allowedPackages,
    };

    // Inject config script before the closing </head> tag
    const configScript = `<script>window.__OPENCDN_CONFIG__ = ${JSON.stringify(config)};</script>`;
    html = html.replace('</head>', `${configScript}\n</head>`);

    // Add version query parameter to bundle.js for cache busting
    html = html.replace('/bundle.js', `/bundle.js?v=${this.projectinfo.version}`);

    res.setHeader('content-type', embeddedFile.contentType);
    res.setHeader('cache-control', 'no-cache');
    res.status(200).send(html);
  }

  /**
   * Serve an embedded file
   */
  private async serveEmbeddedFile(filePath: string, res: plugins.express.Response) {
    const embeddedFile = getEmbeddedFile(filePath);
    if (!embeddedFile) {
      res.status(404).send('File not found');
      return;
    }

    res.setHeader('content-type', embeddedFile.contentType);
    res.setHeader('content-length', embeddedFile.data.length.toString());
    // Only set cache-control if not already set
    if (!res.getHeader('cache-control')) {
      res.setHeader('cache-control', 'public, max-age=31536000, immutable');
    }
    res.status(200).send(embeddedFile.data);
  }

  public disableLogging() {
    this.options.log = false;
  }

  /**
   * stops the server
   */
  public async stopServer() {
    const done = plugins.smartpromise.defer();
    if (!this.server) {
      console.log('server does not seem to be started, so no need to stop it');
      return;
    }
    this.server.close(() => {
      done.resolve();
    });
    await done.promise;
  }

  public requestMap: { [key: string]: plugins.smartpromise.Deferred<any> } = {};

  /**
   * renders a response using interfaces.ISimpleRequest and interfaces.ISimpleResponse
   * @param req
   */
  private render: interfaces.IRenderFunction = async (req) => {
    const pathArray = req.parsedUrl.pathname.split('/');

    // Handle root path - already served by express routes above
    if (pathArray.length < 3 || !pathArray[1].startsWith('@')) {
      return {
        headers: {
          'content-type': 'text/html',
        },
        status: 404,
        body: this.getErrorPage('Not Found', 'The requested path was not found.'),
      };
    }

    // lets care about the npm organization
    const npmOrg = pathArray[1];
    if (!npmOrg.startsWith('@')) {
      console.log('malformed npmorg');
      return {
        status: 400,
        headers: {
          'content-type': 'text/html',
        },
        body: this.getErrorPage('Bad Request', `npm org "${npmOrg}" must start with @`),
      };
    }

    // lets care about the packageName
    const npmPackage = pathArray[2];
    const packageName = `${npmOrg}/${npmPackage}`;
    if (this.options.log) {
      console.log(
        `got a request for package ${packageName} on registry ${this.npmRegistry.options.npmRegistryUrl}`
      );
    }
    if (!this.options.allowedPackages.includes(packageName)) {
      return {
        headers: {
          'content-type': 'text/html',
        },
        status: 403,
        body: this.getErrorPage('Forbidden', 'The requested package is not allowlisted for public access.'),
      };
    }

    // Get per-package configuration
    const baseDirectory = this.getPackageBaseDirectory(packageName);
    const registry = this.getPackageRegistry(packageName);

    // lets care about the inner package path
    let filePath = baseDirectory;
    let first = true;
    for (let i = 3; i < pathArray.length; i++) {
      if (first) {
        filePath += './';
        first = false;
      } else {
        filePath += '/';
      }
      filePath += pathArray[i];
    }

    // lets care about version and disttag

    const version = req.parsedUrl.searchParams.get('version') || '';
    const distTag = req.parsedUrl.searchParams.get('disttag') || '';

    const requestDescriptor = `${packageName}/${filePath}/${distTag}/${version}`;

    let smartfile: plugins.smartfile.SmartFile;

    // protect against parallel requests
    if (this.requestMap[requestDescriptor]) {
      smartfile = await this.requestMap[requestDescriptor].promise;
    } else {
      this.requestMap[requestDescriptor] = plugins.smartpromise.defer();
      smartfile = await registry
        .getFileFromPackage(packageName, filePath, {
          version,
          distTag,
        })
        .catch((err) => {
          console.log(err);
          return null;
        });
      this.requestMap[requestDescriptor].resolve(smartfile);
      delete this.requestMap[requestDescriptor];
    }

    if (!smartfile) {
      return {
        status: 404,
        headers: {
          'content-type': 'text/html',
        },
        body: this.getErrorPage(
          'Not Found',
          `${packageName}@${version || 'latest'} does not have a file at "${filePath}"`
        ),
      };
    }

    // Detect mime type from buffer
    const mimeResult = await plugins.smartmime.detectMimeType({ buffer: smartfile.contentBuffer });
    const contentType = mimeResult?.mime || 'application/octet-stream';

    return {
      headers: {
        'cache-control': `max-age=${
          (version
            ? plugins.smarttime.getMilliSecondsFromUnits({ months: 1 })
            : plugins.smarttime.getMilliSecondsFromUnits({ days: 1 })) / 1000
        }`,
        'content-length': smartfile.contentBuffer.length.toString(),
        'content-type': contentType,
      },
      status: 200,
      body: smartfile.contentBuffer,
    };
  };

  /**
   * Generate a simple error page
   */
  private getErrorPage(title: string, message: string): string {
    return `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>${title} - opencdn</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      min-height: 100vh;
      background: #09090b;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
      color: #fafafa;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 24px;
      -webkit-font-smoothing: antialiased;
    }
    .card {
      background: #09090b;
      border: 1px solid #27272a;
      border-radius: 8px;
      padding: 48px;
      max-width: 500px;
      text-align: center;
    }
    .icon {
      font-size: 64px;
      margin-bottom: 24px;
      opacity: 0.8;
    }
    h2 {
      font-size: 24px;
      font-weight: 600;
      color: #fafafa;
      margin-bottom: 12px;
    }
    p {
      color: #a1a1aa;
      margin-bottom: 32px;
      line-height: 1.5;
    }
    a {
      display: inline-block;
      padding: 10px 20px;
      background: #27272a;
      border: 1px solid #27272a;
      border-radius: 6px;
      color: #fafafa;
      text-decoration: none;
      font-weight: 500;
      font-size: 14px;
      transition: all 0.15s;
    }
    a:hover {
      border-color: #d4d4d8;
    }
  </style>
</head>
<body>
  <div class="card">
    <div class="icon">📦</div>
    <h2>${title}</h2>
    <p>${message}</p>
    <a href="/">← Back to Home</a>
  </div>
</body>
</html>`;
  }
}
