/**
 * EncryptID Key Derivation Module
 *
 * Derives application-specific cryptographic keys from WebAuthn PRF output
 * or passphrase fallback. This is Layer 2 of the EncryptID architecture.
 */

import { bufferToBase64url, base64urlToBuffer } from './webauthn';
import { deriveEOAFromPRF } from './eoa-derivation';
import { p256 } from '@noble/curves/p256';
import { ed25519 } from '@noble/curves/ed25519';

// ============================================================================
// TYPES
// ============================================================================

export interface DerivedKeys {
  /** AES-256-GCM key for file/data encryption */
  encryptionKey: CryptoKey;

  /** ECDSA P-256 key pair for signing */
  signingKeyPair: CryptoKeyPair;

  /** Ed25519 seed for DID key (raw bytes) */
  didSeed: Uint8Array;

  /** The DID identifier (did:key:z6Mk...) */
  did: string;

  /** Whether keys were derived from PRF (true) or passphrase (false) */
  fromPRF: boolean;

  /** secp256k1 private key for EOA wallet operations (derived from PRF only) */
  eoaPrivateKey?: Uint8Array;

  /** Ethereum address derived from the EOA private key */
  eoaAddress?: string;
}

export interface EncryptedData {
  ciphertext: ArrayBuffer;
  iv: Uint8Array;
  tag?: ArrayBuffer;  // For AEAD modes, tag is included in ciphertext
}

export interface SignedData {
  data: ArrayBuffer;
  signature: ArrayBuffer;
  publicKey: ArrayBuffer;
}

// ============================================================================
// KEY DERIVATION
// ============================================================================

/**
 * EncryptID Key Manager
 *
 * Handles derivation and management of all cryptographic keys
 * from a master secret (PRF output or passphrase-derived).
 */
export class EncryptIDKeyManager {
  private masterKey: CryptoKey | null = null;
  private derivedKeys: DerivedKeys | null = null;
  private fromPRF: boolean = false;
  private prfOutputRaw: Uint8Array | null = null;

  /**
   * Initialize from WebAuthn PRF output
   *
   * This is the preferred path - keys are derived directly from
   * the hardware-backed PRF extension output.
   */
  async initFromPRF(prfOutput: ArrayBuffer): Promise<void> {
    // Copy raw PRF output for EOA derivation (uses @noble/hashes directly)
    // Must copy — not just wrap — so clear() doesn't zero the caller's buffer
    this.prfOutputRaw = new Uint8Array(new Uint8Array(prfOutput));

    // Import PRF output as HKDF key material
    this.masterKey = await crypto.subtle.importKey(
      'raw',
      prfOutput,
      { name: 'HKDF' },
      false,  // Not extractable
      ['deriveKey', 'deriveBits']
    );

    this.fromPRF = true;
    this.derivedKeys = null;  // Clear any existing derived keys

    console.log('EncryptID: Key manager initialized from PRF output');
  }

  /**
   * Initialize from passphrase (fallback for non-PRF authenticators)
   *
   * Uses PBKDF2 with high iteration count to derive master key
   * from user's passphrase. The salt should be stored securely.
   */
  async initFromPassphrase(
    passphrase: string,
    salt: Uint8Array
  ): Promise<void> {
    const encoder = new TextEncoder();

    // Import passphrase as PBKDF2 key
    const passphraseKey = await crypto.subtle.importKey(
      'raw',
      encoder.encode(passphrase),
      { name: 'PBKDF2' },
      false,
      ['deriveBits']
    );

    // Derive master key material using PBKDF2
    // OWASP 2023 recommends 600,000 iterations for PBKDF2-SHA256
    const masterKeyMaterial = await crypto.subtle.deriveBits(
      {
        name: 'PBKDF2',
        salt: salt as BufferSource,
        iterations: 600000,
        hash: 'SHA-256',
      },
      passphraseKey,
      256  // 32 bytes
    );

    // Import as HKDF key for further derivation
    this.masterKey = await crypto.subtle.importKey(
      'raw',
      masterKeyMaterial,
      { name: 'HKDF' },
      false,
      ['deriveKey', 'deriveBits']
    );

    this.fromPRF = false;
    this.derivedKeys = null;

    console.log('EncryptID: Key manager initialized from passphrase');
  }

  /**
   * Generate a random salt for passphrase-based derivation
   */
  static generateSalt(): Uint8Array {
    return crypto.getRandomValues(new Uint8Array(32));
  }

  /**
   * Check if the key manager is initialized
   */
  isInitialized(): boolean {
    return this.masterKey !== null;
  }

  /**
   * Get or derive all application keys
   */
  async getKeys(): Promise<DerivedKeys> {
    if (!this.masterKey) {
      throw new Error('Key manager not initialized');
    }

    // Return cached keys if available
    if (this.derivedKeys) {
      return this.derivedKeys;
    }

    // Derive all keys
    const [encryptionKey, signingKeyPair, didSeed] = await Promise.all([
      this.deriveEncryptionKey(),
      this.deriveSigningKeyPair(),
      this.deriveDIDSeed(),
    ]);

    // Generate DID from seed
    const did = await this.generateDID(didSeed);

    // Derive EOA key (PRF-only — passphrase path doesn't get wallet keys)
    const eoa = this.deriveEOAKey();

    this.derivedKeys = {
      encryptionKey,
      signingKeyPair,
      didSeed,
      did,
      fromPRF: this.fromPRF,
      ...(eoa && { eoaPrivateKey: eoa.privateKey, eoaAddress: eoa.address }),
    };

    return this.derivedKeys;
  }

  /**
   * Derive AES-256-GCM encryption key
   */
  private async deriveEncryptionKey(): Promise<CryptoKey> {
    if (!this.masterKey) {
      throw new Error('Key manager not initialized');
    }

    const encoder = new TextEncoder();

    return crypto.subtle.deriveKey(
      {
        name: 'HKDF',
        hash: 'SHA-256',
        salt: encoder.encode('encryptid-encryption-key-v1'),
        info: encoder.encode('AES-256-GCM'),
      },
      this.masterKey,
      {
        name: 'AES-GCM',
        length: 256,
      },
      false,  // Not extractable
      ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']
    );
  }

  /**
   * Derive ECDSA P-256 signing key pair
   *
   * Note: WebCrypto doesn't support deterministic key generation,
   * so we derive a seed and use it to generate the key pair.
   * For production, consider using a library like @noble/curves.
   */
  private async deriveSigningKeyPair(): Promise<CryptoKeyPair> {
    if (!this.masterKey) {
      throw new Error('Key manager not initialized');
    }

    const encoder = new TextEncoder();

    // Derive seed for signing key
    const seed = await crypto.subtle.deriveBits(
      {
        name: 'HKDF',
        hash: 'SHA-256',
        salt: encoder.encode('encryptid-signing-key-v1'),
        info: encoder.encode('ECDSA-P256-seed'),
      },
      this.masterKey,
      256
    );

    // Derive deterministic P-256 key pair from seed using @noble/curves
    const seedBytes = new Uint8Array(seed);
    const publicKeyUncompressed = p256.getPublicKey(seedBytes, false); // 65 bytes: 04 || x || y

    // Import into WebCrypto as ECDSA key pair for sign/verify operations
    // PKCS8 wrapping for private key import
    const pkcs8 = buildP256Pkcs8(seedBytes);
    const privateKey = await crypto.subtle.importKey(
      'pkcs8',
      pkcs8,
      { name: 'ECDSA', namedCurve: 'P-256' },
      false,  // Not extractable
      ['sign']
    );

    const publicKey = await crypto.subtle.importKey(
      'raw',
      publicKeyUncompressed as BufferSource,
      { name: 'ECDSA', namedCurve: 'P-256' },
      true,
      ['verify']
    );

    return { privateKey, publicKey };
  }

  /**
   * Derive Ed25519 seed for DID key
   */
  private async deriveDIDSeed(): Promise<Uint8Array> {
    if (!this.masterKey) {
      throw new Error('Key manager not initialized');
    }

    const encoder = new TextEncoder();

    const seed = await crypto.subtle.deriveBits(
      {
        name: 'HKDF',
        hash: 'SHA-256',
        salt: encoder.encode('encryptid-did-key-v1'),
        info: encoder.encode('Ed25519-seed'),
      },
      this.masterKey,
      256
    );

    return new Uint8Array(seed);
  }

  /**
   * Generate did:key identifier from Ed25519 seed
   *
   * Format: did:key:z6Mk... (multicodec ed25519-pub + base58btc)
   */
  private async generateDID(seed: Uint8Array): Promise<string> {
    // Derive Ed25519 public key from seed using @noble/curves
    const publicKeyBytes = ed25519.getPublicKey(seed);

    // Multicodec prefix for Ed25519 public key: 0xed01
    const multicodecKey = new Uint8Array(34);
    multicodecKey[0] = 0xed;
    multicodecKey[1] = 0x01;
    multicodecKey.set(publicKeyBytes, 2);

    // Encode as base58btc (did:key spec requires 'z' prefix + base58btc)
    return `did:key:z${base58btcEncode(multicodecKey)}`;
  }

  /**
   * Derive secp256k1 EOA key from PRF output.
   *
   * Only available when initialized from PRF (not passphrase).
   * Uses @noble/curves + @noble/hashes for deterministic derivation
   * with domain-separated HKDF salt/info.
   */
  private deriveEOAKey(): { privateKey: Uint8Array; publicKey: Uint8Array; address: string } | null {
    if (!this.fromPRF || !this.prfOutputRaw) {
      return null;
    }

    const result = deriveEOAFromPRF(this.prfOutputRaw);
    console.log('EncryptID: EOA key derived, address:', result.address);
    return result;
  }

  /**
   * Export 32 bytes of raw key material for use with DocBridge.initFromKey().
   * Derives via HKDF with a unique info string so it doesn't collide with other derived keys.
   */
  async getKeyMaterial(): Promise<ArrayBuffer> {
    if (!this.masterKey) {
      throw new Error('Key manager not initialized');
    }
    const encoder = new TextEncoder();
    return crypto.subtle.deriveBits(
      {
        name: 'HKDF',
        hash: 'SHA-256',
        salt: encoder.encode('encryptid-doc-bridge-key-v1'),
        info: encoder.encode('DocBridge-256'),
      },
      this.masterKey,
      256
    );
  }

  /**
   * Clear all keys from memory
   */
  clear(): void {
    // Zero out sensitive key material
    if (this.derivedKeys?.eoaPrivateKey) {
      this.derivedKeys.eoaPrivateKey.fill(0);
    }
    if (this.derivedKeys?.didSeed) {
      this.derivedKeys.didSeed.fill(0);
    }
    if (this.prfOutputRaw) {
      this.prfOutputRaw.fill(0);
      this.prfOutputRaw = null;
    }

    this.masterKey = null;
    this.derivedKeys = null;
    this.fromPRF = false;
    console.log('EncryptID: Key manager cleared');
  }
}

// ============================================================================
// ENCRYPTION UTILITIES
// ============================================================================

/**
 * Encrypt data using the encryption key
 */
export async function encryptData(
  key: CryptoKey,
  data: ArrayBuffer | Uint8Array | string
): Promise<EncryptedData> {
  // Convert string to ArrayBuffer if needed
  let plaintext: ArrayBuffer;
  if (typeof data === 'string') {
    plaintext = new TextEncoder().encode(data).buffer;
  } else if (data instanceof Uint8Array) {
    plaintext = data.buffer as ArrayBuffer;
  } else {
    plaintext = data;
  }

  // Generate random IV
  const iv = crypto.getRandomValues(new Uint8Array(12));

  // Encrypt
  const ciphertext = await crypto.subtle.encrypt(
    {
      name: 'AES-GCM',
      iv: iv,
    },
    key,
    plaintext
  );

  return { ciphertext, iv };
}

/**
 * Decrypt data using the encryption key
 */
export async function decryptData(
  key: CryptoKey,
  encrypted: EncryptedData
): Promise<ArrayBuffer> {
  return crypto.subtle.decrypt(
    {
      name: 'AES-GCM',
      iv: encrypted.iv as BufferSource,
    },
    key,
    encrypted.ciphertext
  );
}

/**
 * Decrypt data and return as string
 */
export async function decryptDataAsString(
  key: CryptoKey,
  encrypted: EncryptedData
): Promise<string> {
  const plaintext = await decryptData(key, encrypted);
  return new TextDecoder().decode(plaintext);
}

// ============================================================================
// SIGNING UTILITIES
// ============================================================================

/**
 * Sign data using the signing key
 */
export async function signData(
  keyPair: CryptoKeyPair,
  data: ArrayBuffer | Uint8Array | string
): Promise<SignedData> {
  // Convert string to ArrayBuffer if needed
  let dataBuffer: ArrayBuffer;
  if (typeof data === 'string') {
    dataBuffer = new TextEncoder().encode(data).buffer;
  } else if (data instanceof Uint8Array) {
    dataBuffer = data.buffer as ArrayBuffer;
  } else {
    dataBuffer = data;
  }

  // Sign
  const signature = await crypto.subtle.sign(
    {
      name: 'ECDSA',
      hash: 'SHA-256',
    },
    keyPair.privateKey,
    dataBuffer
  );

  // Export public key for verification
  const publicKey = await crypto.subtle.exportKey('raw', keyPair.publicKey);

  return {
    data: dataBuffer,
    signature,
    publicKey,
  };
}

/**
 * Verify a signature
 */
export async function verifySignature(
  signed: SignedData
): Promise<boolean> {
  // Import public key
  const publicKey = await crypto.subtle.importKey(
    'raw',
    signed.publicKey,
    {
      name: 'ECDSA',
      namedCurve: 'P-256',
    },
    false,
    ['verify']
  );

  // Verify
  return crypto.subtle.verify(
    {
      name: 'ECDSA',
      hash: 'SHA-256',
    },
    publicKey,
    signed.signature,
    signed.data
  );
}

// ============================================================================
// KEY WRAPPING (FOR SHARING)
// ============================================================================

/**
 * Wrap a key for sharing with another user
 *
 * Used in rfiles to share encrypted files - wrap the file key
 * with the recipient's public key.
 */
export async function wrapKeyForRecipient(
  keyToWrap: CryptoKey,
  recipientPublicKey: CryptoKey
): Promise<ArrayBuffer> {
  return crypto.subtle.wrapKey(
    'raw',
    keyToWrap,
    recipientPublicKey,
    {
      name: 'RSA-OAEP',
    }
  );
}

/**
 * Unwrap a key that was shared with us
 */
export async function unwrapSharedKey(
  wrappedKey: ArrayBuffer,
  privateKey: CryptoKey
): Promise<CryptoKey> {
  return crypto.subtle.unwrapKey(
    'raw',
    wrappedKey,
    privateKey,
    {
      name: 'RSA-OAEP',
    },
    {
      name: 'AES-GCM',
      length: 256,
    },
    false,
    ['encrypt', 'decrypt']
  );
}

// ============================================================================
// HELPERS
// ============================================================================

/**
 * Build a PKCS#8 DER wrapper around a raw P-256 private key (32 bytes)
 * so it can be imported via WebCrypto's importKey('pkcs8', ...).
 *
 * Structure: SEQUENCE { version, AlgorithmIdentifier { ecPublicKey, P-256 }, OCTET STRING { SEQUENCE { version, privateKey } } }
 */
function buildP256Pkcs8(rawPrivateKey: Uint8Array): ArrayBuffer {
  // DER-encoded PKCS#8 template for P-256 with a 32-byte private key
  // Pre-computed ASN.1 header (version + algorithm identifier)
  const header = new Uint8Array([
    0x30, 0x41,       // SEQUENCE (65 bytes total)
    0x02, 0x01, 0x00, // INTEGER version = 0
    0x30, 0x13,       // SEQUENCE (AlgorithmIdentifier)
    0x06, 0x07,       //   OID 1.2.840.10045.2.1 (ecPublicKey)
    0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01,
    0x06, 0x08,       //   OID 1.2.840.10045.3.1.7 (P-256)
    0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07,
    0x04, 0x27,       // OCTET STRING (39 bytes)
    0x30, 0x25,       //   SEQUENCE (37 bytes)
    0x02, 0x01, 0x01, //     INTEGER version = 1
    0x04, 0x20,       //     OCTET STRING (32 bytes) — the private key
  ]);

  const result = new Uint8Array(header.length + 32);
  result.set(header);
  result.set(rawPrivateKey, header.length);
  return result.buffer;
}

/**
 * Base58btc encoding (Bitcoin alphabet).
 * Minimal implementation — no external dependency needed.
 */
const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';

function base58btcEncode(bytes: Uint8Array): string {
  // Count leading zeros
  let zeroes = 0;
  for (const b of bytes) {
    if (b !== 0) break;
    zeroes++;
  }

  // Convert to base58 using bigint arithmetic
  let num = BigInt(0);
  for (const b of bytes) {
    num = num * 256n + BigInt(b);
  }

  let encoded = '';
  while (num > 0n) {
    const remainder = Number(num % 58n);
    num = num / 58n;
    encoded = BASE58_ALPHABET[remainder] + encoded;
  }

  // Prepend '1' for each leading zero byte
  return '1'.repeat(zeroes) + encoded;
}

// ============================================================================
// SINGLETON INSTANCE
// ============================================================================

// Global key manager instance
let keyManagerInstance: EncryptIDKeyManager | null = null;

/**
 * Get the global EncryptID key manager instance
 */
export function getKeyManager(): EncryptIDKeyManager {
  if (!keyManagerInstance) {
    keyManagerInstance = new EncryptIDKeyManager();
  }
  return keyManagerInstance;
}

/**
 * Reset the global key manager (e.g., on logout)
 */
export function resetKeyManager(): void {
  if (keyManagerInstance) {
    keyManagerInstance.clear();
    keyManagerInstance = null;
  }
}
