/**
 * AuditVerificationService — SOC 2 CC7.2 Audit Log Integrity.
 *
 * Walks the audit_events hash chain and verifies that every event's stored hash
 * matches the recomputed hash of its fields, and that its previous_hash matches
 * the hash of the chronologically preceding event.
 *
 * A broken chain indicates potential log tampering, deletion, or insertion of events.
 * The first detected break is reported via `brokenAtEventId`.
 */

import crypto from 'crypto';
import { Pool, QueryResult } from 'pg';

// ============================================================================
// Types
// ============================================================================

/**
 * Result of a single audit chain verification run.
 * Returned by verifyChain() and consumed by ComplianceController and
 * AuditChainVerificationJob.
 */
export interface IChainVerificationResult {
  /** `true` if every event in the checked range maintains an unbroken cryptographic hash chain. */
  verified: boolean;
  /** Total number of audit events examined during this verification run. */
  checkedCount: number;
  /**
   * UUID of the first audit event where chain continuity failed, or `null` when `verified` is `true`.
   * Only the first detected break is reported.
   */
  brokenAtEventId: string | null;
  /** ISO 8601 lower bound applied during this verification run (only present if fromDate was supplied). */
  fromDate?: string;
  /** ISO 8601 upper bound applied during this verification run (only present if toDate was supplied). */
  toDate?: string;
}

// ============================================================================
// Internal row shape
// ============================================================================

/** Raw row from audit_events used during chain traversal. */
interface AuditChainRow {
  event_id: string;
  timestamp: Date;
  action: string;
  outcome: string;
  agent_id: string;
  organization_id: string;
  hash: string;
  previous_hash: string;
}

// ============================================================================
// Service
// ============================================================================

/**
 * Service that performs cryptographic verification of the audit event hash chain.
 * Implements a single-pass walk of all events in an optional date range,
 * recomputing each hash and checking linkage to the previous event.
 */
export class AuditVerificationService {
  /**
   * @param pool - PostgreSQL connection pool used to query audit_events.
   */
  constructor(private readonly pool: Pool) {}

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

  /**
   * Verifies the integrity of the audit event chain across an optional date range.
   *
   * Events are traversed in ascending chronological order (timestamp ASC, event_id ASC).
   * For each event:
   *   1. Recompute the expected hash from the event's fields and the previous event's hash.
   *   2. Compare to the stored `hash`.
   *   3. Verify that `previous_hash` matches the preceding row's hash.
   *
   * Verification stops at the first detected break and returns the broken event's ID.
   * Events seeded with empty-string hashes (pre-chain migration rows) are skipped.
   *
   * @param fromDate - Optional ISO 8601 lower bound (inclusive) for the date range.
   * @param toDate   - Optional ISO 8601 upper bound (inclusive) for the date range.
   * @returns Chain verification result.
   */
  async verifyChain(
    fromDate?: string,
    toDate?: string,
  ): Promise<IChainVerificationResult> {
    const conditions: string[] = [];
    const params: unknown[] = [];
    let paramIndex = 1;

    if (fromDate !== undefined) {
      conditions.push(`timestamp >= $${paramIndex++}`);
      params.push(new Date(fromDate));
    }
    if (toDate !== undefined) {
      conditions.push(`timestamp <= $${paramIndex++}`);
      params.push(new Date(toDate));
    }

    const whereClause =
      conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';

    const result: QueryResult<AuditChainRow> = await this.pool.query(
      `SELECT event_id, timestamp, action, outcome, agent_id, organization_id, hash, previous_hash
       FROM audit_events
       ${whereClause}
       ORDER BY timestamp ASC, event_id ASC`,
      params,
    );

    const rows = result.rows;

    if (rows.length === 0) {
      return {
        verified: true,
        checkedCount: 0,
        brokenAtEventId: null,
        ...(fromDate !== undefined ? { fromDate } : {}),
        ...(toDate !== undefined ? { toDate } : {}),
      };
    }

    let previousHash = '';
    let checkedCount = 0;

    for (const row of rows) {
      // Skip events seeded with empty hashes (pre-chain migration rows)
      if (row.hash === '' && row.previous_hash === '') {
        previousHash = '';
        checkedCount++;
        continue;
      }

      // Verify previous_hash linkage
      if (row.previous_hash !== previousHash) {
        return {
          verified: false,
          checkedCount,
          brokenAtEventId: row.event_id,
          ...(fromDate !== undefined ? { fromDate } : {}),
          ...(toDate !== undefined ? { toDate } : {}),
        };
      }

      // Recompute and verify the stored hash
      const expectedHash = this.computeHash(
        row.event_id,
        row.timestamp,
        row.action,
        row.outcome,
        row.agent_id,
        row.organization_id,
        row.previous_hash,
      );

      if (expectedHash !== row.hash) {
        return {
          verified: false,
          checkedCount,
          brokenAtEventId: row.event_id,
          ...(fromDate !== undefined ? { fromDate } : {}),
          ...(toDate !== undefined ? { toDate } : {}),
        };
      }

      previousHash = row.hash;
      checkedCount++;
    }

    return {
      verified: true,
      checkedCount,
      brokenAtEventId: null,
      ...(fromDate !== undefined ? { fromDate } : {}),
      ...(toDate !== undefined ? { toDate } : {}),
    };
  }

  // ──────────────────────────────────────────────────────────────────────────
  // Private helpers
  // ──────────────────────────────────────────────────────────────────────────

  /**
   * Computes the SHA-256 hash for a given audit event.
   * Must match the algorithm used by AuditRepository.create.
   *
   * @param eventId - The event UUID.
   * @param timestamp - The event timestamp.
   * @param action - The audit action.
   * @param outcome - The audit outcome.
   * @param agentId - The agent UUID.
   * @param organizationId - The organization UUID.
   * @param previousHash - The hash of the preceding event.
   * @returns 64-character hex SHA-256 hash.
   */
  private computeHash(
    eventId: string,
    timestamp: Date,
    action: string,
    outcome: string,
    agentId: string,
    organizationId: string,
    previousHash: string,
  ): string {
    return crypto
      .createHash('sha256')
      .update(
        eventId +
        timestamp.toISOString() +
        action +
        outcome +
        agentId +
        organizationId +
        previousHash,
      )
      .digest('hex');
  }
}

// ============================================================================
// Singleton export
// ============================================================================

/**
 * Module-level singleton instance of AuditVerificationService.
 * Initialised lazily on first call to getAuditVerificationService().
 */
let _instance: AuditVerificationService | null = null;

/**
 * Returns the singleton AuditVerificationService, creating it on first call.
 *
 * @param pool - PostgreSQL pool (required on first call; ignored on subsequent calls).
 * @returns The singleton AuditVerificationService.
 */
export function getAuditVerificationService(pool: Pool): AuditVerificationService {
  if (_instance === null) {
    _instance = new AuditVerificationService(pool);
  }
  return _instance;
}

/**
 * Resets the module singleton (for testing only).
 *
 * @internal
 */
export function _resetAuditVerificationServiceSingleton(): void {
  _instance = null;
}
