import crypto from "node:crypto";
import { Router } from "express";
import type { Db } from "@paperclipai/db";
import { googleOAuthService } from "../services/google-oauth.js";
import { assertBoard, assertCompanyAccess } from "./authz.js";
import { secretService } from "../services/secrets.js";

const TEN_MINUTES_MS = 10 * 60 * 1000;

// In-memory maps for the OAuth PKCE flow
// pendingPkce: keyed by stateId — stores PKCE verifier (NO companyId at authorize time)
const pendingPkce = new Map<string, { verifier: string; createdAt: number }>();
// pendingTokens: keyed by stateId — stores exchanged tokens after callback completes
const pendingTokens = new Map<
  string,
  { accessToken: string; refreshToken?: string; createdAt: number }
>();

function cleanupExpired() {
  const now = Date.now();
  for (const [key, value] of pendingPkce) {
    if (now - value.createdAt > TEN_MINUTES_MS) {
      pendingPkce.delete(key);
    }
  }
  for (const [key, value] of pendingTokens) {
    if (now - value.createdAt > TEN_MINUTES_MS) {
      pendingTokens.delete(key);
    }
  }
}

function buildRedirectUri(req: { protocol: string; get(name: string): string | undefined }): string {
  return `${req.protocol}://${req.get("host")}/api/oauth/google/callback`;
}

export function googleOAuthRoutes(db: Db): Router {
  const router = Router();
  const oauthSvc = googleOAuthService(db);

  // POST /oauth/google/authorize
  // Board auth required. Generates PKCE auth URL.
  // NO companyId accepted — company does not exist yet during onboarding step 3.
  router.post("/oauth/google/authorize", (req, res) => {
    cleanupExpired();
    assertBoard(req);

    const state = crypto.randomUUID();
    const redirectUri = buildRedirectUri(req);
    const { url, verifier } = oauthSvc.generateAuthUrl(redirectUri, state);

    pendingPkce.set(state, { verifier, createdAt: Date.now() });

    res.json({ url, stateId: state });
  });

  // GET /oauth/google/callback
  // No auth required — Google redirects here after OAuth consent.
  // Exchanges code for tokens and parks them in pendingTokens keyed by stateId.
  router.get("/oauth/google/callback", async (req, res) => {
    cleanupExpired();

    const code = req.query["code"] as string | undefined;
    const state = req.query["state"] as string | undefined;

    if (!code || !state) {
      res.status(400).send("Missing code or state");
      return;
    }

    const pkceEntry = pendingPkce.get(state);
    if (!pkceEntry) {
      res.status(400).send("Invalid or expired OAuth state");
      return;
    }

    pendingPkce.delete(state);

    try {
      const redirectUri = buildRedirectUri(req);
      const { accessToken, refreshToken } = await oauthSvc.exchangeCode(
        code,
        redirectUri,
        pkceEntry.verifier,
      );

      pendingTokens.set(state, { accessToken, refreshToken, createdAt: Date.now() });

      res.redirect(`/?google_oauth=success&state=${encodeURIComponent(state)}`);
    } catch (_err) {
      res.redirect("/?google_oauth=error");
    }
  });

  // POST /oauth/google/claim
  // Board auth required. Links pending tokens to a real companyId.
  // stateId links back to the tokens parked by the callback.
  router.post("/oauth/google/claim", async (req, res) => {
    cleanupExpired();
    assertBoard(req);

    const { stateId, companyId } = req.body as { stateId?: string; companyId?: string };

    if (!stateId || typeof stateId !== "string" || stateId.trim() === "") {
      res.status(400).json({ error: "stateId is required" });
      return;
    }
    if (!companyId || typeof companyId !== "string" || companyId.trim() === "") {
      res.status(400).json({ error: "companyId is required" });
      return;
    }

    assertCompanyAccess(req, companyId);

    const tokenEntry = pendingTokens.get(stateId);
    if (!tokenEntry) {
      res.status(404).json({ error: "OAuth session expired or not found" });
      return;
    }

    await oauthSvc.storeTokens(companyId, {
      accessToken: tokenEntry.accessToken,
      refreshToken: tokenEntry.refreshToken,
    });

    pendingTokens.delete(stateId);

    res.json({ ok: true });
  });

  // POST /api-keys/store
  // Board auth required. Stores arbitrary provider API keys.
  router.post("/api-keys/store", async (req, res) => {
    cleanupExpired();
    assertBoard(req);

    const { companyId, provider, apiKey } = req.body as {
      companyId?: string;
      provider?: string;
      apiKey?: string;
    };

    if (!companyId || typeof companyId !== "string" || companyId.trim() === "") {
      res.status(400).json({ error: "companyId is required" });
      return;
    }
    if (!provider || typeof provider !== "string" || provider.trim() === "") {
      res.status(400).json({ error: "provider is required" });
      return;
    }
    if (!apiKey || typeof apiKey !== "string" || apiKey.trim() === "") {
      res.status(400).json({ error: "apiKey is required" });
      return;
    }

    assertCompanyAccess(req, companyId);

    const svc = secretService(db);
    const secretName = `${provider}_api_key`;

    const existing = await svc.getByName(companyId, secretName);
    if (existing) {
      await svc.rotate(existing.id, { value: apiKey });
    } else {
      await svc.create(companyId, {
        name: secretName,
        provider: "local_encrypted",
        value: apiKey,
        description: `API key for ${provider}`,
      });
    }

    res.json({ ok: true });
  });

  return router;
}
