import {
  createServer,
  IncomingMessage,
  Server,
  ServerResponse
} from 'http';
import { createReadStream } from 'fs';
import { AddressInfo } from 'net';
import { pipeline } from 'stream/promises';
import { getDataSource } from '../db/database';
import { LocalApiSettings, readDesktopSettings } from '../desktop-settings';
import { authenticate, matchRoute } from './router';
import { clearAllTokens } from './auth-store';
import {
  HttpError,
  RequestContext,
  getBearerToken,
  readJsonBody,
  sendError,
  sendJson,
  sendText
} from './http-helpers';

export type LocalApiStatus = 'stopped' | 'starting' | 'running' | 'error';

export interface LocalApiSnapshot {
  status: LocalApiStatus;
  host: string | null;
  port: number | null;
  baseUrl: string | null;
  error: string | null;
  exposeOnLan: boolean;
  scalarEnabled: boolean;
  docusaurusEnabled: boolean;
}

let server: Server | null = null;
let currentStatus: LocalApiStatus = 'stopped';
let currentBindHost: string | null = null;
let currentBindPort: number | null = null;
let currentError: string | null = null;
let activeSettings: LocalApiSettings | null = null;

function pickBindHost(settings: LocalApiSettings): string {
  return settings.exposeOnLan ? '0.0.0.0' : '127.0.0.1';
}

function buildBaseUrl(host: string, port: number): string {
  const safeHost = host === '0.0.0.0' ? '127.0.0.1' : host;

  return `http://${safeHost}:${port}`;
}

async function sendFile(res: ServerResponse, status: number, filePath: string, contentType: string): Promise<void> {
  if (!res.headersSent) {
    res.statusCode = status;
    res.setHeader('Content-Type', contentType);
    res.setHeader('Cache-Control', 'no-store');
  }

  await pipeline(createReadStream(filePath), res);
}

export function getLocalApiSnapshot(): LocalApiSnapshot {
  const settings = activeSettings ?? readDesktopSettings().localApi;

  return {
    status: currentStatus,
    host: currentBindHost,
    port: currentBindPort,
    baseUrl: currentBindHost && currentBindPort ? buildBaseUrl(currentBindHost, currentBindPort) : null,
    error: currentError,
    exposeOnLan: settings.exposeOnLan,
    scalarEnabled: settings.scalarEnabled,
    docusaurusEnabled: settings.docusaurusEnabled
  };
}

async function handleRequest(req: IncomingMessage, res: ServerResponse, settings: LocalApiSettings): Promise<void> {
  // CORS for loopback origin only. Local-first; not a public API.
  const origin = req.headers.origin;
  const allowOrigin = origin && /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/iu.test(origin) ? origin : 'null';

  res.setHeader('Access-Control-Allow-Origin', allowOrigin);
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.setHeader('Access-Control-Max-Age', '600');

  if (req.method === 'OPTIONS') {
    res.statusCode = 204;
    res.end();
    return;
  }

  let urlObj: URL;

  try {
    urlObj = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
  } catch {
    sendJson(res, 400, { error: 'Invalid URL', errorCode: 'INVALID_URL' });
    return;
  }

  const requestContext: RequestContext = {
    method: (req.method ?? 'GET').toUpperCase(),
    url: urlObj,
    pathname: urlObj.pathname,
    headers: req.headers,
    remoteAddress: req.socket.remoteAddress ?? '',
    bearerToken: getBearerToken(req.headers)
  };
  const { match, methodNotAllowed } = matchRoute(requestContext.method, requestContext.pathname);

  if (!match) {
    if (methodNotAllowed) {
      sendJson(res, 405, { error: 'Method not allowed', errorCode: 'METHOD_NOT_ALLOWED' });
    } else {
      sendJson(res, 404, { error: 'Not found', errorCode: 'NOT_FOUND' });
    }

    return;
  }

  if (match.requiresAuth) {
    const issued = authenticate(requestContext.bearerToken);

    if (!issued) {
      sendJson(res, 401, { error: 'Authentication required', errorCode: 'UNAUTHORIZED' });
      return;
    }
  }

  const dataSource = getDataSource() ?? null;

  if (match.requiresDatabase && (!dataSource || !dataSource.isInitialized)) {
    sendJson(res, 503, { error: 'Database not initialised', errorCode: 'DB_UNAVAILABLE' });
    return;
  }

  let bodyCache: unknown | undefined;

  try {
    const baseUrl = buildBaseUrl(currentBindHost ?? '127.0.0.1', currentBindPort ?? settings.port);
    const result = await match.handler({
      request: requestContext,
      settings,
      baseUrl,
      dataSource,
      bodyBuffer: async () => {
        if (bodyCache === undefined) {
          bodyCache = await readJsonBody<unknown>(req);
        }

        return bodyCache;
      }
    });

    if (result.status === 204) {
      res.statusCode = 204;
      res.end();
      return;
    }

    if (result.filePath) {
      await sendFile(res, result.status, result.filePath, result.contentType ?? 'application/octet-stream');
      return;
    }

    if (result.rawBody !== undefined) {
      sendText(res, result.status, result.rawBody, result.contentType ?? 'text/plain; charset=utf-8');
      return;
    }

    sendJson(res, result.status, result.body);
  } catch (error) {
    if (!(error instanceof HttpError)) {
      console.error('[LocalApi] Request handler error:', error);
    }

    sendError(res, error);
  }
}

export interface StartResult {
  ok: boolean;
  snapshot: LocalApiSnapshot;
}

export async function startLocalApiServer(settings: LocalApiSettings): Promise<StartResult> {
  if (server) {
    await stopLocalApiServer();
  }

  activeSettings = { ...settings, allowedSignalingServers: [...settings.allowedSignalingServers] };
  currentStatus = 'starting';
  currentError = null;
  currentBindHost = pickBindHost(settings);
  currentBindPort = settings.port;
  const httpServer = createServer((req, res) => {
    void handleRequest(req, res, activeSettings ?? settings).catch((error) => {
      console.error('[LocalApi] Unhandled request error:', error);

      try {
        sendError(res, error);
      } catch {
        // ignore
      }
    });
  });

  return await new Promise<StartResult>((resolve) => {
    httpServer.once('error', (error) => {
      currentStatus = 'error';
      currentError = (error as Error).message;
      currentBindPort = null;
      server = null;
      activeSettings = null;
      console.error('[LocalApi] Failed to start:', error);
      resolve({ ok: false, snapshot: getLocalApiSnapshot() });
    });

    httpServer.listen(settings.port, pickBindHost(settings), () => {
      const address = httpServer.address() as AddressInfo | null;

      server = httpServer;
      currentStatus = 'running';
      currentBindPort = address?.port ?? settings.port;
      currentError = null;
      console.log(`[LocalApi] Listening on http://${currentBindHost}:${currentBindPort}`);
      resolve({ ok: true, snapshot: getLocalApiSnapshot() });
    });
  });
}

export async function stopLocalApiServer(): Promise<LocalApiSnapshot> {
  const httpServer = server;

  if (!httpServer) {
    currentStatus = 'stopped';
    currentBindHost = null;
    currentBindPort = null;
    activeSettings = null;
    return getLocalApiSnapshot();
  }

  await new Promise<void>((resolve) => {
    httpServer.close(() => resolve());
    // close() waits for connections; force-close keep-alives so it returns promptly.
    httpServer.closeAllConnections?.();
  });

  server = null;
  currentStatus = 'stopped';
  currentBindHost = null;
  currentBindPort = null;
  currentError = null;
  activeSettings = null;
  clearAllTokens();
  console.log('[LocalApi] Stopped');
  return getLocalApiSnapshot();
}

export async function applyLocalApiSettings(): Promise<LocalApiSnapshot> {
  const settings = readDesktopSettings().localApi;

  if (!settings.enabled) {
    return await stopLocalApiServer();
  }

  // If already running with the same bind config, no-op (settings like
  // scalarEnabled / allowedSignalingServers are read on every request).
  if (
    server
    && activeSettings
    && currentStatus === 'running'
    && activeSettings.port === settings.port
    && activeSettings.exposeOnLan === settings.exposeOnLan
  ) {
    activeSettings = { ...settings, allowedSignalingServers: [...settings.allowedSignalingServers] };
    return getLocalApiSnapshot();
  }

  const result = await startLocalApiServer(settings);

  return result.snapshot;
}
