"use strict";
/**
 * Wallet material factory.
 *
 * Resolves a WalletSessions row into the WalletMaterial shape that
 * TransactionSubmitter and the provider bundle consume:
 *   - looks up the active session and decrypts its viewing key
 *   - derives a deterministic accountId + private-state storage password from
 *     the viewing key, so the same key maps to the same encrypted state across
 *     reconnects
 *   - builds a wallet adapter sized to what the session carries: a real
 *     signing-capable adapter when an encryptedSeedKey is present, otherwise a
 *     read-only adapter whose signing methods throw.
 */
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.WalletMaterialUnavailable = exports.WalletSigningNotAvailable = exports.SessionNotFoundError = void 0;
exports.buildWalletMaterialForSession = buildWalletMaterialForSession;
exports.deriveAccountId = deriveAccountId;
exports.deriveStoragePassword = deriveStoragePassword;
const cds_1 = __importDefault(require("@sap/cds"));
const { SELECT } = cds_1.default.ql;
const midnight_1 = require("#cds-models/midnight");
const crypto_1 = __importDefault(require("crypto"));
const crypto_2 = require("../utils/crypto");
const sdk_loader_1 = require("../midnight/sdk-loader");
const wallet_hd_1 = require("../utils/wallet-hd");
const wallet_facade_builder_1 = require("./wallet-facade-builder");
// ---- Errors ---------------------------------------------------------------
class SessionNotFoundError extends Error {
    constructor(sessionId) {
        super(`Session '${sessionId}' not found, expired, or inactive`);
        this.name = 'SessionNotFoundError';
    }
}
exports.SessionNotFoundError = SessionNotFoundError;
/**
 * Thrown by the wallet adapter's signing/balancing methods when the session
 * carries a viewing key only. Not thrown by `buildWalletMaterialForSession`
 * itself, which returns successfully and lets the SDK call surface the error
 * through TransactionSubmitter's classification.
 */
class WalletSigningNotAvailable extends Error {
    constructor(method) {
        super(`Wallet signing surface not available for ${method}: session carries a viewing key only. ` +
            `Signing requires the encryptedSeedKey field on WalletSessions to be populated.`);
        this.name = 'WalletSigningNotAvailable';
    }
}
exports.WalletSigningNotAvailable = WalletSigningNotAvailable;
// Retained for back-compat with the handlers.ts 501 mapping. The factory no
// longer throws this; the symbol stays exported so dependents don't break.
class WalletMaterialUnavailable extends Error {
    constructor(reason) {
        super(`Wallet material unavailable: ${reason}.`);
        this.name = 'WalletMaterialUnavailable';
    }
}
exports.WalletMaterialUnavailable = WalletMaterialUnavailable;
const ACCOUNT_ID_LABEL = 'nightgate-account-id-v1';
const PRIVATE_STATE_PASSWORD_LABEL = 'nightgate-private-state-password-v1';
/**
 * Returns a fully-typed `WalletMaterial`. The wallet adapter's signing
 * methods throw `WalletSigningNotAvailable`; deterministic identifiers
 * (accountId) and the storage password work.
 */
async function buildWalletMaterialForSession(opts) {
    const db = opts.db ?? await cds_1.default.connect.to('db');
    const session = await db.run(SELECT.one.from(midnight_1.WalletSessions).where({ sessionId: opts.sessionId, isActive: true }));
    if (!session)
        throw new SessionNotFoundError(opts.sessionId);
    if (session.expiresAt && new Date(session.expiresAt) < new Date()) {
        throw new SessionNotFoundError(opts.sessionId);
    }
    if (!session.encryptedViewingKey) {
        throw new SessionNotFoundError(opts.sessionId);
    }
    const encKey = opts.encryptionKey ?? (0, crypto_2.getEncryptionKey)();
    let viewingKey;
    try {
        viewingKey = (0, crypto_2.decrypt)(session.encryptedViewingKey, encKey);
    }
    catch (err) {
        // Wrong ENCRYPTION_KEY, tampered ciphertext, or rotated key.
        throw new SessionNotFoundError(opts.sessionId);
    }
    const accountId = deriveAccountId(viewingKey);
    const password = deriveStoragePassword(viewingKey);
    let walletAndMidnightProvider;
    if (session.encryptedSeedKey) {
        // Real signing material is present.
        let seedHex;
        try {
            seedHex = (0, crypto_2.decrypt)(session.encryptedSeedKey, encKey);
        }
        catch {
            throw new SessionNotFoundError(opts.sessionId);
        }
        if (opts.facadeConfig) {
            // WalletFacade-backed adapter with working balanceTx and submitTx.
            walletAndMidnightProvider = await createFacadeBackedWalletAdapter(accountId, seedHex, { ...opts.facadeConfig, syncStatePassphrase: password });
        }
        else {
            // Seed present but no facade configured: pubkeys real, signing throws.
            walletAndMidnightProvider = await createSigningCapableWalletAdapter(seedHex);
        }
    }
    else {
        walletAndMidnightProvider = createReadOnlyWalletAdapter();
    }
    return {
        accountId,
        privateStoragePasswordProvider: () => password,
        walletAndMidnightProvider,
        privateStateBackend: opts.privateStateBackend
    };
}
// ---- Determinism helpers --------------------------------------------------
/**
 * `accountId` is opaque storage scope; it just needs to be deterministic per
 * viewing key. We use HMAC-SHA256 with a domain-separation label and hex-encode.
 * 64 chars is well within the 200-char DB column.
 */
function deriveAccountId(viewingKey) {
    return crypto_1.default.createHmac('sha256', ACCOUNT_ID_LABEL).update(viewingKey).digest('hex');
}
/**
 * Storage password for the CAP-DB private state provider. 64-char hex
 * (= 256 bits of entropy), well over the 16-char minimum. Distinct
 * domain-separation label from the accountId so the two values cannot collide.
 */
function deriveStoragePassword(viewingKey) {
    return crypto_1.default.createHmac('sha256', PRIVATE_STATE_PASSWORD_LABEL).update(viewingKey).digest('hex');
}
// ---- Wallet adapter -------------------------------------------------------
/**
 * Read-only adapter for viewing-key-only sessions. All four wallet methods
 * throw `WalletSigningNotAvailable`.
 *
 * Typed `any` for the secret-key/transaction types: importing them would pull
 * in the ESM-only ledger-v8 package, which an adapter that only throws does
 * not need.
 */
function createReadOnlyWalletAdapter() {
    return {
        getCoinPublicKey() { throw new WalletSigningNotAvailable('getCoinPublicKey()'); },
        getEncryptionPublicKey() { throw new WalletSigningNotAvailable('getEncryptionPublicKey()'); },
        async balanceTx(_tx, _ttl) { throw new WalletSigningNotAvailable('balanceTx()'); },
        async submitTx(_tx) { throw new WalletSigningNotAvailable('submitTx()'); }
    };
}
/**
 * Full signing adapter: real pubkeys from the derived ZswapSecretKeys, with
 * balanceTx/submitTx routed through a WalletFacade.
 *
 * The facade is built lazily on the first balanceTx/submitTx call so that
 * request pays the chain-sync cost; it is then cached per accountId.
 * `balanceUnboundTransaction` yields a recipe that `finalizeRecipe` turns into
 * a transaction the SDK's deploy/call flow accepts. Balance TTL defaults to 1
 * hour, matching the SDK default.
 */
async function createFacadeBackedWalletAdapter(accountId, seedHex, facadeConfig) {
    if (!/^[0-9a-fA-F]{128}$/.test(seedHex)) {
        throw new Error('Invalid seed: must be 128 hex characters (64-byte BIP39 seed)');
    }
    // Derive pubkeys eagerly so getCoinPublicKey/getEncryptionPublicKey are
    // synchronous, satisfying the WalletProvider interface contract. seedHex is
    // the BIP39 seed; the shielded account comes from the Zswap HD role (see
    // srv/utils/wallet-hd.ts), not the raw seed.
    const bip39Seed = new Uint8Array(Buffer.from(seedHex, 'hex'));
    const roleSeeds = await (0, wallet_hd_1.deriveRoleSeeds)(bip39Seed);
    const ledger = await (0, sdk_loader_1.loadLedgerV8)();
    const zswapKeys = ledger.ZswapSecretKeys.fromSeed(roleSeeds.zswap);
    const coinPublicKey = zswapKeys.coinPublicKey;
    const encryptionPublicKey = zswapKeys.encryptionPublicKey;
    let facadePromise;
    const getFacade = () => {
        if (!facadePromise) {
            facadePromise = (0, wallet_facade_builder_1.getOrBuildWalletFacade)(accountId, { ...facadeConfig, seedHex });
        }
        return facadePromise;
    };
    return {
        getCoinPublicKey() { return coinPublicKey; },
        getEncryptionPublicKey() { return encryptionPublicKey; },
        async balanceTx(tx, ttl) {
            const { facade, zswapKeys, dustKey } = await getFacade();
            const effectiveTtl = ttl ?? new Date(Date.now() + 60 * 60 * 1000);
            const recipe = await facade.balanceUnboundTransaction(tx, { shieldedSecretKeys: zswapKeys, dustSecretKey: dustKey }, { ttl: effectiveTtl });
            return facade.finalizeRecipe(recipe);
        },
        async submitTx(tx) {
            const { facade } = await getFacade();
            return facade.submitTransaction(tx);
        },
        _internal: { zswapKeys, cacheKey: accountId }
    };
}
/**
 * Adapter for sessions that have seed material but no configured facade. Public
 * keys are derived from the seed; balanceTx/submitTx throw until a facade is
 * wired in.
 */
async function createSigningCapableWalletAdapter(seedHex) {
    if (!/^[0-9a-fA-F]{128}$/.test(seedHex)) {
        // Defense in depth; connectWalletForSigning already validates this.
        throw new Error('Invalid seed: must be 128 hex characters (64-byte BIP39 seed)');
    }
    const bip39Seed = new Uint8Array(Buffer.from(seedHex, 'hex'));
    const roleSeeds = await (0, wallet_hd_1.deriveRoleSeeds)(bip39Seed);
    const ledger = await (0, sdk_loader_1.loadLedgerV8)();
    // Each key type comes from its own HD role (Zswap/Dust), matching Lace —
    // see srv/utils/wallet-hd.ts.
    const zswapKeys = ledger.ZswapSecretKeys.fromSeed(roleSeeds.zswap);
    const dustKey = ledger.DustSecretKey.fromSeed(roleSeeds.dust);
    const coinPublicKey = zswapKeys.coinPublicKey;
    const encryptionPublicKey = zswapKeys.encryptionPublicKey;
    return {
        getCoinPublicKey() { return coinPublicKey; },
        getEncryptionPublicKey() { return encryptionPublicKey; },
        async balanceTx(_tx, _ttl) {
            throw new WalletSigningNotAvailable('balanceTx(): secret keys derived but no WalletFacade configured for this session');
        },
        async submitTx(_tx) {
            throw new WalletSigningNotAvailable('submitTx(): secret keys derived but no WalletFacade configured for this session');
        },
        // Internal handles exposed so a facade-backed adapter can reuse them
        // without re-deriving.
        _internal: { zswapKeys, dustKey }
    };
}
