import { IIdentifier } from "@veramo/core";
import { getRandomBytesSync } from "ethereum-cryptography/random";
import { entropyToMnemonic } from "ethereum-cryptography/bip39";
import { wordlist } from "ethereum-cryptography/bip39/wordlists/english";
import { HDNode } from "@ethersproject/hdnode";

import {
  CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
  createEndorserJwtForDid,
  CONTACT_URL_PATH_ENDORSER_CH_OLD,
  CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI,
  CONTACT_CONFIRM_URL_PATH_TIME_SAFARI,
} from "../../libs/endorserServer";
import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup";
import { logger } from "../../utils/logger";

export const DEFAULT_ROOT_DERIVATION_PATH = "m/84737769'/0'/0'/0'";

export const LOCAL_KMS_NAME = "local";

/**
 *
 *
 * @param {string} address
 * @param {string} publicHex
 * @param {string} privateHex
 * @param {string} derivationPath
 * @return {*}  {Omit<IIdentifier, 'provider'>}
 */
export const newIdentifier = (
  address: string,
  publicHex: string,
  privateHex: string,
  derivationPath: string,
): IIdentifier => {
  return {
    did: DEFAULT_DID_PROVIDER_NAME + ":" + address,
    keys: [
      {
        kid: publicHex,
        kms: LOCAL_KMS_NAME,
        meta: { derivationPath: derivationPath },
        privateKeyHex: privateHex,
        publicKeyHex: publicHex,
        type: "Secp256k1",
      },
    ],
    provider: DEFAULT_DID_PROVIDER_NAME,
    services: [],
  };
};

/**
 *
 *
 * @param {string} mnemonic
 * @return {[string, string, string, string]} address, privateHex, publicHex, derivationPath
 */
export const deriveAddress = (
  mnemonic: string,
  derivationPath: string = DEFAULT_ROOT_DERIVATION_PATH,
): [string, string, string, string] => {
  mnemonic = mnemonic.trim().toLowerCase();

  const hdnode: HDNode = HDNode.fromMnemonic(mnemonic);
  const rootNode: HDNode = hdnode.derivePath(derivationPath);
  const privateHex = rootNode.privateKey.substring(2); // original starts with '0x'
  const publicHex = rootNode.publicKey.substring(2); // original starts with '0x'
  const address = rootNode.address;

  return [address, privateHex, publicHex, derivationPath];
};

export const generateRandomBytes = (numBytes: number): Uint8Array => {
  return getRandomBytesSync(numBytes);
};

/**
 *
 *
 * @return {*}  {string}
 */
export const generateSeed = (): string => {
  const entropy: Uint8Array = getRandomBytesSync(32);
  const mnemonic = entropyToMnemonic(entropy, wordlist);

  return mnemonic;
};

/**
 * Retrieve an access token, or "" if no DID is provided.
 *
 * @param {string} did
 * @return {string} JWT with basic payload
 */
export const accessToken = async (did?: string) => {
  if (did) {
    const nowEpoch = Math.floor(Date.now() / 1000);
    const endEpoch = nowEpoch + 60; // add one minute
    const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did };
    return createEndorserJwtForDid(did, tokenPayload);
  } else {
    return "";
  }
};

/**
 * Extract JWT from various URL formats
 * @param jwtUrlText The URL containing the JWT
 * @returns The extracted JWT or null if not found
 */
export const getContactJwtFromJwtUrl = (jwtUrlText: string) => {
  try {
    let jwtText = jwtUrlText;

    // Try to extract JWT from URL paths
    const paths = [
      CONTACT_CONFIRM_URL_PATH_TIME_SAFARI,
      CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
      CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI,
      CONTACT_URL_PATH_ENDORSER_CH_OLD,
    ];

    for (const path of paths) {
      const pathIndex = jwtText.indexOf(path);
      if (pathIndex > -1) {
        jwtText = jwtText.substring(pathIndex + path.length);
        break;
      }
    }

    // Validate JWT format
    if (!jwtText.match(/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/)) {
      logger.error("Invalid JWT format in URL:", jwtUrlText);
      return null;
    }

    return jwtText;
  } catch (error) {
    logger.error("Error extracting JWT from URL:", error);
    return null;
  }
};

export const nextDerivationPath = (origDerivPath: string) => {
  let lastStr = origDerivPath.split("/").slice(-1)[0];
  if (lastStr.endsWith("'")) {
    lastStr = lastStr.slice(0, -1);
  }
  const lastNum = parseInt(lastStr, 10);
  const newLastNum = lastNum + 1;
  const newLastStr = newLastNum.toString() + (lastStr.endsWith("'") ? "'" : "");
  const newDerivPath = origDerivPath
    .split("/")
    .slice(0, -1)
    .concat([newLastStr])
    .join("/");
  return newDerivPath;
};

// Base64 encoding/decoding utilities for browser
export function base64ToArrayBuffer(base64: string): Uint8Array {
  const binaryString = atob(base64);
  const bytes = new Uint8Array(binaryString.length);
  for (let i = 0; i < binaryString.length; i++) {
    bytes[i] = binaryString.charCodeAt(i);
  }
  return bytes;
}

export function arrayBufferToBase64(buffer: ArrayBuffer): string {
  const binary = String.fromCharCode(...new Uint8Array(buffer));
  return btoa(binary);
}

const SALT_LENGTH = 16;
const IV_LENGTH = 12;
const KEY_LENGTH = 256;
const ITERATIONS = 100000;

// Message encryption helper function, used for onboarding meeting messages
export async function encryptMessage(message: string, password: string) {
  const encoder = new TextEncoder();
  const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
  const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));

  // Derive key from password using PBKDF2
  const keyMaterial = await crypto.subtle.importKey(
    "raw",
    encoder.encode(password),
    "PBKDF2",
    false,
    ["deriveBits", "deriveKey"],
  );

  const key = await crypto.subtle.deriveKey(
    {
      name: "PBKDF2",
      salt,
      iterations: ITERATIONS,
      hash: "SHA-256",
    },
    keyMaterial,
    { name: "AES-GCM", length: KEY_LENGTH },
    false,
    ["encrypt"],
  );

  // Encrypt the message
  const encryptedContent = await crypto.subtle.encrypt(
    {
      name: "AES-GCM",
      iv,
    },
    key,
    encoder.encode(message),
  );

  // Return a JSON structure with base64-encoded components
  const result = {
    salt: arrayBufferToBase64(salt),
    iv: arrayBufferToBase64(iv),
    encrypted: arrayBufferToBase64(encryptedContent),
  };

  return btoa(JSON.stringify(result));
}

// Message decryption helper function, used for onboarding meeting messages
export async function decryptMessage(encryptedJson: string, password: string) {
  const decoder = new TextDecoder();
  const { salt, iv, encrypted } = JSON.parse(atob(encryptedJson));

  // Convert base64 components back to Uint8Arrays
  const saltArray = base64ToArrayBuffer(salt);
  const ivArray = base64ToArrayBuffer(iv);
  const encryptedContent = base64ToArrayBuffer(encrypted);

  // Derive the same key using PBKDF2 with the extracted salt
  const keyMaterial = await crypto.subtle.importKey(
    "raw",
    new TextEncoder().encode(password),
    "PBKDF2",
    false,
    ["deriveBits", "deriveKey"],
  );

  const key = await crypto.subtle.deriveKey(
    {
      name: "PBKDF2",
      salt: saltArray,
      iterations: ITERATIONS,
      hash: "SHA-256",
    },
    keyMaterial,
    { name: "AES-GCM", length: KEY_LENGTH },
    false,
    ["decrypt"],
  );

  // Decrypt the content
  const decryptedContent = await crypto.subtle.decrypt(
    {
      name: "AES-GCM",
      iv: ivArray,
    },
    key,
    encryptedContent,
  );

  // Convert the decrypted content back to a string
  return decoder.decode(decryptedContent);
}

// Test function to verify encryption/decryption
export async function testMessageEncryptionDecryption() {
  try {
    const testMessage = "Hello, this is a test message! 🚀";
    const testPassword = "myTestPassword123";

    logger.log("Original message:", testMessage);

    // Test encryption
    logger.log("Encrypting...");
    const encrypted = await encryptMessage(testMessage, testPassword);
    logger.log("Encrypted result:", encrypted);

    // Test decryption
    logger.log("Decrypting...");
    const decrypted = await decryptMessage(encrypted, testPassword);
    logger.log("Decrypted result:", decrypted);

    // Verify
    const success = testMessage === decrypted;
    logger.log("Test " + (success ? "PASSED ✅" : "FAILED ❌"));
    logger.log("Messages match:", success);

    // Test with wrong password
    logger.log("\nTesting with wrong password...");
    try {
      await decryptMessage(encrypted, "wrongPassword");
      logger.log("Incorrectly decrypted with wrong password ❌");
    } catch (error) {
      logger.log("Correctly failed to decrypt with wrong password ✅");
    }

    return success;
  } catch (error) {
    logger.error("Test failed with error:", error);
    return false;
  }
}

// Simple encryption using Node's crypto, used for the initial encryption of the identity and mnemonic
export async function simpleEncrypt(
  text: string,
  secret: ArrayBuffer,
): Promise<ArrayBuffer> {
  const iv = crypto.getRandomValues(new Uint8Array(16));

  // Derive a 256-bit key from the secret using SHA-256
  const keyData = await crypto.subtle.digest("SHA-256", secret);
  const key = await crypto.subtle.importKey(
    "raw",
    keyData,
    { name: "AES-GCM" },
    false,
    ["encrypt"],
  );

  const encrypted = await crypto.subtle.encrypt(
    { name: "AES-GCM", iv },
    key,
    new TextEncoder().encode(text),
  );

  // Combine IV and encrypted data
  const result = new Uint8Array(iv.length + encrypted.byteLength);
  result.set(iv);
  result.set(new Uint8Array(encrypted), iv.length);

  return result.buffer;
}

// Simple decryption using Node's crypto, used for the default decryption of identity and mnemonic
export async function simpleDecrypt(
  encryptedText: ArrayBuffer,
  secret: ArrayBuffer,
): Promise<string> {
  const data = new Uint8Array(encryptedText);

  // Extract IV and encrypted data
  const iv = data.slice(0, 16);
  const encrypted = data.slice(16);

  // Derive the same 256-bit key from the secret using SHA-256
  const keyData = await crypto.subtle.digest("SHA-256", secret);
  const key = await crypto.subtle.importKey(
    "raw",
    keyData,
    { name: "AES-GCM" },
    false,
    ["decrypt"],
  );

  const decrypted = await crypto.subtle.decrypt(
    { name: "AES-GCM", iv },
    key,
    encrypted,
  );

  return new TextDecoder().decode(decrypted);
}

// Test function for simple encryption/decryption
export async function testSimpleEncryptionDecryption() {
  try {
    const testMessage = "Hello, this is a test message! 🚀";
    const testSecret = crypto.getRandomValues(new Uint8Array(32));

    logger.log("Original message:", testMessage);

    // Test encryption
    logger.log("Encrypting...");
    const encrypted = await simpleEncrypt(testMessage, testSecret);
    const encryptedBase64 = arrayBufferToBase64(encrypted);
    logger.log("Encrypted result:", encryptedBase64);

    // Test decryption
    logger.log("Decrypting...");
    const encryptedArrayBuffer = base64ToArrayBuffer(encryptedBase64);
    const decrypted = await simpleDecrypt(encryptedArrayBuffer, testSecret);
    logger.log("Decrypted result:", decrypted);

    // Verify
    const success = testMessage === decrypted;
    logger.log("Test " + (success ? "PASSED ✅" : "FAILED ❌"));
    logger.log("Messages match:", success);

    // Test with wrong secret
    logger.log("\nTesting with wrong secret...");
    try {
      await simpleDecrypt(encryptedArrayBuffer, new Uint8Array(32));
      logger.log("Incorrectly decrypted with wrong secret ❌");
    } catch (error) {
      logger.log("Correctly failed to decrypt with wrong secret ✅");
    }

    return success;
  } catch (error) {
    logger.error("Test failed with error:", error);
    return false;
  }
}
