import http from "node:http";
import { parseAuthorizationState } from "./authorization-state.js";
export async function createLoopbackAuthorizationSession(options = {}) {
    const callbackPath = options.callbackPath ?? "/callback";
    const server = options.createServer ? options.createServer() : http.createServer();
    const port = await startServer(server);
    const redirectUri = `http://127.0.0.1:${port}${callbackPath}`;
    return {
        redirectUri,
        async waitForCode(authorizationUrl) {
            return waitForAuthorizationCode(server, authorizationUrl, options, callbackPath);
        },
        close() {
            server.closeAllConnections?.();
            server.close();
        }
    };
}
async function startServer(server) {
    return new Promise((resolve, reject) => {
        const handleError = (error) => {
            server.off("error", handleError);
            reject(error);
        };
        server.once("error", handleError);
        server.listen(0, "127.0.0.1", () => {
            server.off("error", handleError);
            const address = server.address();
            resolve(address.port);
        });
    });
}
function waitForAuthorizationCode(server, authorizationUrl, options, callbackPath) {
    const expectedAuthorization = readExpectedAuthorizationCallback(authorizationUrl);
    return new Promise((resolve, reject) => {
        let settled = false;
        const settle = (fn) => {
            if (!settled) {
                settled = true;
                fn();
            }
        };
        server.on("request", (req, res) => {
            const url = new URL(req.url ?? "/", "http://127.0.0.1");
            if (url.pathname !== callbackPath) {
                res.writeHead(404);
                res.end("Not found");
                return;
            }
            const callbackParameters = {
                code: url.searchParams.get("code"),
                error: url.searchParams.get("error"),
                errorDescription: url.searchParams.get("error_description"),
                state: url.searchParams.get("state"),
                iss: url.searchParams.get("iss")
            };
            try {
                validateAuthorizationCallbackBinding(callbackParameters, expectedAuthorization);
            }
            catch (error) {
                res.writeHead(400);
                res.end(error instanceof Error ? error.message : "Invalid OAuth callback");
                settle(() => reject(error instanceof Error ? error : new Error(String(error))));
                return;
            }
            const authorizationError = callbackParameters.error;
            if (authorizationError !== null) {
                const description = callbackParameters.errorDescription ?? authorizationError;
                res.writeHead(400);
                res.end(`Authorization failed: ${description}`);
                settle(() => reject(createAuthorizationError(authorizationError, description)));
                return;
            }
            try {
                const code = validateAuthorizationCallbackParameters(callbackParameters, expectedAuthorization);
                res.writeHead(200, { "Content-Type": "text/html" });
                res.end(buildSuccessPage(options.landingPage));
                settle(() => resolve(code));
            }
            catch (error) {
                res.writeHead(400);
                res.end(error instanceof Error ? error.message : "Invalid OAuth callback");
                settle(() => reject(error instanceof Error ? error : new Error(String(error))));
            }
        });
        if (options.readLine !== undefined) {
            options
                .readLine()
                .then((input) => {
                const callbackParameters = extractCallbackParametersFromInput(input);
                if (callbackParameters === null) {
                    settle(() => reject(new Error("OAuth callback missing authorization code")));
                    return;
                }
                try {
                    validateAuthorizationCallbackBinding(callbackParameters, expectedAuthorization);
                    if (callbackParameters.error !== null) {
                        const description = callbackParameters.errorDescription ?? callbackParameters.error;
                        throw createAuthorizationError(callbackParameters.error, description);
                    }
                    const code = validateAuthorizationCallbackParameters(callbackParameters, expectedAuthorization);
                    settle(() => resolve(code));
                }
                catch (error) {
                    settle(() => reject(error instanceof Error ? error : new Error(String(error))));
                }
            })
                .catch((error) => {
                settle(() => reject(error instanceof Error ? error : new Error(String(error))));
            });
        }
        if (options.openBrowser !== undefined) {
            options.openBrowser(authorizationUrl).catch((error) => {
                settle(() => reject(error));
            });
        }
    });
}
export function extractCodeFromInput(input) {
    return extractCallbackParametersFromInput(input)?.code ?? null;
}
function extractCallbackParametersFromInput(input) {
    const trimmed = input.replaceAll("\r", "").replaceAll("\n", "").trim();
    if (trimmed.length === 0) {
        return null;
    }
    try {
        const url = new URL(trimmed);
        return {
            code: url.searchParams.get("code"),
            error: url.searchParams.get("error"),
            errorDescription: url.searchParams.get("error_description"),
            state: url.searchParams.get("state"),
            iss: url.searchParams.get("iss")
        };
    }
    catch {
        return {
            code: trimmed,
            error: null,
            errorDescription: null,
            state: null,
            iss: null
        };
    }
}
function readExpectedAuthorizationCallback(authorizationUrl) {
    const url = new URL(authorizationUrl);
    const state = url.searchParams.get("state");
    const parsedState = parseAuthorizationState(state);
    return {
        state,
        issuer: parsedState?.issuer ?? null,
        requireIssuer: parsedState?.requireIssuer ?? false
    };
}
function validateAuthorizationCallbackParameters(callback, expected) {
    validateAuthorizationCallbackBinding(callback, expected);
    if (callback.code === null || callback.code.length === 0) {
        throw new Error("OAuth callback missing authorization code");
    }
    return callback.code;
}
function validateAuthorizationCallbackBinding(callback, expected) {
    if (expected.state !== null) {
        if (callback.state === null || callback.state.length === 0) {
            throw new Error("OAuth callback missing state");
        }
        if (callback.state !== expected.state) {
            throw new Error("OAuth callback state mismatch");
        }
    }
    if (expected.requireIssuer) {
        if (callback.iss === null || callback.iss.length === 0) {
            throw new Error("OAuth callback missing issuer");
        }
    }
    if (callback.iss !== null &&
        callback.iss.length > 0 &&
        expected.issuer !== null &&
        callback.iss !== expected.issuer) {
        throw new Error("OAuth callback issuer mismatch");
    }
}
function createAuthorizationError(error, description) {
    return new Error(`OAuth authorization failed: ${error} — ${description}`);
}
function escapeHtml(text) {
    return text
        .replaceAll("&", "&amp;")
        .replaceAll("<", "&lt;")
        .replaceAll(">", "&gt;")
        .replaceAll('"', "&quot;");
}
export function buildSuccessPage(landingPage) {
    const title = landingPage?.title ?? "Connected";
    const body = landingPage?.body ?? "You can close this tab and return to your terminal.";
    return [
        "<!DOCTYPE html>",
        `<html><head><meta charset=utf-8><title>${escapeHtml(title)}</title></head>`,
        '<body style="font-family:system-ui,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0">',
        '<div style="text-align:center">',
        `<h1>${escapeHtml(title)}</h1>`,
        `<p style="color:#666">${escapeHtml(body)}</p>`,
        "</div></body></html>"
    ].join("");
}
