/**
 * @bytelyst/secure-storage-web — Encrypted web storage
 *
 * IndexedDB-backed key-value storage with a non-extractable AES-256-GCM
 * CryptoKey managed by Web Crypto. The encryption key never leaves the
 * browser's crypto subsystem.
 *
 * Falls back to localStorage if IndexedDB or SubtleCrypto is unavailable.
 */

const DB_NAME = 'bytelyst_secure_storage';
const DB_VERSION = 1;
const KEY_STORE = 'crypto_keys';
const DATA_STORE = 'encrypted_data';
const MASTER_KEY_ID = '__master_key__';
const ALGORITHM = 'AES-GCM';
const KEY_SIZE = 256;
const IV_BYTES = 12;
const TAG_BITS = 128;

// ── IndexedDB helpers ───────────────────────────────

function openDb(): Promise<IDBDatabase> {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open(DB_NAME, DB_VERSION);
    request.onupgradeneeded = () => {
      const db = request.result;
      if (!db.objectStoreNames.contains(KEY_STORE)) {
        db.createObjectStore(KEY_STORE);
      }
      if (!db.objectStoreNames.contains(DATA_STORE)) {
        db.createObjectStore(DATA_STORE);
      }
    };
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

function idbGet<T>(db: IDBDatabase, store: string, key: string): Promise<T | undefined> {
  return new Promise((resolve, reject) => {
    const tx = db.transaction(store, 'readonly');
    const req = tx.objectStore(store).get(key);
    req.onsuccess = () => resolve(req.result as T | undefined);
    req.onerror = () => reject(req.error);
  });
}

function idbPut(db: IDBDatabase, store: string, key: string, value: unknown): Promise<void> {
  return new Promise((resolve, reject) => {
    const tx = db.transaction(store, 'readwrite');
    const req = tx.objectStore(store).put(value, key);
    req.onsuccess = () => resolve();
    req.onerror = () => reject(req.error);
  });
}

function idbDelete(db: IDBDatabase, store: string, key: string): Promise<void> {
  return new Promise((resolve, reject) => {
    const tx = db.transaction(store, 'readwrite');
    const req = tx.objectStore(store).delete(key);
    req.onsuccess = () => resolve();
    req.onerror = () => reject(req.error);
  });
}

function idbClear(db: IDBDatabase, store: string): Promise<void> {
  return new Promise((resolve, reject) => {
    const tx = db.transaction(store, 'readwrite');
    const req = tx.objectStore(store).clear();
    req.onsuccess = () => resolve();
    req.onerror = () => reject(req.error);
  });
}

function idbGetAllKeys(db: IDBDatabase, store: string): Promise<string[]> {
  return new Promise((resolve, reject) => {
    const tx = db.transaction(store, 'readonly');
    const req = tx.objectStore(store).getAllKeys();
    req.onsuccess = () => resolve(req.result as string[]);
    req.onerror = () => reject(req.error);
  });
}

// ── Stored encrypted value format ───────────────────

interface StoredValue {
  /** Ciphertext bytes. */
  ct: ArrayBuffer;
  /** Initialization vector. */
  iv: ArrayBuffer;
}

// ── SecureStorage class ─────────────────────────────

/**
 * Encrypted key-value storage backed by IndexedDB + Web Crypto.
 *
 * The AES-256-GCM master key is generated once and stored as a
 * **non-extractable** CryptoKey in IndexedDB. Values are encrypted
 * before storage and decrypted on retrieval. The raw key material
 * never leaves the browser's crypto subsystem.
 *
 * Falls back to plaintext localStorage if IndexedDB or SubtleCrypto
 * is not available (e.g., legacy browsers, server-side rendering).
 *
 * @example
 * ```typescript
 * const storage = new SecureStorage('com.myapp');
 * await storage.set('auth_token', 'eyJhbGci...');
 * const token = await storage.get('auth_token');
 * await storage.delete('auth_token');
 * await storage.clear();
 * ```
 */
export class SecureStorage {
  private readonly namespace: string;
  private db: IDBDatabase | null = null;
  private masterKey: CryptoKey | null = null;
  private readonly fallback: boolean;

  constructor(namespace: string) {
    this.namespace = namespace;
    this.fallback = !SecureStorage.isSupported();
  }

  /** Check if IndexedDB + SubtleCrypto are available. */
  static isSupported(): boolean {
    return (
      typeof indexedDB !== 'undefined' &&
      typeof globalThis !== 'undefined' &&
      !!globalThis.crypto?.subtle
    );
  }

  // ── Public API ──────────────────────────────────

  /** Store an encrypted value. */
  async set(key: string, value: string): Promise<void> {
    if (this.fallback) {
      localStorage.setItem(this.ns(key), value);
      return;
    }
    const { db, masterKey } = await this.ensureReady();
    const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
    const encoded = new TextEncoder().encode(value);

    const ciphertext = await crypto.subtle.encrypt(
      { name: ALGORITHM, iv: iv.buffer as ArrayBuffer, tagLength: TAG_BITS },
      masterKey,
      encoded.buffer as ArrayBuffer
    );

    const stored: StoredValue = { ct: ciphertext, iv: iv.buffer as ArrayBuffer };
    await idbPut(db, DATA_STORE, this.ns(key), stored);
  }

  /** Retrieve and decrypt a value. Returns `null` if not found. */
  async get(key: string): Promise<string | null> {
    if (this.fallback) {
      return localStorage.getItem(this.ns(key));
    }
    const { db, masterKey } = await this.ensureReady();
    const stored = await idbGet<StoredValue>(db, DATA_STORE, this.ns(key));
    if (!stored) return null;

    const plaintext = await crypto.subtle.decrypt(
      { name: ALGORITHM, iv: stored.iv, tagLength: TAG_BITS },
      masterKey,
      stored.ct
    );

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

  /** Delete a stored value. */
  async delete(key: string): Promise<void> {
    if (this.fallback) {
      localStorage.removeItem(this.ns(key));
      return;
    }
    const { db } = await this.ensureReady();
    await idbDelete(db, DATA_STORE, this.ns(key));
  }

  /** Clear all stored values (keeps the master key). */
  async clear(): Promise<void> {
    if (this.fallback) {
      const prefix = this.ns('');
      const toRemove: string[] = [];
      for (let i = 0; i < localStorage.length; i++) {
        const k = localStorage.key(i);
        if (k && k.startsWith(prefix)) toRemove.push(k);
      }
      toRemove.forEach(k => localStorage.removeItem(k));
      return;
    }
    const { db } = await this.ensureReady();
    await idbClear(db, DATA_STORE);
  }

  /** Check if a key exists. */
  async has(key: string): Promise<boolean> {
    if (this.fallback) {
      return localStorage.getItem(this.ns(key)) !== null;
    }
    const { db } = await this.ensureReady();
    const stored = await idbGet<StoredValue>(db, DATA_STORE, this.ns(key));
    return stored !== undefined;
  }

  /** List all stored keys (without namespace prefix). */
  async keys(): Promise<string[]> {
    if (this.fallback) {
      const prefix = this.ns('');
      const result: string[] = [];
      for (let i = 0; i < localStorage.length; i++) {
        const k = localStorage.key(i);
        if (k && k.startsWith(prefix)) result.push(k.slice(prefix.length));
      }
      return result;
    }
    const { db } = await this.ensureReady();
    const allKeys = await idbGetAllKeys(db, DATA_STORE);
    const prefix = this.ns('');
    return allKeys.filter(k => k.startsWith(prefix)).map(k => k.slice(prefix.length));
  }

  // ── Internal ────────────────────────────────────

  private ns(key: string): string {
    return `${this.namespace}:${key}`;
  }

  private async ensureReady(): Promise<{ db: IDBDatabase; masterKey: CryptoKey }> {
    if (this.db && this.masterKey) {
      return { db: this.db, masterKey: this.masterKey };
    }

    const db = await openDb();
    this.db = db;

    // Try to load existing master key
    let masterKey = await idbGet<CryptoKey>(db, KEY_STORE, MASTER_KEY_ID);

    if (!masterKey) {
      // Generate a new non-extractable master key
      masterKey = await crypto.subtle.generateKey(
        { name: ALGORITHM, length: KEY_SIZE },
        false, // non-extractable — key material never leaves crypto subsystem
        ['encrypt', 'decrypt']
      );
      await idbPut(db, KEY_STORE, MASTER_KEY_ID, masterKey);
    }

    this.masterKey = masterKey;
    return { db, masterKey };
  }
}
