import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { getRuntimeHomeDir } from "../config.js";
import { createLogger } from "../utils/logger.js";
const log = createLogger("runtime-binding");
const TOKEN_HASH_ALGORITHM = "sha256";
const pairingCode = generatePairingCode();
function generatePairingCode() {
    const value = crypto.randomInt(0, 1_000_000);
    return String(value)
        .padStart(6, "0")
        .replace(/(\d{3})(\d{3})/, "$1-$2");
}
function bindingDir() {
    return path.join(getRuntimeHomeDir(), ".agentswork", "runtime-bridge");
}
function bindingFile() {
    return path.join(bindingDir(), "binding.json");
}
function tokenStoreFile() {
    return path.join(bindingDir(), "runtime-tokens.properties");
}
function hashToken(token) {
    return crypto
        .createHash(TOKEN_HASH_ALGORITHM)
        .update(token, "utf8")
        .digest("hex");
}
function safeEqual(a, b) {
    const left = Buffer.from(a, "hex");
    const right = Buffer.from(b, "hex");
    if (left.length !== right.length) {
        return false;
    }
    return crypto.timingSafeEqual(left, right);
}
function safeTextEqual(a, b) {
    const left = Buffer.from(a, "utf8");
    const right = Buffer.from(b, "utf8");
    if (left.length !== right.length) {
        return false;
    }
    return crypto.timingSafeEqual(left, right);
}
function normalizeToken(token) {
    return String(token || "").trim();
}
function repairDuplicatePort(raw) {
    return raw.replace(/^((?:https?|wss?):\/\/(?:\[[^\]]+\]|[^/:?#]+):\d+):\d+(?=\/|$)/i, "$1");
}
export function normalizeSchedulerBaseUrl(value) {
    const raw = normalizeToken(value);
    if (!raw) {
        return undefined;
    }
    const repaired = repairDuplicatePort(raw);
    try {
        const url = new URL(repaired);
        if (url.protocol === "ws:") {
            url.protocol = "http:";
        }
        else if (url.protocol === "wss:") {
            url.protocol = "https:";
        }
        else {
            url.protocol = url.protocol.toLowerCase();
        }
        url.hostname = url.hostname.toLowerCase();
        return url.origin;
    }
    catch {
        return repaired.replace(/\/+$/, "");
    }
}
export function getRuntimePairingCode() {
    return pairingCode;
}
export function getRuntimeBindingFilePath() {
    return bindingFile();
}
export function getRuntimeTokenStoreFilePath() {
    return tokenStoreFile();
}
function normalizeServerIp(serverBaseUrl) {
    const raw = normalizeSchedulerBaseUrl(serverBaseUrl) || String(serverBaseUrl || "").trim();
    if (!raw) {
        return "";
    }
    try {
        return new URL(raw).hostname.toLowerCase();
    }
    catch {
        return raw
            .replace(/^wss?:\/\//i, "")
            .replace(/^https?:\/\//i, "")
            .split("/")[0]
            .split(":")[0]
            .toLowerCase();
    }
}
function normalizeRuntimeTokenServerUrl(serverBaseUrl) {
    const normalized = normalizeSchedulerBaseUrl(serverBaseUrl);
    if (normalized) {
        return normalized;
    }
    const raw = String(serverBaseUrl || "").trim();
    if (!raw) {
        return "";
    }
    return raw.replace(/\/+$/, "").toLowerCase();
}
export function buildRuntimeTokenKey(userId, serverBaseUrl) {
    const normalizedUserId = String(userId || "").trim();
    const serverUrl = normalizeRuntimeTokenServerUrl(serverBaseUrl);
    if (!normalizedUserId || !serverUrl) {
        throw new Error("userId and serverBaseUrl are required to build runtime token key");
    }
    return crypto
        .createHash("md5")
        .update(`${normalizedUserId}:${serverUrl}`, "utf8")
        .digest("hex");
}
function loadRuntimeTokenStore() {
    const result = new Map();
    try {
        const content = fs.readFileSync(tokenStoreFile(), "utf8");
        for (const line of content.split(/\r?\n/)) {
            const trimmed = line.trim();
            if (!trimmed || trimmed.startsWith("#")) {
                continue;
            }
            const separatorIndex = trimmed.indexOf("=");
            if (separatorIndex <= 0) {
                continue;
            }
            const key = trimmed.slice(0, separatorIndex).trim();
            const token = trimmed.slice(separatorIndex + 1).trim();
            if (key && token) {
                result.set(key, token);
            }
        }
    }
    catch {
        // Missing token store is valid for an unpaired bridge.
    }
    return result;
}
function saveRuntimeTokenStore(tokens) {
    fs.mkdirSync(bindingDir(), { recursive: true });
    const lines = [...tokens.entries()]
        .sort(([left], [right]) => left.localeCompare(right))
        .map(([key, token]) => `${key}=${token}`);
    fs.writeFileSync(tokenStoreFile(), `${lines.join("\n")}\n`, {
        encoding: "utf8",
        mode: 0o600,
    });
}
export function saveScopedRuntimeAccessToken(input) {
    const accessToken = normalizeToken(input.accessToken);
    if (accessToken.length < 16) {
        throw new Error("accessToken must be at least 16 characters");
    }
    const key = buildRuntimeTokenKey(input.userId, input.serverBaseUrl);
    const tokens = loadRuntimeTokenStore();
    tokens.set(key, accessToken);
    saveRuntimeTokenStore(tokens);
    return key;
}
export function getScopedRuntimeAccessToken(userId, serverBaseUrl) {
    if (!userId || !serverBaseUrl) {
        return undefined;
    }
    try {
        const key = buildRuntimeTokenKey(userId, serverBaseUrl);
        return loadRuntimeTokenStore().get(key);
    }
    catch {
        return undefined;
    }
}
export function clearScopedRuntimeAccessToken(userId, serverBaseUrl) {
    if (!userId || !serverBaseUrl) {
        return false;
    }
    try {
        const key = buildRuntimeTokenKey(userId, serverBaseUrl);
        const tokens = loadRuntimeTokenStore();
        const removed = tokens.delete(key);
        if (removed) {
            saveRuntimeTokenStore(tokens);
        }
        return removed;
    }
    catch {
        return false;
    }
}
function validateRuntimeTokenStore(token) {
    if (!token) {
        return false;
    }
    for (const storedToken of loadRuntimeTokenStore().values()) {
        if (safeTextEqual(token, storedToken)) {
            return true;
        }
    }
    return false;
}
export function loadRuntimeBinding() {
    try {
        const raw = fs.readFileSync(bindingFile(), "utf8");
        const parsed = JSON.parse(raw);
        if (parsed?.status === "paired" && parsed.tokenHash) {
            return {
                ...parsed,
                schedulerBaseUrl: normalizeSchedulerBaseUrl(parsed.schedulerBaseUrl) ||
                    parsed.schedulerBaseUrl,
            };
        }
    }
    catch {
        // Missing or invalid binding file means the bridge is unpaired.
    }
    return { status: "unpaired" };
}
export function getRuntimeBindingPublicState() {
    const state = loadRuntimeBinding();
    const { tokenHash: _tokenHash, accessToken: _accessToken, ...publicState } = state;
    return {
        ...publicState,
        paired: state.status === "paired",
    };
}
export function hasRuntimeBinding() {
    const state = loadRuntimeBinding();
    return state.status === "paired" && Boolean(state.tokenHash);
}
export function validateRuntimeBindingToken(token) {
    const candidate = normalizeToken(token);
    if (!candidate) {
        return false;
    }
    const state = loadRuntimeBinding();
    if (state.status === "paired" && state.tokenHash) {
        if (safeEqual(hashToken(candidate), state.tokenHash)) {
            return true;
        }
    }
    return validateRuntimeTokenStore(candidate);
}
export function getRuntimeAccessToken(userId, serverBaseUrl) {
    const scopedToken = getScopedRuntimeAccessToken(userId, serverBaseUrl);
    if (scopedToken) {
        return scopedToken;
    }
    const state = loadRuntimeBinding();
    if (state.status !== "paired") {
        return undefined;
    }
    if (serverBaseUrl) {
        const requestedSchedulerBaseUrl = normalizeSchedulerBaseUrl(serverBaseUrl);
        const boundSchedulerBaseUrl = normalizeSchedulerBaseUrl(state.schedulerBaseUrl);
        if (requestedSchedulerBaseUrl &&
            boundSchedulerBaseUrl &&
            requestedSchedulerBaseUrl === boundSchedulerBaseUrl) {
            return normalizeToken(state.accessToken) || undefined;
        }
        return undefined;
    }
    return normalizeToken(state.accessToken) || undefined;
}
export function validateRuntimePairingCode(code) {
    return String(code || "").trim() === pairingCode;
}
export function saveRuntimeBinding(input) {
    const accessToken = normalizeToken(input.accessToken);
    if (accessToken.length < 16) {
        throw new Error("accessToken must be at least 16 characters");
    }
    const previous = loadRuntimeBinding();
    const schedulerBaseUrl = normalizeSchedulerBaseUrl(input.schedulerBaseUrl) ||
        normalizeSchedulerBaseUrl(previous.schedulerBaseUrl);
    const now = new Date().toISOString();
    const next = {
        status: "paired",
        instanceId: input.instanceId
            ? String(input.instanceId)
            : previous.instanceId,
        userId: input.userId ? String(input.userId) : previous.userId,
        schedulerBaseUrl,
        accessToken,
        tokenHash: hashToken(accessToken),
        createdAt: previous.createdAt || now,
        updatedAt: now,
    };
    fs.mkdirSync(bindingDir(), { recursive: true });
    fs.writeFileSync(bindingFile(), `${JSON.stringify(next, null, 2)}\n`, {
        encoding: "utf8",
        mode: 0o600,
    });
    if (next.userId && next.schedulerBaseUrl) {
        const key = saveScopedRuntimeAccessToken({
            userId: next.userId,
            serverBaseUrl: next.schedulerBaseUrl,
            accessToken,
        });
        log.info(`[runtime-bridge] scoped runtime token saved: key=${key}`);
    }
    log.info(`[runtime-bridge] runtime binding saved: ${bindingFile()}`);
    return next;
}
export function clearRuntimeBinding() {
    const current = loadRuntimeBinding();
    const revokedState = {
        ...current,
        status: "unpaired",
        accessToken: undefined,
        tokenHash: undefined,
        revokedAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
    };
    fs.mkdirSync(bindingDir(), { recursive: true });
    fs.writeFileSync(bindingFile(), `${JSON.stringify(revokedState, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
    log.info("[runtime-bridge] runtime binding cleared");
}
