<?php

declare(strict_types=1);

namespace App\Services\Baselines;

use App\Models\Policy;
use App\Models\PolicyVersion;
use App\Models\Tenant;
use App\Services\Intune\PolicyCaptureOrchestrator;
use App\Support\Baselines\BaselineEvidenceResumeToken;
use App\Support\Baselines\PolicyVersionCapturePurpose;
use Throwable;

final class BaselineContentCapturePhase
{
    public function __construct(
        private readonly PolicyCaptureOrchestrator $captureOrchestrator,
    ) {}

    /**
     * Capture baseline-purpose policy versions (content + assignments + scope tags) within a run budget.
     *
     * @param  list<array{policy_type: string, subject_external_id: 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>,
     *   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(
        Tenant $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 = [];
        $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'] ?? ''));

                if ($policyType === '' || $externalId === '') {
                    $gaps['invalid_subject'] = ($gaps['invalid_subject'] ?? 0) + 1;
                    $stats['failed']++;

                    continue;
                }

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

                if (isset($seen[$subjectKey])) {
                    $gaps['duplicate_subject'] = ($gaps['duplicate_subject'] ?? 0) + 1;
                    $stats['skipped']++;

                    continue;
                }

                $seen[$subjectKey] = true;

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

                if (! $policy instanceof Policy) {
                    $gaps['policy_not_found'] = ($gaps['policy_not_found'] ?? 0) + 1;
                    $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[$subjectKey] = [
                                '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) {
                        $gaps['throttled'] = ($gaps['throttled'] ?? 0) + 1;
                        $stats['throttled']++;
                    } else {
                        $gaps['capture_failed'] = ($gaps['capture_failed'] ?? 0) + 1;
                        $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) {
                $gaps['budget_exhausted'] = ($gaps['budget_exhausted'] ?? 0) + $remainingCount;
            }
        }

        ksort($gaps);

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

    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;
    }
}
