import { Buffer } from "buffer/";
import { JWTPayload } from "did-jwt";
import { p256 } from "@noble/curves/p256";
import {
  startAuthentication,
  startRegistration,
} from "@simplewebauthn/browser";
import {
  generateAuthenticationOptions,
  generateRegistrationOptions,
  verifyAuthenticationResponse,
  verifyRegistrationResponse,
  VerifyAuthenticationResponseOpts,
} from "@simplewebauthn/server";
import {
  Base64URLString,
  PublicKeyCredentialCreationOptionsJSON,
  PublicKeyCredentialRequestOptionsJSON,
  AuthenticatorAssertionResponse,
} from "@simplewebauthn/types";

import { AppString } from "../../../constants/app";
import { unwrapEC2Signature } from "../../../libs/crypto/vc/passkeyHelpers";
import {
  arrayToBase64Url,
  cborToKeys,
  peerDidToPublicKeyBytes,
  verifyPeerSignature,
} from "../../../libs/crypto/vc/didPeer";
import { logger } from "../../../utils/logger";

export interface JWK {
  kty: string;
  crv: string;
  x: string;
  y: string;
}

export async function registerCredential(passkeyName?: string) {
  const options: PublicKeyCredentialCreationOptionsJSON =
    await generateRegistrationOptions({
      rpName: AppString.APP_NAME,
      rpID: window.location.hostname,
      userName: passkeyName || AppString.APP_NAME + " User",
      // Don't prompt users for additional information about the authenticator
      // (Recommended for smoother UX)
      attestationType: "none",
      authenticatorSelection: {
        // Defaults
        residentKey: "preferred",
        userVerification: "preferred",
        // Optional
        authenticatorAttachment: "platform",
      },
    });
  // someday, instead of simplwebauthn, we'll go direct: navigator.credentials.create with PublicKeyCredentialCreationOptions
  // with pubKeyCredParams: { type: "public-key", alg: -7 }
  const attResp = await startRegistration(options);
  const verification = await verifyRegistrationResponse({
    response: attResp,
    expectedChallenge: options.challenge,
    expectedOrigin: window.location.origin,
    expectedRPID: window.location.hostname,
  });

  // references for parsing auth data and getting the public key
  // https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/parseAuthenticatorData.ts#L11
  // https://chatgpt.com/share/78a5c91d-099d-46dc-aa6d-fc0c916509fa
  // https://chatgpt.com/share/3c13f061-6031-45bc-a2d7-3347c1e7a2d7

  const credIdBase64Url = verification.registrationInfo?.credentialID as string;
  if (attResp.rawId !== credIdBase64Url) {
    logger.warn("Warning! The raw ID does not match the credential ID.");
  }
  const credIdHex = Buffer.from(
    base64URLStringToArrayBuffer(credIdBase64Url),
  ).toString("hex");
  const { publicKeyJwk } = cborToKeys(
    verification.registrationInfo?.credentialPublicKey as Uint8Array,
  );

  return {
    authData: verification.registrationInfo?.attestationObject,
    credIdHex: credIdHex,
    publicKeyJwk: publicKeyJwk,
    publicKeyBytes: verification.registrationInfo
      ?.credentialPublicKey as Uint8Array,
  };
}

export class PeerSetup {
  public authenticatorData?: ArrayBuffer;
  public challenge?: Uint8Array;
  public clientDataJsonBase64Url?: Base64URLString;
  public signature?: Base64URLString;

  public async createJwtSimplewebauthn(
    issuerDid: string,
    payload: object,
    credIdHex: string,
    expMinutes: number = 1,
  ) {
    const credentialId = arrayBufferToBase64URLString(
      Buffer.from(credIdHex, "hex").buffer,
    );
    const issuedAt = Math.floor(Date.now() / 1000);
    const expiryTime = Math.floor(Date.now() / 1000) + expMinutes * 60; // some minutes from now
    const fullPayload = {
      ...payload,
      exp: expiryTime,
      iat: issuedAt,
      iss: issuerDid,
    };
    this.challenge = new Uint8Array(Buffer.from(JSON.stringify(fullPayload)));
    // const payloadHash: Uint8Array = sha256(this.challenge);
    const options: PublicKeyCredentialRequestOptionsJSON =
      await generateAuthenticationOptions({
        challenge: this.challenge,
        rpID: window.location.hostname,
        allowCredentials: [{ id: credentialId }],
      });
    // console.log("simple authentication options", options);

    const clientAuth = await startAuthentication(options);
    // console.log("simple credential get", clientAuth);

    const authenticatorDataBase64Url = clientAuth.response.authenticatorData;
    this.authenticatorData = Buffer.from(
      clientAuth.response.authenticatorData,
      "base64",
    ).buffer;
    this.clientDataJsonBase64Url = clientAuth.response.clientDataJSON;
    // console.log("simple authenticatorData for signing", this.authenticatorData);
    this.signature = clientAuth.response.signature;

    // Our custom type of JWANT means the signature is based on a concatenation of the two Webauthn properties
    const header: JWTPayload = { typ: "JWANT", alg: "ES256" };
    const headerBase64 = Buffer.from(JSON.stringify(header))
      .toString("base64")
      .replace(/\+/g, "-")
      .replace(/\//g, "_")
      .replace(/=+$/, "");

    const dataInJwt = {
      AuthenticationDataB64URL: authenticatorDataBase64Url,
      ClientDataJSONB64URL: this.clientDataJsonBase64Url,
      exp: expiryTime,
      iat: issuedAt,
      iss: issuerDid,
    };
    const dataInJwtString = JSON.stringify(dataInJwt);
    const payloadBase64 = Buffer.from(dataInJwtString)
      .toString("base64")
      .replace(/\+/g, "-")
      .replace(/\//g, "_")
      .replace(/=+$/, "");

    const signature = clientAuth.response.signature;

    return headerBase64 + "." + payloadBase64 + "." + signature;
  }

  public async createJwtNavigator(
    issuerDid: string,
    payload: object,
    credIdHex: string,
    expMinutes: number = 1,
  ) {
    const issuedAt = Math.floor(Date.now() / 1000);
    const expiryTime = Math.floor(Date.now() / 1000) + expMinutes * 60; // some minutes from now
    const fullPayload = {
      ...payload,
      exp: expiryTime,
      iat: issuedAt,
      iss: issuerDid,
    };
    const dataToSignString = JSON.stringify(fullPayload);
    const dataToSignBuffer = Buffer.from(dataToSignString);
    const credentialId = Buffer.from(credIdHex, "hex");

    // console.log("lower credentialId", credentialId);
    this.challenge = new Uint8Array(dataToSignBuffer);
    const options = {
      publicKey: {
        allowCredentials: [
          {
            id: credentialId,
            type: "public-key" as const,
          },
        ],
        challenge: this.challenge.buffer,
        rpID: window.location.hostname,
        userVerification: "preferred" as const,
      },
    };

    const credential = (await navigator.credentials.get(
      options,
    )) as PublicKeyCredential;
    // console.log("nav credential get", credential);

    const response = credential?.response as AuthenticatorAssertionResponse;
    this.authenticatorData = response?.authenticatorData;
    const authenticatorDataBase64Url = arrayBufferToBase64URLString(
      this.authenticatorData as ArrayBuffer,
    );

    this.clientDataJsonBase64Url = arrayBufferToBase64URLString(
      response?.clientDataJSON,
    );

    // Our custom type of JWANT means the signature is based on a concatenation of the two Webauthn properties
    const header: JWTPayload = { typ: "JWANT", alg: "ES256" };
    const headerBase64 = Buffer.from(JSON.stringify(header))
      .toString("base64")
      .replace(/\+/g, "-")
      .replace(/\//g, "_")
      .replace(/=+$/, "");

    const dataInJwt = {
      AuthenticationDataB64URL: authenticatorDataBase64Url,
      ClientDataJSONB64URL: this.clientDataJsonBase64Url,
      exp: expiryTime,
      iat: issuedAt,
      iss: issuerDid,
    };
    const dataInJwtString = JSON.stringify(dataInJwt);
    const payloadBase64 = Buffer.from(dataInJwtString)
      .toString("base64")
      .replace(/\+/g, "-")
      .replace(/\//g, "_")
      .replace(/=+$/, "");

    const origSignature = Buffer.from(response?.signature).toString("base64");
    this.signature = origSignature
      .replace(/\+/g, "-")
      .replace(/\//g, "_")
      .replace(/=+$/, "");

    const jwt = headerBase64 + "." + payloadBase64 + "." + this.signature;
    return jwt;
  }

  // To use this, add the asn1-ber library and add this import:
  // import asn1 from "asn1-ber";
  //
  // return a low-level signing function, similar to createJWS approach
  // async webAuthnES256KSigner(credentialID: string) {
  //   return async (data: string | Uint8Array) => {
  //     // get signature from WebAuthn
  //     const signature = await this.generateWebAuthnSignature(data);
  //
  //     // This converts from the browser ArrayBuffer to a Node.js Buffer, which is a requirement for the asn1 library.
  //     const signatureBuffer = Buffer.from(signature);
  //     console.log("lower signature inside signer", signature);
  //     console.log("lower buffer signature inside signer", signatureBuffer);
  //     console.log("lower base64 buffer signature inside signer", signatureBuffer.toString("base64"));
  //     // Decode the DER-encoded signature to extract R and S values
  //     const reader = new asn1.BerReader(signatureBuffer);
  //     console.log("lower after reader");
  //     reader.readSequence();
  //     console.log("lower after read sequence");
  //     const r = reader.readString(asn1.Ber.Integer, true);
  //     console.log("lower after r");
  //     const s = reader.readString(asn1.Ber.Integer, true);
  //     console.log("lower after r & s");
  //
  //     // Ensure R and S are 32 bytes each
  //     const rBuffer = Buffer.from(r);
  //     const sBuffer = Buffer.from(s);
  //     console.log("lower after rBuffer & sBuffer", rBuffer, sBuffer);
  //     const rWithoutPrefix = rBuffer.length > 32 ? rBuffer.slice(1) : rBuffer;
  //     const sWithoutPrefix = sBuffer.length > 32 ? sBuffer.slice(1) : sBuffer;
  //     const rPadded =
  //       rWithoutPrefix.length < 32
  //         ? Buffer.concat([Buffer.alloc(32 - rWithoutPrefix.length), rBuffer])
  //         : rWithoutPrefix;
  //     const sPadded =
  //       rWithoutPrefix.length < 32
  //         ? Buffer.concat([Buffer.alloc(32 - sWithoutPrefix.length), sBuffer])
  //         : sWithoutPrefix;
  //
  //     // Concatenate R and S to form the 64-byte array (ECDSA signature format expected by JWT)
  //     const combinedSignature = Buffer.concat([rPadded, sPadded]);
  //     console.log(
  //       "lower combinedSignature",
  //       combinedSignature.length,
  //       combinedSignature,
  //     );
  //
  //     const combSig64 = combinedSignature.toString("base64");
  //     console.log("lower combSig64", combSig64);
  //     const combSig64Url = combSig64
  //       .replace(/\+/g, "-")
  //       .replace(/\//g, "_")
  //       .replace(/=+$/, "");
  //     console.log("lower combSig64Url", combSig64Url);
  //     return combSig64Url;
  //   };
  // }
}

export async function createDidPeerJwt(
  did: string,
  credIdHex: string,
  payload: object,
): Promise<string> {
  const peerSetup = new PeerSetup();
  const jwt = await peerSetup.createJwtNavigator(did, payload, credIdHex);
  return jwt;
}

// I'd love to use this but it doesn't verify.
// Requires:
// npm install @noble/curves
// ... and this import:
// import { p256 } from "@noble/curves/p256";
export async function verifyJwtP256(
  issuerDid: string,
  authenticatorData: ArrayBuffer,
  challenge: Uint8Array,
  signature: Base64URLString,
) {
  const authDataFromBase = Buffer.from(authenticatorData);
  const sigBuffer = Buffer.from(signature, "base64");
  const finalSigBuffer = unwrapEC2Signature(sigBuffer);
  const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid);

  // Use challenge in preimage construction
  const preimage = Buffer.concat([authDataFromBase, Buffer.from(challenge)]);

  const isValid = p256.verify(
    finalSigBuffer,
    new Uint8Array(preimage),
    publicKeyBytes,
  );
  return isValid;
}

export async function verifyJwtSimplewebauthn(
  credIdHex: string,
  issuerDid: string,
  authenticatorData: ArrayBuffer,
  challenge: Uint8Array,
  clientDataJsonBase64Url: Base64URLString,
  signature: Base64URLString,
) {
  const authData = arrayToBase64Url(Buffer.from(authenticatorData));
  const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid);
  const credId = arrayBufferToBase64URLString(
    Buffer.from(credIdHex, "hex").buffer,
  );
  const authOpts: VerifyAuthenticationResponseOpts = {
    authenticator: {
      credentialID: credId,
      credentialPublicKey: publicKeyBytes,
      counter: 0,
    },
    expectedChallenge: arrayToBase64Url(challenge),
    expectedOrigin: window.location.origin,
    expectedRPID: window.location.hostname,
    response: {
      authenticatorAttachment: "platform",
      clientExtensionResults: {},
      id: credId,
      rawId: credId,
      response: {
        authenticatorData: authData,
        clientDataJSON: clientDataJsonBase64Url,
        signature: signature,
      },
      type: "public-key",
    },
  };
  const verification = await verifyAuthenticationResponse(authOpts);
  return verification.verified;
}

// similar code is in endorser-ch util-crypto.ts verifyPeerSignature
export async function verifyJwtWebCrypto(
  issuerDid: string,
  authenticatorData: ArrayBuffer,
  challenge: Uint8Array,
  signature: Base64URLString,
) {
  const authDataFromBase = Buffer.from(authenticatorData);
  const sigBuffer = Buffer.from(signature, "base64");
  const finalSigBuffer = unwrapEC2Signature(sigBuffer);

  // Use challenge in preimage construction
  const preimage = Buffer.concat([authDataFromBase, Buffer.from(challenge)]);

  return verifyPeerSignature(preimage, issuerDid, finalSigBuffer);
}

// Remove unused functions:
// - peerDidToDidDocument
// - COSEtoPEM
// - base64urlDecodeArrayBuffer
// - base64urlEncodeArrayBuffer
// - pemToCryptoKey

// Keep only the used functions:
export function base64urlDecodeString(input: string) {
  return atob(input.replace(/-/g, "+").replace(/_/g, "/"));
}

export function base64urlEncodeString(input: string) {
  return btoa(input).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}

// from @simplewebauthn/browser
function arrayBufferToBase64URLString(buffer: ArrayBuffer) {
  const bytes = new Uint8Array(buffer);
  let str = "";
  for (const charCode of bytes) {
    str += String.fromCharCode(charCode);
  }
  const base64String = btoa(str);
  return base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}

// from @simplewebauthn/browser
function base64URLStringToArrayBuffer(base64URLString: string) {
  const base64 = base64URLString.replace(/-/g, "+").replace(/_/g, "/");
  const padLength = (4 - (base64.length % 4)) % 4;
  const padded = base64.padEnd(base64.length + padLength, "=");
  const binary = atob(padded);
  const buffer = new ArrayBuffer(binary.length);
  const bytes = new Uint8Array(buffer);
  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }
  return buffer;
}
