import * as Uint8arrays from "uint8arrays";
import * as Crypto from "../components/crypto/implementation.js";
import * as Check from "../common/type-checks.js";
import * as DID from "../did/index.js";
import * as Linking from "./common.js";
import * as Ucan from "../ucan/index.js";
import { EventEmitter } from "../common/event-emitter.js";
import { LinkingError, LinkingStep, LinkingWarning, tryParseMessage } from "./common.js";
/**
 * Create an account linking consumer
 *
 * @param options consumer options
 * @param options.username username of the account
 * @returns an account linking event emitter and cancel function
 */
export const createConsumer = async (dependencies, options) => {
    const { username } = options;
    const handleLinkingError = (errorOrWarning) => Linking.handleLinkingError(dependencies.manners, errorOrWarning);
    let eventEmitter = new EventEmitter();
    const ls = {
        username,
        sessionKey: null,
        temporaryRsaPair: null,
        step: LinkingStep.Broadcast
    };
    const handleMessage = async (event) => {
        const { data } = event;
        const message = data.arrayBuffer ? new TextDecoder().decode(await data.arrayBuffer()) : data;
        switch (ls.step) {
            // Broadcast
            // ---------
            case LinkingStep.Broadcast:
                return handleLinkingError(new LinkingWarning("Consumer is not ready to start linking"));
            // Negotiation
            // -----------
            case LinkingStep.Negotiation:
                if (ls.sessionKey) {
                    handleLinkingError(new LinkingWarning("Consumer already received a session key"));
                }
                else if (!ls.temporaryRsaPair || !ls.temporaryRsaPair.privateKey) {
                    handleLinkingError(new LinkingError("Consumer missing RSA key pair when handling session key message"));
                }
                else {
                    const sessionKeyResult = await handleSessionKey(dependencies.crypto, ls.temporaryRsaPair.privateKey, message);
                    if (sessionKeyResult.ok) {
                        ls.sessionKey = sessionKeyResult.value;
                        const { pin, challenge } = await generateUserChallenge(dependencies.crypto, ls.sessionKey);
                        channel.send(challenge);
                        eventEmitter?.emit("challenge", { pin: Array.from(pin) });
                        ls.step = LinkingStep.Delegation;
                    }
                    else {
                        handleLinkingError(sessionKeyResult.error);
                    }
                }
                break;
            // Delegation
            // ----------
            case LinkingStep.Delegation:
                if (!ls.sessionKey) {
                    handleLinkingError(new LinkingError("Consumer was missing session key when linking device"));
                }
                else if (!ls.username) {
                    handleLinkingError(new LinkingError("Consumer was missing username when linking device"));
                }
                else {
                    const linkingResult = await linkDevice(dependencies.auth, dependencies.crypto, ls.sessionKey, ls.username, message);
                    if (linkingResult.ok) {
                        const { approved } = linkingResult.value;
                        eventEmitter?.emit("link", { approved, username: ls.username });
                        await done();
                    }
                    else {
                        handleLinkingError(linkingResult.error);
                    }
                }
                break;
        }
    };
    const done = async () => {
        eventEmitter?.emit("done", undefined);
        eventEmitter = null;
        channel.close();
        clearInterval(rsaExchangeInterval);
    };
    const channel = await dependencies.auth.createChannel({ handleMessage, username });
    const rsaExchangeInterval = setInterval(async () => {
        if (!ls.sessionKey) {
            const { temporaryRsaPair, temporaryDID } = await generateTemporaryExchangeKey(dependencies.crypto);
            ls.temporaryRsaPair = temporaryRsaPair;
            ls.step = LinkingStep.Negotiation;
            await channel.send(temporaryDID);
        }
        else {
            clearInterval(rsaExchangeInterval);
        }
    }, 2000);
    return {
        on: (...args) => eventEmitter?.on(...args),
        cancel: done
    };
};
// 🔗 Device Linking Steps
/**
 *  BROADCAST
 *
 * Generate a temporary RSA keypair and extract a temporary DID from it.
 * The temporary DID will be broadcast on the channel to start the linking process.
 *
 * @returns temporary RSA key pair and temporary DID
 */
export const generateTemporaryExchangeKey = async (crypto) => {
    const temporaryRsaPair = await crypto.rsa.genKey();
    const pubKey = await crypto.rsa.exportPublicKey(temporaryRsaPair.publicKey);
    const temporaryDID = DID.publicKeyToDid(crypto, pubKey, "rsa");
    return { temporaryRsaPair, temporaryDID };
};
/**
 *  NEGOTIATION
 *
 * Decrypt the session key and check the closed UCAN for capability.
 * The session key is encrypted with the temporary RSA keypair.
 * The closed UCAN is encrypted with the session key.
 *
 * @param temporaryRsaPrivateKey
 * @param data
 * @returns AES session key
 */
export const handleSessionKey = async (crypto, temporaryRsaPrivateKey, data) => {
    const typeGuard = (message) => {
        return Check.isObject(message)
            && "iv" in message && typeof message.iv === "string"
            && "msg" in message && typeof message.msg === "string"
            && "sessionKey" in message && typeof message.sessionKey === "string";
    };
    const parseResult = tryParseMessage(data, typeGuard, { participant: "Consumer", callSite: "handleSessionKey" });
    if (parseResult.ok) {
        const { iv: encodedIV, msg, sessionKey: encodedSessionKey } = parseResult.value;
        const iv = Uint8arrays.fromString(encodedIV, "base64pad");
        let sessionKey;
        try {
            const encryptedSessionKey = Uint8arrays.fromString(encodedSessionKey, "base64pad");
            sessionKey = await crypto.rsa.decrypt(encryptedSessionKey, temporaryRsaPrivateKey);
        }
        catch {
            return { ok: false, error: new LinkingWarning(`Consumer received a session key in handleSessionKey that it could not decrypt: ${data}. Ignoring message`) };
        }
        let encodedUcan = null;
        try {
            encodedUcan = await crypto.aes.decrypt(Uint8arrays.fromString(msg, "base64pad"), sessionKey, Crypto.SymmAlg.AES_GCM, iv);
        }
        catch {
            return { ok: false, error: new LinkingError("Consumer could not decrypt closed UCAN with provided session key.") };
        }
        const decodedUcan = Ucan.decode(Uint8arrays.toString(encodedUcan, "utf8"));
        if (await Ucan.isValid(crypto, decodedUcan) === false) {
            return { ok: false, error: new LinkingError("Consumer received an invalid closed UCAN") };
        }
        if (decodedUcan.payload.ptc) {
            return { ok: false, error: new LinkingError("Consumer received a closed UCAN with potency. Closed UCAN must not have potency.") };
        }
        const sessionKeyFromFact = decodedUcan.payload.fct[0] && decodedUcan.payload.fct[0].sessionKey;
        if (!sessionKeyFromFact) {
            return { ok: false, error: new LinkingError("Consumer received a closed UCAN that was missing a session key in facts.") };
        }
        const sessionKeyWeAlreadyGot = Uint8arrays.toString(sessionKey, "base64pad");
        if (sessionKeyFromFact !== sessionKeyWeAlreadyGot) {
            return { ok: false, error: new LinkingError("Consumer received a closed UCAN session key does not match the session key") };
        }
        return { ok: true, value: sessionKey };
    }
    else {
        return parseResult;
    }
};
/**
 * NEGOTIATION
 *
 * Generate pin and challenge message for verification by the producer.
 *
 * @param sessionKey
 * @returns pin and challenge message
 */
export const generateUserChallenge = async (crypto, sessionKey) => {
    const pin = Array.from(crypto.misc.randomNumbers({ amount: 6 })).map(n => n % 9);
    const iv = crypto.misc.randomNumbers({ amount: 16 });
    const msg = await crypto.aes.encrypt(Uint8arrays.fromString(JSON.stringify({
        did: await DID.ucan(crypto),
        pin
    }), "utf8"), sessionKey, Crypto.SymmAlg.AES_GCM, iv);
    const challenge = JSON.stringify({
        iv: Uint8arrays.toString(iv, "base64pad"),
        msg: Uint8arrays.toString(msg, "base64pad")
    });
    return { pin, challenge };
};
/**
 * DELEGATION
 *
 * Decrypt the delegated credentials and forward to the dependency injected linkDevice function,
 * or report that delegation was declined.
 *
 * @param sessionKey
 * @param username
 * @param data
 * @returns linking result
 */
export const linkDevice = async (auth, crypto, sessionKey, username, 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: "Consumer", callSite: "linkDevice" });
    if (parseResult.ok) {
        const { iv: encodedIV, msg } = parseResult.value;
        const iv = Uint8arrays.fromString(encodedIV, "base64");
        let message = null;
        try {
            message = await crypto.aes.decrypt(Uint8arrays.fromString(msg, "base64pad"), sessionKey, Crypto.SymmAlg.AES_GCM, iv);
        }
        catch {
            return { ok: false, error: new LinkingWarning("Consumer ignoring message that could not be decrypted in linkDevice.") };
        }
        const response = tryParseMessage(Uint8arrays.toString(message, "utf8"), Check.isObject, { participant: "Consumer", callSite: "linkDevice" });
        if (!response.ok) {
            return response;
        }
        if (response?.value.linkStatus === "DENIED") {
            return { ok: true, value: { approved: false } };
        }
        await auth.linkDevice(username, response.value);
        return { ok: true, value: { approved: true } };
    }
    else {
        return parseResult;
    }
};
//# sourceMappingURL=consumer.js.map