/**
 * API route wrappers.
 *
 * Replaces the 8-line auth + ownership boilerplate that every route file
 * was reimplementing (and getting subtly wrong in 18 of them — see the
 * 2026-05-17 QA pass).
 *
 * Usage:
 *
 *   // Plain session-required handler.
 *   export const GET = withAuth(async (req, ctx, { user }) => {
 *     return NextResponse.json({ email: user.email });
 *   });
 *
 *   // Session + tenant-project ownership in one wrapper.
 *   export const POST = withTenantProject(async (req, ctx, { user, project }) => {
 *     // `project` is guaranteed to belong to `user`.
 *   }, { paramName: 'projectId' });
 *
 *   // Workspace-scoped (session OR vibn_sk_ api key OK).
 *   export const POST = withWorkspace(async (req, ctx, { principal }) => {
 *     // `principal.workspace` is guaranteed to be tenant-checked.
 *   });
 *
 *   // Admin secret (ops endpoint).
 *   export const POST = withAdminSecret(async (req, ctx) => { … }, {
 *     secretEnvVar: 'ADMIN_MIGRATE_SECRET',
 *   });
 */

import { NextResponse } from "next/server";
import type { Session } from "next-auth";
import { authSession } from "@/lib/auth/session-server";
import { queryOne } from "@/lib/db-postgres";
import { requireWorkspacePrincipal } from "@/lib/auth/workspace-auth";
import { timingSafeStringEq } from "@/lib/server/timing-safe";
import { rateLimit, type RateLimitOpts } from "@/lib/server/rate-limit";

type WithAuthCtx = { user: NonNullable<Session["user"]> };
type WithTenantProjectCtx = WithAuthCtx & { project: ProjectRow };
type WithAdminCtx = { secret: string };

export interface ProjectRow {
  id: string;
  data: Record<string, unknown>;
  slug?: string;
}

type RouteHandler<TCtx = unknown, TParams = unknown> = (
  req: Request,
  ctx: { params: Promise<TParams> },
  extra: TCtx,
) => Promise<Response> | Response;

// ─── withAuth ─────────────────────────────────────────────────────────────

export function withAuth<TParams = unknown>(
  handler: RouteHandler<WithAuthCtx, TParams>,
) {
  return async (req: Request, ctx: { params: Promise<TParams> }) => {
    const session = await authSession();
    if (!session?.user?.email) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }
    return handler(req, ctx, { user: session.user });
  };
}

// ─── withTenantProject ────────────────────────────────────────────────────

export interface WithTenantProjectOpts {
  /**
   * Where to find the project id. Default `'projectId'` in `params`.
   *   - 'params:projectId' → ctx.params.projectId  (default for `[projectId]` routes)
   *   - 'search:projectId' → searchParams.projectId
   *   - 'body:projectId'   → body.projectId (consumes body via clone+json)
   */
  source?: "params" | "search" | "body";
  paramName?: string;
}

export function withTenantProject<TParams = Record<string, string>>(
  handler: RouteHandler<WithTenantProjectCtx, TParams>,
  opts: WithTenantProjectOpts = {},
) {
  const source = opts.source ?? "params";
  const name = opts.paramName ?? "projectId";

  return async (req: Request, ctx: { params: Promise<TParams> }) => {
    const session = await authSession();
    if (!session?.user?.email) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    let projectId: string | undefined;
    if (source === "params") {
      const p = (await ctx.params) as Record<string, string>;
      projectId = p?.[name];
    } else if (source === "search") {
      projectId = new URL(req.url).searchParams.get(name) ?? undefined;
    } else if (source === "body") {
      try {
        const body = await req.clone().json();
        projectId = body?.[name];
      } catch {
        // fallthrough; caller will get 400 below
      }
    }

    if (!projectId) {
      return NextResponse.json(
        { error: `${name} is required` },
        { status: 400 },
      );
    }

    // Ownership check: project must belong to the authenticated user.
    const row = await queryOne<ProjectRow>(
      `SELECT p.id::text AS id, p.data
       FROM fs_projects p
       JOIN fs_users u ON u.id = p.user_id
       WHERE p.id = $1::text AND u.data->>'email' = $2::text
       LIMIT 1`,
      [projectId, session.user.email],
    );

    if (!row) {
      return NextResponse.json({ error: "Not found" }, { status: 404 });
    }

    return handler(req, ctx, { user: session.user, project: row });
  };
}

// ─── withWorkspace (re-export of existing helper with consistent shape) ──

type WorkspacePrincipal = Exclude<
  Awaited<ReturnType<typeof requireWorkspacePrincipal>>,
  NextResponse
>;

export function withWorkspace<TParams = Record<string, string>>(
  handler: RouteHandler<{ principal: WorkspacePrincipal }, TParams>,
  opts: { paramName?: string } = {},
) {
  const name = opts.paramName ?? "slug";
  return async (req: Request, ctx: { params: Promise<TParams> }) => {
    const params = (await ctx.params) as Record<string, string> | undefined;
    const targetSlug = params?.[name];
    const principal = await requireWorkspacePrincipal(req, { targetSlug });
    if (principal instanceof NextResponse) return principal;
    return handler(req, ctx, { principal });
  };
}

// ─── withAdminSecret ──────────────────────────────────────────────────────

export interface WithAdminSecretOpts {
  /** env var that holds the expected secret. */
  secretEnvVar: string;
  /** Header to read. Default `authorization` (expects `Bearer <secret>`). */
  header?: string;
  /** Alternate header that may also carry the secret (e.g. `x-admin-secret`). */
  altHeader?: string;
}

export function withAdminSecret<TParams = unknown>(
  handler: RouteHandler<WithAdminCtx, TParams>,
  opts: WithAdminSecretOpts,
) {
  return async (req: Request, ctx: { params: Promise<TParams> }) => {
    const expected = process.env[opts.secretEnvVar]?.trim() ?? "";
    if (!expected) {
      return NextResponse.json(
        { error: `${opts.secretEnvVar} not configured — endpoint disabled` },
        { status: 403 },
      );
    }
    const header = (opts.header ?? "authorization").toLowerCase();
    const raw = req.headers.get(header) ?? "";
    const bearer = raw.toLowerCase().startsWith("bearer ")
      ? raw.slice(7).trim()
      : "";
    const alt = opts.altHeader
      ? (req.headers.get(opts.altHeader) ?? "").trim()
      : "";
    const incoming = bearer || alt;
    if (!incoming || !timingSafeStringEq(expected, incoming)) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }
    return handler(req, ctx, { secret: expected });
  };
}

// ─── withRateLimit ────────────────────────────────────────────────────────

export interface WithRateLimitOpts extends Omit<RateLimitOpts, "key"> {
  /**
   * How to derive the per-call key. Receives the bound auth context if any.
   * Default: client IP.
   */
  keyFn?: (req: Request, extra: unknown) => string | Promise<string>;
}

/** Wrap any other wrapper's handler. Composes neatly with withAuth/withWorkspace. */
export function withRateLimit<THandler extends (...args: any[]) => any>(
  handler: THandler,
  opts: WithRateLimitOpts,
): THandler {
  return (async (req: Request, ...rest: unknown[]) => {
    const extra = rest[1] ?? {};
    const key =
      (opts.keyFn ? await opts.keyFn(req, extra) : null) ??
      `ip:${req.headers.get("x-forwarded-for") ?? "unknown"}`;
    const rl = await rateLimit({
      key,
      limit: opts.limit,
      windowMs: opts.windowMs,
    });
    if (!rl.ok) {
      return NextResponse.json(
        { error: "Rate limit exceeded", retryAfterMs: rl.retryAfterMs },
        {
          status: 429,
          headers: rl.retryAfterMs
            ? { "Retry-After": String(Math.ceil(rl.retryAfterMs / 1000)) }
            : undefined,
        },
      );
    }
    return handler(req, ...rest);
  }) as THandler;
}
