import 'server-only';
import { randomUUID } from 'node:crypto';
import { NextResponse } from 'next/server';
import { z } from 'zod';
import { getEnv } from './env';
import { AppError } from './errors';
import { log } from './logger';
import { tenantFromHost } from '@/features/tenancy/routing';

function defaultPort(protocol: string) {
  return protocol === 'https:' ? '443' : '80';
}
export type OriginOptions = {
  /** Allow https://{store}.{ROOT_DOMAIN} (public storefront / offer posts). */
  allowTenantOrigins?: boolean;
  /** Allow requests with no Origin (rare mobile cases). Prefer false for broker APIs. */
  allowMissingOrigin?: boolean;
};

/** True when Origin is the configured APP_URL or (optionally) a valid tenant host. */
export function isAllowedOrigin(originHeader: string, allowTenantOrigins = false) {
  let originUrl: URL;
  try {
    originUrl = new URL(originHeader);
  } catch {
    return false;
  }

  const env = getEnv();
  const app = new URL(env.APP_URL);
  if (originUrl.origin === app.origin) return true;

  // Dev: allow loopback when APP_URL is a LAN IP (and the reverse), same port.
  if (env.NODE_ENV !== 'production') {
    const samePort = (originUrl.port || defaultPort(originUrl.protocol)) === (app.port || defaultPort(app.protocol));
    const oHost = originUrl.hostname;
    const aHost = app.hostname;
    const loopback = (h: string) => h === 'localhost' || h === '127.0.0.1';
    const lan = (h: string) => /^\d{1,3}(\.\d{1,3}){3}$/.test(h) && !h.startsWith('127.');
    if (samePort && ((loopback(oHost) && (loopback(aHost) || lan(aHost))) || (lan(oHost) && (loopback(aHost) || lan(aHost)))))
      return true;
  }

  if (!allowTenantOrigins) return false;
  // Tenant storefronts must use the same scheme as APP_URL (HTTPS in production).
  if (originUrl.protocol !== app.protocol) return false;
  return tenantFromHost(originUrl.host, env.ROOT_DOMAIN) !== null;
}

export function assertOrigin(request: Request, options: OriginOptions = {}) {
  const origin = request.headers.get('origin');
  if (!origin) {
    if (options.allowMissingOrigin) return;
    throw new AppError(403, 'ORIGIN_REJECTED', 'Request origin is not allowed.');
  }
  if (!isAllowedOrigin(origin, options.allowTenantOrigins)) {
    throw new AppError(403, 'ORIGIN_REJECTED', 'Request origin is not allowed.');
  }
}

export async function readJson<T>(
  request: Request,
  schema: z.ZodType<T>,
  options: OriginOptions = {},
): Promise<T> {
  assertOrigin(request, options);
  if (request.headers.get('content-type')?.split(';')[0].trim() !== 'application/json')
    throw new AppError(415, 'JSON_REQUIRED', 'Send a JSON request.');
  const reader = request.body?.getReader();
  if (!reader) throw new AppError(400, 'INVALID_INPUT', 'Request body is required.');
  const chunks: Uint8Array[] = [];
  let size = 0;
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    size += value.byteLength;
    if (size > 16_384) { await reader.cancel(); throw new AppError(413, 'BODY_TOO_LARGE', 'Request is too large.'); }
    chunks.push(value);
  }
  let input: unknown;
  try { input = JSON.parse(Buffer.concat(chunks).toString('utf8')); }
  catch { throw new AppError(400, 'INVALID_JSON', 'Request is not valid JSON.'); }
  const parsed = schema.safeParse(input);
  if (!parsed.success) throw new AppError(422, 'INVALID_INPUT', 'Check the supplied fields and try again.');
  return parsed.data;
}

export function endpoint(handler: (request: Request, requestId: string) => Promise<Response>) {
  return async (request: Request) => {
    const requestId = randomUUID();
    try {
      const response = await handler(request, requestId);
      response.headers.set('Cache-Control', 'private, no-store');
      response.headers.set('X-Request-Id', requestId);
      return response;
    } catch (error) {
      const known = error instanceof AppError;
      const status = known ? error.status : 500;
      const prismaCode = error && typeof error === 'object' && 'code' in error ? String((error as { code?: unknown }).code ?? '') : '';
      log({
        event: 'request.failed',
        requestId,
        code: known ? error.code : 'INTERNAL_ERROR',
        status,
        ...(prismaCode && !known ? { prismaCode } : {}),
        ...(!known && process.env.NODE_ENV !== 'production'
          ? { detail: (error instanceof Error ? error.message : String(error)).slice(0, 240) }
          : {}),
      });
      return NextResponse.json({ error: known ? error.message : 'Something went wrong. Please try again.', requestId }, {
        status, headers: { 'Cache-Control': 'private, no-store', 'X-Request-Id': requestId, ...(status === 429 ? { 'Retry-After': '900' } : {}) },
      });
    }
  };
}
