/**
 * SSH connection repository.
 *
 * Design rationale (rev 4):
 *   - `resolveConnection(id)` is a raw row lookup with NO access check.
 *     Access decisions are centralized in src/ssh/access.ts → resolveAccess().
 *     Splitting them avoids the getById-blocks-granted-users pitfall.
 *   - Host key state has a two-stage lifecycle: pending (observed but unverified)
 *     → primary (user clicked verify). `setHostKeyPendingWithToken` generates a
 *     fresh UUID token; `setHostKeyVerified` consumes it atomically. Concurrent
 *     observations replace the pending key + token (last-write-wins); the previous
 *     token becomes stale and verification returns 'stale_token'.
 *   - Abuse/lock state is in ssh_abuse_counters (single source of truth). This
 *     module never reads or writes failure_count / lock_until columns on
 *     ssh_connections (they don't exist).
 *
 * Plan: docs/superpowers/plans/2026-05-12-ssh-tool-integration.md (Phase 1).
 */
import type Database from 'better-sqlite3';
import { randomUUID } from 'node:crypto';

export type HostKeyPendingSource = 'tofu_record' | 'mismatch';

export interface SshConnection {
  id: string;
  ownerId: string | null;
  label: string;
  host: string;
  port: number;
  username: string;
  privateKeyEnc: Buffer;
  passphraseEnc: Buffer | null;
  keyVersion: number;
  keyFingerprint: string | null;
  hostKeyType: string | null;
  hostKeyB64: string | null;
  hostKeyFingerprint: string | null;
  hostKeyRecordedAt: string | null;
  hostKeyVerifiedAt: string | null;
  hostKeyPending: boolean;
  hostKeyPendingB64: string | null;
  hostKeyPendingFingerprint: string | null;
  hostKeyPendingToken: string | null;
  hostKeyPendingSource: HostKeyPendingSource | null;
  commandDenyPatterns: string | null;
  commandAllowPatterns: string | null;
  remotePathPrefix: string;
  allowRemoteUnrestricted: boolean;
  allowPrivateAddresses: boolean;
  enabled: boolean;
  disabledByAdmin: boolean;
  disabledByAdminReason: string | null;
  disabledByAdminAt: string | null;
  disabledByAdminUserId: string | null;
  createdAt: string;
  updatedAt: string;
}

export interface CreateConnectionInput {
  ownerId: string | null;
  label: string;
  host: string;
  port: number;
  username: string;
  privateKeyEnc: Buffer;
  passphraseEnc?: Buffer | null;
  keyVersion?: number;
  keyFingerprint: string;
  remotePathPrefix: string;
  allowRemoteUnrestricted?: boolean;
  allowPrivateAddresses?: boolean;
  commandDenyPatterns?: string | null;
  commandAllowPatterns?: string | null;
  /** ISO8601 timestamp; defaults to now. */
  now?: string;
}

export interface UpdateConnectionInput {
  label?: string;
  host?: string;
  port?: number;
  username?: string;
  privateKeyEnc?: Buffer;
  passphraseEnc?: Buffer | null;
  keyVersion?: number;
  keyFingerprint?: string;
  remotePathPrefix?: string;
  commandDenyPatterns?: string | null;
  commandAllowPatterns?: string | null;
  allowRemoteUnrestricted?: boolean;
  allowPrivateAddresses?: boolean;
  enabled?: boolean;
}

export type HostKeyVerifyResult =
  | 'verified'
  | 'stale_token'
  | 'fingerprint_mismatch'
  | 'not_pending';

interface RawRow {
  id: string;
  owner_id: string | null;
  label: string;
  host: string;
  port: number;
  username: string;
  private_key_enc: Buffer;
  passphrase_enc: Buffer | null;
  key_version: number;
  key_fingerprint: string | null;
  host_key_type: string | null;
  host_key_b64: string | null;
  host_key_fingerprint: string | null;
  host_key_recorded_at: string | null;
  host_key_verified_at: string | null;
  host_key_pending: number;
  host_key_pending_b64: string | null;
  host_key_pending_fingerprint: string | null;
  host_key_pending_token: string | null;
  host_key_pending_source: HostKeyPendingSource | null;
  command_deny_patterns: string | null;
  command_allow_patterns: string | null;
  remote_path_prefix: string;
  allow_remote_unrestricted: number;
  allow_private_addresses: number;
  enabled: number;
  disabled_by_admin: number;
  disabled_by_admin_reason: string | null;
  disabled_by_admin_at: string | null;
  disabled_by_admin_user_id: string | null;
  created_at: string;
  updated_at: string;
}

function fromRow(r: RawRow): SshConnection {
  return {
    id: r.id,
    ownerId: r.owner_id,
    label: r.label,
    host: r.host,
    port: r.port,
    username: r.username,
    privateKeyEnc: r.private_key_enc,
    passphraseEnc: r.passphrase_enc,
    keyVersion: r.key_version,
    keyFingerprint: r.key_fingerprint,
    hostKeyType: r.host_key_type,
    hostKeyB64: r.host_key_b64,
    hostKeyFingerprint: r.host_key_fingerprint,
    hostKeyRecordedAt: r.host_key_recorded_at,
    hostKeyVerifiedAt: r.host_key_verified_at,
    hostKeyPending: r.host_key_pending === 1,
    hostKeyPendingB64: r.host_key_pending_b64,
    hostKeyPendingFingerprint: r.host_key_pending_fingerprint,
    hostKeyPendingToken: r.host_key_pending_token,
    hostKeyPendingSource: r.host_key_pending_source,
    commandDenyPatterns: r.command_deny_patterns,
    commandAllowPatterns: r.command_allow_patterns,
    remotePathPrefix: r.remote_path_prefix,
    allowRemoteUnrestricted: r.allow_remote_unrestricted === 1,
    allowPrivateAddresses: r.allow_private_addresses === 1,
    enabled: r.enabled === 1,
    disabledByAdmin: r.disabled_by_admin === 1,
    disabledByAdminReason: r.disabled_by_admin_reason,
    disabledByAdminAt: r.disabled_by_admin_at,
    disabledByAdminUserId: r.disabled_by_admin_user_id,
    createdAt: r.created_at,
    updatedAt: r.updated_at,
  };
}

/**
 * Parse the SSH algorithm name out of a host key in OpenSSH wire format.
 * Wire format: <4-byte big-endian length><algorithm name string><...keydata>.
 * Returns null on malformed input.
 */
export function parseHostKeyType(b64: string): string | null {
  try {
    const buf = Buffer.from(b64, 'base64');
    if (buf.length < 4) return null;
    const len = buf.readUInt32BE(0);
    if (len === 0 || len > 64 || len + 4 > buf.length) return null;
    const name = buf.slice(4, 4 + len).toString('utf8');
    if (!/^[a-z0-9@.-]+$/i.test(name)) return null;
    return name;
  } catch {
    return null;
  }
}

export interface SshConnectionRepo {
  create(input: CreateConnectionInput): SshConnection;
  update(id: string, patch: UpdateConnectionInput, now?: string): boolean;
  delete(id: string): boolean;
  /** Raw lookup. NO access check — caller MUST use resolveAccess() for authorization. */
  resolveConnection(id: string): SshConnection | null;
  listOwned(ownerId: string): SshConnection[];
  /** Admin-only listing. */
  listAll(): SshConnection[];
  disableByAdmin(id: string, reason: string, byUserId: string, now?: string): boolean;
  enableByAdmin(id: string, now?: string): boolean;
  /**
   * Record an observed host key as pending and issue a fresh verify token.
   * Replaces any existing pending key (last-write-wins). Returns the new token
   * — the caller surfaces this to the verifying user.
   */
  setHostKeyPendingWithToken(
    id: string,
    b64: string,
    fingerprint: string,
    source: HostKeyPendingSource,
    now?: string,
  ): { token: string } | null;
  /**
   * Atomically promote pending → primary if the token AND fingerprint match.
   * Used for first-time TOFU verify.
   * 'not_pending'         — no pending key on this connection
   * 'stale_token'         — token mismatch (pending was replaced by a newer observation)
   * 'fingerprint_mismatch' — pending fingerprint differs from caller-provided one
   * 'verified'            — promotion succeeded
   */
  setHostKeyVerified(
    id: string,
    token: string,
    fingerprint: string,
    now?: string,
  ): HostKeyVerifyResult;
  /**
   * Promote a pending key over an existing verified one (host key rotation).
   * Atomic CHECK on token + fingerprint, same return codes as setHostKeyVerified.
   * Audit caller must record `ssh.connection.host_key.replace` with reason.
   */
  replaceHostKey(
    id: string,
    token: string,
    fingerprint: string,
    now?: string,
  ): HostKeyVerifyResult;
}

export function createConnectionRepo(db: Database.Database): SshConnectionRepo {
  const insertStmt = db.prepare(`
    INSERT INTO ssh_connections (
      id, owner_id, label, host, port, username,
      private_key_enc, passphrase_enc, key_version, key_fingerprint,
      remote_path_prefix, allow_remote_unrestricted, allow_private_addresses,
      command_deny_patterns, command_allow_patterns,
      created_at, updated_at
    )
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  `);

  const selectByIdStmt = db.prepare(`SELECT * FROM ssh_connections WHERE id = ?`);
  const deleteStmt = db.prepare(`DELETE FROM ssh_connections WHERE id = ?`);
  const selectByOwnerStmt = db.prepare(
    `SELECT * FROM ssh_connections WHERE owner_id = ? ORDER BY created_at DESC`,
  );
  const selectAllStmt = db.prepare(`SELECT * FROM ssh_connections ORDER BY created_at DESC`);

  const disableStmt = db.prepare(`
    UPDATE ssh_connections
    SET enabled = 0,
        disabled_by_admin = 1,
        disabled_by_admin_reason = ?,
        disabled_by_admin_at = ?,
        disabled_by_admin_user_id = ?,
        updated_at = ?
    WHERE id = ?
  `);

  const enableStmt = db.prepare(`
    UPDATE ssh_connections
    SET enabled = 1,
        disabled_by_admin = 0,
        disabled_by_admin_reason = NULL,
        disabled_by_admin_at = NULL,
        disabled_by_admin_user_id = NULL,
        updated_at = ?
    WHERE id = ?
  `);

  const setPendingStmt = db.prepare(`
    UPDATE ssh_connections
    SET host_key_pending = 1,
        host_key_pending_b64 = ?,
        host_key_pending_fingerprint = ?,
        host_key_pending_token = ?,
        host_key_pending_source = ?,
        updated_at = ?
    WHERE id = ?
  `);

  /**
   * Atomic verify: clear pending fields and either promote them to primary
   * (when source was tofu_record / replacing) or just clear (verify only).
   * SQL CHECK on token + fingerprint inside WHERE = atomic.
   */
  const verifyPromoteStmt = db.prepare(`
    UPDATE ssh_connections
    SET host_key_type = ?,
        host_key_b64 = host_key_pending_b64,
        host_key_fingerprint = host_key_pending_fingerprint,
        host_key_recorded_at = ?,
        host_key_verified_at = ?,
        host_key_pending = 0,
        host_key_pending_b64 = NULL,
        host_key_pending_fingerprint = NULL,
        host_key_pending_token = NULL,
        host_key_pending_source = NULL,
        updated_at = ?
    WHERE id = ?
      AND host_key_pending = 1
      AND host_key_pending_token = ?
      AND host_key_pending_fingerprint = ?
  `);

  function readPendingState(id: string):
    | { exists: true; pending: boolean; token: string | null; fingerprint: string | null }
    | { exists: false } {
    const row = selectByIdStmt.get(id) as RawRow | undefined;
    if (!row) return { exists: false };
    return {
      exists: true,
      pending: row.host_key_pending === 1,
      token: row.host_key_pending_token,
      fingerprint: row.host_key_pending_fingerprint,
    };
  }

  function runVerifyPromote(
    id: string,
    token: string,
    fingerprint: string,
    now: string,
  ): HostKeyVerifyResult {
    // We use a transaction so that the state classification (no pending /
    // stale_token / fingerprint_mismatch) reads from the same snapshot as
    // the conditional update.
    const tx = db.transaction((): HostKeyVerifyResult => {
      const state = readPendingState(id);
      if (!state.exists) return 'not_pending';
      if (!state.pending) return 'not_pending';
      if (state.token !== token) return 'stale_token';
      if (state.fingerprint !== fingerprint) return 'fingerprint_mismatch';
      // Derive type from the (already-validated) pending b64.
      const row = selectByIdStmt.get(id) as RawRow;
      const hostKeyType = row.host_key_pending_b64
        ? parseHostKeyType(row.host_key_pending_b64)
        : null;
      const r = verifyPromoteStmt.run(hostKeyType, now, now, now, id, token, fingerprint);
      return r.changes === 1 ? 'verified' : 'stale_token';
    });
    return tx();
  }

  return {
    create(input) {
      const id = randomUUID();
      const now = input.now ?? new Date().toISOString();
      if (input.remotePathPrefix.length === 0) {
        throw new Error('connection: remotePathPrefix must be non-empty');
      }
      insertStmt.run(
        id,
        input.ownerId,
        input.label,
        input.host,
        input.port,
        input.username,
        input.privateKeyEnc,
        input.passphraseEnc ?? null,
        input.keyVersion ?? 1,
        input.keyFingerprint,
        input.remotePathPrefix,
        input.allowRemoteUnrestricted ? 1 : 0,
        input.allowPrivateAddresses ? 1 : 0,
        input.commandDenyPatterns ?? null,
        input.commandAllowPatterns ?? null,
        now,
        now,
      );
      const row = selectByIdStmt.get(id) as RawRow;
      return fromRow(row);
    },

    update(id, patch, now) {
      const fields: string[] = [];
      const values: unknown[] = [];
      const push = (col: string, v: unknown) => {
        fields.push(`${col} = ?`);
        values.push(v);
      };
      if (patch.label !== undefined) push('label', patch.label);
      if (patch.host !== undefined) push('host', patch.host);
      if (patch.port !== undefined) push('port', patch.port);
      if (patch.username !== undefined) push('username', patch.username);
      if (patch.privateKeyEnc !== undefined) push('private_key_enc', patch.privateKeyEnc);
      if (patch.passphraseEnc !== undefined) push('passphrase_enc', patch.passphraseEnc);
      if (patch.keyVersion !== undefined) push('key_version', patch.keyVersion);
      if (patch.keyFingerprint !== undefined) push('key_fingerprint', patch.keyFingerprint);
      if (patch.remotePathPrefix !== undefined) {
        if (patch.remotePathPrefix.length === 0) {
          throw new Error('connection: remotePathPrefix must be non-empty');
        }
        push('remote_path_prefix', patch.remotePathPrefix);
      }
      if (patch.commandDenyPatterns !== undefined)
        push('command_deny_patterns', patch.commandDenyPatterns);
      if (patch.commandAllowPatterns !== undefined)
        push('command_allow_patterns', patch.commandAllowPatterns);
      if (patch.allowRemoteUnrestricted !== undefined)
        push('allow_remote_unrestricted', patch.allowRemoteUnrestricted ? 1 : 0);
      if (patch.allowPrivateAddresses !== undefined)
        push('allow_private_addresses', patch.allowPrivateAddresses ? 1 : 0);
      if (patch.enabled !== undefined) push('enabled', patch.enabled ? 1 : 0);
      if (fields.length === 0) return false;
      push('updated_at', now ?? new Date().toISOString());
      values.push(id);
      const sql = `UPDATE ssh_connections SET ${fields.join(', ')} WHERE id = ?`;
      const r = db.prepare(sql).run(...values);
      return r.changes > 0;
    },

    delete(id) {
      const r = deleteStmt.run(id);
      return r.changes > 0;
    },

    resolveConnection(id) {
      const row = selectByIdStmt.get(id) as RawRow | undefined;
      return row ? fromRow(row) : null;
    },

    listOwned(ownerId) {
      return (selectByOwnerStmt.all(ownerId) as RawRow[]).map(fromRow);
    },

    listAll() {
      return (selectAllStmt.all() as RawRow[]).map(fromRow);
    },

    disableByAdmin(id, reason, byUserId, now) {
      const ts = now ?? new Date().toISOString();
      const r = disableStmt.run(reason, ts, byUserId, ts, id);
      return r.changes > 0;
    },

    enableByAdmin(id, now) {
      const ts = now ?? new Date().toISOString();
      const r = enableStmt.run(ts, id);
      return r.changes > 0;
    },

    setHostKeyPendingWithToken(id, b64, fingerprint, source, now) {
      const ts = now ?? new Date().toISOString();
      const token = randomUUID();
      const r = setPendingStmt.run(b64, fingerprint, token, source, ts, id);
      if (r.changes === 0) return null;
      return { token };
    },

    setHostKeyVerified(id, token, fingerprint, now) {
      return runVerifyPromote(id, token, fingerprint, now ?? new Date().toISOString());
    },

    replaceHostKey(id, token, fingerprint, now) {
      // Same atomic promotion as verify. The semantic distinction (first-time
      // verify vs replacing an existing key) lives in the audit log + UI flow.
      return runVerifyPromote(id, token, fingerprint, now ?? new Date().toISOString());
    },
  };
}
