import { Injectable } from '@angular/core';

/**
 * Manages the browser-local Ed25519 keypair used to sign admin auth
 * challenges. Real WebCrypto Ed25519 (RFC 8032 support landed in evergreen
 * browsers) - not a placeholder. The private key is generated
 * non-extractable and kept only in IndexedDB as a CryptoKey handle; it is
 * never serialized, never sent anywhere, and cannot be exported by design.
 *
 * Registering `publicKey` with an admin's account (associating it with a
 * role) is a backend-side, out-of-band operation (e.g. an Owner approving a
 * new admin's public key) - entirely outside this frontend's scope.
 */
const DB_NAME = 'admin-auth-ed25519';
const DB_VERSION = 1;
const STORE_NAME = 'keypair';
const KEY_RECORD_ID = 'device-keypair';

interface StoredKeyPair {
  id: string;
  publicKey: CryptoKey;
  privateKey: CryptoKey;
  publicKeyBase64: string;
}

@Injectable({ providedIn: 'root' })
export class Ed25519KeypairService {
  private cached: StoredKeyPair | null = null;

  isSupported(): boolean {
    return typeof crypto !== 'undefined' && !!crypto.subtle && typeof indexedDB !== 'undefined';
  }

  /** Returns the device's Ed25519 keypair, generating and persisting one on first use. */
  async getOrCreateKeyPair(): Promise<{ publicKeyBase64: string }> {
    if (!this.isSupported()) {
      throw new Error('Ed25519 is not supported in this browser (requires WebCrypto + IndexedDB).');
    }
    const existing = await this.loadFromStore();
    if (existing) {
      this.cached = existing;
      return { publicKeyBase64: existing.publicKeyBase64 };
    }

    const generated = await this.generateAndPersist();
    this.cached = generated;
    return { publicKeyBase64: generated.publicKeyBase64 };
  }

  async sign(message: string): Promise<string> {
    const keyPair = this.cached ?? (await this.loadFromStore());
    if (!keyPair) {
      throw new Error('No Ed25519 keypair available - call getOrCreateKeyPair() first.');
    }

    const signatureBuffer = await crypto.subtle.sign('Ed25519', keyPair.privateKey, new TextEncoder().encode(message));
    return this.toBase64(new Uint8Array(signatureBuffer));
  }

  /** Discards the local keypair (e.g. "forget this device"). A new keypair on next login requires re-registration with the backend. */
  async clear(): Promise<void> {
    this.cached = null;
    const db = await this.openDatabase();
    await new Promise<void>((resolve, reject) => {
      const tx = db.transaction(STORE_NAME, 'readwrite');
      tx.objectStore(STORE_NAME).delete(KEY_RECORD_ID);
      tx.oncomplete = () => resolve();
      tx.onerror = () => reject(tx.error);
    });
  }

  private async generateAndPersist(): Promise<StoredKeyPair> {
    const keyPair = (await crypto.subtle.generateKey({ name: 'Ed25519' }, false, ['sign', 'verify'])) as CryptoKeyPair;
    const publicKeyRaw = await crypto.subtle.exportKey('raw', keyPair.publicKey);
    const publicKeyBase64 = this.toBase64(new Uint8Array(publicKeyRaw));

    const record: StoredKeyPair = {
      id: KEY_RECORD_ID,
      publicKey: keyPair.publicKey,
      privateKey: keyPair.privateKey,
      publicKeyBase64
    };

    const db = await this.openDatabase();
    await new Promise<void>((resolve, reject) => {
      const tx = db.transaction(STORE_NAME, 'readwrite');
      tx.objectStore(STORE_NAME).put(record);
      tx.oncomplete = () => resolve();
      tx.onerror = () => reject(tx.error);
    });

    return record;
  }

  private async loadFromStore(): Promise<StoredKeyPair | null> {
    const db = await this.openDatabase();
    return new Promise<StoredKeyPair | null>((resolve, reject) => {
      const tx = db.transaction(STORE_NAME, 'readonly');
      const request = tx.objectStore(STORE_NAME).get(KEY_RECORD_ID);
      request.onsuccess = () => resolve((request.result as StoredKeyPair | undefined) ?? null);
      request.onerror = () => reject(request.error);
    });
  }

  private openDatabase(): Promise<IDBDatabase> {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(DB_NAME, DB_VERSION);
      request.onupgradeneeded = () => {
        if (!request.result.objectStoreNames.contains(STORE_NAME)) {
          request.result.createObjectStore(STORE_NAME, { keyPath: 'id' });
        }
      };
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }

  private toBase64(bytes: Uint8Array): string {
    let binary = '';
    for (const byte of bytes) {
      binary += String.fromCharCode(byte);
    }
    return btoa(binary);
  }
}
