/**
 * OAuth 2.0 Authorization Code + PKCE flow for Microsoft Identity Platform.
 *
 * Adapted from nsakki55/outlook-mcp@e49d8e68ac8d69db653c25803127dc3b98626cda
 * (MIT). Adaptations:
 *   - Ephemeral loopback port via `server.listen(0)` (upstream uses fixed
 *     49152). The Entra app is registered with `http://localhost` (no port);
 *     Microsoft Identity Platform treats native-app loopback redirects with
 *     port-flexible matching.
 *   - No `xdg-open` browser auto-launch; the auth URL is logged to stderr
 *     and returned to the caller for the operator to open in the VNC browser.
 *   - Tool handler awaits the full PKCE flow synchronously (5-min timeout)
 *     instead of upstream's background-promise + immediate-error pattern.
 */
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";
import { URL } from "node:url";
import { log, challengePrefix } from "../lib/log.js";
const FLOW_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
const SCOPES = [
    "offline_access",
    "User.Read",
    "Mail.Read",
    "Calendars.Read",
    "Contacts.Read",
];
function buildUrls(tenantId) {
    const tenant = tenantId || "common";
    return {
        authorizeUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize`,
        tokenUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
    };
}
/**
 * Start the PKCE flow:
 *   1. Generate code_verifier + code_challenge (S256) + state.
 *   2. Bind a one-shot HTTP server on an ephemeral loopback port.
 *   3. Build the authorize URL with the actual port; return it to the caller.
 *   4. The returned `result` promise resolves when the callback arrives (or
 *      rejects on state mismatch / OAuth error / timeout) and tokens are
 *      exchanged.
 *
 * The caller (the outlook-account-register tool) prints the auth URL,
 * returns it in the tool response, and awaits `result`.
 */
export async function startPkceFlow(config) {
    const codeVerifier = randomBytes(32).toString("base64url");
    const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
    const state = randomBytes(16).toString("hex");
    const bound = await bindCallbackServer(state);
    const redirectUri = `http://localhost:${bound.port}/callback`;
    const { authorizeUrl, tokenUrl } = buildUrls(config.tenantId);
    const authUrl = new URL(authorizeUrl);
    authUrl.searchParams.set("client_id", config.clientId);
    authUrl.searchParams.set("response_type", "code");
    authUrl.searchParams.set("redirect_uri", redirectUri);
    authUrl.searchParams.set("scope", SCOPES.join(" "));
    authUrl.searchParams.set("state", state);
    authUrl.searchParams.set("code_challenge", codeChallenge);
    authUrl.searchParams.set("code_challenge_method", "S256");
    authUrl.searchParams.set("prompt", "select_account");
    log({
        event: "auth-init",
        account: config.accountId,
        codeChallenge: challengePrefix(codeChallenge),
        redirectPath: `/callback (port ${bound.port})`,
    });
    const result = (async () => {
        const startMs = Date.now();
        try {
            const code = await bound.codePromise;
            log({
                event: "auth-callback",
                account: config.accountId,
                elapsedMs: Date.now() - startMs,
            });
            const tokenResponse = await exchangeCode({
                code,
                codeVerifier,
                redirectUri,
                clientId: config.clientId,
                tokenUrl,
            });
            const scopes = (tokenResponse.scope ?? SCOPES.join(" ")).split(/\s+/);
            return { tokenResponse, scopes };
        }
        finally {
            bound.close();
        }
    })();
    return {
        authUrl: authUrl.toString(),
        redirectUri,
        result,
    };
}
function bindCallbackServer(expectedState) {
    return new Promise((resolveBind, rejectBind) => {
        let resolved = false;
        let codeResolve = null;
        let codeReject = null;
        const codePromise = new Promise((resolve, reject) => {
            codeResolve = resolve;
            codeReject = reject;
        });
        let timer = null;
        const server = createServer((req, res) => {
            if (!req.url) {
                res.writeHead(400);
                res.end();
                return;
            }
            const parsed = new URL(req.url, "http://localhost");
            if (parsed.pathname !== "/callback") {
                res.writeHead(404, { "Content-Type": "text/plain" });
                res.end("not found");
                return;
            }
            const code = parsed.searchParams.get("code");
            const returnedState = parsed.searchParams.get("state");
            const error = parsed.searchParams.get("error");
            const errorDescription = parsed.searchParams.get("error_description");
            // Reject Host header rebinding (RFC 6749 native-client guidance).
            const host = req.headers.host ?? "";
            if (!/^(localhost|127\.0\.0\.1)(:\d+)?$/.test(host)) {
                respondHtml(res, 400, "Invalid host", "Loopback callback rejected non-localhost host header.");
                finish();
                codeReject?.(new Error(`Loopback callback received unexpected host header: ${host}`));
                return;
            }
            if (error) {
                respondHtml(res, 400, "Authentication error", `${error}: ${errorDescription ?? ""}`);
                finish();
                codeReject?.(new Error(`OAuth error: ${error} - ${errorDescription ?? ""}`));
                return;
            }
            if (!returnedState || !sameLengthEqual(returnedState, expectedState)) {
                respondHtml(res, 400, "State mismatch", "Authorization state did not match. Possible CSRF; retry register.");
                finish();
                codeReject?.(new Error("State mismatch — possible CSRF"));
                return;
            }
            if (!code) {
                respondHtml(res, 400, "No authorization code", "The authorization callback did not include a code.");
                finish();
                codeReject?.(new Error("No authorization code received"));
                return;
            }
            respondHtml(res, 200, "Outlook authorized", "You may close this window. Return to admin chat to continue.");
            finish();
            codeResolve?.(code);
        });
        server.on("error", (err) => {
            finish();
            // If we never resolved bind, fail bind; otherwise fail the code wait.
            if (!resolvedBind)
                rejectBind(err);
            codeReject?.(err);
        });
        let resolvedBind = false;
        server.listen(0, "127.0.0.1", () => {
            const address = server.address();
            if (!address || typeof address !== "object") {
                rejectBind(new Error("Loopback bind failed: address unavailable"));
                return;
            }
            resolvedBind = true;
            timer = setTimeout(() => {
                finish();
                codeReject?.(new Error("Authentication timeout (5 minutes)"));
            }, FLOW_TIMEOUT_MS);
            resolveBind({
                port: address.port,
                codePromise,
                close: finish,
            });
        });
        function finish() {
            if (resolved)
                return;
            resolved = true;
            if (timer)
                clearTimeout(timer);
            timer = null;
            try {
                server.close();
            }
            catch {
                // best-effort
            }
        }
    });
}
async function exchangeCode(args) {
    const params = new URLSearchParams({
        client_id: args.clientId,
        scope: SCOPES.join(" "),
        code: args.code,
        redirect_uri: args.redirectUri,
        grant_type: "authorization_code",
        code_verifier: args.codeVerifier,
    });
    const response = await fetch(args.tokenUrl, {
        method: "POST",
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
        body: params.toString(),
    });
    if (!response.ok) {
        const text = await response.text();
        throw new Error(`Token exchange failed (${response.status}): ${text}`);
    }
    return (await response.json());
}
export async function refreshAccessToken(args) {
    const { tokenUrl } = buildUrls(args.tenantId);
    const params = new URLSearchParams({
        client_id: args.clientId,
        scope: SCOPES.join(" "),
        refresh_token: args.refreshToken,
        grant_type: "refresh_token",
    });
    const response = await fetch(tokenUrl, {
        method: "POST",
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
        body: params.toString(),
    });
    if (!response.ok) {
        const text = await response.text();
        throw new Error(`Token refresh failed (${response.status}): ${text}`);
    }
    return (await response.json());
}
function respondHtml(res, status, title, message) {
    const color = status === 200 ? "#107c10" : "#d83b01";
    const body = `<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><title>${title}</title>
<style>
body{font-family:'Segoe UI',sans-serif;text-align:center;padding:60px;background:#f3f2f1;}
.card{background:white;border-radius:8px;padding:40px;max-width:480px;margin:0 auto;
  box-shadow:0 2px 8px rgba(0,0,0,0.1);}
h1{color:${color};margin:0 0 16px;}
p{color:#605e5c;margin:0;line-height:1.5;}
</style></head>
<body><div class="card"><h1>${title}</h1><p>${message}</p></div></body></html>`;
    res.writeHead(status, { "Content-Type": "text/html; charset=utf-8" });
    res.end(body);
}
/**
 * Constant-time equality on equal-length strings. Returns false (not throws)
 * for length mismatch so the caller can branch without leaking length via
 * timing. State comparison MUST go through this helper, not `!==`.
 */
function sameLengthEqual(a, b) {
    if (a.length !== b.length)
        return false;
    const aBuf = Buffer.from(a, "utf8");
    const bBuf = Buffer.from(b, "utf8");
    return timingSafeEqual(aBuf, bBuf);
}
export const _internals = { SCOPES, sameLengthEqual };
//# sourceMappingURL=pkce-flow.js.map