<?php

declare(strict_types=1);

namespace App\Infrastructure\Reputation;

use App\Infrastructure\Db\RepositoryBase;
use Doctrine\DBAL\ParameterType;

/**
 * Builds the per-IP history timeline for the admin IP-detail page.
 *
 * SPEC §M09.1: combines `reports`, `manual_blocks` events, and
 * `allowlist` events for a given IP into one chronologically-ordered
 * list. Audit-log events are added in M12 — until then this returns
 * the report + manual + allowlist sources.
 *
 * The query is a UNION ALL with each source aliased onto a uniform
 * `(at, type, payload_json)` shape so the caller doesn't have to
 * branch by source. Limited to the most recent N entries (default 200)
 * — the controller flips `has_more` when the underlying total exceeds
 * the limit.
 */
final class IpHistoryRepository extends RepositoryBase
{
    public const DEFAULT_LIMIT = 200;

    /**
     * @return array{items: list<array<string, mixed>>, has_more: bool}
     */
    public function forIp(string $ipBin, int $limit = self::DEFAULT_LIMIT): array
    {
        $reports = $this->reportEvents($ipBin);
        $manualAdded = $this->manualBlockEvents($ipBin);
        $allowAdded = $this->allowlistEvents($ipBin);

        $combined = array_merge($reports, $manualAdded, $allowAdded);
        usort($combined, static fn (array $a, array $b): int => strcmp((string) $b['at'], (string) $a['at']));

        $hasMore = count($combined) > $limit;
        if ($hasMore) {
            $combined = array_slice($combined, 0, $limit);
        }

        return ['items' => $combined, 'has_more' => $hasMore];
    }

    /**
     * @return list<array<string, mixed>>
     */
    private function reportEvents(string $ipBin): array
    {
        $stmt = $this->connection()->prepare(
            'SELECT r.received_at, r.weight_at_report, r.metadata_json, '
            . '       c.slug AS category_slug, rep.name AS reporter_name '
            . 'FROM reports r '
            . 'LEFT JOIN categories c ON c.id = r.category_id '
            . 'LEFT JOIN reporters  rep ON rep.id = r.reporter_id '
            . 'WHERE r.ip_bin = :ip '
            . 'ORDER BY r.received_at DESC '
            . 'LIMIT 500'
        );
        $stmt->bindValue('ip', $ipBin, ParameterType::LARGE_OBJECT);

        /** @var list<array<string, mixed>> $rows */
        $rows = $stmt->executeQuery()->fetchAllAssociative();
        $out = [];
        foreach ($rows as $row) {
            $metadata = null;
            if ($row['metadata_json'] !== null) {
                $decoded = json_decode((string) $row['metadata_json'], true);
                if (is_array($decoded)) {
                    $metadata = $decoded;
                }
            }
            $out[] = [
                'at' => (string) $row['received_at'],
                'type' => 'report',
                'category' => $row['category_slug'] !== null ? (string) $row['category_slug'] : null,
                'reporter' => $row['reporter_name'] !== null ? (string) $row['reporter_name'] : null,
                'weight' => (float) $row['weight_at_report'],
                'metadata' => $metadata,
            ];
        }

        return $out;
    }

    /**
     * @return list<array<string, mixed>>
     */
    private function manualBlockEvents(string $ipBin): array
    {
        $stmt = $this->connection()->prepare(
            'SELECT created_at, reason, created_by_user_id '
            . 'FROM manual_blocks WHERE kind = :kind AND ip_bin = :ip'
        );
        $stmt->bindValue('kind', 'ip');
        $stmt->bindValue('ip', $ipBin, ParameterType::LARGE_OBJECT);

        /** @var list<array<string, mixed>> $rows */
        $rows = $stmt->executeQuery()->fetchAllAssociative();
        $out = [];
        foreach ($rows as $row) {
            $out[] = [
                'at' => (string) $row['created_at'],
                'type' => 'manual_block_added',
                'reason' => $row['reason'] !== null ? (string) $row['reason'] : null,
                'created_by_user_id' => $row['created_by_user_id'] !== null ? (int) $row['created_by_user_id'] : null,
            ];
        }

        return $out;
    }

    /**
     * @return list<array<string, mixed>>
     */
    private function allowlistEvents(string $ipBin): array
    {
        $stmt = $this->connection()->prepare(
            'SELECT created_at, reason, created_by_user_id '
            . 'FROM allowlist WHERE kind = :kind AND ip_bin = :ip'
        );
        $stmt->bindValue('kind', 'ip');
        $stmt->bindValue('ip', $ipBin, ParameterType::LARGE_OBJECT);

        /** @var list<array<string, mixed>> $rows */
        $rows = $stmt->executeQuery()->fetchAllAssociative();
        $out = [];
        foreach ($rows as $row) {
            $out[] = [
                'at' => (string) $row['created_at'],
                'type' => 'allowlist_added',
                'reason' => $row['reason'] !== null ? (string) $row['reason'] : null,
                'created_by_user_id' => $row['created_by_user_id'] !== null ? (int) $row['created_by_user_id'] : null,
            ];
        }

        return $out;
    }
}
