interface CredentialKeyMaterial {
  algorithm: string;
  expiresAt: string;
  fingerprint: string;
  keyId: string;
  nonce: string;
  publicKey: string;
  transitions?: CredentialKeyTransition[];
}

interface CredentialKeyPin {
  fingerprint: string;
  keyId: string;
  publicKey: string;
}

interface CredentialKeyTransition {
  fromKeyId: string;
  payload: string;
  signature: string;
  toKeyId: string;
}

interface TransitionPayload {
  newFingerprint: string;
  newKeyId: string;
  newPublicKey: string;
  oldKeyId: string;
}

interface EncryptedCredentialEnvelope {
  ciphertext: string;
  encryptedKey: string;
  iv: string;
  keyId: string;
  nonce: string;
}

interface CredentialCryptoEngine {
  encryptPayload(
    material: CredentialKeyMaterial,
    plaintext: string,
  ): Promise<EncryptedCredentialEnvelope>;
  verifyTransition(
    publicKeyPem: string,
    transition: CredentialKeyTransition,
  ): Promise<boolean>;
}

const PIN_STORAGE_KEY = 'easyflow:credential-key-pin:v1';
const SECURITY_ERROR = '登录安全校验失败，请刷新或联系管理员';
let forgeModulePromise: Promise<any> | null = null;

const webCryptoEngine: CredentialCryptoEngine = {
  async encryptPayload(material, plaintext) {
    const aesKey = await window.crypto.subtle.generateKey(
      { length: 256, name: 'AES-GCM' },
      true,
      ['encrypt'],
    );
    const iv = window.crypto.getRandomValues(new Uint8Array(12));
    const ciphertext = await window.crypto.subtle.encrypt(
      { iv, name: 'AES-GCM' },
      aesKey,
      stringToUtf8Bytes(plaintext),
    );
    const rawAesKey = await window.crypto.subtle.exportKey('raw', aesKey);
    const publicKey = await importRsaPublicKey(material.publicKey, ['encrypt']);
    const encryptedKey = await window.crypto.subtle.encrypt(
      { name: 'RSA-OAEP' },
      publicKey,
      rawAesKey,
    );
    return {
      ciphertext: bytesToBase64(new Uint8Array(ciphertext)),
      encryptedKey: bytesToBase64(new Uint8Array(encryptedKey)),
      iv: bytesToBase64(iv),
      keyId: material.keyId,
      nonce: material.nonce,
    };
  },

  async verifyTransition(publicKeyPem, transition) {
    const publicKey = await importSignPublicKey(publicKeyPem);
    return window.crypto.subtle.verify(
      { name: 'RSASSA-PKCS1-v1_5' },
      publicKey,
      base64ToBytes(transition.signature),
      stringToUtf8Bytes(transition.payload),
    );
  },
};

const forgeCryptoEngine: CredentialCryptoEngine = {
  async encryptPayload(material, plaintext) {
    const forge = await loadForge();
    const aesKey = forge.random.getBytesSync(32);
    const iv = forge.random.getBytesSync(12);
    const cipher = forge.cipher.createCipher('AES-GCM', aesKey);
    cipher.start({ iv, tagLength: 128 });
    cipher.update(forge.util.createBuffer(plaintext, 'utf8'));
    cipher.finish();
    const ciphertextWithTag = cipher.output.getBytes() + cipher.mode.tag.getBytes();
    const publicKey = forge.pki.publicKeyFromPem(material.publicKey);
    const encryptedKey = publicKey.encrypt(aesKey, 'RSA-OAEP', {
      md: forge.md.sha256.create(),
      mgf1: {
        md: forge.md.sha256.create(),
      },
    });
    return {
      ciphertext: binaryToBase64(ciphertextWithTag),
      encryptedKey: binaryToBase64(encryptedKey),
      iv: binaryToBase64(iv),
      keyId: material.keyId,
      nonce: material.nonce,
    };
  },

  async verifyTransition(publicKeyPem, transition) {
    const forge = await loadForge();
    const publicKey = forge.pki.publicKeyFromPem(publicKeyPem);
    const md = forge.md.sha256.create();
    md.update(transition.payload, 'utf8');
    return publicKey.verify(
      md.digest().bytes(),
      forge.util.decode64(transition.signature),
    );
  },
};

/**
 * 加密密码类请求载荷。
 * @param loadMaterial 获取后端公钥材料的方法
 * @param payload 待加密载荷
 * @returns 加密信封
 */
export async function encryptCredentialPayload(
  loadMaterial: () => Promise<CredentialKeyMaterial>,
  payload: Record<string, any>,
) {
  const material = await loadMaterial();
  const engine = getCredentialCryptoEngine();
  try {
    await verifyAndPersistPin(material, engine);
    return await engine.encryptPayload(
      material,
      JSON.stringify({
        ...payload,
        nonce: material.nonce,
        timestamp: Date.now(),
      }),
    );
  } catch (error) {
    if (error instanceof Error && error.message === SECURITY_ERROR) {
      throw error;
    }
    throw new Error(SECURITY_ERROR);
  }
}

function getCredentialCryptoEngine() {
  return isWebCryptoAvailable() ? webCryptoEngine : forgeCryptoEngine;
}

function isWebCryptoAvailable() {
  return Boolean(window.crypto?.subtle && window.crypto.getRandomValues);
}

async function loadForge() {
  forgeModulePromise ||= import('node-forge');
  const forgeModule = await forgeModulePromise;
  return forgeModule.default || forgeModule;
}

async function verifyAndPersistPin(
  material: CredentialKeyMaterial,
  engine: CredentialCryptoEngine,
) {
  const current = readPin();
  if (!current) {
    writePin(material);
    return;
  }
  if (current.fingerprint === material.fingerprint) {
    writePin(material);
    return;
  }
  const nextPin = await verifyTransitionChain(current, material, engine);
  if (!nextPin || nextPin.fingerprint !== material.fingerprint) {
    throw new Error(SECURITY_ERROR);
  }
  writePin(material);
}

async function verifyTransitionChain(
  current: CredentialKeyPin,
  material: CredentialKeyMaterial,
  engine: CredentialCryptoEngine,
) {
  let cursor = { ...current };
  const transitions = material.transitions || [];
  const visited = new Set<string>();
  while (cursor.fingerprint !== material.fingerprint) {
    if (visited.has(cursor.keyId)) {
      return null;
    }
    visited.add(cursor.keyId);
    const transition = transitions.find((item) => item.fromKeyId === cursor.keyId);
    if (!transition) {
      return null;
    }
    const payload = decodeTransitionPayload(transition.payload);
    if (
      payload.oldKeyId !== cursor.keyId ||
      payload.newKeyId !== transition.toKeyId
    ) {
      return null;
    }
    const verified = await safeVerifyTransition(engine, cursor.publicKey, transition);
    if (!verified) {
      return null;
    }
    cursor = {
      fingerprint: payload.newFingerprint,
      keyId: payload.newKeyId,
      publicKey: payload.newPublicKey,
    };
  }
  return cursor;
}

async function safeVerifyTransition(
  engine: CredentialCryptoEngine,
  publicKey: string,
  transition: CredentialKeyTransition,
) {
  try {
    return await engine.verifyTransition(publicKey, transition);
  } catch {
    return false;
  }
}

function decodeTransitionPayload(payload: string): TransitionPayload {
  const json = utf8BytesToString(base64UrlToBytes(payload));
  return JSON.parse(json) as TransitionPayload;
}

async function importRsaPublicKey(publicKeyPem: string, usages: KeyUsage[]) {
  return window.crypto.subtle.importKey(
    'spki',
    pemToBytes(publicKeyPem),
    { hash: 'SHA-256', name: 'RSA-OAEP' },
    false,
    usages,
  );
}

async function importSignPublicKey(publicKeyPem: string) {
  return window.crypto.subtle.importKey(
    'spki',
    pemToBytes(publicKeyPem),
    { hash: 'SHA-256', name: 'RSASSA-PKCS1-v1_5' },
    false,
    ['verify'],
  );
}

function readPin(): CredentialKeyPin | null {
  const raw = window.localStorage.getItem(PIN_STORAGE_KEY);
  if (!raw) {
    return null;
  }
  try {
    return JSON.parse(raw) as CredentialKeyPin;
  } catch {
    window.localStorage.removeItem(PIN_STORAGE_KEY);
    return null;
  }
}

function writePin(material: CredentialKeyMaterial) {
  window.localStorage.setItem(
    PIN_STORAGE_KEY,
    JSON.stringify({
      fingerprint: material.fingerprint,
      keyId: material.keyId,
      publicKey: material.publicKey,
    }),
  );
}

function pemToBytes(pem: string) {
  const base64 = pem
    .replace('-----BEGIN PUBLIC KEY-----', '')
    .replace('-----END PUBLIC KEY-----', '')
    .replace(/\s/g, '');
  return base64ToBytes(base64);
}

function base64ToBytes(value: string) {
  return binaryToBytes(base64ToBinary(value));
}

function base64ToBinary(value: string) {
  return window.atob(value);
}

function binaryToBytes(binary: string) {
  const bytes = new Uint8Array(binary.length);
  for (let index = 0; index < binary.length; index += 1) {
    bytes[index] = binary.charCodeAt(index);
  }
  return bytes;
}

function base64UrlToBytes(value: string) {
  const normalized = value.replace(/-/g, '+').replace(/_/g, '/');
  let padded = normalized;
  while (padded.length % 4 !== 0) {
    padded += '=';
  }
  return base64ToBytes(padded);
}

function bytesToBase64(bytes: Uint8Array) {
  return binaryToBase64(bytesToBinary(bytes));
}

function bytesToBinary(bytes: Uint8Array) {
  let binary = '';
  for (let index = 0; index < bytes.length; index += 1) {
    binary += String.fromCharCode(bytes[index] ?? 0);
  }
  return binary;
}

function binaryToBase64(binary: string) {
  return window.btoa(binary);
}

function stringToUtf8Bytes(value: string) {
  return new TextEncoder().encode(value);
}

function utf8BytesToString(bytes: Uint8Array) {
  return new TextDecoder().decode(bytes);
}

export type { CredentialKeyMaterial };
