import aes from "keystore-idb/aes/index.js";
import rsa from "keystore-idb/rsa/index.js";
import utils from "keystore-idb/utils.js";
import { KeyUse, SymmAlg, HashAlg, CharSize } from "keystore-idb/types.js";
import { webcrypto } from "one-webcrypto";
import * as uint8arrays from "uint8arrays";
import * as check from "../../common/type-checks.js";
import * as did from "../../did/index.js";
import * as ucan from "../../ucan/index.js";
import { impl as auth } from "../implementation.js";
import { EventEmitter } from "../../common/event-emitter.js";
import { LinkingError, LinkingStep, LinkingWarning, handleLinkingError, tryParseMessage } from "../linking.js";
/**
 * Create an account linking producer
 *
 * @param options producer options
 * @param options.username username of the account
 * @returns an account linking event emitter and cancel function
 */
export const createProducer = async (options) => {
    const { username } = options;
    const canDelegate = await auth.checkCapability(username);
    if (!canDelegate) {
        throw new LinkingError(`Producer cannot delegate for username ${username}`);
    }
    let eventEmitter = new EventEmitter();
    const ls = {
        username,
        sessionKey: null,
        step: LinkingStep.Broadcast
    };
    const handleMessage = async (event) => {
        const { data } = event;
        const message = data.arrayBuffer ? new TextDecoder().decode(await data.arrayBuffer()) : data;
        if (ls.step === LinkingStep.Broadcast) {
            const { sessionKey, sessionKeyMessage } = await generateSessionKey(message);
            ls.sessionKey = sessionKey;
            ls.step = LinkingStep.Negotiation;
            channel.send(sessionKeyMessage);
        }
        else if (ls.step === LinkingStep.Negotiation) {
            if (ls.sessionKey) {
                const userChallengeResult = await handleUserChallenge(ls.sessionKey, message);
                ls.step = LinkingStep.Delegation;
                if (userChallengeResult.ok) {
                    const { pin, audience } = userChallengeResult.value;
                    const challengeOnce = () => {
                        let called = false;
                        return {
                            confirmPin: async () => {
                                if (!called) {
                                    called = true;
                                    if (ls.sessionKey) {
                                        await delegateAccount(ls.sessionKey, username, audience, finishDelegation);
                                    }
                                    else {
                                        handleLinkingError(new LinkingError("Producer missing session key when delegating account"));
                                    }
                                }
                            },
                            rejectPin: async () => {
                                if (!called) {
                                    called = true;
                                    if (ls.sessionKey) {
                                        await declineDelegation(ls.sessionKey, finishDelegation);
                                    }
                                    else {
                                        handleLinkingError(new LinkingError("Producer missing session key when declining account delegation"));
                                    }
                                }
                            }
                        };
                    };
                    const { confirmPin, rejectPin } = challengeOnce();
                    eventEmitter?.emit("challenge", { pin, confirmPin, rejectPin });
                }
                else {
                    handleLinkingError(userChallengeResult.error);
                }
            }
            else {
                handleLinkingError(new LinkingError("Producer missing session key when handling user challenge"));
            }
        }
        else if (ls.step === LinkingStep.Delegation) {
            handleLinkingError(new LinkingWarning("Producer received an unexpected message while delegating an account. The message will be ignored."));
        }
    };
    const finishDelegation = async (delegationMessage, approved) => {
        await channel.send(delegationMessage);
        if (ls.username == null)
            return; // or throw error?
        eventEmitter?.emit("link", { approved, username: ls.username });
        resetLinkingState();
    };
    const resetLinkingState = () => {
        ls.sessionKey = null;
        ls.step = LinkingStep.Broadcast;
    };
    const cancel = async () => {
        eventEmitter?.emit("done", undefined);
        eventEmitter = null;
        channel.close();
    };
    const channel = await auth.createChannel({ username, handleMessage });
    return {
        on: (...args) => eventEmitter?.on(...args),
        cancel
    };
};
/**
 * BROADCAST
 *
 * Generate a session key and prepare a session key message to send to the consumer.
 *
 * @param didThrowaway
 * @returns session key and session key message
 */
export const generateSessionKey = async (didThrowaway) => {
    const sessionKey = await aes.makeKey({ alg: SymmAlg.AES_GCM, length: 256 });
    const exportedSessionKey = await aes.exportKey(sessionKey);
    const { publicKey } = did.didToPublicKey(didThrowaway);
    const publicCryptoKey = await rsa.importPublicKey(publicKey, HashAlg.SHA_256, KeyUse.Exchange);
    // Note: rsa.encrypt expects a B16 string
    const rawSessionKey = utils.arrBufToStr(utils.base64ToArrBuf(exportedSessionKey), CharSize.B16);
    const encryptedSessionKey = await rsa.encrypt(rawSessionKey, publicCryptoKey);
    const u = await ucan.build({
        issuer: await did.ucan(),
        audience: didThrowaway,
        lifetimeInSeconds: 60 * 5,
        facts: [{ sessionKey: exportedSessionKey }],
        potency: null
    });
    const iv = utils.randomBuf(16);
    const msg = utils.arrBufToBase64(await webcrypto.subtle.encrypt({
        name: "AES-GCM",
        iv,
    }, sessionKey, uint8arrays.fromString(ucan.encode(u), "utf8")));
    const sessionKeyMessage = JSON.stringify({
        iv: utils.arrBufToBase64(iv),
        msg,
        sessionKey: utils.arrBufToBase64(encryptedSessionKey)
    });
    return {
        sessionKey,
        sessionKeyMessage
    };
};
/**
 * NEGOTIATION
 *
 * Decrypt the user challenge and the consumer audience DID.
 *
 * @param data
 * @returns pin and audience
 */
export const handleUserChallenge = async (sessionKey, data) => {
    const typeGuard = (message) => {
        return check.isObject(message)
            && "iv" in message && typeof message.iv === "string"
            && "msg" in message && typeof message.msg === "string";
    };
    const parseResult = tryParseMessage(data, typeGuard, { participant: "Producer", callSite: "handleUserChallenge" });
    if (parseResult.ok) {
        const { iv: encodedIV, msg } = parseResult.value;
        const iv = utils.base64ToArrBuf(encodedIV);
        let message = null;
        try {
            message = uint8arrays.toString(await webcrypto.subtle.decrypt({
                name: "AES-GCM",
                iv,
            }, sessionKey, utils.base64ToArrBuf(msg)), "utf8");
        }
        catch {
            return { ok: false, error: new LinkingWarning("Ignoring message that could not be decrypted.") };
        }
        const json = JSON.parse(message);
        const pin = json.pin ?? null;
        const audience = json.did ?? null;
        if (pin !== null && audience !== null) {
            return { ok: true, value: { pin, audience } };
        }
        else {
            return { ok: false, error: new LinkingError(`Producer received invalid pin ${json.pin} or audience ${json.audience}`) };
        }
    }
    else {
        return parseResult;
    }
};
/**
 * DELEGATION: Delegate account
 *
 * Request delegation from the dependency injected delegateAccount function.
 * Prepare a delegation message to send to the consumer.
 *
 * @param sesionKey
 * @param audience
 * @param finishDelegation
 */
export const delegateAccount = async (sessionKey, username, audience, finishDelegation) => {
    const delegation = await auth.delegateAccount(username, audience);
    const message = JSON.stringify(delegation);
    const iv = utils.randomBuf(16);
    const msg = utils.arrBufToBase64(await webcrypto.subtle.encrypt({
        name: "AES-GCM",
        iv,
    }, sessionKey, uint8arrays.fromString(message, "utf8")));
    const delegationMessage = JSON.stringify({
        iv: utils.arrBufToBase64(iv),
        msg
    });
    await finishDelegation(delegationMessage, true);
};
/**
 * DELEGATION: Decline delegation
 *
 * Prepare a delegation declined message to send to the consumer.
 *
 * @param sessionKey
 * @param finishDelegation
 */
export const declineDelegation = async (sessionKey, finishDelegation) => {
    const message = JSON.stringify({ linkStatus: "DENIED" });
    const iv = utils.randomBuf(16);
    const msg = utils.arrBufToBase64(await webcrypto.subtle.encrypt({
        name: "AES-GCM",
        iv,
    }, sessionKey, uint8arrays.fromString(message, "utf8")));
    const delegationMessage = JSON.stringify({
        iv: utils.arrBufToBase64(iv),
        msg
    });
    await finishDelegation(delegationMessage, false);
};
//# sourceMappingURL=producer.js.map