import { Http, IsomorphicBuffer, parser } from "@akala/core";
import { Cursor, parserWrite } from "@akala/protocol-parser";
import State from "../state.js";
import { chacha20_poly1305_decryptAndVerify, chacha20_poly1305_encryptAndSeal, PairMessage, pairMessage, PairMethod, PairState, PairTypeFlags } from "./setup-pair.js";
import assert from 'assert/strict'
import { SrpClient } from "fast-srp-hap";
import hkdf from 'futoin-hkdf'
import tweetnacl from "tweetnacl";


export default async function verifyPair(this: State, accessoryFqdn: string)
{
    if (!this.pairedAccessories[accessoryFqdn])
        throw new Error('There is no such accessory');
}

type Unpromisify<T> = T extends Promise<infer X> ? X : never;

export type PairVerifyM2 = Pick<PairMessage, 'state' | 'publicKey' | 'encryptedData'>;
export type PairVerifyM3 = Pick<PairMessage, 'state' | 'proof'>;
export type PairVerifyM4 = Pick<PairMessage, 'state' | 'error'>;
export type PairVerifyM5 = Pick<PairMessage, 'state' | 'publicKey' | 'encryptedData'>;
export type PairVerifyM6 = Pick<PairMessage, 'state' | 'encryptedData'>;
export type SubPairVerifyM6 = Pick<PairMessage, 'identifier' | 'signature' | 'publicKey'>;


interface PairVerifyServerInfo
{
    username: string;
    publicKey: Buffer;
}

interface PairVerifyClientInfo
{
    username: string;
    privateKey: Buffer;
}


export class HAPEncryption
{

    readonly clientPublicKey: Buffer;
    readonly secretKey: Buffer;
    readonly publicKey: Buffer;
    readonly sharedSecret: Buffer;
    readonly hkdfPairEncryptionKey: Buffer;

    accessoryToControllerCount = 0;
    controllerToAccessoryCount = 0;
    accessoryToControllerKey: Buffer;
    controllerToAccessoryKey: Buffer;

    incompleteFrame?: Buffer;

    public constructor(clientPublicKey: Buffer, secretKey: Buffer, publicKey: Buffer, sharedSecret: Buffer, hkdfPairEncryptionKey: Buffer)
    {
        this.clientPublicKey = clientPublicKey;
        this.secretKey = secretKey;
        this.publicKey = publicKey;
        this.sharedSecret = sharedSecret;
        this.hkdfPairEncryptionKey = hkdfPairEncryptionKey;

        this.accessoryToControllerKey = Buffer.alloc(0);
        this.controllerToAccessoryKey = Buffer.alloc(0);
    }
}


class PairSetupClient
{
    private readonly keyPair: tweetnacl.BoxKeyPair;
    constructor(private readonly accessoryAddress: string, private readonly http: Http, private readonly accessories: State['pairedAccessories'])
    {
        this.keyPair = tweetnacl.box.keyPair();
    }


    async sendPairVerify(): Promise<HAPEncryption>
    {
        // M1
        const m2 = await this.sendM1();

        const m4 = await this.sendM3(m2);

        // verify that encryption works!
        const encryption = new HAPEncryption(
            Buffer.from(m2.pairedAccessory.accessory.publicKey.toArray()),
            Buffer.from(m2.pairedAccessory.controllerInfo.privateKey.toArray()),
            Buffer.from(m2.pairedAccessory.controllerInfo.publicKey.toArray()),
            m2.sharedSecret,
            m2.sessionKey,
        );

        // our HAPCrypto is engineered for the server side, so we have to switch the keys here (deliberately wrongfully)
        // such that hapCrypto uses the controllerToAccessoryKey for encryption!
        encryption.accessoryToControllerKey = m4.controllerToAccessoryKey;
        encryption.controllerToAccessoryKey = m4.accessoryToControllerKey;

        return encryption;
    }

    async sendM1()
    {
        return await this.http.call({
            url: `http://${this.accessoryAddress}/pair-verify`, body: parserWrite(pairMessage,
                {
                    state: PairState.M1,
                    publicKey: new IsomorphicBuffer(this.keyPair.publicKey),
                }).toArray(),
            method: 'post',
            type: 'raw'
        }).
            then(async r => pairMessage.read(IsomorphicBuffer.fromArrayBuffer(await r.arrayBuffer()), new Cursor()) as PairVerifyM2).
            then(m =>
            {
                assert.equal(m.state, PairState.M2, 'an M2 response was expected');
                const accessoryPublicKey = m.publicKey;
                const sharedSecret = Buffer.from(tweetnacl.scalarMult(
                    this.keyPair.secretKey,
                    accessoryPublicKey.toArray(),
                ));

                const sessionKey = hkdf(
                    sharedSecret,
                    32,
                    {
                        hash: "sha512",
                        salt: Buffer.from("Pair-Verify-Encrypt-Salt"),
                        info: Buffer.from("Pair-Verify-Encrypt-Info"),
                    }
                );
                const cipherTextM2 = m.encryptedData.subarray(0, -16);
                const authTagM2 = m.encryptedData.subarray(-16);

                const plaintextM2 = new IsomorphicBuffer(0);
                chacha20_poly1305_decryptAndVerify(
                    sessionKey,
                    Buffer.from("PV-Msg02"),
                    null,
                    Buffer.from(cipherTextM2.toArray()),
                    Buffer.from(authTagM2.toArray()),
                );

                const m2 = pairMessage.read(plaintextM2, new Cursor());
                const accessoryIdentifier = m2.identifier;
                const pairedAccessory = Object.values(this.accessories).find(a => a.accessory.identifier == accessoryIdentifier);
                if (!pairedAccessory)
                    throw new Error('The accessory is not recorded as paired, please run setup-pair beforehand');
                const accessorySignature = m2.signature;

                const accessoryInfo = Buffer.concat([
                    accessoryPublicKey.toArray(),
                    Buffer.from(m2.identifier!),
                    this.keyPair.publicKey,
                ]);

                if (!tweetnacl.sign.detached.verify(accessoryInfo, accessorySignature.toArray(), pairedAccessory.controllerInfo.publicKey.toArray()))
                    throw new Error('signature could not be verified');

                return {
                    sharedSecret,
                    sessionKey,
                    serverEphemeralPublicKey: accessoryPublicKey,
                    pairedAccessory
                };
            });
    }

    async sendM3(m2: Unpromisify<ReturnType<PairSetupClient['sendM1']>>)
    {
        const controllerInfo = Buffer.concat([
            this.keyPair.publicKey,
            Buffer.from(m2.pairedAccessory.controllerInfo.username),
            m2.serverEphemeralPublicKey.toArray(),
        ]);

        // step 8
        const controllerSignature = new IsomorphicBuffer(
            tweetnacl.sign.detached(controllerInfo, m2.pairedAccessory.controllerInfo.privateKey.toArray()),
        );

        // step 9
        const plainTextTLV_M3 = parserWrite(pairMessage, {
            identifier: m2.pairedAccessory.accessory.identifier,
            signature: controllerSignature,
        });

        // step 10
        const encrypted_M3 = chacha20_poly1305_encryptAndSeal(
            m2.sessionKey,
            Buffer.from("PV-Msg03"),
            null,
            Buffer.from(plainTextTLV_M3.toArray()),
        );


        return this.http.call({
            url: `http://${this.accessoryAddress}/pair-verify`,
            body: parserWrite(pairMessage, {

                state: PairState.M3,
                encryptedData: IsomorphicBuffer.concat([encrypted_M3.ciphertext, encrypted_M3.authTag]),
            }).toArray(),
            type: "raw"
        }).
            then(r => r.arrayBuffer()).
            then(b => pairMessage.read(IsomorphicBuffer.fromArrayBuffer(b), new Cursor()) as PairVerifyM4).
            then(m4 =>
            {
                assert.equal(m4.state, PairState.M4);
                assert.equal(m4.error, undefined);

                const salt = Buffer.from("Control-Salt");
                const accessoryToControllerKey = hkdf(
                    m2.sharedSecret,
                    32,
                    {
                        hash: "sha512",
                        salt,
                        info: Buffer.from("Control-Read-Encryption-Key"),
                    });

                const controllerToAccessoryKey = hkdf(
                    m2.sharedSecret,
                    32,
                    {
                        hash: "sha512",
                        salt,
                        info: Buffer.from("Control-Write-Encryption-Key"),
                    }
                );

                return {
                    accessoryToControllerKey,
                    controllerToAccessoryKey,
                };

            })
            ;
    }
}