"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.X402Client = void 0;
const fs = __importStar(require("node:fs"));
const path = __importStar(require("node:path"));
const types_1 = require("./types");
const escrow_1 = require("./escrow");
/**
 * HTTP client that resolves kaspa-tab 402 responses automatically,
 * within the local spending policy.
 *
 * Tabs are persisted per origin so credit survives restarts and is reused
 * across requests — the deposit is on-chain once, then requests are free.
 */
class X402Client {
    wallet;
    policy;
    tabsPath;
    escrowsPath;
    tabs;
    escrows;
    /** Exhausted escrows kept for later refund of their unspent balance. */
    retired;
    constructor(wallet, policy, dataDir) {
        this.wallet = wallet;
        this.policy = policy;
        fs.mkdirSync(dataDir, { recursive: true, mode: 0o700 });
        this.tabsPath = path.join(dataDir, "client-tabs.json");
        this.escrowsPath = path.join(dataDir, "client-escrows.json");
        const escrowData = fs.existsSync(this.escrowsPath) ? JSON.parse(fs.readFileSync(this.escrowsPath, "utf8")) : {};
        this.tabs = fs.existsSync(this.tabsPath) ? JSON.parse(fs.readFileSync(this.tabsPath, "utf8")) : {};
        this.escrows = escrowData.active ?? escrowData; // tolerate the older flat shape
        this.retired = escrowData.retired ?? [];
    }
    /** Active and retired escrow channels, for refund tooling. */
    escrowChannels() {
        return { active: Object.values(this.escrows), retired: [...this.retired] };
    }
    async paidFetch(url, init) {
        const origin = new URL(url).origin;
        // If we already have an escrow channel for this origin, pay from it directly.
        if (this.escrows[origin]) {
            return this.escrowRequest(origin, url, init);
        }
        const known = this.tabs[origin];
        let response = await this.fetchWithTab(url, init, known?.tabId);
        if (response.status !== 402) {
            return await this.toResult(response, known?.tabId);
        }
        // 402: choose the scheme. Prefer escrow (trust-minimized) when offered.
        const body = (await response.json());
        const escrowOffer = body.accepts?.find((o) => o.scheme === "kaspa-escrow");
        if (escrowOffer) {
            await this.openEscrow(origin, escrowOffer);
            return this.escrowRequest(origin, url, init);
        }
        const offer = body.accepts?.find((o) => o.scheme === "kaspa-tab");
        if (!offer)
            throw new Error("402 response carries no supported payment offer");
        if (offer.network !== this.wallet.networkId) {
            throw new Error(`server wants payment on ${offer.network} but wallet is on ${this.wallet.networkId}`);
        }
        const amount = BigInt(offer.minDepositSompi);
        this.policy.authorize(offer.payTo, amount);
        const { txid } = await this.wallet.send(offer.payTo, amount);
        this.policy.record(amount);
        this.tabs[origin] = { tabId: offer.tabId, payTo: offer.payTo, network: offer.network, depositedSompi: amount.toString() };
        this.persist();
        for (let attempt = 0; attempt < 10; attempt++) {
            await sleep(1_000);
            response = await this.fetchWithTab(url, init, offer.tabId, txid);
            if (response.status !== 402) {
                const result = await this.toResult(response, offer.tabId);
                result.deposit = { txid, amountSompi: amount.toString(), payTo: offer.payTo };
                return result;
            }
        }
        throw new Error(`deposit ${txid} sent but server still returns 402 after retries; tab ${offer.tabId}, address ${offer.payTo}`);
    }
    /** Fund a new escrow channel for an origin from a kaspa-escrow offer. */
    async openEscrow(origin, offer) {
        if (offer.network !== this.wallet.networkId) {
            throw new Error(`server wants payment on ${offer.network} but wallet is on ${this.wallet.networkId}`);
        }
        const client = (0, escrow_1.generateChannelKey)();
        const refundTimeout = BigInt(offer.refundTimeout);
        const escrowAddress = (0, escrow_1.deriveEscrowAddress)({ clientPublic: client.publicKey, serverPublic: offer.serverPublic, timeout: refundTimeout }, this.wallet.networkId);
        const deposit = BigInt(offer.minDepositSompi);
        this.policy.authorize(escrowAddress, deposit);
        await this.wallet.send(escrowAddress, deposit);
        this.policy.record(deposit);
        this.escrows[origin] = {
            clientPrivate: client.privateKey,
            clientPublic: client.publicKey,
            serverPublic: offer.serverPublic,
            refundTimeout: refundTimeout.toString(),
            escrowAddress,
            network: offer.network,
            depositedSompi: deposit.toString(),
            pricePerRequestSompi: String(offer.pricePerRequestSompi),
            authorizedSompi: "0",
        };
        this.persist();
        await sleep(1_500); // let the deposit confirm before the first voucher
    }
    /** Retire the exhausted escrow and open a fresh one for the same origin. */
    async rotateEscrow(origin) {
        const old = this.escrows[origin];
        this.retired.push(old);
        delete this.escrows[origin];
        await this.openEscrow(origin, {
            network: old.network,
            serverPublic: old.serverPublic,
            refundTimeout: old.refundTimeout,
            minDepositSompi: old.depositedSompi,
            pricePerRequestSompi: old.pricePerRequestSompi,
        });
    }
    /** Issue a cumulative voucher and make one escrow-paid request. */
    async escrowRequest(origin, url, init) {
        let state = this.escrows[origin];
        const price = BigInt(state.pricePerRequestSompi);
        // Rotate before the escrow is fully consumed: the claim contract always
        // returns change to escrow, so authorized must stay strictly below the
        // deposit (>= leaves one price-unit of claimable headroom).
        if (BigInt(state.authorizedSompi) + price >= BigInt(state.depositedSompi)) {
            // Escrow exhausted: rotate to a fresh escrow for this origin. The old one
            // is retired (its remaining balance stays refundable after the timeout)
            // and a new deposit opens a clean channel — the agent sees no
            // interruption. A fresh channel avoids multi-UTXO claim ambiguity.
            await this.rotateEscrow(origin);
            await sleep(1_500); // let the new deposit confirm before the first voucher
            state = this.escrows[origin];
        }
        const nextAuthorized = BigInt(state.authorizedSompi) + price;
        const voucher = (0, escrow_1.makeVoucher)(state.clientPrivate, nextAuthorized);
        const header = (0, types_1.encodePaymentHeader)({
            scheme: "kaspa-escrow",
            clientPublic: state.clientPublic,
            voucherAmountSompi: nextAuthorized.toString(),
            voucherHex: voucher.voucherHex,
        });
        let response;
        for (let attempt = 0; attempt < 12; attempt++) {
            const headers = new Headers(init?.headers);
            headers.set(types_1.X_PAYMENT_HEADER, header);
            response = await fetch(url, { ...init, headers });
            if (response.status !== 402)
                break;
            await sleep(1_000); // deposit may still be confirming
        }
        if (!response)
            throw new Error("no response");
        if (response.status !== 402) {
            state.authorizedSompi = nextAuthorized.toString();
            this.persist();
        }
        const result = await this.toResult(response);
        result.scheme = "kaspa-escrow";
        result.authorizedSompi = state.authorizedSompi;
        return result;
    }
    async fetchWithTab(url, init, tabId, depositTxid) {
        const headers = new Headers(init?.headers);
        if (tabId) {
            headers.set(types_1.X_PAYMENT_HEADER, (0, types_1.encodePaymentHeader)({ scheme: "kaspa-tab", tabId, depositTxid }));
        }
        return fetch(url, { ...init, headers });
    }
    async toResult(response, tabId) {
        return {
            status: response.status,
            body: await response.text(),
            tabId,
            remainingSompi: response.headers.get("x-payment-remaining-sompi") ?? undefined,
        };
    }
    persist() {
        fs.writeFileSync(this.tabsPath, JSON.stringify(this.tabs), { mode: 0o600 });
        fs.writeFileSync(this.escrowsPath, JSON.stringify({ active: this.escrows, retired: this.retired }), { mode: 0o600 });
    }
}
exports.X402Client = X402Client;
function sleep(ms) {
    return new Promise((r) => setTimeout(r, ms));
}
