<?php

declare(strict_types=1);

namespace App\Domain\Reputation;

use App\Domain\Ip\Cidr;
use App\Domain\Ip\IpAddress;

/**
 * In-memory CIDR / IP containment evaluator.
 *
 * Built once from the current `manual_blocks` and `allowlist` snapshot, then
 * queried per IP. Linear scan over subnet lists; PHP-fine for up to ~10k
 * subnet entries per the SPEC §15 manual-only constraint.
 *
 * Single-IP lookups use a binary-key hash for O(1) membership; subnet
 * lookups walk the list and apply prefix masking using `Cidr::contains()`.
 */
final class CidrEvaluator
{
    /** @var array<string, true> ip-bin keyed set */
    private array $manualIps;
    /** @var array<string, true> ip-bin keyed set */
    private array $allowlistIps;
    /** @var list<Cidr> */
    private array $manualSubnets;
    /** @var list<Cidr> */
    private array $allowlistSubnets;

    /**
     * @param list<string> $manualIpBins
     * @param list<Cidr>   $manualSubnets
     * @param list<string> $allowlistIpBins
     * @param list<Cidr>   $allowlistSubnets
     */
    public function __construct(
        array $manualIpBins,
        array $manualSubnets,
        array $allowlistIpBins,
        array $allowlistSubnets,
    ) {
        $this->manualIps = self::indexBinaries($manualIpBins);
        $this->allowlistIps = self::indexBinaries($allowlistIpBins);
        $this->manualSubnets = $manualSubnets;
        $this->allowlistSubnets = $allowlistSubnets;
    }

    public function isAllowlisted(IpAddress $ip): bool
    {
        if (isset($this->allowlistIps[$ip->binary()])) {
            return true;
        }
        foreach ($this->allowlistSubnets as $cidr) {
            if ($cidr->contains($ip)) {
                return true;
            }
        }

        return false;
    }

    public function isManuallyBlocked(IpAddress $ip): bool
    {
        if (isset($this->manualIps[$ip->binary()])) {
            return true;
        }
        foreach ($this->manualSubnets as $cidr) {
            if ($cidr->contains($ip)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Subnet entries (CIDRs) of `manual_blocks` — used by the M07
     * distribution endpoint to emit one CIDR line per manual subnet.
     *
     * @return list<Cidr>
     */
    public function manualBlockedSubnets(): array
    {
        return $this->manualSubnets;
    }

    /**
     * @return list<Cidr>
     */
    public function allowlistedSubnets(): array
    {
        return $this->allowlistSubnets;
    }

    /**
     * Single-IP entries of `manual_blocks` as binary keys — exposed so the
     * distribution endpoint can iterate them without re-querying.
     *
     * @return list<string>
     */
    public function manualBlockedIpBins(): array
    {
        return array_keys($this->manualIps);
    }

    /**
     * @return list<string>
     */
    public function allowlistedIpBins(): array
    {
        return array_keys($this->allowlistIps);
    }

    /**
     * @param list<string> $bins
     * @return array<string, true>
     */
    private static function indexBinaries(array $bins): array
    {
        $out = [];
        foreach ($bins as $b) {
            $out[$b] = true;
        }

        return $out;
    }
}
