/**
 * Audit Log Service for SentryAgent.ai AgentIdP.
 * Provides methods for logging and querying immutable audit events.
 *
 * SOC 2 CC7.2 — Audit Log Integrity:
 * Each event is cryptographically linked to the previous one via a SHA-256 hash chain.
 * The hash is computed as:
 *   SHA-256(eventId + timestamp.toISOString() + action + outcome + agentId + organizationId + previousHash)
 * This makes any tampering, deletion, or insertion detectable via AuditVerificationService.
 */

import crypto from 'crypto';
import { AuditRepository } from '../repositories/AuditRepository.js';
import {
  IAuditEvent,
  IAuditListFilters,
  IPaginatedAuditEventsResponse,
  AuditAction,
  AuditOutcome,
} from '../types/index.js';
import {
  AuditEventNotFoundError,
  RetentionWindowError,
  ValidationError,
} from '../utils/errors.js';

const FREE_TIER_RETENTION_DAYS = 90;

/**
 * Service for creating and querying audit log events.
 * Enforces 90-day retention window on all queries.
 */
export class AuditService {
  /**
   * @param auditRepository - The audit event repository.
   */
  constructor(private readonly auditRepository: AuditRepository) {}

  /**
   * Computes the earliest allowed timestamp for audit queries (90-day retention).
   *
   * @returns The retention cutoff Date.
   */
  private getRetentionCutoff(): Date {
    const cutoff = new Date();
    cutoff.setUTCDate(cutoff.getUTCDate() - FREE_TIER_RETENTION_DAYS);
    cutoff.setUTCHours(0, 0, 0, 0);
    return cutoff;
  }

  /**
   * Computes the SHA-256 hash for an audit event in the chain.
   * Used internally and by AuditVerificationService.
   *
   * @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.
   */
  static 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');
  }

  /**
   * Logs an audit event. This is a fire-and-forget async insert for token
   * endpoints (do not await). For DB-backed operations, await this method.
   *
   * @param agentId - The agent that triggered the event.
   * @param action - The action that occurred.
   * @param outcome - Whether the action succeeded or failed.
   * @param ipAddress - The client IP address.
   * @param userAgent - The client User-Agent header.
   * @param metadata - Action-specific structured context data.
   * @param organizationId - Optional organization UUID for hash chain computation.
   * @returns Promise resolving to the created audit event.
   */
  async logEvent(
    agentId: string,
    action: AuditAction,
    outcome: AuditOutcome,
    ipAddress: string,
    userAgent: string,
    metadata: Record<string, unknown>,
    organizationId?: string,
  ): Promise<IAuditEvent> {
    return this.auditRepository.create({
      agentId,
      organizationId,
      action,
      outcome,
      ipAddress,
      userAgent,
      metadata,
    });
  }

  /**
   * Queries the audit log with optional filters, pagination, and retention enforcement.
   *
   * @param filters - Query filters and pagination parameters.
   * @returns Paginated audit events response.
   * @throws RetentionWindowError if fromDate is before the 90-day retention cutoff.
   * @throws ValidationError if fromDate is after toDate.
   */
  async queryEvents(filters: IAuditListFilters): Promise<IPaginatedAuditEventsResponse> {
    const retentionCutoff = this.getRetentionCutoff();

    if (filters.fromDate !== undefined) {
      const fromDate = new Date(filters.fromDate);
      if (fromDate < retentionCutoff) {
        throw new RetentionWindowError(
          FREE_TIER_RETENTION_DAYS,
          retentionCutoff.toISOString(),
        );
      }
    }

    if (filters.fromDate !== undefined && filters.toDate !== undefined) {
      const fromDate = new Date(filters.fromDate);
      const toDate = new Date(filters.toDate);
      if (fromDate > toDate) {
        throw new ValidationError('Invalid date range.', {
          reason: 'fromDate must be before or equal to toDate.',
        });
      }
    }

    const { events, total } = await this.auditRepository.findAll(filters, retentionCutoff);

    return {
      data: events,
      total,
      page: filters.page,
      limit: filters.limit,
    };
  }

  /**
   * Retrieves a single audit event by its UUID.
   *
   * @param eventId - The audit event UUID.
   * @returns The audit event record.
   * @throws AuditEventNotFoundError if the event does not exist.
   */
  async getEventById(eventId: string): Promise<IAuditEvent> {
    const event = await this.auditRepository.findById(eventId);
    if (!event) {
      throw new AuditEventNotFoundError(eventId);
    }

    // Check retention window — events older than 90 days are not accessible
    const retentionCutoff = this.getRetentionCutoff();
    if (event.timestamp < retentionCutoff) {
      throw new AuditEventNotFoundError(eventId);
    }

    return event;
  }
}
