<?php

declare(strict_types=1);

namespace App\Services\Baselines;

use App\Models\Policy;
use App\Models\PolicyVersion;
use App\Models\ManagedEnvironment;
use App\Services\Intune\PolicyCaptureOrchestrator;
use App\Support\Baselines\BaselineEvidenceResumeToken;
use App\Support\Baselines\PolicyVersionCapturePurpose;
use App\Support\Baselines\ResolutionOutcomeRecord;
use App\Support\Baselines\ResolutionPath;
use App\Support\Baselines\SubjectDescriptor;
use App\Support\Baselines\SubjectResolver;
use Throwable;

final class BaselineContentCapturePhase
{
    public function __construct(
        private readonly PolicyCaptureOrchestrator $captureOrchestrator,
        private readonly ?SubjectResolver $subjectResolver = null,
    ) {}

    /**
     * Capture baseline-purpose policy versions (content + assignments + scope tags) within a run budget.
     *
     * @param  list<array{policy_type: string, subject_external_id: string, subject_key?: string}>  $subjects
     * @param  array{max_items_per_run: int, max_concurrency: int, max_retries: int}  $budgets
     * @return array{
     *   stats: array{requested: int, succeeded: int, skipped: int, failed: int, throttled: int},
     *   gaps: array<string, int>,
     *   gap_subjects: list<array<string, mixed>>,
     *   resume_token: ?string,
     *   captured_versions: array<string, array{
     *     policy_type: string,
     *     subject_external_id: string,
     *     version: PolicyVersion,
     *     observed_at: string,
     *     observed_operation_run_id: ?int
     *   }>
     * }
     */
    public function capture(
        ManagedEnvironment $tenant,
        array $subjects,
        PolicyVersionCapturePurpose $purpose,
        array $budgets,
        ?string $resumeToken = null,
        ?int $operationRunId = null,
        ?int $baselineProfileId = null,
        ?string $createdBy = null,
    ): array {
        $subjects = array_values($subjects);

        $maxItemsPerRun = max(0, (int) ($budgets['max_items_per_run'] ?? 0));
        $maxConcurrency = max(1, (int) ($budgets['max_concurrency'] ?? 1));
        $maxRetries = max(0, (int) ($budgets['max_retries'] ?? 0));

        $offset = 0;

        if (is_string($resumeToken) && $resumeToken !== '') {
            $state = BaselineEvidenceResumeToken::decode($resumeToken) ?? [];
            $offset = is_numeric($state['offset'] ?? null) ? max(0, (int) $state['offset']) : 0;
        }

        if ($offset >= count($subjects)) {
            $offset = 0;
        }

        $remaining = array_slice($subjects, $offset);
        $batch = $maxItemsPerRun > 0 ? array_slice($remaining, 0, $maxItemsPerRun) : [];

        $stats = [
            'requested' => count($batch),
            'succeeded' => 0,
            'skipped' => 0,
            'failed' => 0,
            'throttled' => 0,
        ];

        /** @var array<string, int> $gaps */
        $gaps = [];
        /** @var list<array<string, mixed>> $gapSubjects */
        $gapSubjects = [];
        $capturedVersions = [];

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

        foreach (array_chunk($batch, $maxConcurrency) as $chunk) {
            foreach ($chunk as $subject) {
                $policyType = trim((string) ($subject['policy_type'] ?? ''));
                $externalId = trim((string) ($subject['subject_external_id'] ?? ''));
                $subjectKey = trim((string) ($subject['subject_key'] ?? ''));
                $descriptor = $this->resolver()->describeForCapture(
                    $policyType !== '' ? $policyType : 'unknown',
                    $externalId !== '' ? $externalId : null,
                    $subjectKey !== '' ? $subjectKey : null,
                );

                if ($policyType === '' || $externalId === '') {
                    $this->recordGap($gaps, $gapSubjects, $descriptor, $this->resolver()->invalidSubject($descriptor));
                    $stats['failed']++;

                    continue;
                }

                $captureKey = $policyType.'|'.$externalId;

                if (isset($seen[$captureKey])) {
                    $this->recordGap($gaps, $gapSubjects, $descriptor, $this->resolver()->duplicateSubject($descriptor));
                    $stats['skipped']++;

                    continue;
                }

                $seen[$captureKey] = true;

                if (
                    $descriptor->resolutionPath === ResolutionPath::FoundationInventory
                    || $descriptor->resolutionPath === ResolutionPath::Inventory
                ) {
                    $this->recordGap($gaps, $gapSubjects, $descriptor, $this->resolver()->structuralInventoryOnly($descriptor));
                    $stats['skipped']++;

                    continue;
                }

                $policy = Policy::query()
                    ->where('managed_environment_id', (int) $tenant->getKey())
                    ->where('policy_type', $policyType)
                    ->where('external_id', $externalId)
                    ->first();

                if (! $policy instanceof Policy) {
                    $this->recordGap($gaps, $gapSubjects, $descriptor, $this->resolver()->missingExpectedRecord($descriptor));
                    $stats['failed']++;

                    continue;
                }

                $attempt = 0;
                $result = null;

                while (true) {
                    try {
                        $result = $this->captureOrchestrator->capture(
                            policy: $policy,
                            tenant: $tenant,
                            includeAssignments: true,
                            includeScopeTags: true,
                            createdBy: $createdBy,
                            metadata: [
                                'capture_source' => 'baseline_evidence',
                            ],
                            capturePurpose: $purpose,
                            operationRunId: $operationRunId,
                            baselineProfileId: $baselineProfileId,
                        );
                    } catch (Throwable $throwable) {
                        $result = [
                            'failure' => [
                                'reason' => $throwable->getMessage(),
                                'status' => is_numeric($throwable->getCode()) ? (int) $throwable->getCode() : null,
                            ],
                        ];
                    }

                    if (! (is_array($result) && array_key_exists('failure', $result))) {
                        $stats['succeeded']++;

                        $version = $result['version'] ?? null;

                        if ($version instanceof PolicyVersion) {
                            $capturedVersions[$captureKey] = [
                                'policy_type' => $policyType,
                                'subject_external_id' => $externalId,
                                'version' => $version,
                                'observed_at' => now()->toIso8601String(),
                                'observed_operation_run_id' => $operationRunId,
                            ];
                        }

                        break;
                    }

                    $failure = is_array($result['failure'] ?? null) ? $result['failure'] : [];
                    $status = is_numeric($failure['status'] ?? null) ? (int) $failure['status'] : null;

                    $isThrottled = in_array($status, [429, 503], true);

                    if ($isThrottled && $attempt < $maxRetries) {
                        $delayMs = $this->retryDelayMs($attempt);
                        usleep($delayMs * 1000);
                        $attempt++;

                        continue;
                    }

                    if ($isThrottled) {
                        $this->recordGap($gaps, $gapSubjects, $descriptor, $this->resolver()->throttled($descriptor));
                        $stats['throttled']++;
                    } else {
                        $this->recordGap($gaps, $gapSubjects, $descriptor, $this->resolver()->captureFailed($descriptor));
                        $stats['failed']++;
                    }

                    break;
                }
            }
        }

        $processed = $offset + count($batch);
        $resumeTokenOut = null;

        if ($processed < count($subjects)) {
            $resumeTokenOut = BaselineEvidenceResumeToken::encode([
                'offset' => $processed,
                'total' => count($subjects),
            ]);

            $remainingCount = max(0, count($subjects) - $processed);
            if ($remainingCount > 0) {
                foreach (array_slice($subjects, $processed) as $remainingSubject) {
                    $remainingPolicyType = trim((string) ($remainingSubject['policy_type'] ?? ''));
                    $remainingExternalId = trim((string) ($remainingSubject['subject_external_id'] ?? ''));
                    $remainingSubjectKey = trim((string) ($remainingSubject['subject_key'] ?? ''));

                    if ($remainingPolicyType === '' || $remainingExternalId === '') {
                        continue;
                    }

                    $remainingDescriptor = $this->resolver()->describeForCapture(
                        $remainingPolicyType,
                        $remainingExternalId,
                        $remainingSubjectKey !== '' ? $remainingSubjectKey : null,
                    );
                    $this->recordGap($gaps, $gapSubjects, $remainingDescriptor, $this->resolver()->budgetExhausted($remainingDescriptor));
                }
            }
        }

        ksort($gaps);

        return [
            'stats' => $stats,
            'gaps' => $gaps,
            'gap_subjects' => $gapSubjects,
            'resume_token' => $resumeTokenOut,
            'captured_versions' => $capturedVersions,
        ];
    }

    /**
     * @param  array<string, int>  $gaps
     * @param  list<array<string, mixed>>  $gapSubjects
     */
    private function recordGap(array &$gaps, array &$gapSubjects, SubjectDescriptor $descriptor, ResolutionOutcomeRecord $outcome): void
    {
        $gaps[$outcome->reasonCode] = ($gaps[$outcome->reasonCode] ?? 0) + 1;
        $gapSubjects[] = array_merge($descriptor->toArray(), $outcome->toArray());
    }

    private function resolver(): SubjectResolver
    {
        return $this->subjectResolver ?? app(SubjectResolver::class);
    }

    private function retryDelayMs(int $attempt): int
    {
        $attempt = max(0, $attempt);

        $baseDelayMs = 500;
        $maxDelayMs = 30_000;

        $delayMs = (int) min($maxDelayMs, $baseDelayMs * (2 ** $attempt));
        $jitterMs = random_int(0, 250);

        return $delayMs + $jitterMs;
    }
}
