<?php

declare(strict_types=1);

namespace App\ApiClient;

use App\ApiClient\DTOs\DashboardStatsDto;
use App\ApiClient\DTOs\IpDetailDto;
use App\ApiClient\DTOs\IpListDto;
use App\ApiClient\DTOs\UserDto;

/**
 * Wraps the api's `/api/v1/admin/*` endpoints. Calls go out with the
 * service token plus `X-Acting-User-Id` from the current session — the
 * api uses that to resolve the impersonated user's role and enforce
 * RBAC.
 *
 * For most CRUD endpoints (manual_blocks, allowlist, policies,
 * reporters, consumers, tokens, categories) we return raw associative
 * arrays mirroring the api's JSON shape. Templates bind onto these
 * directly. The richer DTO pattern is reserved for endpoints whose
 * response shape benefits from a typed accessor (`UserDto`, the
 * IP-detail / dashboard payloads).
 *
 * Throws the typed `ApiException` subclasses on non-2xx; controllers
 * catch them to render validation messages or "API unreachable" states.
 */
final class AdminClient
{
    public function __construct(private readonly ApiClient $api)
    {
    }

    // ---- identity ----

    public function getMe(int $actingUserId): UserDto
    {
        $payload = $this->api->request('GET', '/api/v1/admin/me', [], $actingUserId);

        return UserDto::fromArray($payload);
    }

    // ---- IPs / dashboard (M09) ----

    /**
     * @param array<string, mixed> $filters
     */
    public function searchIps(int $actingUserId, array $filters, int $page = 1, int $pageSize = 25): IpListDto
    {
        $query = ['page' => $page, 'page_size' => $pageSize];
        foreach (['q', 'category', 'min_score', 'max_score', 'country', 'asn', 'status'] as $key) {
            if (isset($filters[$key]) && $filters[$key] !== '' && $filters[$key] !== null) {
                $query[$key] = $filters[$key];
            }
        }
        $payload = $this->api->request('GET', '/api/v1/admin/ips', ['query' => $query], $actingUserId);

        return IpListDto::fromArray($payload);
    }

    public function getIp(int $actingUserId, string $ip): IpDetailDto
    {
        $payload = $this->api->request('GET', '/api/v1/admin/ips/' . rawurlencode($ip), [], $actingUserId);

        return IpDetailDto::fromArray($payload);
    }

    public function getDashboardStats(int $actingUserId): DashboardStatsDto
    {
        $payload = $this->api->request('GET', '/api/v1/admin/stats/dashboard', [], $actingUserId);

        return DashboardStatsDto::fromArray($payload);
    }

    /**
     * @return list<array{code: string, count: int}>
     */
    public function listCountries(int $actingUserId): array
    {
        $payload = $this->api->request('GET', '/api/v1/admin/ips/countries', [], $actingUserId);
        $items = $payload['items'] ?? [];
        if (!is_array($items)) {
            return [];
        }
        $out = [];
        foreach ($items as $item) {
            if (!is_array($item)) {
                continue;
            }
            $out[] = [
                'code' => (string) ($item['code'] ?? ''),
                'count' => (int) ($item['count'] ?? 0),
            ];
        }

        return $out;
    }

    // ---- manual blocks (M10) ----

    /**
     * @return array<string, mixed>
     */
    public function listManualBlocks(int $actingUserId, ?string $kind = null): array
    {
        $query = ['limit' => 200];
        if ($kind !== null && $kind !== '') {
            $query['kind'] = $kind;
        }

        return $this->api->request('GET', '/api/v1/admin/manual-blocks', ['query' => $query], $actingUserId);
    }

    /**
     * @param array<string, mixed> $body
     * @return array<string, mixed>
     */
    public function createManualBlock(int $actingUserId, array $body): array
    {
        return $this->api->request('POST', '/api/v1/admin/manual-blocks', ['json' => $body], $actingUserId);
    }

    public function deleteManualBlock(int $actingUserId, int $id): void
    {
        $this->api->request('DELETE', '/api/v1/admin/manual-blocks/' . $id, [], $actingUserId);
    }

    // ---- allowlist (M10) ----

    /**
     * @return array<string, mixed>
     */
    public function listAllowlist(int $actingUserId, ?string $kind = null): array
    {
        $query = ['limit' => 200];
        if ($kind !== null && $kind !== '') {
            $query['kind'] = $kind;
        }

        return $this->api->request('GET', '/api/v1/admin/allowlist', ['query' => $query], $actingUserId);
    }

    /**
     * @param array<string, mixed> $body
     * @return array<string, mixed>
     */
    public function createAllowlist(int $actingUserId, array $body): array
    {
        return $this->api->request('POST', '/api/v1/admin/allowlist', ['json' => $body], $actingUserId);
    }

    public function deleteAllowlist(int $actingUserId, int $id): void
    {
        $this->api->request('DELETE', '/api/v1/admin/allowlist/' . $id, [], $actingUserId);
    }

    // ---- policies (M10) ----

    /**
     * @return array<string, mixed>
     */
    public function listPolicies(int $actingUserId): array
    {
        return $this->api->request('GET', '/api/v1/admin/policies', [], $actingUserId);
    }

    /**
     * @return array<string, mixed>
     */
    public function getPolicy(int $actingUserId, int $id): array
    {
        return $this->api->request('GET', '/api/v1/admin/policies/' . $id, [], $actingUserId);
    }

    /**
     * @param array<string, mixed> $body
     * @return array<string, mixed>
     */
    public function createPolicy(int $actingUserId, array $body): array
    {
        return $this->api->request('POST', '/api/v1/admin/policies', ['json' => $body], $actingUserId);
    }

    /**
     * @param array<string, mixed> $body
     * @return array<string, mixed>
     */
    public function updatePolicy(int $actingUserId, int $id, array $body): array
    {
        return $this->api->request('PATCH', '/api/v1/admin/policies/' . $id, ['json' => $body], $actingUserId);
    }

    public function deletePolicy(int $actingUserId, int $id): void
    {
        $this->api->request('DELETE', '/api/v1/admin/policies/' . $id, [], $actingUserId);
    }

    /**
     * @return array<string, mixed>
     */
    public function previewPolicy(int $actingUserId, int $id): array
    {
        return $this->api->request('GET', '/api/v1/admin/policies/' . $id . '/preview', [], $actingUserId);
    }

    /**
     * @return array<string, mixed>
     */
    public function policyScoreDistribution(int $actingUserId, int $id): array
    {
        return $this->api->request('GET', '/api/v1/admin/policies/' . $id . '/score-distribution', [], $actingUserId);
    }

    // ---- reporters (M10) ----

    /**
     * @return array<string, mixed>
     */
    public function listReporters(int $actingUserId): array
    {
        return $this->api->request('GET', '/api/v1/admin/reporters', ['query' => ['limit' => 200]], $actingUserId);
    }

    /**
     * @return array<string, mixed>
     */
    public function getReporter(int $actingUserId, int $id): array
    {
        return $this->api->request('GET', '/api/v1/admin/reporters/' . $id, [], $actingUserId);
    }

    /**
     * @param array<string, mixed> $body
     * @return array<string, mixed>
     */
    public function createReporter(int $actingUserId, array $body): array
    {
        return $this->api->request('POST', '/api/v1/admin/reporters', ['json' => $body], $actingUserId);
    }

    /**
     * @param array<string, mixed> $body
     * @return array<string, mixed>
     */
    public function updateReporter(int $actingUserId, int $id, array $body): array
    {
        return $this->api->request('PATCH', '/api/v1/admin/reporters/' . $id, ['json' => $body], $actingUserId);
    }

    public function deleteReporter(int $actingUserId, int $id): void
    {
        $this->api->request('DELETE', '/api/v1/admin/reporters/' . $id, [], $actingUserId);
    }

    // ---- consumers (M10) ----

    /**
     * @return array<string, mixed>
     */
    public function listConsumers(int $actingUserId): array
    {
        return $this->api->request('GET', '/api/v1/admin/consumers', ['query' => ['limit' => 200]], $actingUserId);
    }

    /**
     * @return array<string, mixed>
     */
    public function getConsumer(int $actingUserId, int $id): array
    {
        return $this->api->request('GET', '/api/v1/admin/consumers/' . $id, [], $actingUserId);
    }

    /**
     * @param array<string, mixed> $body
     * @return array<string, mixed>
     */
    public function createConsumer(int $actingUserId, array $body): array
    {
        return $this->api->request('POST', '/api/v1/admin/consumers', ['json' => $body], $actingUserId);
    }

    /**
     * @param array<string, mixed> $body
     * @return array<string, mixed>
     */
    public function updateConsumer(int $actingUserId, int $id, array $body): array
    {
        return $this->api->request('PATCH', '/api/v1/admin/consumers/' . $id, ['json' => $body], $actingUserId);
    }

    public function deleteConsumer(int $actingUserId, int $id): void
    {
        $this->api->request('DELETE', '/api/v1/admin/consumers/' . $id, [], $actingUserId);
    }

    // ---- tokens (M10) ----

    /**
     * @return array<string, mixed>
     */
    public function listTokens(int $actingUserId): array
    {
        return $this->api->request('GET', '/api/v1/admin/tokens', ['query' => ['limit' => 200]], $actingUserId);
    }

    /**
     * @param array<string, mixed> $body
     * @return array<string, mixed>
     */
    public function createToken(int $actingUserId, array $body): array
    {
        return $this->api->request('POST', '/api/v1/admin/tokens', ['json' => $body], $actingUserId);
    }

    public function deleteToken(int $actingUserId, int $id): void
    {
        $this->api->request('DELETE', '/api/v1/admin/tokens/' . $id, [], $actingUserId);
    }

    public function purgeToken(int $actingUserId, int $id): void
    {
        $this->api->request('DELETE', '/api/v1/admin/tokens/' . $id . '/purge', [], $actingUserId);
    }

    // ---- categories (M10) ----

    /**
     * @return array<string, mixed>
     */
    public function listCategories(int $actingUserId): array
    {
        return $this->api->request('GET', '/api/v1/admin/categories', [], $actingUserId);
    }

    /**
     * @return array<string, mixed>
     */
    public function getCategory(int $actingUserId, int $id): array
    {
        return $this->api->request('GET', '/api/v1/admin/categories/' . $id, [], $actingUserId);
    }

    /**
     * @param array<string, mixed> $body
     * @return array<string, mixed>
     */
    public function createCategory(int $actingUserId, array $body): array
    {
        return $this->api->request('POST', '/api/v1/admin/categories', ['json' => $body], $actingUserId);
    }

    /**
     * @param array<string, mixed> $body
     * @return array<string, mixed>
     */
    public function updateCategory(int $actingUserId, int $id, array $body): array
    {
        return $this->api->request('PATCH', '/api/v1/admin/categories/' . $id, ['json' => $body], $actingUserId);
    }

    public function deleteCategory(int $actingUserId, int $id): void
    {
        $this->api->request('DELETE', '/api/v1/admin/categories/' . $id, [], $actingUserId);
    }

    // ---- users (admin user disable/enable; SEC_REVIEW F11) ----

    /**
     * @return array<string, mixed>
     */
    public function listUsers(int $actingUserId, int $page = 1, int $pageSize = 50): array
    {
        return $this->api->request(
            'GET',
            '/api/v1/admin/users',
            ['query' => ['page' => $page, 'page_size' => $pageSize]],
            $actingUserId,
        );
    }

    /**
     * @return array<string, mixed>
     */
    public function setUserDisabled(int $actingUserId, int $userId, bool $disabled): array
    {
        return $this->api->request(
            'PATCH',
            '/api/v1/admin/users/' . $userId,
            ['json' => ['disabled' => $disabled]],
            $actingUserId,
        );
    }

    // ---- audit / settings (M12) ----

    /**
     * @param array<string, mixed> $filters
     * @return array<string, mixed>
     */
    public function listAuditLog(int $actingUserId, array $filters, int $page = 1, int $pageSize = 50): array
    {
        $query = ['page' => $page, 'page_size' => $pageSize];
        foreach (['actor_kind', 'actor_id', 'action', 'entity_type', 'entity_id', 'subject_kind', 'subject_id', 'from', 'to'] as $key) {
            if (isset($filters[$key]) && $filters[$key] !== '' && $filters[$key] !== null) {
                $query[$key] = $filters[$key];
            }
        }

        return $this->api->request('GET', '/api/v1/admin/audit-log', ['query' => $query], $actingUserId);
    }

    /**
     * @return array<string, mixed>
     */
    public function getJobsStatus(int $actingUserId): array
    {
        return $this->api->request('GET', '/api/v1/admin/jobs/status', [], $actingUserId);
    }

    /**
     * @param array<string, mixed> $params
     * @return array<string, mixed>
     */
    public function triggerJob(int $actingUserId, string $name, array $params = []): array
    {
        return $this->api->request(
            'POST',
            '/api/v1/admin/jobs/trigger/' . rawurlencode($name),
            $params === [] ? [] : ['json' => $params],
            $actingUserId,
        );
    }

    /**
     * @return array<string, mixed>
     */
    public function getConfig(int $actingUserId): array
    {
        return $this->api->request('GET', '/api/v1/admin/config', [], $actingUserId);
    }

    /**
     * Runtime feature flags (audit-emission toggles).
     *
     * @return array<string, bool>
     */
    public function getAppSettings(int $actingUserId): array
    {
        $payload = $this->api->request('GET', '/api/v1/admin/app-settings', [], $actingUserId);
        $out = [];
        foreach ($payload as $k => $v) {
            $out[(string) $k] = (bool) $v;
        }

        return $out;
    }

    /**
     * @param array<string, bool> $values
     * @return array<string, bool>
     */
    public function updateAppSettings(int $actingUserId, array $values): array
    {
        $payload = $this->api->request(
            'PATCH',
            '/api/v1/admin/app-settings',
            ['json' => $values],
            $actingUserId,
        );
        $out = [];
        foreach ($payload as $k => $v) {
            $out[(string) $k] = (bool) $v;
        }

        return $out;
    }

    /**
     * Wipe operational data on the api side. The API requires
     * `confirm: "PURGE"` in the body — anything else returns 400.
     *
     * @return array<string, mixed>
     */
    public function purgeData(int $actingUserId): array
    {
        return $this->api->request(
            'POST',
            '/api/v1/admin/maintenance/purge',
            ['json' => ['confirm' => 'PURGE']],
            $actingUserId,
        );
    }

    /**
     * Load the demo dataset. The api requires `confirm: "SEED"` in the body —
     * symmetric with `purgeData`'s `"PURGE"` gate. Returns 409 if data is
     * already seeded.
     *
     * @return array<string, mixed>
     */
    public function seedDemo(int $actingUserId): array
    {
        return $this->api->request(
            'POST',
            '/api/v1/admin/maintenance/seed-demo',
            ['json' => ['confirm' => 'SEED']],
            $actingUserId,
        );
    }
}
