import type { Subprocess } from 'bun';
import { join } from 'node:path';
import { existsSync } from 'node:fs';
import { createServer } from 'node:net';
import { createRouter } from '@@/create-router';
import { getUserProjectsDir } from '@@/data-path';
import { CustomError } from '@@/custom-errors';
import * as errors from '@@/custom-errors';
import { verify } from '@@/jwt';
import { isTokenBlacklisted } from 'officerdb';
import { rewriteHtml, rewriteAssetPaths } from '../_shared/html-rewrite';

export const devServerRouter = createRouter();
export const devServerProxyRouter = createRouter();

export type ServerEntry = { proc: Subprocess; port: number; slug: string; proxyId: string; logs: string[]; idleTimer: Timer | null };

const IDLE_TIMEOUT_MS = 5 * 60 * 1000;

const servers = new Map<string, ServerEntry>();
const proxyIdIndex = new Map<string, ServerEntry>();
const pendingStarts = new Set<string>();

const serverKey = (email: string, slug: string) => `${email}:${slug}`;

export const findEntryByProxyId = (proxyId: string): ServerEntry | undefined => proxyIdIndex.get(proxyId);

export const touchEntry = (entry: ServerEntry) => {
  if (entry.idleTimer) clearTimeout(entry.idleTimer);
  entry.idleTimer = setTimeout(() => {
    console.log(`[dev-server] ${entry.slug} idle for 5m, stopping`);
    entry.proc.kill();
  }, IDLE_TIMEOUT_MS);
};

const killEntry = (entry: ServerEntry, key: string) => {
  if (entry.idleTimer) clearTimeout(entry.idleTimer);
  entry.proc.kill();
  proxyIdIndex.delete(entry.proxyId);
  servers.delete(key);
};

const findFreePort = (): Promise<number> =>
  new Promise((resolve, reject) => {
    const server = createServer();
    server.listen(0, '127.0.0.1', () => {
      const addr = server.address();
      const port = typeof addr === 'object' && addr ? addr.port : 0;
      server.close(() => resolve(port));
    });
    server.on('error', reject);
  });

const collectLogs = (entry: ServerEntry, stream: ReadableStream<Uint8Array> | null, prefix: string) => {
  if (!stream) return;
  const reader = stream.getReader();
  const decoder = new TextDecoder();
  const pump = async () => {
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      const text = decoder.decode(value, { stream: true });
      for (const line of text.split('\n').filter(Boolean)) {
        entry.logs.push(`[${prefix}] ${line}`);
        if (entry.logs.length > 200) entry.logs.shift();
      }
    }
  };
  pump().catch(() => {});
};

const waitForPort = async (port: number, timeoutMs = 8000) => {
  const start = Date.now();
  while (Date.now() - start < timeoutMs) {
    try {
      const res = await fetch(`http://localhost:${port}`, { signal: AbortSignal.timeout(500) });
      await res.arrayBuffer();
      return true;
    } catch {
      await Bun.sleep(300);
    }
  }
  return false;
};

devServerRouter.post('/start', async (ctx) => {
  const user = ctx.get('user');
  const { slug } = ctx.get('body') as { slug: string };

  if (!slug) throw errors.BAD_REQUEST('Missing slug');

  const key = serverKey(user.email, slug);

  const existing = servers.get(key);
  if (existing) {
    touchEntry(existing);
    return ctx.json({ url: `/api/dev-server-proxy/${existing.proxyId}/`, port: existing.port });
  }

  if (pendingStarts.has(key)) throw errors.BAD_REQUEST('Server is already starting');
  pendingStarts.add(key);

  try {
    const projectDir = join(getUserProjectsDir(user.email), slug);
    if (!existsSync(projectDir)) throw errors.BAD_REQUEST(`Project directory not found: ${slug}`);

    const pkgPath = join(projectDir, 'package.json');
    if (!existsSync(pkgPath)) throw errors.BAD_REQUEST('No package.json found in project');
    try {
      const pkg = JSON.parse(await Bun.file(pkgPath).text());
      if (!pkg.scripts?.dev) throw errors.BAD_REQUEST('No "dev" script in package.json');
    } catch (err) {
      if (err instanceof CustomError) throw err;
      throw errors.BAD_REQUEST('Failed to read package.json');
    }

    const port = await findFreePort();
    const proxyId = crypto.randomUUID();

    const proc = Bun.spawn(['bun', 'dev'], {
      cwd: projectDir,
      env: { ...process.env, PORT: String(port) },
      stdout: 'pipe',
      stderr: 'pipe',
    });
    console.log(`[dev-server] started ${slug} on port ${port} (pid ${proc.pid}) proxyId=${proxyId}`);

    const entry: ServerEntry = { proc, port, slug, proxyId, logs: [], idleTimer: null };
    servers.set(key, entry);
    proxyIdIndex.set(proxyId, entry);
    touchEntry(entry);

    collectLogs(entry, proc.stdout, 'stdout');
    collectLogs(entry, proc.stderr, 'stderr');

    proc.exited.then((code) => {
      console.log(`[dev-server] ${slug} exited with code ${code}`);
      entry.logs.push(`[system] Process exited with code ${code}`);
      if (entry.idleTimer) clearTimeout(entry.idleTimer);
      proxyIdIndex.delete(entry.proxyId);
      servers.delete(key);
    });

    const ready = await waitForPort(port);
    if (!ready) {
      const exitCode = proc.exitCode;
      if (exitCode !== null) {
        if (entry.idleTimer) clearTimeout(entry.idleTimer);
        proxyIdIndex.delete(entry.proxyId);
        servers.delete(key);
        throw errors.BAD_REQUEST(`Dev server exited with code ${exitCode}. Logs:\n${entry.logs.slice(-20).join('\n')}`);
      }
    }

    return ctx.json({ url: `/api/dev-server-proxy/${proxyId}/`, port });
  } finally {
    pendingStarts.delete(key);
  }
});

devServerRouter.post('/stop', async (ctx) => {
  const user = ctx.get('user');
  const { slug } = ctx.get('body') as { slug: string };

  if (!slug) throw errors.BAD_REQUEST('Missing slug');

  const key = serverKey(user.email, slug);
  const entry = servers.get(key);

  if (entry) killEntry(entry, key);

  return ctx.json({ ok: true });
});

devServerRouter.get('/status', async (ctx) => {
  const user = ctx.get('user');
  const slug = ctx.req.query('slug');

  if (!slug) throw errors.BAD_REQUEST('Missing slug');

  const key = serverKey(user.email, slug);
  const entry = servers.get(key);

  if (entry) {
    return ctx.json({ running: true, url: `/api/dev-server-proxy/${entry.proxyId}/`, port: entry.port });
  }

  return ctx.json({ running: false });
});

devServerRouter.get('/logs', async (ctx) => {
  const user = ctx.get('user');
  const slug = ctx.req.query('slug');

  if (!slug) throw errors.BAD_REQUEST('Missing slug');

  const key = serverKey(user.email, slug);
  const entry = servers.get(key);

  if (!entry) return ctx.json({ logs: [] });

  return ctx.json({ logs: entry.logs.slice(-100) });
});

// Proxy router — mounted outside protected router (no auth needed for sub-resources).
// Security: only proxies to ports that were started via authenticated /start calls.

type ProxyParams = { entry: ServerEntry; proxyId: string; proxyPath: string; search: string; method: string; rawReq: Request };

async function proxyRequest({ entry, proxyId, proxyPath, search, method, rawReq }: ProxyParams): Promise<Response> {
  const prefix = `/api/dev-server-proxy/${proxyId}`;
  const targetUrl = `http://localhost:${entry.port}${proxyPath}${search}`;

  touchEntry(entry);

  try {
    const headers = new Headers(rawReq.headers);
    headers.delete('host');
    headers.delete('authorization');

    const res = await fetch(targetUrl, {
      method,
      headers,
      body: method !== 'GET' && method !== 'HEAD' ? rawReq.body : undefined,
      redirect: 'manual',
    });

    const contentType = res.headers.get('content-type') ?? '';
    const needsRewrite =
      contentType.includes('text/html') ||
      contentType.includes('javascript') ||
      contentType.includes('text/css');

    if (needsRewrite) {
      const text = await res.text();
      const rewritten = contentType.includes('text/html')
        ? rewriteHtml(text, prefix)
        : rewriteAssetPaths(text, prefix);
      const newHeaders = new Headers(res.headers);
      newHeaders.delete('content-length');
      newHeaders.delete('content-encoding');
      return new Response(rewritten, { status: res.status, headers: newHeaders });
    }

    return new Response(res.body, { status: res.status, headers: res.headers });
  } catch {
    return new Response('Dev server not reachable', { status: 502 });
  }
}

const isLikelyHtmlRequest = (req: Request): boolean => {
  const accept = req.headers.get('accept') ?? '';
  return accept.includes('text/html');
};

const stripTokenParam = (search: string): string => {
  if (!search) return search;
  const params = new URLSearchParams(search);
  params.delete('token');
  const result = params.toString();
  return result ? `?${result}` : '';
};

async function validateJwt(req: Request): Promise<boolean> {
  const authHeader = req.headers.get('authorization');
  const token = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : new URL(req.url).searchParams.get('token');
  if (!token) return false;
  try {
    const payload = await verify(token);
    if (!payload) return false;
    if (payload.jti && await isTokenBlacklisted(payload.jti)) return false;
    return true;
  } catch {
    return false;
  }
}

// Proxy handler: tries proxyId from URL first, falls back to Referer for misrouted
// chunk requests caused by relative imports resolving at wrong path depth.
devServerProxyRouter.all('/:proxyId/*', async (ctx) => {
  const proxyId = ctx.req.param('proxyId');
  if (!proxyId) return ctx.text('Not found', 404);

  const originalUrl = new URL(ctx.req.url);
  const entry = proxyIdIndex.get(proxyId);
  if (entry) {
    if (isLikelyHtmlRequest(ctx.req.raw)) {
      const valid = await validateJwt(ctx.req.raw);
      if (!valid) return ctx.text('Unauthorized', 401);
    }
    const prefix = `/api/dev-server-proxy/${proxyId}`;
    const proxyPath = originalUrl.pathname.replace(prefix, '') || '/';
    const search = stripTokenParam(originalUrl.search);
    return proxyRequest({ entry, proxyId, proxyPath, search, method: ctx.req.method, rawReq: ctx.req.raw });
  }

  // proxyId didn't match — check Referer for the real proxyId
  const referer = ctx.req.header('referer');
  if (referer) {
    const match = referer.match(/\/api\/dev-server-proxy\/([^/]+)/);
    if (match) {
      const realProxyId = match[1]!;
      const realEntry = proxyIdIndex.get(realProxyId);
      if (realEntry) {
        const proxyPath = originalUrl.pathname.replace('/api/dev-server-proxy', '') || '/';
        const search = stripTokenParam(originalUrl.search);
        return proxyRequest({ entry: realEntry, proxyId: realProxyId, proxyPath, search, method: ctx.req.method, rawReq: ctx.req.raw });
      }
    }
  }

  return ctx.text('No dev server running', 404);
});

