/**
 * SSH audit log: dedicated table with pending → completed/failed/denied/aborted lifecycle.
 *
 * Design rationale (codex review CRITICAL #6):
 *   Tool operations begin a pending audit row, commit, THEN issue the remote call.
 *   If the orchestrator crashes mid-call, the pending row remains and is reconciled
 *   to 'aborted' on next boot (see ssh-recovery.ts). This gives us "execution
 *   happened, outcome unknown" forensics rather than "no record at all".
 *
 * All write operations are inside a transaction (better-sqlite3 default for prepare+run
 * with .transaction wrappers when needed).
 *
 * Plan: docs/superpowers/plans/2026-05-12-ssh-tool-integration.md (Phase 1).
 */
import type Database from 'better-sqlite3';

export type SshAuditOutcome = 'pending' | 'success' | 'failed' | 'denied' | 'aborted';

export interface BeginAuditInput {
  action: string;
  entityType?: string;
  entityId?: string;
  connectionId?: string;
  ownerId?: string | null;
  actingUserId?: string;
  jobId?: string;
  pieceName?: string;
  reason?: string;
  detail?: Record<string, unknown>;
  /** ISO8601 timestamp; defaults to now. */
  startedAt?: string;
}

export interface SshAuditRow {
  id: number;
  action: string;
  entityType: string | null;
  entityId: string | null;
  connectionId: string | null;
  ownerId: string | null;
  actingUserId: string | null;
  jobId: string | null;
  pieceName: string | null;
  outcome: SshAuditOutcome;
  reason: string | null;
  detail: Record<string, unknown> | null;
  startedAt: string;
  completedAt: string | null;
}

interface RawRow {
  id: number;
  action: string;
  entity_type: string | null;
  entity_id: string | null;
  connection_id: string | null;
  owner_id: string | null;
  acting_user_id: string | null;
  job_id: string | null;
  piece_name: string | null;
  outcome: SshAuditOutcome;
  reason: string | null;
  detail: string | null;
  started_at: string;
  completed_at: string | null;
}

function fromRow(r: RawRow): SshAuditRow {
  return {
    id: r.id,
    action: r.action,
    entityType: r.entity_type,
    entityId: r.entity_id,
    connectionId: r.connection_id,
    ownerId: r.owner_id,
    actingUserId: r.acting_user_id,
    jobId: r.job_id,
    pieceName: r.piece_name,
    outcome: r.outcome,
    reason: r.reason,
    detail: r.detail ? (JSON.parse(r.detail) as Record<string, unknown>) : null,
    startedAt: r.started_at,
    completedAt: r.completed_at,
  };
}

export interface SshAuditRepo {
  /** Insert a pending audit row. Commits before returning (synchronous via better-sqlite3). */
  begin(input: BeginAuditInput): number;
  /**
   * Update an existing pending row to a terminal outcome. Idempotent: if the row
   * was already completed, this is a no-op (returns false). Returns true if the
   * row was 'pending' and was updated.
   */
  complete(id: number, outcome: Exclude<SshAuditOutcome, 'pending'>, detail?: Record<string, unknown>): boolean;
  /** Convenience: begin + complete in one call, for actions with no remote operation. */
  beginAndComplete(input: BeginAuditInput, outcome: Exclude<SshAuditOutcome, 'pending'>): number;
  listForConnection(connectionId: string, limit?: number): SshAuditRow[];
  listForOwner(ownerId: string, limit?: number): SshAuditRow[];
  listPending(): SshAuditRow[];
  getById(id: number): SshAuditRow | null;
}

export function createAuditRepo(db: Database.Database): SshAuditRepo {
  const insertStmt = db.prepare(`
    INSERT INTO ssh_audit_log (
      action, entity_type, entity_id, connection_id, owner_id,
      acting_user_id, job_id, piece_name, outcome, reason, detail,
      started_at, completed_at
    )
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?, NULL)
  `);

  const completeStmt = db.prepare(`
    UPDATE ssh_audit_log
    SET outcome = ?, detail = ?, completed_at = ?
    WHERE id = ? AND outcome = 'pending'
  `);

  const selectByIdStmt = db.prepare(`SELECT * FROM ssh_audit_log WHERE id = ?`);
  const selectByConnStmt = db.prepare(
    `SELECT * FROM ssh_audit_log WHERE connection_id = ? ORDER BY started_at DESC LIMIT ?`,
  );
  const selectByOwnerStmt = db.prepare(
    `SELECT * FROM ssh_audit_log WHERE owner_id = ? ORDER BY started_at DESC LIMIT ?`,
  );
  const selectPendingStmt = db.prepare(
    `SELECT * FROM ssh_audit_log WHERE outcome = 'pending' ORDER BY started_at ASC`,
  );

  function begin(input: BeginAuditInput): number {
    const startedAt = input.startedAt ?? new Date().toISOString();
    const detailJson = input.detail ? JSON.stringify(input.detail) : null;
    const result = insertStmt.run(
      input.action,
      input.entityType ?? null,
      input.entityId ?? null,
      input.connectionId ?? null,
      input.ownerId ?? null,
      input.actingUserId ?? null,
      input.jobId ?? null,
      input.pieceName ?? null,
      input.reason ?? null,
      detailJson,
      startedAt,
    );
    return result.lastInsertRowid as number;
  }

  function complete(
    id: number,
    outcome: Exclude<SshAuditOutcome, 'pending'>,
    detail?: Record<string, unknown>,
  ): boolean {
    // Merge detail with any existing detail (caller-supplied wins for overlapping keys).
    let mergedDetail: string | null = null;
    const existing = selectByIdStmt.get(id) as RawRow | undefined;
    if (existing) {
      const existingDetail = existing.detail
        ? (JSON.parse(existing.detail) as Record<string, unknown>)
        : {};
      const merged: Record<string, unknown> = { ...existingDetail, ...(detail ?? {}) };
      mergedDetail = JSON.stringify(merged);
    } else if (detail) {
      mergedDetail = JSON.stringify(detail);
    }
    const completedAt = new Date().toISOString();
    const r = completeStmt.run(outcome, mergedDetail, completedAt, id);
    return r.changes > 0;
  }

  function beginAndComplete(
    input: BeginAuditInput,
    outcome: Exclude<SshAuditOutcome, 'pending'>,
  ): number {
    return db.transaction(() => {
      const id = begin(input);
      complete(id, outcome);
      return id;
    })();
  }

  return {
    begin,
    complete,
    beginAndComplete,
    listForConnection(connectionId, limit = 50) {
      return (selectByConnStmt.all(connectionId, limit) as RawRow[]).map(fromRow);
    },
    listForOwner(ownerId, limit = 50) {
      return (selectByOwnerStmt.all(ownerId, limit) as RawRow[]).map(fromRow);
    },
    listPending() {
      return (selectPendingStmt.all() as RawRow[]).map(fromRow);
    },
    getById(id) {
      const r = selectByIdStmt.get(id) as RawRow | undefined;
      return r ? fromRow(r) : null;
    },
  };
}
