<?php

declare(strict_types=1);

namespace App\Infrastructure\Allowlist;

use App\Domain\Allowlist\AllowlistEntry;
use App\Domain\Ip\Cidr;
use App\Domain\Ip\IpAddress;
use App\Infrastructure\Db\RepositoryBase;
use DateTimeImmutable;
use DateTimeZone;
use Doctrine\DBAL\ParameterType;

/**
 * DBAL gateway for `allowlist`. Mirrors ManualBlockRepository minus the
 * `expires_at` column — allowlist entries don't expire on a clock; admins
 * remove them explicitly.
 */
final class AllowlistRepository extends RepositoryBase
{
    /**
     * Find a single-IP allowlist entry by exact `ip_bin` match. Used by
     * the admin IP-detail endpoint to render the allowlist panel.
     */
    public function findByIpBin(string $ipBin): ?AllowlistEntry
    {
        $row = $this->fetchByIpBin('allowlist', $ipBin);
        if ($row === null) {
            return null;
        }
        if (($row['kind'] ?? '') !== AllowlistEntry::KIND_IP) {
            return null;
        }

        return self::hydrate($row);
    }

    public function findById(int $id): ?AllowlistEntry
    {
        /** @var array<string, mixed>|false $row */
        $row = $this->connection()->fetchAssociative(
            'SELECT id, kind, ip_bin, network_bin, prefix_length, reason, created_at, created_by_user_id '
            . 'FROM allowlist WHERE id = :id',
            ['id' => $id]
        );

        return $row === false ? null : self::hydrate($row);
    }

    /**
     * @param array{kind?: ?string} $filters
     * @return list<AllowlistEntry>
     */
    public function list(?int $limit, ?int $offset, array $filters = []): array
    {
        $sql = 'SELECT id, kind, ip_bin, network_bin, prefix_length, reason, created_at, created_by_user_id '
            . 'FROM allowlist';
        $params = [];
        $types = [];
        $where = [];

        if (isset($filters['kind']) && $filters['kind'] !== null) {
            $where[] = 'kind = :kind';
            $params['kind'] = $filters['kind'];
        }
        if ($where !== []) {
            $sql .= ' WHERE ' . implode(' AND ', $where);
        }
        $sql .= ' ORDER BY id DESC';

        if ($limit !== null) {
            $sql .= ' LIMIT :limit';
            $params['limit'] = $limit;
            $types['limit'] = ParameterType::INTEGER;
            if ($offset !== null) {
                $sql .= ' OFFSET :offset';
                $params['offset'] = $offset;
                $types['offset'] = ParameterType::INTEGER;
            }
        }

        /** @var list<array<string, mixed>> $rows */
        $rows = $this->connection()->fetchAllAssociative($sql, $params, $types);

        return array_map(self::hydrate(...), $rows);
    }

    public function count(?string $kindFilter = null): int
    {
        if ($kindFilter !== null) {
            return (int) $this->connection()->fetchOne(
                'SELECT COUNT(*) FROM allowlist WHERE kind = :kind',
                ['kind' => $kindFilter]
            );
        }

        return (int) $this->connection()->fetchOne('SELECT COUNT(*) FROM allowlist');
    }

    public function createIp(IpAddress $ip, ?string $reason, ?int $createdByUserId): int
    {
        $this->insertRow('allowlist', [
            'kind' => AllowlistEntry::KIND_IP,
            'ip_bin' => $ip->binary(),
            'network_bin' => null,
            'prefix_length' => null,
            'reason' => $reason,
            'created_by_user_id' => $createdByUserId,
        ], ['ip_bin' => ParameterType::LARGE_OBJECT]);

        return (int) $this->connection()->lastInsertId();
    }

    public function createSubnet(Cidr $cidr, ?string $reason, ?int $createdByUserId): int
    {
        $this->insertRow('allowlist', [
            'kind' => AllowlistEntry::KIND_SUBNET,
            'ip_bin' => null,
            'network_bin' => $cidr->network(),
            'prefix_length' => $cidr->prefixLength(),
            'reason' => $reason,
            'created_by_user_id' => $createdByUserId,
        ], ['network_bin' => ParameterType::LARGE_OBJECT]);

        return (int) $this->connection()->lastInsertId();
    }

    public function delete(int $id): void
    {
        $this->connection()->executeStatement('DELETE FROM allowlist WHERE id = :id', ['id' => $id]);
    }

    /**
     * @return list<AllowlistEntry>
     */
    public function listSubnets(): array
    {
        /** @var list<array<string, mixed>> $rows */
        $rows = $this->connection()->fetchAllAssociative(
            'SELECT id, kind, ip_bin, network_bin, prefix_length, reason, created_at, created_by_user_id '
            . 'FROM allowlist WHERE kind = :kind ORDER BY id',
            ['kind' => AllowlistEntry::KIND_SUBNET]
        );

        return array_map(self::hydrate(...), $rows);
    }

    /**
     * @return list<AllowlistEntry>
     */
    public function listIps(): array
    {
        /** @var list<array<string, mixed>> $rows */
        $rows = $this->connection()->fetchAllAssociative(
            'SELECT id, kind, ip_bin, network_bin, prefix_length, reason, created_at, created_by_user_id '
            . 'FROM allowlist WHERE kind = :kind ORDER BY id',
            ['kind' => AllowlistEntry::KIND_IP]
        );

        return array_map(self::hydrate(...), $rows);
    }

    /**
     * @param array<string, mixed> $row
     */
    private static function hydrate(array $row): AllowlistEntry
    {
        $tz = new DateTimeZone('UTC');
        $createdAt = isset($row['created_at']) && $row['created_at'] !== null
            ? new DateTimeImmutable((string) $row['created_at'], $tz)
            : new DateTimeImmutable('now', $tz);

        $kind = (string) $row['kind'];
        $ip = null;
        $cidr = null;
        if ($kind === AllowlistEntry::KIND_IP && $row['ip_bin'] !== null) {
            $ip = IpAddress::fromBinary((string) $row['ip_bin']);
        } elseif ($kind === AllowlistEntry::KIND_SUBNET && $row['network_bin'] !== null) {
            $cidr = Cidr::fromBinary((string) $row['network_bin'], (int) $row['prefix_length']);
        }

        return new AllowlistEntry(
            id: (int) $row['id'],
            kind: $kind,
            ip: $ip,
            cidr: $cidr,
            reason: $row['reason'] !== null ? (string) $row['reason'] : null,
            createdAt: $createdAt,
            createdByUserId: $row['created_by_user_id'] !== null ? (int) $row['created_by_user_id'] : null,
        );
    }
}
