/**
 * Request lifecycle helpers — wraps a route handler so it runs inside an
 * AsyncLocalStorage context with a `reqId`, emits one `request.start` and one
 * `request.end` log line, and echoes `x-request-id` back on the response.
 *
 * `wrapRouteHandler` is the entry point for Next.js route handlers (called by
 * `withErrorHandler`). `runWithRequestContextForServer` is for the custom
 * Node HTTP server in `restroagent/server.ts`.
 */
import { randomUUID } from 'crypto';
import type { NextResponse } from 'next/server';
import { childLogger } from './index';
import { runWithRequestContext, type RequestContext } from './context';

const reqLogger = childLogger('http');

const REQUEST_ID_HEADER = 'x-request-id';

function readRequestId(headers: Headers): string {
  const incoming = headers.get(REQUEST_ID_HEADER);
  if (incoming && /^[A-Za-z0-9_.\-:]{8,128}$/.test(incoming)) return incoming;
  return randomUUID();
}

function safeRoute(req: Request): { method: string; pathname: string; route: string } {
  const method = req.method.toUpperCase();
  let pathname = '/';
  try {
    pathname = new URL(req.url).pathname;
  } catch {
    /* noop */
  }
  return { method, pathname, route: `${method} ${pathname}` };
}

export async function runRequestLifecycle<T extends Response | NextResponse>(
  req: Request,
  handler: () => Promise<T>
): Promise<T> {
  const reqId = readRequestId(req.headers);
  const { method, pathname, route } = safeRoute(req);
  const ctx: RequestContext = { reqId, route };
  const startedAt = process.hrtime.bigint();

  return runWithRequestContext(ctx, async () => {
    reqLogger.info({ method, path: pathname, type: 'request.start' }, 'request.start');
    let res: T;
    let status = 500;
    try {
      res = await handler();
      status = (res as Response).status;
      try {
        (res as Response).headers.set(REQUEST_ID_HEADER, reqId);
      } catch {
        /* immutable headers (rare) — ignore */
      }
      return res;
    } catch (err) {
      reqLogger.error(
        { err, method, path: pathname, type: 'request.error' },
        'unhandled request error'
      );
      throw err;
    } finally {
      const durationMs =
        Number(process.hrtime.bigint() - startedAt) / 1_000_000;
      reqLogger.info(
        {
          method,
          path: pathname,
          status,
          durationMs: Math.round(durationMs * 1000) / 1000,
          type: 'request.end',
        },
        'request.end'
      );
    }
  });
}

/**
 * Wrap a Next.js route handler (POST/GET/etc.) so it runs inside the request
 * lifecycle. Use for webhook endpoints and any other route that does NOT go
 * through `withErrorHandler` (which already calls `runRequestLifecycle`).
 */
export function wrapRouteHandler<Args extends unknown[], R extends Response | NextResponse>(
  handler: (req: Request, ...rest: Args) => Promise<R>
): (req: Request, ...rest: Args) => Promise<R> {
  return (req, ...rest) => runRequestLifecycle(req, () => handler(req, ...rest));
}

/**
 * Variant for the custom Node HTTP server (static uploads). Sets `x-request-id`
 * on the raw response and emits the same start/end lines.
 */
export function runNodeRequestLifecycle(
  req: import('http').IncomingMessage,
  res: import('http').ServerResponse,
  fn: () => void
): void {
  const incoming = (req.headers[REQUEST_ID_HEADER] as string | undefined)?.trim();
  const reqId =
    incoming && /^[A-Za-z0-9_.\-:]{8,128}$/.test(incoming) ? incoming : randomUUID();
  const method = (req.method || 'GET').toUpperCase();
  const pathname = (req.url || '/').split('?')[0];
  const startedAt = process.hrtime.bigint();

  try {
    res.setHeader(REQUEST_ID_HEADER, reqId);
  } catch {
    /* headers already sent */
  }

  const ctx: RequestContext = { reqId, route: `${method} ${pathname}` };

  res.on('finish', () => {
    const durationMs = Number(process.hrtime.bigint() - startedAt) / 1_000_000;
    runWithRequestContext(ctx, () => {
      reqLogger.info(
        {
          method,
          path: pathname,
          status: res.statusCode,
          durationMs: Math.round(durationMs * 1000) / 1000,
          type: 'request.end',
        },
        'request.end'
      );
    });
  });

  runWithRequestContext(ctx, () => {
    reqLogger.info({ method, path: pathname, type: 'request.start' }, 'request.start');
    fn();
  });
}
