import type { Sql } from 'postgres';
import postgres from 'postgres';
import type {
  BroadcastChannelRecord,
  BroadcastMemberRecord,
  IdentityKeyPair,
  OneTimePreKey,
  PeerVerification,
  PeerVerificationSource,
  PersistedStreamState,
  RetiredIdentity,
  SessionState,
  SignedPreKey,
  StorageProvider,
} from '@shade/core';
import { constantTimeEqual, toBase64 } from '@shade/core';
import { KeyManager } from '../crypto/key-manager.js';
import {
  openBroadcastChannelSensitive, openConfig, openIdentity, openOneTimePreKey,
  openRetired, openSession, openSignedPreKey, openStreamSensitive, openTrust,
  sealBroadcastChannelSensitive, sealConfig, sealIdentity, sealOneTimePreKey,
  sealRetired, sealSession, sealSignedPreKey, sealStreamSensitive, sealTrust,
} from '../crypto/row-codec.js';

/**
 * PostgreSQL-backed StorageProvider with at-rest encryption (V3.2).
 *
 * Tables prefixed `shade_*_enc` to allow side-by-side migration with the
 * unencrypted `shade_*` tables. Sensitive payloads are sealed with
 * AES-256-GCM bound to (table, column, pk) AAD.
 */
export class EncryptedPostgresStorage implements StorageProvider {
  private constructor(
    private readonly sql: Sql,
    private readonly km: KeyManager,
    private readonly ownsConnection: boolean,
  ) {}

  /** Create from connection string (owns connection). */
  static async create(connectionString: string, km: KeyManager): Promise<EncryptedPostgresStorage> {
    const sql = postgres(connectionString);
    const store = new EncryptedPostgresStorage(sql, km, true);
    await ensureEncryptedClientTables(sql);
    await store.assertKeyMatchesOrPersistFingerprint();
    return store;
  }

  /** Wrap an existing postgres-js Sql client (caller owns it). */
  static async fromClient(sql: Sql, km: KeyManager): Promise<EncryptedPostgresStorage> {
    const store = new EncryptedPostgresStorage(sql, km, false);
    await ensureEncryptedClientTables(sql);
    await store.assertKeyMatchesOrPersistFingerprint();
    return store;
  }

  async close(): Promise<void> {
    if (this.ownsConnection) await this.sql.end();
    this.km.destroy();
  }

  private async assertKeyMatchesOrPersistFingerprint(): Promise<void> {
    const expected = toBase64(this.km.storageKeyFingerprint());
    const rows = await this.sql<Array<{ value: string }>>`
      SELECT value FROM shade_meta_enc WHERE key = 'storage_key_fingerprint'
    `;
    if (rows.length === 0) {
      await this.sql`
        INSERT INTO shade_meta_enc (key, value) VALUES ('storage_key_fingerprint', ${expected})
      `;
      return;
    }
    if (rows[0]!.value !== expected) {
      throw new Error(
        'storage key mismatch — the supplied passphrase / keychain entry does not unlock this database',
      );
    }
  }

  // ─── Identity ──────────────────────────────────────────────

  async getIdentityKeyPair(): Promise<IdentityKeyPair | null> {
    const rows = await this.sql<Array<{ ciphertext: Uint8Array }>>`
      SELECT ciphertext FROM shade_identity_enc WHERE id = 1
    `;
    if (rows.length === 0) return null;
    return openIdentity(this.km, rows[0]!.ciphertext);
  }

  async saveIdentityKeyPair(kp: IdentityKeyPair): Promise<void> {
    const blob = await sealIdentity(this.km, kp);
    await this.sql`
      INSERT INTO shade_identity_enc (id, ciphertext) VALUES (1, ${blob})
      ON CONFLICT (id) DO UPDATE SET ciphertext = EXCLUDED.ciphertext
    `;
  }

  async getLocalRegistrationId(): Promise<number> {
    const rows = await this.sql<Array<{ ciphertext: Uint8Array }>>`
      SELECT ciphertext FROM shade_config_enc WHERE key = 'registrationId'
    `;
    if (rows.length === 0) return 0;
    return parseInt(await openConfig(this.km, 'registrationId', rows[0]!.ciphertext), 10);
  }

  async saveLocalRegistrationId(id: number): Promise<void> {
    const blob = await sealConfig(this.km, 'registrationId', String(id));
    await this.sql`
      INSERT INTO shade_config_enc (key, ciphertext) VALUES ('registrationId', ${blob})
      ON CONFLICT (key) DO UPDATE SET ciphertext = EXCLUDED.ciphertext
    `;
  }

  // ─── Signed PreKeys ────────────────────────────────────────

  async getSignedPreKey(keyId: number): Promise<SignedPreKey | null> {
    const rows = await this.sql<Array<{ ciphertext: Uint8Array }>>`
      SELECT ciphertext FROM shade_signed_prekeys_enc WHERE key_id = ${keyId}
    `;
    if (rows.length === 0) return null;
    return openSignedPreKey(this.km, keyId, rows[0]!.ciphertext);
  }

  async saveSignedPreKey(key: SignedPreKey): Promise<void> {
    const blob = await sealSignedPreKey(this.km, key);
    await this.sql`
      INSERT INTO shade_signed_prekeys_enc (key_id, ciphertext) VALUES (${key.keyId}, ${blob})
      ON CONFLICT (key_id) DO UPDATE SET ciphertext = EXCLUDED.ciphertext
    `;
  }

  async removeSignedPreKey(keyId: number): Promise<void> {
    await this.sql`DELETE FROM shade_signed_prekeys_enc WHERE key_id = ${keyId}`;
  }

  // ─── One-Time PreKeys ──────────────────────────────────────

  async getOneTimePreKey(keyId: number): Promise<OneTimePreKey | null> {
    const rows = await this.sql<Array<{ ciphertext: Uint8Array }>>`
      SELECT ciphertext FROM shade_one_time_prekeys_enc WHERE key_id = ${keyId}
    `;
    if (rows.length === 0) return null;
    return openOneTimePreKey(this.km, keyId, rows[0]!.ciphertext);
  }

  async saveOneTimePreKey(key: OneTimePreKey): Promise<void> {
    const blob = await sealOneTimePreKey(this.km, key);
    await this.sql`
      INSERT INTO shade_one_time_prekeys_enc (key_id, ciphertext) VALUES (${key.keyId}, ${blob})
      ON CONFLICT (key_id) DO UPDATE SET ciphertext = EXCLUDED.ciphertext
    `;
  }

  async removeOneTimePreKey(keyId: number): Promise<void> {
    await this.sql`DELETE FROM shade_one_time_prekeys_enc WHERE key_id = ${keyId}`;
  }

  async getOneTimePreKeyCount(): Promise<number> {
    const rows = await this.sql<Array<{ count: string }>>`
      SELECT COUNT(*)::text as count FROM shade_one_time_prekeys_enc
    `;
    return parseInt(rows[0]!.count, 10);
  }

  // ─── Sessions ──────────────────────────────────────────────

  async getSession(address: string): Promise<SessionState | null> {
    const rows = await this.sql<Array<{ ciphertext: Uint8Array }>>`
      SELECT ciphertext FROM shade_sessions_enc WHERE address = ${address}
    `;
    if (rows.length === 0) return null;
    return openSession(this.km, address, rows[0]!.ciphertext);
  }

  async saveSession(address: string, state: SessionState): Promise<void> {
    const blob = await sealSession(this.km, address, state);
    await this.sql`
      INSERT INTO shade_sessions_enc (address, ciphertext) VALUES (${address}, ${blob})
      ON CONFLICT (address) DO UPDATE SET ciphertext = EXCLUDED.ciphertext
    `;
  }

  async removeSession(address: string): Promise<void> {
    await this.sql`DELETE FROM shade_sessions_enc WHERE address = ${address}`;
  }

  // ─── Trust ─────────────────────────────────────────────────

  async isTrustedIdentity(address: string, identityKey: Uint8Array): Promise<boolean> {
    const rows = await this.sql<Array<{ ciphertext: Uint8Array }>>`
      SELECT ciphertext FROM shade_trusted_identities_enc WHERE address = ${address}
    `;
    if (rows.length === 0) return true; // TOFU
    const stored = await openTrust(this.km, address, rows[0]!.ciphertext);
    return constantTimeEqual(stored, identityKey);
  }

  async saveTrustedIdentity(address: string, identityKey: Uint8Array): Promise<void> {
    const blob = await sealTrust(this.km, address, identityKey);
    await this.sql`
      INSERT INTO shade_trusted_identities_enc (address, ciphertext) VALUES (${address}, ${blob})
      ON CONFLICT (address) DO UPDATE SET ciphertext = EXCLUDED.ciphertext
    `;
  }

  // ─── Identity History ──────────────────────────────────────

  async addRetiredIdentity(identity: RetiredIdentity): Promise<void> {
    const blob = await sealRetired(this.km, identity);
    await this.sql`
      INSERT INTO shade_retired_identities_enc (retired_at, ciphertext)
      VALUES (${identity.retiredAt}, ${blob})
      ON CONFLICT (retired_at) DO UPDATE SET ciphertext = EXCLUDED.ciphertext
    `;
  }

  async getRetiredIdentities(): Promise<RetiredIdentity[]> {
    const rows = await this.sql<Array<{ retired_at: string; ciphertext: Uint8Array }>>`
      SELECT retired_at, ciphertext FROM shade_retired_identities_enc ORDER BY retired_at DESC
    `;
    return Promise.all(rows.map((r) => openRetired(this.km, Number(r.retired_at), r.ciphertext)));
  }

  async pruneRetiredIdentities(olderThan: number): Promise<void> {
    await this.sql`DELETE FROM shade_retired_identities_enc WHERE retired_at < ${olderThan}`;
  }

  // ─── Stream-transfer resume state ──────────────────────────

  async saveStreamState(state: PersistedStreamState): Promise<void> {
    const blob = await sealStreamSensitive(this.km, state);
    await this.sql`
      INSERT INTO shade_stream_state_enc (
        stream_id, direction, peer_address, status, ciphertext, created_at, updated_at
      ) VALUES (
        ${state.streamId}, ${state.direction}, ${state.peerAddress}, ${state.status},
        ${blob}, ${state.createdAt}, ${state.updatedAt}
      )
      ON CONFLICT (stream_id) DO UPDATE SET
        direction    = EXCLUDED.direction,
        peer_address = EXCLUDED.peer_address,
        status       = EXCLUDED.status,
        ciphertext   = EXCLUDED.ciphertext,
        updated_at   = EXCLUDED.updated_at
    `;
  }

  async getStreamState(streamId: string): Promise<PersistedStreamState | null> {
    const rows = await this.sql<Array<StreamRow>>`
      SELECT * FROM shade_stream_state_enc WHERE stream_id = ${streamId}
    `;
    if (rows.length === 0) return null;
    return this.rowToStreamState(rows[0]!);
  }

  async removeStreamState(streamId: string): Promise<void> {
    await this.sql`DELETE FROM shade_stream_state_enc WHERE stream_id = ${streamId}`;
  }

  async listActiveStreamStates(direction?: 'send' | 'receive'): Promise<PersistedStreamState[]> {
    const rows =
      direction === undefined
        ? await this.sql<Array<StreamRow>>`
            SELECT * FROM shade_stream_state_enc
            WHERE status IN ('active','paused')
            ORDER BY updated_at DESC
          `
        : await this.sql<Array<StreamRow>>`
            SELECT * FROM shade_stream_state_enc
            WHERE status IN ('active','paused') AND direction = ${direction}
            ORDER BY updated_at DESC
          `;
    return Promise.all(rows.map((r) => this.rowToStreamState(r)));
  }

  async pruneStreamStates(olderThan: number): Promise<void> {
    await this.sql`
      DELETE FROM shade_stream_state_enc
      WHERE status IN ('finished','aborted') AND updated_at < ${olderThan}
    `;
  }

  // ─── Peer verifications (V3.3) ────────────────────────────
  // Fingerprints are public-by-design (intended for OOB display), so we
  // keep them plaintext alongside the encrypted tables for symmetry.

  async savePeerVerification(v: PeerVerification): Promise<void> {
    await this.sql`
      INSERT INTO shade_peer_verifications_enc
        (peer_address, fingerprint, verified_at, verified_by, identity_version)
      VALUES (${v.peerAddress}, ${v.fingerprint}, ${v.verifiedAt}, ${v.verifiedBy}, ${v.identityVersion})
      ON CONFLICT (peer_address) DO UPDATE SET
        fingerprint = EXCLUDED.fingerprint,
        verified_at = EXCLUDED.verified_at,
        verified_by = EXCLUDED.verified_by,
        identity_version = EXCLUDED.identity_version
    `;
  }

  async getPeerVerification(address: string): Promise<PeerVerification | null> {
    const rows = await this.sql<Array<{ peer_address: string; fingerprint: string; verified_at: string; verified_by: string; identity_version: string }>>`
      SELECT peer_address, fingerprint, verified_at, verified_by, identity_version
      FROM shade_peer_verifications_enc WHERE peer_address = ${address}
    `;
    if (rows.length === 0) return null;
    const r = rows[0]!;
    return {
      peerAddress: r.peer_address,
      fingerprint: r.fingerprint,
      verifiedAt: Number(r.verified_at),
      verifiedBy: r.verified_by as PeerVerificationSource,
      identityVersion: Number(r.identity_version),
    };
  }

  async removePeerVerification(address: string): Promise<void> {
    await this.sql`DELETE FROM shade_peer_verifications_enc WHERE peer_address = ${address}`;
  }

  async getPeerIdentityVersion(address: string): Promise<number> {
    const rows = await this.sql<Array<{ version: string }>>`
      SELECT version FROM shade_peer_identity_versions_enc WHERE peer_address = ${address}
    `;
    return rows.length ? Number(rows[0]!.version) : 1;
  }

  async bumpPeerIdentityVersion(address: string): Promise<number> {
    const current = await this.getPeerIdentityVersion(address);
    const next = current + 1;
    await this.sql`
      INSERT INTO shade_peer_identity_versions_enc (peer_address, version)
      VALUES (${address}, ${next})
      ON CONFLICT (peer_address) DO UPDATE SET version = EXCLUDED.version
    `;
    return next;
  }

  // ─── Broadcast channels (V4.6) ────────────────────────────

  async saveBroadcastChannel(channel: BroadcastChannelRecord): Promise<void> {
    const sealed = await sealBroadcastChannelSensitive(this.km, channel.channelId, {
      chainKey: channel.chainKey,
      iteration: channel.iteration,
      signingPublicKey: channel.signingPublicKey,
      ...(channel.signingPrivateKey !== undefined ? { signingPrivateKey: channel.signingPrivateKey } : {}),
    });
    await this.sql`
      INSERT INTO shade_broadcast_channels_enc
        (channel_id, owner_role, owner_address, label, generation, ciphertext, created_at, updated_at)
      VALUES
        (${channel.channelId}, ${channel.ownerRole}, ${channel.ownerAddress},
         ${channel.label ?? null}, ${channel.generation}, ${sealed},
         ${channel.createdAt}, ${channel.updatedAt})
      ON CONFLICT (channel_id) DO UPDATE SET
        owner_role    = EXCLUDED.owner_role,
        owner_address = EXCLUDED.owner_address,
        label         = EXCLUDED.label,
        generation    = EXCLUDED.generation,
        ciphertext    = EXCLUDED.ciphertext,
        updated_at    = EXCLUDED.updated_at
    `;
  }

  async getBroadcastChannel(channelId: string): Promise<BroadcastChannelRecord | null> {
    const rows = await this.sql<Array<BroadcastChannelEncRow>>`
      SELECT * FROM shade_broadcast_channels_enc WHERE channel_id = ${channelId}
    `;
    if (rows.length === 0) return null;
    return this.encRowToChannel(rows[0]!);
  }

  async listBroadcastChannels(): Promise<BroadcastChannelRecord[]> {
    const rows = await this.sql<Array<BroadcastChannelEncRow>>`
      SELECT * FROM shade_broadcast_channels_enc ORDER BY created_at ASC
    `;
    return Promise.all(rows.map((r) => this.encRowToChannel(r)));
  }

  async removeBroadcastChannel(channelId: string): Promise<void> {
    await this.sql`DELETE FROM shade_broadcast_members_enc WHERE channel_id = ${channelId}`;
    await this.sql`DELETE FROM shade_broadcast_channels_enc WHERE channel_id = ${channelId}`;
  }

  async saveBroadcastMember(member: BroadcastMemberRecord): Promise<void> {
    await this.sql`
      INSERT INTO shade_broadcast_members_enc
        (channel_id, peer_address, joined_at, removed_at)
      VALUES
        (${member.channelId}, ${member.peerAddress}, ${member.joinedAt}, ${member.removedAt})
      ON CONFLICT (channel_id, peer_address) DO UPDATE SET
        joined_at  = EXCLUDED.joined_at,
        removed_at = EXCLUDED.removed_at
    `;
  }

  async getBroadcastMembers(channelId: string): Promise<BroadcastMemberRecord[]> {
    const rows = await this.sql<Array<{ channel_id: string; peer_address: string; joined_at: string | number; removed_at: string | number | null }>>`
      SELECT channel_id, peer_address, joined_at, removed_at
      FROM shade_broadcast_members_enc
      WHERE channel_id = ${channelId}
      ORDER BY joined_at ASC
    `;
    return rows.map((r) => ({
      channelId: r.channel_id,
      peerAddress: r.peer_address,
      joinedAt: Number(r.joined_at),
      removedAt: r.removed_at === null ? null : Number(r.removed_at),
    }));
  }

  async removeBroadcastMember(channelId: string, peerAddress: string): Promise<void> {
    await this.sql`
      DELETE FROM shade_broadcast_members_enc
      WHERE channel_id = ${channelId} AND peer_address = ${peerAddress}
    `;
  }

  private async encRowToChannel(row: BroadcastChannelEncRow): Promise<BroadcastChannelRecord> {
    const sensitive = await openBroadcastChannelSensitive(
      this.km,
      String(row.channel_id),
      row.ciphertext,
    );
    const out: BroadcastChannelRecord = {
      channelId: String(row.channel_id),
      ownerRole: row.owner_role,
      ownerAddress: String(row.owner_address),
      generation: Number(row.generation),
      chainKey: sensitive.chainKey,
      iteration: sensitive.iteration,
      signingPublicKey: sensitive.signingPublicKey,
      createdAt: Number(row.created_at),
      updatedAt: Number(row.updated_at),
    };
    if (row.label !== null && row.label !== undefined) out.label = row.label;
    if (sensitive.signingPrivateKey !== undefined) out.signingPrivateKey = sensitive.signingPrivateKey;
    return out;
  }

  private async rowToStreamState(row: StreamRow): Promise<PersistedStreamState> {
    const sensitive = await openStreamSensitive(this.km, String(row.stream_id), row.ciphertext);
    const out: PersistedStreamState = {
      streamId: String(row.stream_id),
      direction: row.direction,
      peerAddress: String(row.peer_address),
      status: row.status,
      metadataJson: sensitive.metadataJson,
      partitionJson: sensitive.partitionJson,
      laneStateJson: sensitive.laneStateJson,
      ioDescriptorJson: sensitive.ioDescriptorJson,
      secretEnc: sensitive.secretEnc,
      secretNonce: sensitive.secretNonce,
      createdAt: Number(row.created_at),
      updatedAt: Number(row.updated_at),
    };
    if (sensitive.overallHashState !== undefined) out.overallHashState = sensitive.overallHashState;
    return out;
  }
}

interface StreamRow {
  stream_id: string;
  direction: 'send' | 'receive';
  peer_address: string;
  status: 'active' | 'paused' | 'finished' | 'aborted';
  ciphertext: Uint8Array;
  created_at: string | number;
  updated_at: string | number;
}

interface BroadcastChannelEncRow {
  channel_id: string;
  owner_role: 'sender' | 'receiver';
  owner_address: string;
  label: string | null;
  generation: string | number;
  ciphertext: Uint8Array;
  created_at: string | number;
  updated_at: string | number;
}

export async function ensureEncryptedClientTables(sql: Sql): Promise<void> {
  await sql`
    CREATE TABLE IF NOT EXISTS shade_meta_enc (
      key   TEXT PRIMARY KEY,
      value TEXT NOT NULL
    )
  `;
  await sql`
    CREATE TABLE IF NOT EXISTS shade_identity_enc (
      id INTEGER PRIMARY KEY CHECK (id = 1),
      ciphertext BYTEA NOT NULL
    )
  `;
  await sql`
    CREATE TABLE IF NOT EXISTS shade_config_enc (
      key TEXT PRIMARY KEY,
      ciphertext BYTEA NOT NULL
    )
  `;
  await sql`
    CREATE TABLE IF NOT EXISTS shade_signed_prekeys_enc (
      key_id INTEGER PRIMARY KEY,
      ciphertext BYTEA NOT NULL
    )
  `;
  await sql`
    CREATE TABLE IF NOT EXISTS shade_one_time_prekeys_enc (
      key_id INTEGER PRIMARY KEY,
      ciphertext BYTEA NOT NULL
    )
  `;
  await sql`
    CREATE TABLE IF NOT EXISTS shade_sessions_enc (
      address TEXT PRIMARY KEY,
      ciphertext BYTEA NOT NULL
    )
  `;
  await sql`
    CREATE TABLE IF NOT EXISTS shade_trusted_identities_enc (
      address TEXT PRIMARY KEY,
      ciphertext BYTEA NOT NULL
    )
  `;
  await sql`
    CREATE TABLE IF NOT EXISTS shade_retired_identities_enc (
      retired_at BIGINT PRIMARY KEY,
      ciphertext BYTEA NOT NULL
    )
  `;
  await sql`
    CREATE INDEX IF NOT EXISTS shade_retired_at_enc_idx
    ON shade_retired_identities_enc(retired_at)
  `;
  await sql`
    CREATE TABLE IF NOT EXISTS shade_stream_state_enc (
      stream_id    TEXT PRIMARY KEY,
      direction    TEXT NOT NULL CHECK (direction IN ('send','receive')),
      peer_address TEXT NOT NULL,
      status       TEXT NOT NULL CHECK (status IN ('active','paused','finished','aborted')),
      ciphertext   BYTEA NOT NULL,
      created_at   BIGINT NOT NULL,
      updated_at   BIGINT NOT NULL
    )
  `;
  await sql`
    CREATE INDEX IF NOT EXISTS shade_stream_enc_peer_idx
    ON shade_stream_state_enc(peer_address)
  `;
  await sql`
    CREATE INDEX IF NOT EXISTS shade_stream_enc_updated_idx
    ON shade_stream_state_enc(updated_at)
  `;
  await sql`
    CREATE INDEX IF NOT EXISTS shade_stream_enc_status_idx
    ON shade_stream_state_enc(status, direction)
  `;
  await sql`
    CREATE TABLE IF NOT EXISTS shade_peer_verifications_enc (
      peer_address     TEXT PRIMARY KEY,
      fingerprint      TEXT NOT NULL,
      verified_at      BIGINT NOT NULL,
      verified_by      TEXT NOT NULL,
      identity_version BIGINT NOT NULL
    )
  `;
  await sql`
    CREATE TABLE IF NOT EXISTS shade_peer_identity_versions_enc (
      peer_address TEXT PRIMARY KEY,
      version      BIGINT NOT NULL
    )
  `;
  await sql`
    CREATE TABLE IF NOT EXISTS shade_broadcast_channels_enc (
      channel_id    TEXT PRIMARY KEY,
      owner_role    TEXT NOT NULL CHECK (owner_role IN ('sender','receiver')),
      owner_address TEXT NOT NULL,
      label         TEXT,
      generation    BIGINT NOT NULL,
      ciphertext    BYTEA NOT NULL,
      created_at    BIGINT NOT NULL,
      updated_at    BIGINT NOT NULL
    )
  `;
  await sql`
    CREATE TABLE IF NOT EXISTS shade_broadcast_members_enc (
      channel_id   TEXT NOT NULL,
      peer_address TEXT NOT NULL,
      joined_at    BIGINT NOT NULL,
      removed_at   BIGINT,
      PRIMARY KEY (channel_id, peer_address)
    )
  `;
  await sql`
    CREATE INDEX IF NOT EXISTS shade_broadcast_members_enc_channel_idx
    ON shade_broadcast_members_enc(channel_id)
  `;
}
