/**
 * Workspace-scoped auth.
 *
 * Two principal types are accepted on `/api/...` routes:
 *   1. NextAuth session (browser users) — `authSession()`
 *   2. Per-workspace bearer API key (`Authorization: Bearer vibn_sk_...`)
 *
 * Either way we resolve a `WorkspacePrincipal` that is scoped to one
 * workspace. Routes that touch Coolify/Gitea must call
 * `requireWorkspacePrincipal()` and use `principal.workspace` to fetch
 * the right Coolify Project / Gitea org.
 */

import { createHash, randomBytes } from "crypto";
import { NextResponse } from "next/server";
import { authSession } from "@/lib/auth/session-server";
import { encryptSecret, decryptSecret } from "@/lib/auth/secret-box";
import { query, queryOne } from "@/lib/db-postgres";
import {
  type VibnWorkspace,
  getWorkspaceById,
  getWorkspaceBySlug,
  getWorkspaceByOwner,
  userHasWorkspaceAccess,
} from "@/lib/workspaces";

const KEY_PREFIX = "vibn_sk_";
const KEY_RANDOM_BYTES = 32; // 256-bit secret

export interface WorkspacePrincipal {
  /** "session" = browser user; "api_key" = automated/AI client */
  source: "session" | "api_key";
  workspace: VibnWorkspace;
  /** fs_users.id of the human ultimately responsible for this request */
  userId: string;
  /** When source = "api_key", which key id was used */
  apiKeyId?: string;
}

/**
 * Resolve the workspace principal from either a NextAuth session or a
 * `Bearer vibn_sk_...` token. Optional `targetSlug` enforces that the
 * principal is for the requested workspace.
 *
 * Returns:
 *   - principal on success
 *   - NextResponse on failure (401 / 403) — return it directly from the route
 */
export async function requireWorkspacePrincipal(
  request: Request,
  opts: { targetSlug?: string; targetId?: string } = {},
): Promise<WorkspacePrincipal | NextResponse> {
  const isDevBypass =
    process.env.NEXT_PUBLIC_DEV_BYPASS_PROJECT_AUTH === "true";

  const apiKey = extractApiKey(request);
  if (apiKey) {
    const principal = await resolveApiKey(apiKey);
    if (!principal) {
      if (isDevBypass) {
        const userRow = await queryOne<{ id: string }>(
          `SELECT id FROM fs_users WHERE data->>'email' = $1 LIMIT 1`,
          [process.env.NEXT_PUBLIC_DEV_LOCAL_AUTH_EMAIL]
        );
        if (userRow) {
          const workspace = await getWorkspaceByOwner(userRow.id);
          if (workspace) {
            return { source: 'api_key', workspace, userId: userRow.id, apiKeyId: 'dev_bypass' };
          }
        }
      }
      return NextResponse.json({ error: 'Invalid or revoked API key' }, { status: 401 });
    }
    if (!matchesTarget(principal.workspace, opts)) {
      return NextResponse.json({ error: 'API key not authorized for this workspace' }, { status: 403 });
    }
    return principal;
  }

  // Fall through to NextAuth session
  const session = await authSession();
  const devEmail = process.env.NEXT_PUBLIC_DEV_LOCAL_AUTH_EMAIL;
  const email = session?.user?.email || (isDevBypass ? devEmail : null);

  if (!email) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }
  const userRow = await queryOne<{ id: string }>(
    `SELECT id FROM fs_users WHERE data->>'email' = $1 LIMIT 1`,
    [email],
  );
  if (!userRow) {
    return NextResponse.json(
      { error: "No fs_users row for session" },
      { status: 401 },
    );
  }

  let workspace: VibnWorkspace | null = null;
  if (opts.targetSlug) workspace = await getWorkspaceBySlug(opts.targetSlug);
  else if (opts.targetId) workspace = await getWorkspaceById(opts.targetId);
  else workspace = await getWorkspaceByOwner(userRow.id);

  if (!workspace) {
    return NextResponse.json({ error: "Workspace not found" }, { status: 404 });
  }

  const ok = await userHasWorkspaceAccess(userRow.id, workspace.id);
  if (!ok) {
    return NextResponse.json({ error: "Forbidden" }, { status: 403 });
  }

  return { source: "session", workspace, userId: userRow.id };
}

function matchesTarget(
  workspace: VibnWorkspace,
  opts: { targetSlug?: string; targetId?: string },
): boolean {
  if (opts.targetSlug && workspace.slug !== opts.targetSlug) return false;
  if (opts.targetId && workspace.id !== opts.targetId) return false;
  return true;
}

function extractApiKey(request: Request): string | null {
  const auth = request.headers.get("authorization");
  if (!auth) return null;
  const m = /^Bearer\s+(.+)$/i.exec(auth.trim());
  if (!m) return null;
  const token = m[1].trim();
  if (!token.startsWith(KEY_PREFIX)) return null;
  return token;
}

async function resolveApiKey(
  token: string,
): Promise<WorkspacePrincipal | null> {
  const hash = hashKey(token);
  const row = await queryOne<{
    id: string;
    workspace_id: string;
    created_by: string;
    revoked_at: Date | null;
  }>(
    `SELECT id, workspace_id, created_by, revoked_at
       FROM vibn_workspace_api_keys
      WHERE key_hash = $1
      LIMIT 1`,
    [hash],
  );
  if (!row || row.revoked_at) return null;

  const workspace = await getWorkspaceById(row.workspace_id);
  if (!workspace) return null;

  // Touch last_used_at without blocking
  void query(
    `UPDATE vibn_workspace_api_keys SET last_used_at = now() WHERE id = $1`,
    [row.id],
  ).catch(() => undefined);

  return {
    source: "api_key",
    workspace,
    userId: row.created_by,
    apiKeyId: row.id,
  };
}

// ──────────────────────────────────────────────────
// Key minting + hashing
// ──────────────────────────────────────────────────

export interface MintedApiKey {
  id: string;
  /** Full plaintext key — shown once at creation, never stored. */
  token: string;
  prefix: string;
  workspace_id: string;
  name: string;
  created_at: Date;
}

export async function mintWorkspaceApiKey(opts: {
  workspaceId: string;
  name: string;
  createdBy: string;
  scopes?: string[];
}): Promise<MintedApiKey> {
  const random = randomBytes(KEY_RANDOM_BYTES).toString("base64url");
  const token = `${KEY_PREFIX}${random}`;
  const hash = hashKey(token);
  const prefix = token.slice(0, 12); // e.g. "vibn_sk_AbCd"
  // AES-256-GCM encrypt the plaintext so session-authenticated users can
  // reveal the key later (see revealWorkspaceApiKey). Encryption uses
  // VIBN_SECRETS_KEY — same envelope as Gitea bot PATs and GCS HMAC
  // secrets. If that env var isn't set we'd rather fail loudly here
  // than silently mint unrevealable keys.
  const encrypted = encryptSecret(token);

  const inserted = await query<{ id: string; created_at: Date }>(
    `INSERT INTO vibn_workspace_api_keys
       (workspace_id, name, key_prefix, key_hash, key_encrypted, scopes, created_by)
     VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7)
     RETURNING id, created_at`,
    [
      opts.workspaceId,
      opts.name,
      prefix,
      hash,
      encrypted,
      JSON.stringify(opts.scopes ?? ["workspace:*"]),
      opts.createdBy,
    ],
  );

  return {
    id: inserted[0].id,
    token,
    prefix,
    workspace_id: opts.workspaceId,
    name: opts.name,
    created_at: inserted[0].created_at,
  };
}

/**
 * Return the plaintext for an active key belonging to the workspace, if
 * we have it stored encrypted. Returns `null` when:
 *   - the key doesn't exist or is in another workspace
 *   - the key is revoked
 *   - the key predates the revealability migration (key_encrypted is NULL)
 *   - decryption fails (VIBN_SECRETS_KEY rotated without re-provisioning)
 *
 * Intentionally agnostic to auth — the caller MUST have already checked
 * that the principal is a session user for this workspace. Never call
 * this behind an API-key principal, or a compromised key could exfiltrate
 * its siblings.
 */
export async function revealWorkspaceApiKey(
  workspaceId: string,
  keyId: string,
): Promise<{ id: string; name: string; prefix: string; token: string } | null> {
  const row = await queryOne<{
    id: string;
    name: string;
    key_prefix: string;
    key_encrypted: string | null;
    revoked_at: Date | null;
  }>(
    `SELECT id, name, key_prefix, key_encrypted, revoked_at
       FROM vibn_workspace_api_keys
      WHERE id = $1 AND workspace_id = $2
      LIMIT 1`,
    [keyId, workspaceId],
  );
  if (!row || row.revoked_at || !row.key_encrypted) return null;
  try {
    const token = decryptSecret(row.key_encrypted);
    return { id: row.id, name: row.name, prefix: row.key_prefix, token };
  } catch (err) {
    console.error("[reveal] decrypt failed for key", keyId, err);
    return null;
  }
}

export async function listWorkspaceApiKeys(workspaceId: string): Promise<
  Array<{
    id: string;
    name: string;
    prefix: string;
    scopes: string[];
    created_by: string;
    last_used_at: Date | null;
    revoked_at: Date | null;
    created_at: Date;
    revealable: boolean;
  }>
> {
  const rows = await query<{
    id: string;
    name: string;
    key_prefix: string;
    scopes: string[];
    created_by: string;
    last_used_at: Date | null;
    revoked_at: Date | null;
    created_at: Date;
    revealable: boolean;
  }>(
    `SELECT id, name, key_prefix, scopes, created_by, last_used_at, revoked_at, created_at,
            (key_encrypted IS NOT NULL) AS revealable
       FROM vibn_workspace_api_keys
      WHERE workspace_id = $1
      ORDER BY created_at DESC`,
    [workspaceId],
  );
  return rows.map((r) => ({
    id: r.id,
    name: r.name,
    prefix: r.key_prefix,
    scopes: r.scopes,
    created_by: r.created_by,
    last_used_at: r.last_used_at,
    revoked_at: r.revoked_at,
    created_at: r.created_at,
    revealable: r.revealable,
  }));
}

export async function revokeWorkspaceApiKey(
  workspaceId: string,
  keyId: string,
): Promise<boolean> {
  const updated = await query<{ id: string }>(
    `UPDATE vibn_workspace_api_keys
        SET revoked_at = now()
      WHERE id = $1 AND workspace_id = $2 AND revoked_at IS NULL
      RETURNING id`,
    [keyId, workspaceId],
  );
  return updated.length > 0;
}

function hashKey(token: string): string {
  return createHash("sha256").update(token).digest("hex");
}
