<?php

declare(strict_types=1);

namespace App\Services\Baselines\Evidence;

use App\Models\Policy;
use App\Models\PolicyVersion;
use App\Models\ManagedEnvironment;
use App\Services\Baselines\CurrentStateEvidenceProvider;
use App\Services\Drift\DriftHasher;
use App\Services\Drift\Normalizers\AssignmentsNormalizer;
use App\Services\Drift\Normalizers\ScopeTagsNormalizer;
use App\Services\Drift\Normalizers\SettingsNormalizer;
use Carbon\CarbonImmutable;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

final class ContentEvidenceProvider implements CurrentStateEvidenceProvider
{
    public function __construct(
        private readonly DriftHasher $hasher,
        private readonly SettingsNormalizer $settingsNormalizer,
        private readonly AssignmentsNormalizer $assignmentsNormalizer,
        private readonly ScopeTagsNormalizer $scopeTagsNormalizer,
    ) {}

    public function name(): string
    {
        return 'policy_version';
    }

    public function fromPolicyVersion(
        PolicyVersion $version,
        string $subjectExternalId,
        ?CarbonImmutable $observedAt = null,
        ?int $observedOperationRunId = null,
    ): ResolvedEvidence {
        return $this->buildResolvedEvidence(
            policyType: (string) $version->policy_type,
            subjectExternalId: $subjectExternalId,
            platform: is_string($version->platform) ? $version->platform : null,
            snapshot: $version->snapshot,
            assignments: $version->assignments,
            scopeTags: $version->scope_tags,
            secretFingerprints: $version->secret_fingerprints,
            redactionVersion: $version->redaction_version,
            capturedAt: $version->captured_at,
            policyVersionId: (int) $version->getKey(),
            operationRunId: $version->operation_run_id,
            capturePurpose: $version->capture_purpose?->value,
            observedAt: $observedAt,
            observedOperationRunId: $observedOperationRunId,
        );
    }

    public function resolve(ManagedEnvironment $tenant, array $subjects, ?CarbonImmutable $since = null, ?int $latestInventorySyncRunId = null): array
    {
        if ($subjects === []) {
            return [];
        }

        $requestedKeys = $this->requestedKeys($subjects);

        if ($requestedKeys === []) {
            return [];
        }

        $policyTypes = array_values(array_unique(array_map(
            static fn (array $subject): string => trim((string) ($subject['policy_type'] ?? '')),
            $subjects,
        )));
        $policyTypes = array_values(array_filter($policyTypes, static fn (string $value): bool => $value !== ''));

        $externalIds = array_values(array_unique(array_map(
            static fn (array $subject): string => trim((string) ($subject['subject_external_id'] ?? '')),
            $subjects,
        )));
        $externalIds = array_values(array_filter($externalIds, static fn (string $value): bool => $value !== ''));

        if ($policyTypes === [] || $externalIds === []) {
            return [];
        }

        $policies = Policy::query()
            ->where('managed_environment_id', (int) $tenant->getKey())
            ->whereIn('policy_type', $policyTypes)
            ->whereIn('external_id', $externalIds)
            ->get(['id', 'policy_type', 'external_id']);

        /** @var Collection<int, Policy> $policies */
        $policyIdToKey = [];
        $policyIds = [];

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

            $key = (string) $policy->policy_type.'|'.(string) $policy->external_id;

            if (! array_key_exists($key, $requestedKeys)) {
                continue;
            }

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

        if ($policyIds === []) {
            return [];
        }

        $baseQuery = DB::table('policy_versions')
            ->select([
                'policy_versions.id',
                'policy_versions.operation_run_id',
                'policy_versions.capture_purpose',
                'policy_versions.policy_id',
                'policy_versions.policy_type',
                'policy_versions.platform',
                'policy_versions.captured_at',
                'policy_versions.snapshot',
                'policy_versions.assignments',
                'policy_versions.scope_tags',
                'policy_versions.secret_fingerprints',
                'policy_versions.redaction_version',
                'policy_versions.version_number',
            ])
            ->selectRaw('ROW_NUMBER() OVER (PARTITION BY policy_id ORDER BY captured_at DESC, version_number DESC, id DESC) as rn')
            ->where('managed_environment_id', (int) $tenant->getKey())
            ->whereIn('policy_id', $policyIds)
            ->whereNull('deleted_at');

        if ($since instanceof CarbonImmutable) {
            $baseQuery->where('captured_at', '>=', $since->toDateTimeString());
        }

        $versions = DB::query()
            ->fromSub($baseQuery, 'ranked_policy_versions')
            ->where('rn', 1)
            ->get();

        $resolved = [];

        foreach ($versions as $version) {
            $policyId = is_numeric($version->policy_id ?? null) ? (int) $version->policy_id : null;
            $key = $policyId !== null ? ($policyIdToKey[$policyId] ?? null) : null;

            if (! is_string($key) || $key === '' || ! array_key_exists($key, $requestedKeys)) {
                continue;
            }

            $policyType = is_string($version->policy_type ?? null) ? (string) $version->policy_type : '';
            $subjectExternalId = (string) ($requestedKeys[$key] ?? '');

            if ($policyType === '' || $subjectExternalId === '') {
                continue;
            }

            $resolved[$key] = $this->buildResolvedEvidence(
                policyType: $policyType,
                subjectExternalId: $subjectExternalId,
                platform: is_string($version->platform ?? null) ? (string) $version->platform : null,
                snapshot: $version->snapshot ?? null,
                assignments: $version->assignments ?? null,
                scopeTags: $version->scope_tags ?? null,
                secretFingerprints: $version->secret_fingerprints ?? null,
                redactionVersion: is_numeric($version->redaction_version ?? null) ? (int) $version->redaction_version : null,
                capturedAt: $version->captured_at ?? null,
                policyVersionId: is_numeric($version->id ?? null) ? (int) $version->id : null,
                operationRunId: is_numeric($version->operation_run_id ?? null) ? (int) $version->operation_run_id : null,
                capturePurpose: is_string($version->capture_purpose ?? null) ? trim((string) $version->capture_purpose) : null,
            );
        }

        return $resolved;
    }

    /**
     * @param  list<array{policy_type: string, subject_external_id: string}>  $subjects
     * @return array<string, string>
     */
    private function requestedKeys(array $subjects): array
    {
        $keys = [];

        foreach ($subjects as $subject) {
            $policyType = trim((string) ($subject['policy_type'] ?? ''));
            $externalId = trim((string) ($subject['subject_external_id'] ?? ''));

            if ($policyType === '' || $externalId === '') {
                continue;
            }

            $keys[$policyType.'|'.$externalId] = $externalId;
        }

        return $keys;
    }

    private function buildResolvedEvidence(
        string $policyType,
        string $subjectExternalId,
        ?string $platform,
        mixed $snapshot,
        mixed $assignments,
        mixed $scopeTags,
        mixed $secretFingerprints,
        ?int $redactionVersion,
        mixed $capturedAt,
        ?int $policyVersionId,
        mixed $operationRunId,
        ?string $capturePurpose,
        ?CarbonImmutable $observedAt = null,
        ?int $observedOperationRunId = null,
    ): ResolvedEvidence {
        $snapshot = is_array($snapshot) ? $snapshot : (is_string($snapshot) ? json_decode($snapshot, true) : null);
        $snapshot = is_array($snapshot) ? $snapshot : [];

        $assignments = is_array($assignments) ? $assignments : (is_string($assignments) ? json_decode($assignments, true) : null);
        $assignments = is_array($assignments) ? $assignments : [];

        $scopeTags = is_array($scopeTags) ? $scopeTags : (is_string($scopeTags) ? json_decode($scopeTags, true) : null);
        $scopeTags = is_array($scopeTags) ? $scopeTags : [];

        $secretFingerprints = is_array($secretFingerprints)
            ? $secretFingerprints
            : (is_string($secretFingerprints) ? json_decode($secretFingerprints, true) : null);
        $secretFingerprints = is_array($secretFingerprints) ? $secretFingerprints : [];

        $normalized = $this->settingsNormalizer->normalizeForDiff(
            snapshot: $snapshot,
            policyType: $policyType,
            platform: $platform,
        );

        $normalizedAssignments = $this->assignmentsNormalizer->normalizeForDiff($assignments);
        $normalizedScopeTagIds = $this->scopeTagsNormalizer->normalizeIds($scopeTags);

        $hash = $this->hasher->hashNormalized([
            'settings' => $normalized,
            'assignments' => $normalizedAssignments,
            'scope_tag_ids' => $normalizedScopeTagIds,
            'secret_fingerprints' => [
                'snapshot' => $this->fingerprintBucket($secretFingerprints, 'snapshot'),
                'assignments' => $this->fingerprintBucket($secretFingerprints, 'assignments'),
                'scope_tags' => $this->fingerprintBucket($secretFingerprints, 'scope_tags'),
            ],
            'redaction_version' => $redactionVersion,
        ]);

        $observedAt ??= is_string($capturedAt) ? CarbonImmutable::parse($capturedAt) : null;
        $observedOperationRunId ??= is_numeric($operationRunId) ? (int) $operationRunId : null;
        $capturePurpose = is_string($capturePurpose) ? trim($capturePurpose) : null;
        $capturePurpose = $capturePurpose !== '' ? $capturePurpose : null;

        $meta = [
            'policy_version_id' => $policyVersionId,
            'operation_run_id' => is_numeric($operationRunId) ? (int) $operationRunId : null,
            'capture_purpose' => $capturePurpose,
            'redaction_version' => $redactionVersion,
        ];

        if ($policyType === 'intuneRoleDefinition') {
            $meta['normalized_settings'] = $normalized;
            $meta['rbac'] = [
                'is_built_in' => is_bool($snapshot['isBuiltIn'] ?? null) ? $snapshot['isBuiltIn'] : null,
                'role_permission_count' => is_array($snapshot['rolePermissions'] ?? null) ? count($snapshot['rolePermissions']) : null,
            ];
        }

        return new ResolvedEvidence(
            policyType: $policyType,
            subjectExternalId: $subjectExternalId,
            hash: $hash,
            fidelity: EvidenceProvenance::FidelityContent,
            source: EvidenceProvenance::SourcePolicyVersion,
            observedAt: $observedAt,
            observedOperationRunId: $observedOperationRunId,
            meta: $meta,
        );
    }

    /**
     * @param  array<string, mixed>  $secretFingerprints
     * @return array<string, string>
     */
    private function fingerprintBucket(array $secretFingerprints, string $bucket): array
    {
        $bucketFingerprints = $secretFingerprints[$bucket] ?? [];

        return is_array($bucketFingerprints) ? $bucketFingerprints : [];
    }
}
