/**
 * Audit Repository for SentryAgent.ai AgentIdP.
 * All SQL queries for the audit_events table live exclusively here.
 *
 * SOC 2 CC7.2 — Hash chain: each event INSERT also fetches the previous hash and
 * computes the new hash via SHA-256, linking it cryptographically to the prior event.
 */

import crypto from 'crypto';
import { Pool, QueryResult } from 'pg';
import { v4 as uuidv4 } from 'uuid';
import { IAuditEvent, ICreateAuditEventInput, IAuditListFilters } from '../types/index.js';

/** Raw database row for an audit event. */
interface AuditEventRow {
  event_id: string;
  agent_id: string;
  organization_id: string;
  action: string;
  outcome: string;
  ip_address: string;
  user_agent: string;
  metadata: Record<string, unknown>;
  timestamp: Date;
  hash: string;
  previous_hash: string;
}

/** Raw row returned by the previous-hash query. */
interface PreviousHashRow {
  hash: string;
}

/**
 * Maps a raw database row to the IAuditEvent domain model.
 *
 * @param row - Raw row from the audit_events table.
 * @returns Typed IAuditEvent object.
 */
function mapRowToAuditEvent(row: AuditEventRow): IAuditEvent {
  return {
    eventId: row.event_id,
    agentId: row.agent_id,
    action: row.action as IAuditEvent['action'],
    outcome: row.outcome as IAuditEvent['outcome'],
    ipAddress: row.ip_address,
    userAgent: row.user_agent,
    metadata: row.metadata,
    timestamp: row.timestamp,
  };
}

/**
 * Computes the SHA-256 hash for an audit event in the chain.
 *
 * @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 ('' for the first event).
 * @returns 64-character hex SHA-256 hash.
 */
function computeAuditHash(
  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');
}

/**
 * Repository for all audit event database operations.
 * Receives a pg.Pool via constructor injection.
 */
export class AuditRepository {
  /**
   * @param pool - The PostgreSQL connection pool.
   */
  constructor(private readonly pool: Pool) {}

  /**
   * Creates a new audit event record with hash chain linkage.
   *
   * Before the INSERT, fetches the hash of the most recent event to use as
   * `previous_hash`. The new event's hash is computed as SHA-256 over its
   * key fields plus the previous hash — cryptographically linking it to the
   * preceding event in the chain (SOC 2 CC7.2).
   *
   * @param event - The audit event input data.
   * @returns The created audit event.
   */
  async create(event: ICreateAuditEventInput): Promise<IAuditEvent> {
    const eventId = uuidv4();
    const organizationId = event.organizationId ?? 'org_system';

    // Fetch the previous event's hash for chain linkage
    const prevResult: QueryResult<PreviousHashRow> = await this.pool.query(
      `SELECT hash FROM audit_events ORDER BY timestamp DESC, event_id DESC LIMIT 1`,
    );
    const previousHash = prevResult.rows.length > 0 ? prevResult.rows[0].hash : '';

    // We need the exact timestamp that will be inserted to compute the hash.
    // Use a sub-select for the INSERT and capture it in the RETURNING clause.
    // The hash is computed client-side using the current timestamp.
    const timestamp = new Date();
    const hash = computeAuditHash(
      eventId,
      timestamp,
      event.action,
      event.outcome,
      event.agentId,
      organizationId,
      previousHash,
    );

    const result: QueryResult<AuditEventRow> = await this.pool.query(
      `INSERT INTO audit_events
         (event_id, agent_id, organization_id, action, outcome, ip_address, user_agent, metadata, timestamp, hash, previous_hash)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
       RETURNING *`,
      [
        eventId,
        event.agentId,
        organizationId,
        event.action,
        event.outcome,
        event.ipAddress,
        event.userAgent,
        JSON.stringify(event.metadata),
        timestamp,
        hash,
        previousHash,
      ],
    );
    return mapRowToAuditEvent(result.rows[0]);
  }

  /**
   * Finds a single audit event by its UUID.
   *
   * @param eventId - The audit event UUID.
   * @returns The audit event, or null if not found.
   */
  async findById(eventId: string): Promise<IAuditEvent | null> {
    const result: QueryResult<AuditEventRow> = await this.pool.query(
      'SELECT * FROM audit_events WHERE event_id = $1',
      [eventId],
    );
    if (result.rows.length === 0) return null;
    return mapRowToAuditEvent(result.rows[0]);
  }

  /**
   * Returns a paginated, filtered list of audit events.
   * Automatically enforces the 90-day retention window on query results.
   *
   * @param filters - Query filters and pagination parameters.
   * @param retentionCutoff - The earliest date to include (retention window).
   * @returns Object containing the audit events list and total count.
   */
  async findAll(
    filters: IAuditListFilters,
    retentionCutoff: Date,
  ): Promise<{ events: IAuditEvent[]; total: number }> {
    const conditions: string[] = ['timestamp >= $1'];
    const params: unknown[] = [retentionCutoff];
    let paramIndex = 2;

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

    const whereClause = `WHERE ${conditions.join(' AND ')}`;

    const countResult: QueryResult<{ count: string }> = await this.pool.query(
      `SELECT COUNT(*) as count FROM audit_events ${whereClause}`,
      params,
    );
    const total = parseInt(countResult.rows[0].count, 10);

    const offset = (filters.page - 1) * filters.limit;
    const dataParams = [...params, filters.limit, offset];

    const dataResult: QueryResult<AuditEventRow> = await this.pool.query(
      `SELECT * FROM audit_events ${whereClause}
       ORDER BY timestamp DESC
       LIMIT $${paramIndex++} OFFSET $${paramIndex}`,
      dataParams,
    );

    return {
      events: dataResult.rows.map(mapRowToAuditEvent),
      total,
    };
  }
}
