/**
 * Shared authentication helpers for native extensions and CLI commands.
 *
 * Provides:
 *   - `runOAuthFlow()`      — opens browser for OAuth, returns JWT token
 *   - `ensureAuthenticated()` — checks token validity, triggers OAuth re-login on 401/403
 *   - `isAuthError()`       — checks if an error indicates auth failure
 *
 * Extensions (aexol-mcp, kanban-bridge) call ensureAuthenticated() when they
 * detect an auth failure so the user can re-authenticate without dropping
 * out to the terminal and running `spectral login` manually.
 */
import { createServer } from "node:http";
import { exec } from "node:child_process";
import pc from "picocolors";
import { DEFAULT_API_URL, readConfig, writeConfig, } from "./config.js";
import { AexolMcpClient, AexolMcpError } from "./mcp-client.js";
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const DEFAULT_BACKEND_URL = "https://aexol.ai";
const OAUTH_TIMEOUT_MS = 120_000; // 2 minutes
// ---------------------------------------------------------------------------
// OAuth flow
// ---------------------------------------------------------------------------
/** Derive the landing base URL from the backend URL. */
function deriveLandingUrl(backendUrl) {
    const env = process.env.SPECTRAL_LANDING_URL;
    if (env)
        return env;
    if (backendUrl.includes("localhost") || backendUrl.includes("127.0.0.1")) {
        return "http://localhost:3000";
    }
    return DEFAULT_BACKEND_URL;
}
/**
 * Start a temporary HTTP server to receive the OAuth callback.
 * Returns the server and a promise that resolves with the JWT token.
 */
function listenForCallback(timeoutMs) {
    const server = createServer();
    const tokenPromise = new Promise((resolve, reject) => {
        const timeout = setTimeout(() => {
            server.close();
            reject(new Error(`Timed out after ${timeoutMs / 1000}s waiting for browser authorization.`));
        }, timeoutMs);
        server.on("request", (req, res) => {
            const url = new URL(req.url ?? "/", `http://localhost`);
            const token = url.searchParams.get("token");
            if (token) {
                clearTimeout(timeout);
                res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
                res.end("<html><body><h1>✓ Authorized</h1><p>You can close this tab and return to your terminal.</p></body></html>");
                resolve(token);
            }
            else {
                res.writeHead(400, { "Content-Type": "text/plain" });
                res.end("Missing token parameter.");
            }
        });
        server.on("error", (err) => {
            clearTimeout(timeout);
            server.close();
            reject(new Error(`Failed to start local server: ${err.message}`));
        });
    });
    return { server, tokenPromise };
}
/**
 * Run the OAuth browser flow to obtain a fresh JWT token.
 *
 * Opens the user's browser to the Aexol Studio CLI auth page. The landing
 * app redirects back to localhost with a JWT in the query string.
 *
 * Returns `{ ok: true, token }` on success, `{ ok: false, reason }` on failure.
 */
export async function runOAuthFlow(apiUrl, timeoutMs = OAUTH_TIMEOUT_MS) {
    // Load existing config to preserve teamApiKey
    const existingCfg = await readConfig();
    const resolvedApiUrl = apiUrl ?? existingCfg?.apiUrl ?? process.env.SPECTRAL_MCP_URL ?? DEFAULT_API_URL;
    const landingUrl = deriveLandingUrl(resolvedApiUrl);
    let server;
    try {
        const { server: srv, tokenPromise } = listenForCallback(timeoutMs);
        server = srv;
        // Start listening on a random port
        server.listen(0, "127.0.0.1");
        await new Promise((resolve, reject) => {
            server.once("listening", resolve);
            server.once("error", reject);
        });
        const addr = server.address();
        const port = addr.port;
        // Open browser to the CLI auth page
        const authUrl = `${landingUrl}/cli-auth?port=${port}`;
        const openCmd = process.platform === "darwin"
            ? `open "${authUrl}"`
            : process.platform === "win32"
                ? `start "" "${authUrl}"`
                : `xdg-open "${authUrl}"`;
        exec(openCmd, (err) => {
            if (err) {
                process.stderr.write(pc.yellow(`⚠ Could not open browser automatically. Please visit:\n  ${authUrl}\n`));
            }
        });
        const token = await tokenPromise;
        // Save the token, preserving existing config (especially teamApiKey).
        const cfg = existingCfg ?? {
            apiUrl: resolvedApiUrl,
            teamApiKey: "",
        };
        cfg.userJwt = token;
        await writeConfig(cfg);
        return { ok: true, token };
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        return { ok: false, reason: msg };
    }
    finally {
        server?.close();
    }
}
// ---------------------------------------------------------------------------
// Auth helpers
// ---------------------------------------------------------------------------
/** Check if an error indicates an authentication failure (401 or 403). */
export function isAuthError(err) {
    if (err instanceof AexolMcpError) {
        return err.status === 401 || err.status === 403;
    }
    // Generic HTTP errors from fetch
    if (err instanceof Error && "status" in err) {
        const status = err.status;
        return status === 401 || status === 403;
    }
    return false;
}
/**
 * Ensure the user is authenticated.
 *
 * Call this when a tool or extension encounters an auth error (401/403).
 * It verifies the current token is valid by pinging the backend. If the
 * token is expired or missing, it triggers the OAuth browser flow.
 *
 * Only attempts OAuth re-login when stdout is a TTY (interactive session).
 * In headless/CI environments, it returns `{ ok: false }` immediately.
 *
 * Returns `{ ok: true, config }` with fresh config on success,
 * or `{ ok: false, reason }` when auth could not be established.
 */
export async function ensureAuthenticated() {
    // Check if we're in an interactive session where browser OAuth is possible
    if (!process.stdout.isTTY) {
        return { ok: false, config: null, reason: "Not an interactive terminal — cannot open browser for OAuth." };
    }
    const existingCfg = await readConfig();
    const apiUrl = existingCfg?.apiUrl ?? process.env.SPECTRAL_MCP_URL ?? DEFAULT_API_URL;
    const token = existingCfg?.teamApiKey || existingCfg?.userJwt || "";
    // If we have a token, verify it still works
    if (token) {
        try {
            const client = new AexolMcpClient(apiUrl, token);
            await client.listTools();
            // Token is still valid
            return { ok: true, config: existingCfg };
        }
        catch (err) {
            if (!isAuthError(err)) {
                // Non-auth error (network, server down) — don't re-auth, just report
                const msg = err instanceof Error ? err.message : String(err);
                return { ok: false, config: existingCfg, reason: `Backend unreachable: ${msg}` };
            }
            // Token is invalid/expired — fall through to OAuth
        }
    }
    // No valid token — run OAuth flow
    process.stderr.write(pc.yellow("\n🔐 Authentication required. Opening browser for Aexol Studio login...\n"));
    const result = await runOAuthFlow(apiUrl);
    if (!result.ok) {
        process.stderr.write(pc.red(`✗ Authentication failed: ${result.reason}\n`));
        return { ok: false, config: existingCfg, reason: result.reason };
    }
    // Re-read config after OAuth saved it
    const freshCfg = await readConfig();
    if (!freshCfg || !freshCfg.userJwt) {
        return { ok: false, config: existingCfg, reason: "OAuth completed but config was not saved correctly." };
    }
    process.stderr.write(pc.green("✓ Authenticated.\n"));
    return { ok: true, config: freshCfg };
}
