/**
 * Verifiable Credential & DID functions, specifically for EndorserSearch.org tools
 *
 * The goal is to make this folder similar across projects, then move it to a library.
 * Other projects: endorser-ch, image-api
 *
 */

import { Buffer } from "buffer/";
import * as didJwt from "did-jwt";
import { JWTVerified } from "did-jwt";
import { Resolver } from "did-resolver";
import { IIdentifier } from "@veramo/core";
import * as u8a from "uint8arrays";

import { didEthLocalResolver } from "./did-eth-local-resolver";
import { PEER_DID_PREFIX, verifyPeerSignature } from "./didPeer";
import { base64urlDecodeString, createDidPeerJwt } from "./passkeyDidPeer";
import { urlBase64ToUint8Array } from "./util";
import { KeyMeta, KeyMetaWithPrivate } from "../../../interfaces/common";

export const ETHR_DID_PREFIX = "did:ethr:";
export const JWT_VERIFY_FAILED_CODE = "JWT_VERIFY_FAILED";
export const UNSUPPORTED_DID_METHOD_CODE = "UNSUPPORTED_DID_METHOD";

const ethLocalResolver = new Resolver({ ethr: didEthLocalResolver });

/**
 * Tell whether a key is from a passkey
 * @param keyMeta contains info about the key, whose passkeyCredIdHex determines if the key is from a passkey
 */
export function isFromPasskey(keyMeta?: KeyMeta): boolean {
  return !!keyMeta?.passkeyCredIdHex;
}

export async function createEndorserJwtForKey(
  account: KeyMetaWithPrivate,
  payload: object,
  expiresIn?: number,
) {
  if (account?.identity) {
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    const identity: IIdentifier = JSON.parse(account.identity!);
    const privateKeyHex = identity.keys[0].privateKeyHex;
    const signer = await SimpleSigner(privateKeyHex as string);
    const options = {
      // alg: "ES256K", // "K" is the default, "K-R" is used by the server in tests
      issuer: account.did,
      signer: signer,
      expiresIn: undefined as number | undefined,
    };
    if (expiresIn) {
      options.expiresIn = expiresIn;
    }
    return didJwt.createJWT(payload, options);
  } else if (account?.passkeyCredIdHex) {
    return createDidPeerJwt(account.did, account.passkeyCredIdHex, payload);
  } else {
    throw new Error("No identity data found to sign for DID " + account.did);
  }
}

/**
 *  Copied out of did-jwt since it's deprecated in that library.
 *
 *  The SimpleSigner returns a configured function for signing data.
 *
 *  @example
 *  const signer = SimpleSigner(privateKeyHexString)
 *  signer(data, (err, signature) => {
 *    ...
 *  })
 *
 *  @param    {String}         hexPrivateKey    a hex encoded private key
 *  @return   {Function}                     a configured signer function
 */
function SimpleSigner(hexPrivateKey: string): didJwt.Signer {
  const signer = didJwt.ES256KSigner(didJwt.hexToBytes(hexPrivateKey), true);
  return async (data) => {
    const signature = (await signer(data)) as string;
    return fromJose(signature);
  };
}

// from did-jwt/util; see SimpleSigner above
function fromJose(signature: string): {
  r: string;
  s: string;
  recoveryParam?: number;
} {
  const signatureBytes: Uint8Array = didJwt.base64ToBytes(signature);
  if (signatureBytes.length < 64 || signatureBytes.length > 65) {
    throw new TypeError(
      `Wrong size for signature. Expected 64 or 65 bytes, but got ${signatureBytes.length}`,
    );
  }
  const r = bytesToHex(signatureBytes.slice(0, 32));
  const s = bytesToHex(signatureBytes.slice(32, 64));
  const recoveryParam =
    signatureBytes.length === 65 ? signatureBytes[64] : undefined;
  return { r, s, recoveryParam };
}

// from did-jwt/util; see SimpleSigner above
function bytesToHex(b: Uint8Array): string {
  return u8a.toString(b, "base16");
}

// We should be calling 'verify' in more places, showing warnings if it fails.
// @returns JWTDecoded with { header: JWTHeader, payload: any, signature: string, data: string } (but doesn't verify the signature)
export function decodeEndorserJwt(jwt: string) {
  return didJwt.decodeJWT(jwt);
}

// return Promise of at least { issuer, payload, verified boolean }
// ... and also if successfully verified by did-jwt (not JWANT): data, doc, signature, signer
export async function decodeAndVerifyJwt(
  jwt: string,
): Promise<Omit<JWTVerified, "didResolutionResult" | "signer" | "jwt">> {
  const pieces = jwt.split(".");
  const header = JSON.parse(base64urlDecodeString(pieces[0]));
  const payload = JSON.parse(base64urlDecodeString(pieces[1]));
  const issuerDid = payload.iss;
  if (!issuerDid) {
    return Promise.reject({
      clientError: {
        message: `Missing "iss" field in JWT.`,
      },
    });
  }

  if (issuerDid.startsWith(ETHR_DID_PREFIX)) {
    try {
      const verified = await didJwt.verifyJWT(jwt, {
        resolver: ethLocalResolver,
      });
      return verified;
    } catch (e: unknown) {
      return Promise.reject({
        clientError: {
          // eslint-disable-next-line @typescript-eslint/ban-ts-comment
          // @ts-expect-error
          message: `JWT failed verification: ` + e.toString(),
          code: JWT_VERIFY_FAILED_CODE,
        },
      });
    }
  }

  if (issuerDid.startsWith(PEER_DID_PREFIX) && header.typ === "JWANT") {
    const verified = await verifyPeerSignature(
      Buffer.from(payload),
      issuerDid,
      urlBase64ToUint8Array(pieces[2]),
    );
    if (!verified) {
      return Promise.reject({
        clientError: {
          // eslint-disable-next-line @typescript-eslint/ban-ts-comment
          // @ts-expect-error
          message: `JWT failed verification: ` + e.toString(),
          code: JWT_VERIFY_FAILED_CODE,
        },
      });
    } else {
      return { issuer: issuerDid, payload: payload, verified: true };
    }
  }

  if (issuerDid.startsWith(PEER_DID_PREFIX)) {
    return Promise.reject({
      clientError: {
        message: `JWT with a PEER DID currently only supported with typ == JWANT. Contact us us for JWT suport since it should be straightforward.`,
      },
    });
  }

  return Promise.reject({
    clientError: {
      message: `Unsupported DID method ${issuerDid}`,
      code: UNSUPPORTED_DID_METHOD_CODE,
    },
  });
}
