<?php
/**
 * NIS2 Agile — Audit Trail Certificato SHA-256
 *
 * Hash chain: ogni record include l'hash del record precedente per la stessa
 * organization_id. La manomissione di qualsiasi record rompe la catena
 * e viene rilevata da verifyChain().
 *
 * Adattato dal pattern AuditTrailService di 231 Agile (lg231-agile/shared/audit-lib).
 *
 * Uso:
 *   AuditService::log($orgId, $userId, 'incident.create', 'incident', 42, ['severity'=>'high']);
 *   $result = AuditService::verifyChain($db, $orgId);
 */
class AuditService
{
    /**
     * Registra un'azione nell'audit trail con hash chain SHA-256.
     * Non lancia eccezioni — il fallimento del log non blocca l'operazione principale.
     *
     * @param int         $orgId       organization_id
     * @param int|null    $userId      user_id (null = sistema/anonimo)
     * @param string      $action      Azione es. 'incident.create', 'risk.update', 'policy.approve'
     * @param string|null $entityType  Tipo entità: 'incident','risk','policy','supplier','asset',...
     * @param int|null    $entityId    ID entità
     * @param array|null  $details     Dati aggiuntivi (serializzati come JSON)
     * @param string      $ipAddress   IP del client
     * @param string|null $userAgent   User-Agent
     * @param string      $severity    'info' | 'warning' | 'critical'
     * @param string|null $performedBy Email/identificatore utente (per export certificato)
     */
    public static function log(
        int     $orgId,
        ?int    $userId,
        string  $action,
        ?string $entityType  = null,
        ?int    $entityId    = null,
        ?array  $details     = null,
        string  $ipAddress   = '',
        ?string $userAgent   = null,
        string  $severity    = 'info',
        ?string $performedBy = null
    ): void {
        try {
            $db = Database::getInstance();

            $prevHash    = self::getLastHash($db, $orgId);
            $detailsJson = $details ? self::canonicalJson($details) : '';
            $createdAt   = date('Y-m-d H:i:s');

            // Calcola hash catena: deterministico su dati noti prima dell'INSERT
            $entryHash = hash('sha256',
                $orgId       .
                ($userId ?? 'system') .
                $action      .
                ($entityType ?? '') .
                ($entityId   ?? '') .
                $detailsJson .
                $createdAt   .
                ($prevHash   ?? '')
            );

            $stmt = $db->prepare(
                "INSERT INTO audit_logs
                 (organization_id, user_id, action, entity_type, entity_id,
                  details, ip_address, user_agent,
                  prev_hash, entry_hash, severity, performed_by, created_at)
                 VALUES
                 (:org, :uid, :act, :et, :eid,
                  :det, :ip, :ua,
                  :prev, :hash, :sev, :by, :ts)"
            );
            $stmt->execute([
                'org'  => $orgId,
                'uid'  => $userId,
                'act'  => $action,
                'et'   => $entityType,
                'eid'  => $entityId,
                'det'  => $details ? json_encode($details, JSON_UNESCAPED_UNICODE) : null,
                'ip'   => $ipAddress ?: ($_SERVER['REMOTE_ADDR'] ?? ''),
                'ua'   => $userAgent  ?: ($_SERVER['HTTP_USER_AGENT'] ?? null),
                'prev' => $prevHash,
                'hash' => $entryHash,
                'sev'  => $severity,
                'by'   => $performedBy,
                'ts'   => $createdAt,
            ]);
        } catch (Throwable $e) {
            error_log('[AuditService] log error: ' . $e->getMessage());
        }
    }

    /**
     * Verifica l'integrità della hash chain per un'organizzazione.
     * Ricalcola ogni entry_hash e confronta con il valore salvato.
     *
     * @param PDO $db          Connessione PDO
     * @param int $orgId       organization_id
     * @return array {valid: bool, total: int, hashed: int, coverage_pct: float,
     *               broken_at: int|null, last_hash: string|null, verified_at: string}
     */
    public static function verifyChain(PDO $db, int $orgId): array
    {
        $stmt = $db->prepare(
            "SELECT id, organization_id, user_id, action, entity_type, entity_id,
                    details, prev_hash, entry_hash, severity, performed_by, created_at
             FROM audit_logs
             WHERE organization_id = :org
             ORDER BY id ASC"
        );
        $stmt->execute(['org' => $orgId]);
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

        $valid     = true;
        $brokenAt  = null;
        $lastHash  = null;
        $hashed    = 0;

        foreach ($rows as $row) {
            if (!empty($row['entry_hash'])) {
                $hashed++;
            }

            // Record senza hash (pre-migrazione) → skip verifica ma non rompe catena
            if (empty($row['entry_hash'])) {
                continue;
            }

            $detailsDecoded  = $row['details'] ? json_decode($row['details'], true) : null;
            $detailsJson     = $detailsDecoded ? self::canonicalJson($detailsDecoded) : '';

            $expected = hash('sha256',
                $row['organization_id'] .
                ($row['user_id'] ?? 'system') .
                $row['action']              .
                ($row['entity_type'] ?? '') .
                ($row['entity_id']   ?? '') .
                $detailsJson                .
                $row['created_at']          .
                ($row['prev_hash']   ?? '')
            );

            if ($expected !== $row['entry_hash']) {
                $valid    = false;
                $brokenAt = (int) $row['id'];
                break;
            }

            $lastHash = $row['entry_hash'];
        }

        $total = count($rows);
        return [
            'valid'        => $valid,
            'total'        => $total,
            'hashed'       => $hashed,
            'coverage_pct' => $total > 0 ? round($hashed / $total * 100, 1) : 0.0,
            'broken_at'    => $brokenAt,
            'last_hash'    => $lastHash,
            'verified_at'  => date('Y-m-d H:i:s'),
        ];
    }

    /**
     * Esporta il registro audit in formato JSON certificato con SHA-256.
     *
     * @return array {data: array, export_hash: string, records_count: int,
     *               chain_valid: bool, exported_at: string}
     */
    public static function exportCertified(
        PDO     $db,
        int     $orgId,
        int     $exportedBy,
        string  $performedBy = 'system',
        string  $purpose     = 'export_certificato',
        ?string $dateFrom    = null,
        ?string $dateTo      = null
    ): array {
        $sql    = "SELECT * FROM audit_logs WHERE organization_id = :org";
        $params = ['org' => $orgId];

        if ($dateFrom) { $sql .= " AND created_at >= :from"; $params['from'] = $dateFrom . ' 00:00:00'; }
        if ($dateTo)   { $sql .= " AND created_at <= :to";   $params['to']   = $dateTo   . ' 23:59:59'; }
        $sql .= " ORDER BY id ASC";

        $stmt = $db->prepare($sql);
        $stmt->execute($params);
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

        $chainResult = self::verifyChain($db, $orgId);
        $exportedAt  = date('Y-m-d\TH:i:sP');

        $exportData = [
            'meta' => [
                'standard'       => 'NIS2 EU 2022/2555 — D.Lgs.138/2024 — ISO27001:2022',
                'generator'      => 'NIS2 Agile AuditService v1.0',
                'org_id'         => $orgId,
                'exported_by'    => $performedBy,
                'exported_at'    => $exportedAt,
                'purpose'        => $purpose,
                'records_count'  => count($rows),
                'chain_valid'    => $chainResult['valid'],
                'chain_coverage' => $chainResult['coverage_pct'],
                'last_hash'      => $chainResult['last_hash'],
                'date_from'      => $dateFrom,
                'date_to'        => $dateTo,
            ],
            'records' => $rows,
        ];

        $exportJson = json_encode($exportData, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
        $exportHash = hash('sha256', $exportJson);

        // Salva metadati export
        try {
            $ins = $db->prepare(
                "INSERT INTO audit_exports
                 (organization_id, exported_by, performed_by, format, records_count,
                  date_from, date_to, export_hash, chain_valid, purpose)
                 VALUES (:org, :uid, :by, 'json', :cnt, :df, :dt, :hash, :cv, :pur)"
            );
            $ins->execute([
                'org'  => $orgId,
                'uid'  => $exportedBy,
                'by'   => $performedBy,
                'cnt'  => count($rows),
                'df'   => $dateFrom,
                'dt'   => $dateTo,
                'hash' => $exportHash,
                'cv'   => $chainResult['valid'] ? 1 : 0,
                'pur'  => $purpose,
            ]);
        } catch (Throwable $e) {
            error_log('[AuditService] exportCertified save error: ' . $e->getMessage());
        }

        return [
            'data'          => $exportData,
            'export_hash'   => $exportHash,
            'records_count' => count($rows),
            'chain_valid'   => $chainResult['valid'],
            'exported_at'   => $exportedAt,
        ];
    }

    /**
     * Recupera l'entry_hash dell'ultimo record per questa org (chain linking).
     */
    private static function getLastHash(PDO $db, int $orgId): ?string
    {
        $stmt = $db->prepare(
            "SELECT entry_hash FROM audit_logs
             WHERE organization_id = :org AND entry_hash != ''
             ORDER BY id DESC LIMIT 1"
        );
        $stmt->execute(['org' => $orgId]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return $row ? ($row['entry_hash'] ?: null) : null;
    }

    /**
     * Serializzazione JSON canonica con chiavi ordinate ricorsivamente.
     * Garantisce che logAction e verifyChain producano la stessa stringa di input all'hash.
     */
    private static function canonicalJson(array $data): string
    {
        $data = self::sortKeysRecursively($data);
        return json_encode($data, JSON_UNESCAPED_UNICODE);
    }

    private static function sortKeysRecursively(array $data): array
    {
        ksort($data);
        foreach ($data as $k => $v) {
            if (is_array($v)) {
                $data[$k] = self::sortKeysRecursively($v);
            }
        }
        return $data;
    }

    /**
     * Mappa action → severity per auto-classificazione degli eventi.
     */
    public static function resolveSeverity(string $action, ?array $details = null): string
    {
        $criticalActions = [
            'incident.create', 'incident.significant', 'incident.deadline_warning',
            'risk.create_high', 'risk.create_critical',
            'whistleblowing.received', 'whistleblowing.critical',
            'audit.chain_broken', 'auth.login_failed_repeated',
        ];
        $warningActions = [
            'risk.create_medium', 'incident.update', 'policy.rejected',
            'supplier.risk_flagged', 'normative.update', 'auth.password_changed',
        ];

        if (in_array($action, $criticalActions, true)) return 'critical';
        if (in_array($action, $warningActions,  true)) return 'warning';

        // Escalate su risk level nei details
        if ($details) {
            $level = $details['risk_level'] ?? $details['severity'] ?? $details['priority'] ?? null;
            if (in_array($level, ['critical', 'high'], true))   return 'critical';
            if (in_array($level, ['medium'], true))              return 'warning';
        }

        return 'info';
    }
}
