<?php

declare(strict_types=1);

namespace App\Services\Baselines;

use App\Jobs\CaptureBaselineSnapshotJob;
use App\Models\BaselineProfile;
use App\Models\OperationRun;
use App\Models\ManagedEnvironment;
use App\Models\User;
use App\Services\OperationRunService;
use App\Support\Baselines\BaselineCaptureMode;
use App\Support\Baselines\BaselineFullContentRolloutGate;
use App\Support\Baselines\BaselineProfileStatus;
use App\Support\Baselines\BaselineReasonCodes;
use App\Support\Baselines\BaselineScope;
use App\Support\Baselines\BaselineSupportCapabilityGuard;
use App\Support\Inventory\InventoryCoverage;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\OperationRunType;
use InvalidArgumentException;

final class BaselineCaptureService
{
    public function __construct(
        private readonly OperationRunService $runs,
        private readonly BaselineFullContentRolloutGate $rolloutGate,
        private readonly BaselineSupportCapabilityGuard $capabilityGuard,
    ) {}

    /**
     * @return array{ok: bool, run?: OperationRun, reason_code?: string}
     */
    public function startCapture(
        BaselineProfile $profile,
        ManagedEnvironment $sourceTenant,
        User $initiator,
    ): array {
        $precondition = $this->validatePreconditions($profile, $sourceTenant);

        if ($precondition !== null) {
            return ['ok' => false, 'reason_code' => $precondition];
        }

        try {
            $effectiveScope = BaselineScope::fromJsonb(
                is_array($profile->scope_jsonb) ? $profile->scope_jsonb : null,
            );
        } catch (InvalidArgumentException) {
            return ['ok' => false, 'reason_code' => BaselineReasonCodes::CAPTURE_INVALID_SCOPE];
        }

        if ($effectiveScope->allTypes() === []) {
            return ['ok' => false, 'reason_code' => BaselineReasonCodes::CAPTURE_INVALID_SCOPE];
        }

        $eligibility = $effectiveScope->operationEligibility('capture', $this->capabilityGuard);

        if (! $eligibility['ok']) {
            return [
                'ok' => false,
                'reason_code' => BaselineReasonCodes::CAPTURE_UNSUPPORTED_SCOPE,
            ];
        }

        $truthfulTypes = $effectiveScope->toEffectiveScopeContext($this->capabilityGuard, 'capture')['truthful_types'] ?? null;
        $inventoryEligibility = $this->latestInventoryEligibilityDecision($sourceTenant, $effectiveScope, is_array($truthfulTypes) ? $truthfulTypes : null);

        if (! $inventoryEligibility['ok']) {
            return [
                'ok' => false,
                'reason_code' => $inventoryEligibility['reason_code'],
            ];
        }

        $captureMode = $profile->capture_mode instanceof BaselineCaptureMode
            ? $profile->capture_mode
            : BaselineCaptureMode::Opportunistic;

        $context = [
            'target_scope' => [
                'entra_tenant_id' => $sourceTenant->graphTenantId(),
                'entra_tenant_name' => (string) $sourceTenant->name,
            ],
            'progress' => [
                'phase' => [
                    'key' => 'preparing',
                    'label' => 'Preparing baseline capture.',
                ],
            ],
            'baseline_profile_id' => (int) $profile->getKey(),
            'source_environment_id' => (int) $sourceTenant->getKey(),
            'effective_scope' => $effectiveScope->toEffectiveScopeContext($this->capabilityGuard, 'capture'),
            'capture_mode' => $captureMode->value,
            'baseline_capture' => [
                'inventory_sync_run_id' => $inventoryEligibility['inventory_sync_run_id'],
                'eligibility' => $this->eligibilityContextPayload($inventoryEligibility, phase: 'preflight'),
            ],
        ];

        $run = $this->runs->ensureRunWithIdentity(
            tenant: $sourceTenant,
            type: OperationRunType::BaselineCapture->value,
            identityInputs: [
                'baseline_profile_id' => (int) $profile->getKey(),
            ],
            context: $context,
            initiator: $initiator,
        );

        if ($run->wasRecentlyCreated) {
            CaptureBaselineSnapshotJob::dispatch($run);
        }

        return ['ok' => true, 'run' => $run];
    }

    private function validatePreconditions(BaselineProfile $profile, ManagedEnvironment $sourceTenant): ?string
    {
        if ($profile->status !== BaselineProfileStatus::Active) {
            return BaselineReasonCodes::CAPTURE_PROFILE_NOT_ACTIVE;
        }

        if ($profile->capture_mode === BaselineCaptureMode::FullContent && ! $this->rolloutGate->enabled()) {
            return BaselineReasonCodes::CAPTURE_ROLLOUT_DISABLED;
        }

        if ($sourceTenant->workspace_id === null) {
            return BaselineReasonCodes::CAPTURE_MISSING_SOURCE_TENANT;
        }

        if ((int) $sourceTenant->workspace_id !== (int) $profile->workspace_id) {
            return BaselineReasonCodes::CAPTURE_MISSING_SOURCE_TENANT;
        }

        return null;
    }

    /**
     * @param  list<string>|null  $truthfulTypes
     * @return array{
     *     ok: bool,
     *     reason_code: ?string,
     *     inventory_sync_run_id: ?int,
     *     inventory_outcome: ?string,
     *     effective_types: list<string>,
     *     covered_types: list<string>,
     *     uncovered_types: list<string>
     * }
     */
    public function latestInventoryEligibilityDecision(
        ManagedEnvironment $sourceTenant,
        BaselineScope $effectiveScope,
        ?array $truthfulTypes = null,
    ): array {
        $effectiveTypes = is_array($truthfulTypes) && $truthfulTypes !== []
            ? array_values(array_unique(array_filter($truthfulTypes, 'is_string')))
            : $effectiveScope->allTypes();

        sort($effectiveTypes, SORT_STRING);

        $run = OperationRun::query()
            ->where('managed_environment_id', (int) $sourceTenant->getKey())
            ->where('type', OperationRunType::InventorySync->value)
            ->where('status', OperationRunStatus::Completed->value)
            ->orderByDesc('completed_at')
            ->orderByDesc('id')
            ->first();

        if (! $run instanceof OperationRun) {
            return [
                'ok' => false,
                'reason_code' => BaselineReasonCodes::CAPTURE_INVENTORY_MISSING,
                'inventory_sync_run_id' => null,
                'inventory_outcome' => null,
                'effective_types' => $effectiveTypes,
                'covered_types' => [],
                'uncovered_types' => $effectiveTypes,
            ];
        }

        $outcome = is_string($run->outcome) ? trim($run->outcome) : null;

        if ($outcome === OperationRunOutcome::Blocked->value) {
            return [
                'ok' => false,
                'reason_code' => BaselineReasonCodes::CAPTURE_INVENTORY_BLOCKED,
                'inventory_sync_run_id' => (int) $run->getKey(),
                'inventory_outcome' => $outcome,
                'effective_types' => $effectiveTypes,
                'covered_types' => [],
                'uncovered_types' => $effectiveTypes,
            ];
        }

        if ($outcome === OperationRunOutcome::Failed->value) {
            return [
                'ok' => false,
                'reason_code' => BaselineReasonCodes::CAPTURE_INVENTORY_FAILED,
                'inventory_sync_run_id' => (int) $run->getKey(),
                'inventory_outcome' => $outcome,
                'effective_types' => $effectiveTypes,
                'covered_types' => [],
                'uncovered_types' => $effectiveTypes,
            ];
        }

        $coverage = InventoryCoverage::fromContext($run->context);
        $coveredTypes = $coverage instanceof InventoryCoverage
            ? array_values(array_intersect($effectiveTypes, $coverage->coveredTypes()))
            : [];

        sort($coveredTypes, SORT_STRING);

        $uncoveredTypes = array_values(array_diff($effectiveTypes, $coveredTypes));
        sort($uncoveredTypes, SORT_STRING);

        if ($coveredTypes === []) {
            return [
                'ok' => false,
                'reason_code' => BaselineReasonCodes::CAPTURE_UNUSABLE_COVERAGE,
                'inventory_sync_run_id' => (int) $run->getKey(),
                'inventory_outcome' => $outcome,
                'effective_types' => $effectiveTypes,
                'covered_types' => [],
                'uncovered_types' => $effectiveTypes,
            ];
        }

        return [
            'ok' => true,
            'reason_code' => null,
            'inventory_sync_run_id' => (int) $run->getKey(),
            'inventory_outcome' => $outcome,
            'effective_types' => $effectiveTypes,
            'covered_types' => $coveredTypes,
            'uncovered_types' => $uncoveredTypes,
        ];
    }

    /**
     * @param  array{
     *     ok: bool,
     *     reason_code: ?string,
     *     inventory_sync_run_id: ?int,
     *     inventory_outcome: ?string,
     *     effective_types: list<string>,
     *     covered_types: list<string>,
     *     uncovered_types: list<string>
     * }  $decision
     * @return array<string, mixed>
     */
    public function eligibilityContextPayload(array $decision, string $phase): array
    {
        return [
            'phase' => $phase,
            'ok' => (bool) ($decision['ok'] ?? false),
            'reason_code' => is_string($decision['reason_code'] ?? null) ? $decision['reason_code'] : null,
            'inventory_sync_run_id' => is_numeric($decision['inventory_sync_run_id'] ?? null)
                ? (int) $decision['inventory_sync_run_id']
                : null,
            'inventory_outcome' => is_string($decision['inventory_outcome'] ?? null) ? $decision['inventory_outcome'] : null,
            'effective_types' => array_values(array_filter((array) ($decision['effective_types'] ?? []), 'is_string')),
            'covered_types' => array_values(array_filter((array) ($decision['covered_types'] ?? []), 'is_string')),
            'uncovered_types' => array_values(array_filter((array) ($decision['uncovered_types'] ?? []), 'is_string')),
        ];
    }
}
