/**
 * SQLite migration: unencrypted shade tables → encrypted `_enc` tables.
 *
 * Strategy:
 *   1. Refuse to start if WAL has uncommitted writes.
 *   2. Optionally take a `.bak` snapshot of the DB file.
 *   3. Open EncryptedSQLiteStorage on the same DB (creates `_enc` tables).
 *   4. Read each row from the unencrypted table → decode → re-write through
 *      the encrypted store. This re-uses the existing serialize helpers,
 *      so the at-rest format is identical to a fresh write.
 *   5. After all rows are written, drop the unencrypted tables (unless
 *      `--keep-original` is set).
 *
 * The migration is *resumable*: re-running it on a partially-migrated DB
 * skips rows that already exist (INSERT OR REPLACE — re-writes are
 * idempotent per row). It is also *atomic* per-row: either a row is
 * fully encrypted or the unencrypted version stays.
 */

import { Database } from 'bun:sqlite';
import { existsSync, copyFileSync } from 'node:fs';
import {
  type IdentityKeyPair,
  type SignedPreKey,
  type OneTimePreKey,
  type SessionState,
  type RetiredIdentity,
  type PersistedStreamState,
  fromBase64,
  deserializeIdentityKeyPair,
  deserializeSignedPreKey,
  deserializeOneTimePreKey,
  deserializeSessionState,
} from '@shade/core';
import { KeyManager } from '../crypto/key-manager.js';
import { EncryptedSQLiteStorage } from '../storage/encrypted-sqlite.js';

export interface MigrateOptions {
  /** Source SQLite file path. Defaults to SHADE_DB_PATH or /data/shade-client.db. */
  dbPath?: string;
  /** KeyManager pre-opened with the new master key. */
  keyManager: KeyManager;
  /** When true, preserve the unencrypted source tables. Default: false (drop after migrate). */
  keepOriginal?: boolean;
  /** Validate decryption of every row without writing. Useful for `--dry-run`. */
  dryRun?: boolean;
  /** When true, copy the DB to `<path>.bak` before migrating. Default: true. */
  backup?: boolean;
  /** Logger callback for progress messages. */
  log?: (msg: string) => void;
}

export interface MigrateReport {
  identity:           number;
  config:             number;
  signedPrekeys:      number;
  oneTimePrekeys:     number;
  sessions:           number;
  trustedIdentities:  number;
  retiredIdentities:  number;
  streamStates:       number;
  durationMs:         number;
  backupPath?:        string;
  keptOriginal:       boolean;
  dryRun:             boolean;
}

const UNENCRYPTED_TABLES = [
  'identity', 'config', 'signed_prekeys', 'one_time_prekeys',
  'sessions', 'trusted_identities', 'retired_identities', 'stream_state',
] as const;

export async function migrateSqliteToEncrypted(opts: MigrateOptions): Promise<MigrateReport> {
  const path = opts.dbPath ?? process.env.SHADE_DB_PATH ?? '/data/shade-client.db';
  const log = opts.log ?? (() => {});
  const dryRun = opts.dryRun ?? false;
  const keepOriginal = opts.keepOriginal ?? false;
  const backup = opts.backup ?? !dryRun;

  if (!existsSync(path)) {
    throw new Error(`migrate-storage: source DB not found at ${path}`);
  }

  let backupPath: string | undefined;
  if (backup) {
    backupPath = `${path}.bak.${Date.now()}`;
    copyFileSync(path, backupPath);
    log(`backup written to ${backupPath}`);
  }

  const db = new Database(path);
  db.exec('PRAGMA journal_mode=WAL');

  // Refuse if WAL has uncommitted writes (must checkpoint first).
  const walCheck = db.prepare('PRAGMA wal_checkpoint(PASSIVE)').get() as { busy?: number } | undefined;
  if (walCheck?.busy) {
    throw new Error('migrate-storage: WAL has uncommitted writes — call PRAGMA wal_checkpoint(FULL) first');
  }

  const start = Date.now();
  const report: MigrateReport = {
    identity: 0, config: 0, signedPrekeys: 0, oneTimePrekeys: 0,
    sessions: 0, trustedIdentities: 0, retiredIdentities: 0, streamStates: 0,
    durationMs: 0, keptOriginal: keepOriginal, dryRun,
    ...(backupPath !== undefined ? { backupPath } : {}),
  };

  const enc = await EncryptedSQLiteStorage.wrap(db, opts.keyManager);

  // ─── identity (single row) ──────────────────────
  if (tableExists(db, 'identity')) {
    const row = db.prepare('SELECT signing_public_key, signing_private_key, dh_public_key, dh_private_key FROM identity WHERE id = 1')
      .get() as { signing_public_key: string; signing_private_key: string; dh_public_key: string; dh_private_key: string } | undefined;
    if (row) {
      const kp: IdentityKeyPair = {
        signingPublicKey: fromBase64(row.signing_public_key),
        signingPrivateKey: fromBase64(row.signing_private_key),
        dhPublicKey: fromBase64(row.dh_public_key),
        dhPrivateKey: fromBase64(row.dh_private_key),
      };
      if (!dryRun) await enc.saveIdentityKeyPair(kp);
      report.identity = 1;
    }
  }

  // ─── config ──────────────────────────────────────
  if (tableExists(db, 'config')) {
    const rows = db.prepare('SELECT key, value FROM config').all() as { key: string; value: string }[];
    for (const r of rows) {
      if (r.key === 'registrationId') {
        if (!dryRun) await enc.saveLocalRegistrationId(parseInt(r.value, 10));
        report.config++;
      }
    }
  }

  // ─── signed_prekeys ──────────────────────────────
  if (tableExists(db, 'signed_prekeys')) {
    const rows = db.prepare('SELECT data_json FROM signed_prekeys').all() as { data_json: string }[];
    for (const r of rows) {
      const k: SignedPreKey = deserializeSignedPreKey(r.data_json);
      if (!dryRun) await enc.saveSignedPreKey(k);
      report.signedPrekeys++;
    }
  }

  // ─── one_time_prekeys ────────────────────────────
  if (tableExists(db, 'one_time_prekeys')) {
    const rows = db.prepare('SELECT data_json FROM one_time_prekeys').all() as { data_json: string }[];
    for (const r of rows) {
      const k: OneTimePreKey = deserializeOneTimePreKey(r.data_json);
      if (!dryRun) await enc.saveOneTimePreKey(k);
      report.oneTimePrekeys++;
    }
  }

  // ─── sessions ────────────────────────────────────
  if (tableExists(db, 'sessions')) {
    const rows = db.prepare('SELECT address, state_json FROM sessions').all() as { address: string; state_json: string }[];
    for (const r of rows) {
      const s: SessionState = deserializeSessionState(r.state_json);
      if (!dryRun) await enc.saveSession(r.address, s);
      report.sessions++;
    }
  }

  // ─── trusted_identities ──────────────────────────
  if (tableExists(db, 'trusted_identities')) {
    const rows = db.prepare('SELECT address, identity_key FROM trusted_identities').all() as { address: string; identity_key: string }[];
    for (const r of rows) {
      if (!dryRun) await enc.saveTrustedIdentity(r.address, fromBase64(r.identity_key));
      report.trustedIdentities++;
    }
  }

  // ─── retired_identities ──────────────────────────
  if (tableExists(db, 'retired_identities')) {
    const rows = db.prepare('SELECT data_json, retired_at FROM retired_identities ORDER BY retired_at')
      .all() as { data_json: string; retired_at: number }[];
    for (const r of rows) {
      const ri: RetiredIdentity = {
        keyPair: deserializeIdentityKeyPair(r.data_json),
        retiredAt: Number(r.retired_at),
      };
      if (!dryRun) await enc.addRetiredIdentity(ri);
      report.retiredIdentities++;
    }
  }

  // ─── stream_state ────────────────────────────────
  if (tableExists(db, 'stream_state')) {
    const rows = db.prepare('SELECT * FROM stream_state').all() as Array<{
      stream_id: string;
      direction: 'send' | 'receive';
      peer_address: string;
      status: 'active' | 'paused' | 'finished' | 'aborted';
      metadata_json: string;
      partition_json: string;
      lane_state_json: string;
      io_descriptor_json: string;
      secret_enc: Uint8Array | ArrayBuffer | number[];
      secret_nonce: Uint8Array | ArrayBuffer | number[];
      overall_hash_state: string | null;
      created_at: number | bigint;
      updated_at: number | bigint;
    }>;
    for (const r of rows) {
      const state: PersistedStreamState = {
        streamId: r.stream_id,
        direction: r.direction,
        peerAddress: r.peer_address,
        status: r.status,
        metadataJson: r.metadata_json,
        partitionJson: r.partition_json,
        laneStateJson: r.lane_state_json,
        ioDescriptorJson: r.io_descriptor_json,
        secretEnc: toBytes(r.secret_enc),
        secretNonce: toBytes(r.secret_nonce),
        createdAt: Number(r.created_at),
        updatedAt: Number(r.updated_at),
      };
      if (r.overall_hash_state !== null) state.overallHashState = r.overall_hash_state;
      if (!dryRun) await enc.saveStreamState(state);
      report.streamStates++;
    }
  }

  if (!dryRun && !keepOriginal) {
    log('dropping unencrypted tables…');
    db.transaction(() => {
      for (const t of UNENCRYPTED_TABLES) {
        db.exec(`DROP TABLE IF EXISTS ${t}`);
      }
    })();
  }

  report.durationMs = Date.now() - start;
  log(`migration ${dryRun ? 'dry-run ' : ''}complete in ${report.durationMs}ms`);
  return report;
}

export interface RotateOptions {
  dbPath?: string;
  /** KeyManager opened with the *current* master key (for reads). */
  oldKeyManager: KeyManager;
  /** KeyManager opened with the *new* master key (for writes). */
  newKeyManager: KeyManager;
  log?: (msg: string) => void;
}

/**
 * Online ratchet rotation. Reads each encrypted row under `oldKeyManager`,
 * re-seals under `newKeyManager`, writes back. The DB stays online; brief
 * read-after-write inconsistency for in-flight readers is acceptable for
 * the use cases this targets (CLI tools, single-process servers).
 *
 * On completion the storage_key_fingerprint meta is updated so subsequent
 * opens require the new key.
 */
export async function rotateSqliteEncryptionKey(opts: RotateOptions): Promise<{ rowsRotated: number }> {
  const path = opts.dbPath ?? process.env.SHADE_DB_PATH ?? '/data/shade-client.db';
  const log = opts.log ?? (() => {});
  if (!existsSync(path)) throw new Error(`rotate-storage-key: source DB not found at ${path}`);

  const db = new Database(path);
  db.exec('PRAGMA journal_mode=WAL');

  const oldStore = await EncryptedSQLiteStorage.wrap(db, opts.oldKeyManager);
  let rotated = 0;

  // Identity
  const id = await oldStore.getIdentityKeyPair();
  // Config (registrationId)
  const reg = await oldStore.getLocalRegistrationId();
  // Sessions, prekeys, retired, stream — collect via list / scan
  const ssRows = db.prepare('SELECT address FROM sessions_enc').all() as { address: string }[];
  const sessions = await Promise.all(ssRows.map((r) => oldStore.getSession(r.address).then((s) => ({ address: r.address, s }))));
  const tiRows = db.prepare('SELECT address FROM trusted_identities_enc').all() as { address: string }[];
  const trusts = await Promise.all(tiRows.map(async (r) => {
    // Read raw blob then decrypt: openTrust returns identity_key bytes.
    const row = db.prepare('SELECT ciphertext FROM trusted_identities_enc WHERE address = ?').get(r.address) as { ciphertext: Uint8Array | ArrayBuffer };
    const { openTrust } = await import('../crypto/row-codec.js');
    return { address: r.address, identityKey: await openTrust(opts.oldKeyManager, r.address, toBytes(row.ciphertext)) };
  }));
  const sp = db.prepare('SELECT key_id FROM signed_prekeys_enc').all() as { key_id: number }[];
  const signed = await Promise.all(sp.map(async (r) => oldStore.getSignedPreKey(r.key_id)));
  const otp = db.prepare('SELECT key_id FROM one_time_prekeys_enc').all() as { key_id: number }[];
  const oneTime = await Promise.all(otp.map(async (r) => oldStore.getOneTimePreKey(r.key_id)));
  const retired = await oldStore.getRetiredIdentities();
  const streamRowsIds = db.prepare('SELECT stream_id FROM stream_state_enc').all() as { stream_id: string }[];
  const streams = await Promise.all(streamRowsIds.map((r) => oldStore.getStreamState(r.stream_id)));

  // Drop the old fingerprint and let the new store re-create it.
  db.prepare('DELETE FROM shade_meta_enc WHERE key = ?').run('storage_key_fingerprint');

  const newStore = await EncryptedSQLiteStorage.wrap(db, opts.newKeyManager);

  if (id) { await newStore.saveIdentityKeyPair(id); rotated++; }
  if (reg) { await newStore.saveLocalRegistrationId(reg); rotated++; }
  for (const s of sessions) if (s.s) { await newStore.saveSession(s.address, s.s); rotated++; }
  for (const t of trusts) { await newStore.saveTrustedIdentity(t.address, t.identityKey); rotated++; }
  for (const k of signed) if (k) { await newStore.saveSignedPreKey(k); rotated++; }
  for (const k of oneTime) if (k) { await newStore.saveOneTimePreKey(k); rotated++; }
  for (const r of retired) { await newStore.addRetiredIdentity(r); rotated++; }
  for (const s of streams) if (s) { await newStore.saveStreamState(s); rotated++; }

  log(`rotated ${rotated} encrypted rows`);
  return { rowsRotated: rotated };
}

function tableExists(db: Database, name: string): boolean {
  const r = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = ?").get(name) as { name: string } | undefined;
  return r !== undefined;
}

function toBytes(value: Uint8Array | ArrayBuffer | number[] | unknown): Uint8Array {
  if (value instanceof Uint8Array) return value;
  if (value instanceof ArrayBuffer) return new Uint8Array(value);
  if (Array.isArray(value)) return new Uint8Array(value as number[]);
  throw new Error(`Unsupported BLOB representation: ${typeof value}`);
}
