<?php

declare(strict_types=1);

namespace App\Services\Baselines\Evidence;

use App\Models\Policy;
use App\Models\PolicyVersion;
use App\Models\ManagedEnvironment;
use App\Support\Baselines\BaselineSubjectKey;
use Carbon\CarbonImmutable;
use Throwable;

final class BaselinePolicyVersionResolver
{
    /**
     * Cached map of (managed_environment_id, policy_type) => subject_key => policy_id.
     *
     * @var array<int, array<string, array<string, int>>>
     */
    private array $policyIdIndex = [];

    public function resolve(
        ManagedEnvironment $tenant,
        string $policyType,
        string $subjectKey,
        ?string $observedAt,
    ): ?int {
        $tenantId = (int) $tenant->getKey();

        $policyType = trim($policyType);
        $subjectKey = trim($subjectKey);

        if ($tenantId <= 0 || $policyType === '' || $subjectKey === '') {
            return null;
        }

        $observedAtCarbon = $this->parseObservedAt($observedAt);

        if (! $observedAtCarbon instanceof CarbonImmutable) {
            return null;
        }

        $policyId = $this->resolvePolicyId($tenantId, $policyType, $subjectKey);

        if ($policyId === null) {
            return null;
        }

        $rangeStart = $observedAtCarbon;
        $rangeEnd = $observedAtCarbon->addSecond();

        $versionId = PolicyVersion::query()
            ->where('managed_environment_id', $tenantId)
            ->where('policy_id', $policyId)
            ->whereNull('deleted_at')
            ->where('captured_at', '>=', $rangeStart)
            ->where('captured_at', '<', $rangeEnd)
            ->orderByDesc('captured_at')
            ->orderByDesc('version_number')
            ->orderByDesc('id')
            ->value('id');

        return is_numeric($versionId) ? (int) $versionId : null;
    }

    private function parseObservedAt(?string $observedAt): ?CarbonImmutable
    {
        if (! is_string($observedAt)) {
            return null;
        }

        $observedAt = trim($observedAt);

        if ($observedAt === '') {
            return null;
        }

        try {
            return CarbonImmutable::parse($observedAt);
        } catch (Throwable) {
            return null;
        }
    }

    private function resolvePolicyId(int $tenantId, string $policyType, string $subjectKey): ?int
    {
        if (! array_key_exists($tenantId, $this->policyIdIndex) || ! array_key_exists($policyType, $this->policyIdIndex[$tenantId])) {
            $this->policyIdIndex[$tenantId][$policyType] = $this->buildIndex($tenantId, $policyType);
        }

        $policyId = $this->policyIdIndex[$tenantId][$policyType][$subjectKey] ?? null;

        return is_numeric($policyId) ? (int) $policyId : null;
    }

    /**
     * Build a subject_key => policy_id map for a given tenant + policy_type.
     *
     * If multiple policies map to the same subject_key, that key is treated as ambiguous and excluded.
     *
     * @return array<string, int>
     */
    private function buildIndex(int $tenantId, string $policyType): array
    {
        $policies = Policy::query()
            ->where('managed_environment_id', $tenantId)
            ->where('policy_type', $policyType)
            ->get(['id', 'display_name', 'external_id']);

        /** @var array<string, int> $index */
        $index = [];

        /** @var array<string, true> $ambiguous */
        $ambiguous = [];

        foreach ($policies as $policy) {
            if (! $policy instanceof Policy) {
                continue;
            }

            $key = BaselineSubjectKey::forPolicy(
                $policyType,
                is_string($policy->display_name) ? $policy->display_name : null,
                is_string($policy->external_id) ? $policy->external_id : null,
            );

            if ($key === null) {
                continue;
            }

            if (array_key_exists($key, $index)) {
                $ambiguous[$key] = true;

                continue;
            }

            $index[$key] = (int) $policy->getKey();
        }

        foreach (array_keys($ambiguous) as $key) {
            unset($index[$key]);
        }

        return $index;
    }
}
