import * as fs from 'node:fs';
import * as http from 'node:http';
import * as net from 'node:net';
import * as path from 'node:path';
import * as tls from 'node:tls';
import type { AddressInfo } from 'node:net';
import type { Duplex } from 'node:stream';

import type { ILoadedMatrixPackageEnvironment } from './package-environment.js';
import type { IResolvedRunnerTransportPlan } from './runner-transport.js';
import type { ILoadedMatrixWebappManifest } from './webapp-manifest.js';
import { jwtCredentialsFromCredentialsRef } from '@open-matrix/nats-auth';

export interface IStandaloneWebappServerOptions {
  readonly loadedEnvironment: ILoadedMatrixPackageEnvironment;
  readonly webapp: ILoadedMatrixWebappManifest;
  readonly runnerTransport: IResolvedRunnerTransportPlan;
}

export interface IStandaloneWebappServerHandle {
  readonly port: number;
  readonly baseUrl: string;
  readonly webapp: ILoadedMatrixWebappManifest;
  readonly bootstrapPath: '/api/bootstrap';
  readonly natsWsPath: '/nats-ws';
  shutdown(): Promise<void>;
}

interface IMimeTypeMap {
  readonly [key: string]: string;
}

const MIME_TYPES: IMimeTypeMap = {
  '.css': 'text/css; charset=utf-8',
  '.html': 'text/html; charset=utf-8',
  '.ico': 'image/x-icon',
  '.js': 'text/javascript; charset=utf-8',
  '.json': 'application/json; charset=utf-8',
  '.map': 'application/json; charset=utf-8',
  '.png': 'image/png',
  '.svg': 'image/svg+xml',
  '.txt': 'text/plain; charset=utf-8',
};

export async function startStandaloneWebappServer(
  options: IStandaloneWebappServerOptions,
): Promise<IStandaloneWebappServerHandle> {
  const httpConfig = options.loadedEnvironment.environment.http;
  if (httpConfig?.enabled !== true || typeof httpConfig.port !== 'number' || httpConfig.port <= 0) {
    throw new Error(
      `Package environment "${options.loadedEnvironment.envName}" must enable http.port for --serve mode`,
    );
  }
  if (!fs.existsSync(options.webapp.distDir)) {
    throw new Error(
      `Built webapp dist directory not found for ${options.webapp.packageName}: ${options.webapp.distDir}`,
    );
  }
  const entryPath = path.join(options.webapp.distDir, options.webapp.entryFile);
  if (!fs.existsSync(entryPath)) {
    throw new Error(
      `Built webapp entry file not found for ${options.webapp.packageName}: ${entryPath}`,
    );
  }
  if (!options.runnerTransport.wsUrl) {
    throw new Error(
      `Runner transport for ${options.webapp.packageName} does not expose wsUrl required for /nats-ws proxy`,
    );
  }

  const runtimeRoot = options.runnerTransport.root;
  const brandingDir = resolveBrandingDir(options.loadedEnvironment.packageDir);
  const server = http.createServer((req, res) => {
    void handleRequest(req, res, {
      entryPath,
      brandingDir,
      runtimeRoot,
      webapp: options.webapp,
      loadedEnvironment: options.loadedEnvironment,
      runnerTransport: options.runnerTransport,
    }).catch((error) => {
      res.statusCode = 500;
      res.setHeader('Content-Type', 'application/json; charset=utf-8');
      res.end(JSON.stringify({
        error: error instanceof Error ? error.message : String(error),
      }));
    });
  });

  server.on('upgrade', (req, socket, head) => {
    if ((req.url ?? '').split('?')[0] !== '/nats-ws') {
      socket.write('HTTP/1.1 404 Not Found\r\nConnection: close\r\nContent-Length: 0\r\n\r\n');
      socket.destroy();
      return;
    }
    proxyWebSocketUpgrade(req, socket, head, options.runnerTransport.wsUrl!);
  });

  await new Promise<void>((resolve, reject) => {
    server.once('error', reject);
    server.listen(httpConfig.port, '127.0.0.1', () => {
      server.off('error', reject);
      resolve();
    });
  });

  const address = server.address();
  if (!address || typeof address !== 'object') {
    throw new Error(`Standalone webapp server for ${options.webapp.packageName} did not expose a TCP address`);
  }
  const port = (address as AddressInfo).port;

  return {
    port,
    baseUrl: `http://127.0.0.1:${port}`,
    webapp: options.webapp,
    bootstrapPath: '/api/bootstrap',
    natsWsPath: '/nats-ws',
    async shutdown(): Promise<void> {
      await new Promise<void>((resolve, reject) => {
        server.close((error) => (error ? reject(error) : resolve()));
      });
    },
  };
}

async function handleRequest(
  req: http.IncomingMessage,
  res: http.ServerResponse<http.IncomingMessage>,
  options: {
    readonly entryPath: string;
    readonly brandingDir: string | null;
    readonly runtimeRoot: string;
    readonly webapp: ILoadedMatrixWebappManifest;
    readonly loadedEnvironment: ILoadedMatrixPackageEnvironment;
    readonly runnerTransport: IResolvedRunnerTransportPlan;
  },
): Promise<void> {
  const method = req.method ?? 'GET';
  const requestUrl = new URL(req.url ?? '/', 'http://127.0.0.1');
  const pathname = requestUrl.pathname;
  const isNatsAuthRequest = pathname === '/api/nats-jwt' && method === 'POST';
  if (method !== 'GET' && method !== 'HEAD' && !isNatsAuthRequest) {
    res.statusCode = 405;
    res.setHeader('Allow', pathname === '/api/nats-jwt' ? 'GET, HEAD, POST' : 'GET, HEAD');
    res.end();
    return;
  }

  if (pathname === '/api/bootstrap') {
    const payload = buildBootstrapPayload(req, options.runtimeRoot, options.runnerTransport, options.loadedEnvironment);
    res.statusCode = 200;
    res.setHeader('Content-Type', 'application/json; charset=utf-8');
    res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
    writeBody(res, method, JSON.stringify(payload));
    return;
  }

  if (pathname === '/api/auth/me') {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'application/json; charset=utf-8');
    writeBody(res, method, JSON.stringify({
      authenticated: true,
      principal: {
        id: 'local-runner',
        displayName: 'Local Runner',
        email: null,
      },
      session: {
        sub: 'local-runner',
        email: null,
        exp: null,
        localClient: true,
      },
    }));
    return;
  }

  if (pathname === '/api/config') {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'application/json; charset=utf-8');
    writeBody(res, method, JSON.stringify({
      root: options.runtimeRoot,
      environment: options.loadedEnvironment.environment.name,
      transport: {
        mode: options.runnerTransport.mode,
        url: options.runnerTransport.url,
        wsUrl: options.runnerTransport.wsUrl ?? null,
      },
    }));
    return;
  }

  if (pathname === '/api/nats-jwt') {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'application/json; charset=utf-8');
    writeBody(res, method, JSON.stringify(buildNatsAuthPayload(
      options.runnerTransport,
      options.loadedEnvironment.packageDir,
    )));
    return;
  }

  if (pathname === '/healthz') {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'application/json; charset=utf-8');
    writeBody(res, method, JSON.stringify({
      ok: true,
      ready: true,
      phase: 'ready',
      root: options.runtimeRoot,
    }));
    return;
  }

  if (pathname === '/nats-ws') {
    res.statusCode = 426;
    res.setHeader('Content-Type', 'text/plain; charset=utf-8');
    writeBody(res, method, 'Upgrade Required');
    return;
  }

  if (pathname === '/favicon.ico' || pathname === '/manifest.json' || pathname.startsWith('/branding/')) {
    const assetPath = resolveBrandingAssetPath(pathname, options.brandingDir);
    if (!assetPath) {
      res.statusCode = 404;
      res.end();
      return;
    }
    await serveFile(res, method, assetPath);
    return;
  }

  const resolvedPath = resolveStaticAssetPath(pathname, options.webapp, options.entryPath);
  if (!resolvedPath) {
    res.statusCode = 404;
    res.end();
    return;
  }
  await serveFile(res, method, resolvedPath);
}

function buildBootstrapPayload(
  req: http.IncomingMessage,
  runtimeRoot: string,
  runnerTransport: IResolvedRunnerTransportPlan,
  loadedEnvironment: ILoadedMatrixPackageEnvironment,
): Record<string, unknown> {
  const hostHeader = Array.isArray(req.headers.host)
    ? req.headers.host[0]
    : req.headers.host;
  const host = hostHeader || `127.0.0.1:${loadedEnvironment.environment.http?.port ?? 0}`;
  const protocol = 'ws:';
  const wsUrl = `${protocol}//${host}/nats-ws`;
  const hasCredentialsRef = typeof runnerTransport.credentialsRef === 'string'
    && runnerTransport.credentialsRef.trim().length > 0;
  const authorityContext = {
    assetHostRoot: runtimeRoot,
    platformServiceRoot: runtimeRoot,
    sessionAuthorityRoot: runtimeRoot,
    selectedAuthorityRoot: runtimeRoot,
    runtimeInstanceRoot: null,
    isAuthenticated: true,
    isPlatformAdmin: false,
    selectableAuthorityRoots: [runtimeRoot],
  };
  return {
    authorityContext,
    natsWsUrl: wsUrl,
    root: runtimeRoot,
    authorityRoot: runtimeRoot,
    assetHostRoot: runtimeRoot,
    platformServiceRoot: runtimeRoot,
    sessionAuthorityRoot: runtimeRoot,
    selectedAuthorityRoot: runtimeRoot,
    runtimeInstanceRoot: null,
    selectableAuthorityRoots: [runtimeRoot],
    isPlatformAdmin: false,
    busRoot: runtimeRoot,
    platformRoot: runtimeRoot,
    role: 'client',
    addressRoot: runtimeRoot,
    provisioned: true,
    authorityReady: true,
    authenticated: true,
    principal: {
      principalId: 'local-runner',
      displayName: 'Local Runner',
    },
    natsOwnership: {
      mode: runnerTransport.mode,
      source: 'package-environment:nats',
    },
    federation: {
      connected: false,
      hubUrl: null,
      source: 'none',
    },
    transportPlan: {
      protocolVersion: 1,
      kind: 'nats',
      wsMode: 'same-origin-proxy',
      wsPath: '/nats-ws',
      authMode: hasCredentialsRef ? 'jwt' : 'anonymous',
      authEndpoint: hasCredentialsRef ? '/api/nats-jwt' : null,
      busAuthMode: 'none',
      sessionMode: 'local-client',
    },
    sources: {
      root: 'package-environment:runtime.root',
      authorityRoot: 'package-environment:runtime.root',
      assetHostRoot: 'package-environment:runtime.root',
      platformServiceRoot: 'package-environment:runtime.root',
      sessionAuthorityRoot: 'package-environment:runtime.root',
      selectedAuthorityRoot: 'package-environment:runtime.root',
      runtimeInstanceRoot: 'none',
      platformRoot: 'package-environment:runtime.root',
      addressRoot: 'package-environment:runtime.root',
      browserWsPath: 'transportPlan.wsPath',
      transportAuthMode: hasCredentialsRef ? 'credentials-ref' : 'standalone-runner',
      busToken: 'none',
      sessionIdentity: 'local-client',
      federation: 'none',
    },
  };
}

function buildNatsAuthPayload(
  runnerTransport: IResolvedRunnerTransportPlan,
  baseDir: string,
): Record<string, unknown> {
  const credentials = jwtCredentialsFromCredentialsRef(
    runnerTransport.credentialsRef,
    baseDir,
    `matrix-browser-${process.pid}`,
  );
  if (!credentials) {
    return { mode: 'anonymous' };
  }
  return {
    mode: 'jwt',
    jwt: credentials.jwt,
    seed: credentials.seed,
  };
}

function resolveStaticAssetPath(
  pathname: string,
  webapp: ILoadedMatrixWebappManifest,
  entryPath: string,
): string | null {
  const decodedPath = safeDecodePath(pathname);
  if (!decodedPath) {
    return null;
  }
  const appBasePath = `/apps/${webapp.appName}`;
  const cleanPath = decodedPath === appBasePath || decodedPath.startsWith(`${appBasePath}/`)
    ? decodedPath.slice(appBasePath.length) || '/'
    : decodedPath;
  if (cleanPath === '/') {
    return entryPath;
  }

  const relativePath = cleanPath.replace(/^\/+/, '');
  const distRoot = path.resolve(webapp.distDir);
  const resolved = path.resolve(webapp.distDir, relativePath);
  if (resolved.startsWith(distRoot) && fs.existsSync(resolved) && fs.statSync(resolved).isFile()) {
    return resolved;
  }

  if (!path.extname(relativePath)) {
    return entryPath;
  }

  return null;
}

function resolveBrandingAssetPath(pathname: string, brandingDir: string | null): string | null {
  if (!brandingDir) {
    return null;
  }

  let assetPath: string;
  if (pathname === '/favicon.ico') {
    assetPath = path.join(brandingDir, 'favicon.ico');
  } else if (pathname === '/manifest.json') {
    assetPath = path.join(brandingDir, 'manifest.json');
  } else {
    const relativePath = safeDecodePath(pathname.slice('/branding/'.length));
    if (!relativePath) {
      return null;
    }
    assetPath = path.join(brandingDir, relativePath);
  }

  const resolved = path.resolve(assetPath);
  if (!resolved.startsWith(path.resolve(brandingDir)) || !fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) {
    return null;
  }
  return resolved;
}

async function serveFile(
  res: http.ServerResponse<http.IncomingMessage>,
  method: string,
  filePath: string,
): Promise<void> {
  const stat = await fs.promises.stat(filePath);
  const ext = path.extname(filePath).toLowerCase();
  res.statusCode = 200;
  res.setHeader('Content-Type', MIME_TYPES[ext] ?? 'application/octet-stream');
  res.setHeader('Content-Length', String(stat.size));
  res.setHeader('Cache-Control', 'no-cache');
  if (method === 'HEAD') {
    res.end();
    return;
  }
  const stream = fs.createReadStream(filePath);
  await new Promise<void>((resolve, reject) => {
    stream.once('error', reject);
    res.once('error', reject);
    res.once('finish', resolve);
    stream.pipe(res);
  });
}

function writeBody(
  res: http.ServerResponse<http.IncomingMessage>,
  method: string,
  body: string,
): void {
  res.setHeader('Content-Length', Buffer.byteLength(body));
  if (method === 'HEAD') {
    res.end();
    return;
  }
  res.end(body);
}

function proxyWebSocketUpgrade(
  req: http.IncomingMessage,
  socket: Duplex,
  head: Buffer,
  targetOrigin: string,
): void {
  let target: URL;
  try {
    target = new URL(targetOrigin);
  } catch {
    socket.write('HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\nContent-Length: 0\r\n\r\n');
    socket.destroy();
    return;
  }
  if (!['ws:', 'wss:', 'http:', 'https:'].includes(target.protocol)) {
    socket.write('HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\nContent-Length: 0\r\n\r\n');
    socket.destroy();
    return;
  }

  const port = target.port
    ? Number(target.port)
    : (target.protocol === 'wss:' || target.protocol === 'https:' ? 443 : 80);
  const upstream: Duplex = (target.protocol === 'wss:' || target.protocol === 'https:'
    ? tls.connect(port, target.hostname, { servername: target.hostname })
    : net.connect(port, target.hostname));

  upstream.once('connect', () => {
    const forwardedPath = buildProxyPath(target, req.url);
    let rawRequest = `GET ${forwardedPath} HTTP/${req.httpVersion}\r\n`;
    rawRequest += `Host: ${target.host}\r\n`;
    for (const [key, value] of Object.entries(req.headers)) {
      const lowerKey = key.toLowerCase();
      if (lowerKey === 'host' || lowerKey === 'origin' || value === undefined) {
        continue;
      }
      const values = Array.isArray(value) ? value : [value];
      for (const headerValue of values) {
        rawRequest += `${key}: ${headerValue}\r\n`;
      }
    }
    rawRequest += '\r\n';
    upstream.write(rawRequest);
    if (head.length > 0) {
      upstream.write(head);
    }
    socket.pipe(upstream);
    upstream.pipe(socket);
  });

  upstream.once('error', () => {
    socket.destroy();
  });
  socket.once('error', () => {
    upstream.destroy();
  });
}

function buildProxyPath(target: URL, rawUrl: string | undefined): string {
  const incoming = new URL(rawUrl ?? '/', 'http://matrix.local');
  const basePath = target.pathname === '/' ? '' : target.pathname.replace(/\/$/, '');
  if (basePath.length > 0) {
    return `${basePath}${incoming.search}`;
  }
  return `${incoming.pathname}${incoming.search}`;
}

function resolveBrandingDir(packageDir: string): string | null {
  const brandingDir = path.join(packageDir, 'branding');
  return fs.existsSync(path.join(brandingDir, 'manifest.json')) ? brandingDir : null;
}

function safeDecodePath(pathname: string): string | null {
  try {
    const decoded = decodeURIComponent(pathname);
    if (decoded.includes('\0')) {
      return null;
    }
    return decoded;
  } catch {
    return null;
  }
}
