<?php

declare(strict_types=1);

namespace App\Application\Admin;

use App\Domain\Allowlist\AllowlistEntry;
use App\Domain\Audit\AuditAction;
use App\Domain\Audit\AuditEmitter;
use App\Domain\Ip\Cidr;
use App\Domain\Ip\InvalidCidrException;
use App\Domain\Ip\InvalidIpException;
use App\Domain\Ip\IpAddress;
use App\Infrastructure\Allowlist\AllowlistRepository;
use App\Infrastructure\Reputation\BlocklistCache;
use App\Infrastructure\Reputation\CidrEvaluatorFactory;
use Doctrine\DBAL\Connection;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

/**
 * Admin CRUD over `allowlist`. Mirrors ManualBlocksController minus
 * `expires_at` — allowlist entries don't expire on a clock.
 *
 * SPEC §6 RBAC: Operator may create/delete; Viewer may list/get.
 */
final class AllowlistController
{
    use AdminControllerSupport;

    public function __construct(
        private readonly AllowlistRepository $allowlist,
        private readonly CidrEvaluatorFactory $evaluator,
        private readonly BlocklistCache $blocklistCache,
        private readonly AuditEmitter $audit,
        private readonly Connection $connection,
    ) {
    }

    public function list(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
    {
        $page = self::pagination($request);
        $params = $request->getQueryParams();
        $kind = null;
        if (isset($params['kind']) && is_string($params['kind']) && in_array($params['kind'], [AllowlistEntry::KIND_IP, AllowlistEntry::KIND_SUBNET], true)) {
            $kind = $params['kind'];
        }

        $rows = $this->allowlist->list($page['limit'], $page['offset'], ['kind' => $kind]);
        $total = $this->allowlist->count($kind);

        return self::json($response, 200, [
            'items' => array_map(static fn (AllowlistEntry $e) => $e->toArray(), $rows),
            'page' => $page['page'],
            'limit' => $page['limit'],
            'total' => $total,
        ]);
    }

    /**
     * @param array{id: string} $args
     */
    public function show(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
    {
        $id = self::parseId($args['id']);
        if ($id === null) {
            return self::error($response, 404, 'not_found');
        }

        $entry = $this->allowlist->findById($id);
        if ($entry === null) {
            return self::error($response, 404, 'not_found');
        }

        return self::json($response, 200, $entry->toArray());
    }

    public function create(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
    {
        $body = self::jsonBody($request);
        $errors = [];

        $kind = $body['kind'] ?? null;
        if (!in_array($kind, [AllowlistEntry::KIND_IP, AllowlistEntry::KIND_SUBNET], true)) {
            return self::validationFailed($response, ['kind' => 'required, "ip" or "subnet"']);
        }

        $reason = null;
        if (array_key_exists('reason', $body)) {
            if ($body['reason'] !== null && !is_string($body['reason'])) {
                $errors['reason'] = 'must be string or null';
            } else {
                // SEC_REVIEW F52: strip C0/C1 control characters before
                // the reason lands in `audit_log.target_label` /
                // `details_json` to defeat log-injection and terminal-
                // escape attacks on log viewers.
                $reason = is_string($body['reason'])
                    ? self::stripControlChars($body['reason'])
                    : null;
            }
        }

        if ($kind === AllowlistEntry::KIND_IP) {
            if (!isset($body['ip']) || !is_string($body['ip']) || $body['ip'] === '') {
                $errors['ip'] = 'required for kind=ip';
            }
            if (isset($body['cidr'])) {
                $errors['cidr'] = 'forbidden for kind=ip';
            }
            if ($errors !== []) {
                return self::validationFailed($response, $errors);
            }

            try {
                /** @var string $ipText */
                $ipText = $body['ip'];
                $ip = IpAddress::fromString($ipText);
            } catch (InvalidIpException $e) {
                return self::validationFailed($response, ['ip' => $e->getMessage()]);
            }

            $userId = self::actingUserId($request);
            $auditCtx = self::auditContext($request);
            $id = $this->connection->transactional(function () use ($ip, $reason, $userId, $auditCtx): int {
                $id = $this->allowlist->createIp($ip, $reason, $userId);
                $this->audit->emitOrThrow(
                    AuditAction::ALLOWLIST_CREATED,
                    'allowlist',
                    $id,
                    ['kind' => 'ip', 'ip' => $ip->text(), 'reason' => $reason],
                    $auditCtx,
                    $ip->text(),
                );

                return $id;
            });

            $this->evaluator->invalidate();
            $this->blocklistCache->invalidateAll();
            // Eager rebuild so an allowlist/manual-block overlap surfaces
            // as a WARNING log entry inside this request — matches the
            // ManualBlocksController behaviour for symmetry.
            $this->evaluator->get();
            $created = $this->allowlist->findById($id);
            if ($created === null) {
                return self::error($response, 500, 'create_failed');
            }

            return self::json($response, 201, $created->toArray());
        }

        // kind=subnet
        if (!isset($body['cidr']) || !is_string($body['cidr']) || $body['cidr'] === '') {
            $errors['cidr'] = 'required for kind=subnet';
        }
        if (isset($body['ip'])) {
            $errors['ip'] = 'forbidden for kind=subnet';
        }
        if ($errors !== []) {
            return self::validationFailed($response, $errors);
        }

        try {
            /** @var string $cidrInput */
            $cidrInput = $body['cidr'];
            $cidr = Cidr::fromString($cidrInput);
        } catch (InvalidCidrException $e) {
            return self::validationFailed($response, ['cidr' => $e->getMessage()]);
        }

        $userId = self::actingUserId($request);
        $auditCtx = self::auditContext($request);
        $id = $this->connection->transactional(function () use ($cidr, $reason, $userId, $auditCtx): int {
            $id = $this->allowlist->createSubnet($cidr, $reason, $userId);
            $this->audit->emitOrThrow(
                AuditAction::ALLOWLIST_CREATED,
                'allowlist',
                $id,
                ['kind' => 'subnet', 'cidr' => $cidr->text(), 'reason' => $reason],
                $auditCtx,
                $cidr->text(),
            );

            return $id;
        });

        $this->evaluator->invalidate();
        $this->blocklistCache->invalidateAll();
        $this->evaluator->get();
        $created = $this->allowlist->findById($id);
        if ($created === null) {
            return self::error($response, 500, 'create_failed');
        }

        $payload = $created->toArray();
        if ($cidrInput !== $cidr->text()) {
            $payload['normalized_from'] = $cidrInput;
        }

        return self::json($response, 201, $payload);
    }

    /**
     * @param array{id: string} $args
     */
    public function delete(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
    {
        $id = self::parseId($args['id']);
        if ($id === null) {
            return self::error($response, 404, 'not_found');
        }
        $existing = $this->allowlist->findById($id);
        if ($existing === null) {
            return self::error($response, 404, 'not_found');
        }

        $label = $existing->kind === AllowlistEntry::KIND_IP
            ? $existing->ip?->text()
            : $existing->cidr?->text();
        $auditCtx = self::auditContext($request);

        $this->connection->transactional(function () use ($id, $existing, $label, $auditCtx): void {
            $this->allowlist->delete($id);
            $this->audit->emitOrThrow(
                AuditAction::ALLOWLIST_DELETED,
                'allowlist',
                $id,
                ['kind' => $existing->kind, 'ip' => $existing->ip?->text(), 'cidr' => $existing->cidr?->text(), 'reason' => $existing->reason],
                $auditCtx,
                $label,
            );
        });

        $this->evaluator->invalidate();
        $this->blocklistCache->invalidateAll();

        return $response->withStatus(204);
    }

    private static function parseId(string $raw): ?int
    {
        return preg_match('/^[1-9][0-9]*$/', $raw) === 1 ? (int) $raw : null;
    }
}
