<?php
declare(strict_types=1);

namespace App\Services;

final class SecurityAuditIntegrityService
{
    /** @return array<string,mixed> */
    public function report(): array
    {
        $file = self::ledgerPath();
        if (!is_file($file)) {
            return [
                'status' => 'initialisation',
                'valid' => true,
                'records' => 0,
                'first_at' => null,
                'last_at' => null,
                'last_hash' => null,
                'errors' => [],
                'generated_at' => date('Y-m-d H:i:s'),
            ];
        }

        $handle = fopen($file, 'rb');
        if ($handle === false) {
            return $this->failure('Le registre d’intégrité est illisible.');
        }

        $previous = str_repeat('0', 64);
        $count = 0;
        $firstAt = null;
        $lastAt = null;
        $lastHash = null;
        $errors = [];

        while (($line = fgets($handle)) !== false) {
            $line = trim($line);
            if ($line === '') continue;
            $count++;
            if ($count > 100000) {
                $errors[] = 'Limite de vérification atteinte.';
                break;
            }
            $row = json_decode($line, true);
            if (!is_array($row)) {
                $errors[] = 'Enregistrement ' . $count . ' non décodable.';
                break;
            }
            $storedHash = (string)($row['hash'] ?? '');
            $storedMac = (string)($row['mac'] ?? '');
            $previousHash = (string)($row['previous_hash'] ?? '');
            if (!hash_equals($previous, $previousHash)) {
                $errors[] = 'Rupture de chaîne à l’enregistrement ' . $count . '.';
                break;
            }
            unset($row['hash'], $row['mac']);
            $canonical = self::canonicalJson($row);
            $calculated = hash('sha256', $canonical);
            $expectedMac = hash_hmac('sha256', $calculated, self::integrityKey());
            if (!hash_equals($storedHash, $calculated) || !hash_equals($storedMac, $expectedMac)) {
                $errors[] = 'Empreinte invalide à l’enregistrement ' . $count . '.';
                break;
            }
            $previous = $storedHash;
            $lastHash = $storedHash;
            $firstAt ??= (string)($row['timestamp'] ?? '');
            $lastAt = (string)($row['timestamp'] ?? '');
        }
        fclose($handle);

        return [
            'status' => $errors === [] ? 'intègre' : 'altéré',
            'valid' => $errors === [],
            'records' => $count,
            'first_at' => $firstAt,
            'last_at' => $lastAt,
            'last_hash' => $lastHash ? substr($lastHash, 0, 16) . '…' : null,
            'errors' => $errors,
            'generated_at' => date('Y-m-d H:i:s'),
        ];
    }

    /** @param array<string,mixed> $record */
    public static function append(array $record): void
    {
        $file = self::ledgerPath();
        $directory = dirname($file);
        if (!is_dir($directory) && !mkdir($directory, 0770, true) && !is_dir($directory)) {
            throw new \RuntimeException('Impossible de créer le registre d’intégrité.');
        }
        $lock = fopen($file . '.lock', 'c+');
        if ($lock === false || !flock($lock, LOCK_EX)) {
            throw new \RuntimeException('Impossible de verrouiller le registre d’intégrité.');
        }
        try {
            $previous = self::lastHash($file);
            $row = [
                'sequence' => self::nextSequence($file),
                'timestamp' => (string)($record['timestamp'] ?? date(DATE_ATOM)),
                'event' => mb_substr((string)($record['event'] ?? 'security.unknown'), 0, 180),
                'severity' => mb_substr((string)($record['severity'] ?? 'warning'), 0, 20),
                'request_id' => mb_substr((string)($record['request_id'] ?? ''), 0, 64),
                'actor_hash' => hash('sha256', (string)($record['user_id'] ?? 'anonymous')),
                'organisation_hash' => hash('sha256', (string)($record['organisation_id'] ?? 'none')),
                'path' => mb_substr((string)($record['path'] ?? '/'), 0, 500),
                'previous_hash' => $previous,
            ];
            $hash = hash('sha256', self::canonicalJson($row));
            $row['hash'] = $hash;
            $row['mac'] = hash_hmac('sha256', $hash, self::integrityKey());
            $encoded = json_encode($row, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
            if ($encoded === false || file_put_contents($file, $encoded . PHP_EOL, FILE_APPEND | LOCK_EX) === false) {
                throw new \RuntimeException('Écriture du registre d’intégrité impossible.');
            }
            @chmod($file, 0600);
        } finally {
            flock($lock, LOCK_UN);
            fclose($lock);
            @chmod($file . '.lock', 0600);
        }
    }

    private static function ledgerPath(): string
    {
        $base = defined('BASE_PATH') ? BASE_PATH : dirname(__DIR__, 2);
        return $base . '/storage/private/security/audit-integrity.jsonl';
    }

    private static function integrityKey(): string
    {
        return (string)($_ENV['SECURITY_LOG_HASH_SALT'] ?? 'axiom-local-security-log');
    }

    private static function lastHash(string $file): string
    {
        if (!is_file($file) || filesize($file) === 0) return str_repeat('0', 64);
        $handle = fopen($file, 'rb');
        if ($handle === false) return str_repeat('0', 64);
        $position = -1;
        $line = '';
        fseek($handle, 0, SEEK_END);
        $size = ftell($handle);
        while ($size + $position >= 0) {
            fseek($handle, $position, SEEK_END);
            $char = fgetc($handle);
            if ($char === "\n" && $line !== '') break;
            if ($char !== "\n" && $char !== false) $line = $char . $line;
            $position--;
        }
        fclose($handle);
        $row = json_decode(trim($line), true);
        return is_array($row) && preg_match('/^[a-f0-9]{64}$/', (string)($row['hash'] ?? ''))
            ? (string)$row['hash'] : str_repeat('0', 64);
    }

    private static function nextSequence(string $file): int
    {
        if (!is_file($file)) return 1;
        $count = 0;
        $handle = fopen($file, 'rb');
        if ($handle === false) return 1;
        while (fgets($handle) !== false) $count++;
        fclose($handle);
        return $count + 1;
    }

    /** @param array<string,mixed> $value */
    private static function canonicalJson(array $value): string
    {
        ksort($value, SORT_STRING);
        $json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if ($json === false) throw new \RuntimeException('Canonicalisation impossible.');
        return $json;
    }

    /** @return array<string,mixed> */
    private function failure(string $message): array
    {
        return ['status'=>'indisponible','valid'=>false,'records'=>0,'first_at'=>null,'last_at'=>null,'last_hash'=>null,'errors'=>[$message],'generated_at'=>date('Y-m-d H:i:s')];
    }
}
