<?php
/*
 * Copyright 2026 Alessandro Chiapparini <sprint_planer_web@chiapparini.org>
 * SPDX-License-Identifier: Apache-2.0
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * See the LICENSE file in the project root for the full license text.
 */

declare(strict_types=1);

namespace App\Repositories;

use PDO;

/**
 * Read-only view over audit_log. There is no write API on purpose — writes
 * go through App\Services\AuditLogger, and the spec forbids ever truncating
 * or deleting audit rows.
 */
final class AuditRepository
{
    public function __construct(private readonly PDO $pdo)
    {
    }

    /**
     * Paged listing with optional filters.
     *
     * @param array{
     *   user_email?:    string,
     *   action?:        string,
     *   entity_type?:   string,
     *   entity_id?:     string,    // substring match, free text
     *   from_date?:     string,    // ISO date 'Y-m-d' (inclusive, UTC)
     *   to_date?:       string,    // ISO date 'Y-m-d' (inclusive, UTC)
     * } $filters
     *
     * @return array{
     *   rows:  list<array<string,mixed>>,
     *   total: int,
     *   page:  int,
     *   pageSize: int,
     *   pages: int,
     * }
     */
    public function findPaged(array $filters, int $page, int $pageSize): array
    {
        $page     = max(1, $page);
        $pageSize = max(1, min(200, $pageSize));
        $offset   = ($page - 1) * $pageSize;

        [$where, $params] = self::buildWhere($filters);

        $countStmt = $this->pdo->prepare(
            'SELECT COUNT(*) FROM audit_log' . $where
        );
        $countStmt->execute($params);
        $total = (int) $countStmt->fetchColumn();

        $dataStmt = $this->pdo->prepare(
            'SELECT id, occurred_at, user_id, user_email, action, entity_type,
                    entity_id, before_json, after_json, ip_address, user_agent
               FROM audit_log'
            . $where
            . ' ORDER BY id DESC LIMIT ? OFFSET ?'
        );
        $i = 1;
        foreach ($params as $p) {
            $dataStmt->bindValue($i++, $p);
        }
        $dataStmt->bindValue($i++, $pageSize, PDO::PARAM_INT);
        $dataStmt->bindValue($i,   $offset,   PDO::PARAM_INT);
        $dataStmt->execute();

        $rows  = $dataStmt->fetchAll();
        $pages = (int) max(1, ceil($total / $pageSize));

        return [
            'rows'     => is_array($rows) ? $rows : [],
            'total'    => $total,
            'page'     => $page,
            'pageSize' => $pageSize,
            'pages'    => $pages,
        ];
    }

    /** @return list<string> */
    public function distinctActions(): array
    {
        return $this->distinctColumn('action');
    }

    /** @return list<string> */
    public function distinctEntityTypes(): array
    {
        return $this->distinctColumn('entity_type');
    }

    /** @return list<string> */
    public function distinctUserEmails(): array
    {
        $stmt = $this->pdo->query(
            'SELECT DISTINCT user_email FROM audit_log
             WHERE user_email IS NOT NULL AND user_email != ""
             ORDER BY user_email ASC'
        );
        $out = [];
        foreach ($stmt as $row) {
            $out[] = (string) $row['user_email'];
        }
        return $out;
    }

    /**
     * @param 'action'|'entity_type' $col
     * @return list<string>
     */
    private function distinctColumn(string $col): array
    {
        // Whitelist guard: $col is interpolated into SQL below, so it must
        // never come from user input. Both internal callers pass a literal,
        // but enforce the contract at runtime so a future refactor can't
        // turn this into an injection vector (R01-N11).
        if (!in_array($col, ['action', 'entity_type'], true)) {
            throw new \InvalidArgumentException(
                "AuditRepository::distinctColumn: unsupported column '{$col}'"
            );
        }

        $stmt = $this->pdo->query(
            "SELECT DISTINCT {$col} FROM audit_log ORDER BY {$col} ASC"
        );
        $out = [];
        foreach ($stmt as $row) {
            $v = (string) $row[$col];
            if ($v !== '') {
                $out[] = $v;
            }
        }
        return $out;
    }

    /**
     * @param array<string,mixed> $filters
     * @return array{0:string, 1:list<mixed>}  [' WHERE …', [bindings...]]
     */
    private static function buildWhere(array $filters): array
    {
        $clauses = [];
        $params  = [];

        if (!empty($filters['user_email'])) {
            $clauses[] = 'user_email = ?';
            $params[]  = (string) $filters['user_email'];
        }
        if (!empty($filters['action'])) {
            $clauses[] = 'action = ?';
            $params[]  = (string) $filters['action'];
        }
        if (!empty($filters['entity_type'])) {
            $clauses[] = 'entity_type = ?';
            $params[]  = (string) $filters['entity_type'];
        }
        if (!empty($filters['entity_id'])) {
            $clauses[] = 'CAST(entity_id AS TEXT) LIKE ?';
            $params[]  = '%' . (string) $filters['entity_id'] . '%';
        }
        if (!empty($filters['from_date'])) {
            $clauses[] = 'occurred_at >= ?';
            $params[]  = (string) $filters['from_date'] . 'T00:00:00Z';
        }
        if (!empty($filters['to_date'])) {
            $clauses[] = 'occurred_at <= ?';
            $params[]  = (string) $filters['to_date'] . 'T23:59:59Z';
        }

        if ($clauses === []) {
            return ['', []];
        }
        return [' WHERE ' . implode(' AND ', $clauses), $params];
    }
}
