<?php

declare(strict_types=1);

namespace App\Infrastructure\Audit;

use App\Domain\Audit\AuditContext;
use App\Domain\Time\Clock;
use App\Infrastructure\Db\RepositoryBase;
use Doctrine\DBAL\ParameterType;

/**
 * Insert + paginated search over `audit_log`. The search supports the
 * filter set the brief calls out: actor_kind, actor_id, action,
 * entity_type, entity_id, from, to. All filters are optional; missing
 * filters degrade to "any".
 */
class AuditRepository extends RepositoryBase
{
    public function __construct(
        \Doctrine\DBAL\Connection $connection,
        private readonly Clock $clock,
    ) {
        parent::__construct($connection);
    }

    public function insert(
        string $action,
        ?string $entityType,
        int|string|null $entityId,
        ?string $entityLabel,
        string $detailsJson,
        AuditContext $context,
    ): int {
        $now = $this->clock->now()->format('Y-m-d H:i:s');
        $id = $this->insertRow('audit_log', [
            'actor_kind' => $context->actorKind,
            'actor_id' => $context->actorId !== null ? (string) $context->actorId : null,
            'actor_via' => $context->actorVia,
            'action' => $action,
            'target_type' => $entityType,
            'target_id' => $entityId !== null ? (string) $entityId : null,
            'target_label' => $entityLabel,
            'details_json' => $detailsJson,
            'ip_address' => $context->sourceIp,
            'created_at' => $now,
        ]);

        return $id > 0 ? (int) $this->connection()->lastInsertId() : 0;
    }

    /**
     * @param array{
     *     actor_kind?: ?string,
     *     actor_id?: ?int,
     *     actor_via?: ?string,
     *     action?: ?string,
     *     entity_type?: ?string,
     *     entity_id?: ?string,
     *     subject_kind?: ?string,
     *     subject_id?: ?string,
     *     from?: ?string,
     *     to?: ?string,
     * } $filters
     * @return array{
     *     items: list<array{id: int, occurred_at: string, actor_kind: string, actor_id: ?string, actor_via: ?string, action: string, entity_type: ?string, entity_id: ?string, entity_label: ?string, details: array<string, mixed>|null, source_ip: ?string}>,
     *     total: int,
     * }
     */
    public function search(array $filters, int $limit, int $offset): array
    {
        $where = [];
        $params = [];

        if (!empty($filters['actor_kind'])) {
            $where[] = 'actor_kind = :actor_kind';
            $params['actor_kind'] = $filters['actor_kind'];
        }
        if (!empty($filters['actor_id'])) {
            $where[] = 'actor_id = :actor_id';
            $params['actor_id'] = (string) $filters['actor_id'];
        }
        if (!empty($filters['actor_via'])) {
            $where[] = 'actor_via = :actor_via';
            $params['actor_via'] = $filters['actor_via'];
        }
        if (!empty($filters['action'])) {
            $where[] = 'action = :action';
            $params['action'] = $filters['action'];
        }
        if (!empty($filters['entity_type'])) {
            $where[] = 'target_type = :entity_type';
            $params['entity_type'] = $filters['entity_type'];
        }
        if (!empty($filters['entity_id'])) {
            $where[] = 'target_id = :entity_id';
            $params['entity_id'] = $filters['entity_id'];
        }
        // `subject_*` matches a row whose target OR actor is the given (kind, id)
        // pair. Used by the reporter / consumer detail pages so a single
        // page lists both admin actions on the entity and events the entity
        // itself emitted (report.received, blocklist.requested).
        if (!empty($filters['subject_kind']) && !empty($filters['subject_id'])) {
            $where[] = '((target_type = :subject_kind AND target_id = :subject_id)'
                . ' OR (actor_kind = :subject_kind AND actor_id = :subject_id))';
            $params['subject_kind'] = $filters['subject_kind'];
            $params['subject_id'] = (string) $filters['subject_id'];
        }
        if (!empty($filters['from'])) {
            $where[] = 'created_at >= :from';
            $params['from'] = $filters['from'];
        }
        if (!empty($filters['to'])) {
            $where[] = 'created_at <= :to';
            $params['to'] = $filters['to'];
        }

        $whereSql = $where === [] ? '' : (' WHERE ' . implode(' AND ', $where));

        $itemsParams = $params;
        $itemsParams['limit'] = $limit;
        $itemsParams['offset'] = $offset;
        $itemTypes = ['limit' => ParameterType::INTEGER, 'offset' => ParameterType::INTEGER];

        $rows = $this->connection()->fetchAllAssociative(
            'SELECT id, actor_kind, actor_id, actor_via, action, target_type, target_id, target_label, details_json, ip_address, created_at '
            . 'FROM audit_log' . $whereSql . ' ORDER BY id DESC LIMIT :limit OFFSET :offset',
            $itemsParams,
            $itemTypes,
        );

        $total = (int) $this->connection()->fetchOne(
            'SELECT COUNT(*) FROM audit_log' . $whereSql,
            $params,
        );

        $items = [];
        foreach ($rows as $row) {
            $detailsRaw = $row['details_json'] ?? null;
            $details = null;
            if (is_string($detailsRaw) && $detailsRaw !== '') {
                $decoded = json_decode($detailsRaw, true);
                if (is_array($decoded)) {
                    $details = $decoded;
                }
            }
            $items[] = [
                'id' => (int) $row['id'],
                'occurred_at' => self::isoTimestamp((string) $row['created_at']),
                'actor_kind' => (string) $row['actor_kind'],
                'actor_id' => $row['actor_id'] !== null ? (string) $row['actor_id'] : null,
                'actor_via' => $row['actor_via'] !== null ? (string) $row['actor_via'] : null,
                'action' => (string) $row['action'],
                'entity_type' => $row['target_type'] !== null ? (string) $row['target_type'] : null,
                'entity_id' => $row['target_id'] !== null ? (string) $row['target_id'] : null,
                'entity_label' => $row['target_label'] !== null ? (string) $row['target_label'] : null,
                'details' => $details,
                'source_ip' => $row['ip_address'] !== null ? (string) $row['ip_address'] : null,
            ];
        }

        return ['items' => $items, 'total' => $total];
    }

    private static function isoTimestamp(string $value): string
    {
        $trimmed = trim($value);
        if (str_contains($trimmed, 'T')) {
            return $trimmed;
        }

        return str_replace(' ', 'T', $trimmed) . 'Z';
    }
}
