<?php

declare(strict_types=1);

namespace App\Services\Baselines;

use App\Jobs\CompareBaselineToTenantJob;
use App\Models\BaselineProfile;
use App\Models\BaselineSnapshot;
use App\Models\BaselineTenantAssignment;
use App\Models\OperationRun;
use App\Models\ManagedEnvironment;
use App\Models\User;
use App\Services\Auth\CapabilityResolver;
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\Baselines\Compare\CompareStrategyRegistry;
use App\Support\Baselines\Compare\CompareStrategySelection;
use App\Support\Baselines\Compare\StrategySelectionState;
use App\Support\OperationRunType;
use App\Support\ReasonTranslation\ReasonPresenter;
use InvalidArgumentException;

final class BaselineCompareService
{
    public function __construct(
        private readonly OperationRunService $runs,
        private readonly BaselineFullContentRolloutGate $rolloutGate,
        private readonly BaselineSnapshotTruthResolver $snapshotTruthResolver,
        private readonly BaselineSupportCapabilityGuard $capabilityGuard,
        private readonly CapabilityResolver $capabilityResolver,
        private readonly CompareStrategyRegistry $compareStrategyRegistry,
    ) {}

    /**
     * @return array{ok: bool, run?: OperationRun, reason_code?: string, reason_translation?: array<string, mixed>}
     */
    public function startCompare(
        ManagedEnvironment $tenant,
        User $initiator,
        ?int $baselineSnapshotId = null,
    ): array {
        $assignment = BaselineTenantAssignment::query()
            ->where('workspace_id', $tenant->workspace_id)
            ->where('managed_environment_id', $tenant->getKey())
            ->first();

        if (! $assignment instanceof BaselineTenantAssignment) {
            return $this->failedStart(BaselineReasonCodes::COMPARE_NO_ASSIGNMENT);
        }

        $profile = $assignment->baselineProfile;

        if (! $profile instanceof BaselineProfile) {
            return $this->failedStart(BaselineReasonCodes::COMPARE_PROFILE_NOT_ACTIVE);
        }

        return $this->startCompareForProfile($profile, $tenant, $initiator, $baselineSnapshotId);
    }

    /**
     * @return array{ok: bool, run?: OperationRun, reason_code?: string, reason_translation?: array<string, mixed>}
     */
    public function startCompareForProfile(
        BaselineProfile $profile,
        ManagedEnvironment $tenant,
        User $initiator,
        ?int $baselineSnapshotId = null,
    ): array {
        $assignment = BaselineTenantAssignment::query()
            ->where('workspace_id', (int) $profile->workspace_id)
            ->where('managed_environment_id', (int) $tenant->getKey())
            ->where('baseline_profile_id', (int) $profile->getKey())
            ->first();

        if (! $assignment instanceof BaselineTenantAssignment) {
            return $this->failedStart(BaselineReasonCodes::COMPARE_NO_ASSIGNMENT);
        }

        $precondition = $this->validatePreconditions($profile);

        if ($precondition !== null) {
            return $this->failedStart($precondition);
        }

        $selectedSnapshot = null;

        if (is_int($baselineSnapshotId) && $baselineSnapshotId > 0) {
            $selectedSnapshot = BaselineSnapshot::query()
                ->where('workspace_id', (int) $profile->workspace_id)
                ->where('baseline_profile_id', (int) $profile->getKey())
                ->whereKey((int) $baselineSnapshotId)
                ->first();

            if (! $selectedSnapshot instanceof BaselineSnapshot) {
                return $this->failedStart(BaselineReasonCodes::COMPARE_INVALID_SNAPSHOT);
            }
        }

        $snapshotResolution = $this->snapshotTruthResolver->resolveCompareSnapshot($profile, $selectedSnapshot);

        if (! ($snapshotResolution['ok'] ?? false)) {
            return $this->failedStart($snapshotResolution['reason_code'] ?? BaselineReasonCodes::COMPARE_NO_CONSUMABLE_SNAPSHOT);
        }

        /** @var BaselineSnapshot $snapshot */
        $snapshot = $snapshotResolution['snapshot'];
        $snapshotId = (int) $snapshot->getKey();

        try {
            $profileScope = $profile->normalizedScope();
            $overrideScope = $assignment->override_scope_jsonb !== null
                ? BaselineScope::fromJsonb(
                    is_array($assignment->override_scope_jsonb) ? $assignment->override_scope_jsonb : null,
                    allowEmptyLegacyAsNoOverride: true,
                )
                : null;
            $effectiveScope = BaselineScope::effective($profileScope, $overrideScope);
        } catch (InvalidArgumentException) {
            return $this->failedStart(BaselineReasonCodes::COMPARE_INVALID_SCOPE);
        }

        if ($effectiveScope->allTypes() === []) {
            return $this->failedStart(BaselineReasonCodes::COMPARE_INVALID_SCOPE);
        }

        $selection = $this->compareStrategyRegistry->select($effectiveScope);

        if (! $selection->isSupported()) {
            return $this->failedStart($this->selectionFailureReasonCode($selection));
        }

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

        $context = [
            'target_scope' => [
                'entra_tenant_id' => $tenant->graphTenantId(),
                'entra_tenant_name' => (string) $tenant->name,
            ],
            'progress' => [
                'phase' => [
                    'key' => 'preparing',
                    'label' => 'Preparing baseline comparison.',
                ],
            ],
            'baseline_profile_id' => (int) $profile->getKey(),
            'baseline_snapshot_id' => $snapshotId,
            'effective_scope' => $effectiveScope->toEffectiveScopeContext($this->capabilityGuard, 'compare'),
            'capture_mode' => $captureMode->value,
            'baseline_compare' => [
                'strategy' => $this->strategyContext($selection),
            ],
        ];

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

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

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

    /**
     * @return array{
     *     baselineProfileId: int,
     *     visibleAssignedTenantCount: int,
     *     queuedCount: int,
     *     alreadyQueuedCount: int,
     *     blockedCount: int,
     *     targets: list<array{tenantId: int, runId: ?int, launchState: string, reasonCode: ?string}>
     * }
     */
    public function startCompareForVisibleAssignments(BaselineProfile $profile, User $initiator): array
    {
        $assignments = BaselineTenantAssignment::query()
            ->where('workspace_id', (int) $profile->workspace_id)
            ->where('baseline_profile_id', (int) $profile->getKey())
            ->with('tenant')
            ->get();

        $queuedCount = 0;
        $alreadyQueuedCount = 0;
        $blockedCount = 0;
        $targets = [];

        $this->capabilityResolver->primeMemberships(
            $initiator,
            $assignments
                ->pluck('managed_environment_id')
                ->filter(static fn (mixed $id): bool => is_numeric($id))
                ->map(static fn (mixed $id): int => (int) $id)
                ->all(),
        );

        foreach ($assignments as $assignment) {
            $tenant = $assignment->tenant;

            if (! $tenant instanceof ManagedEnvironment) {
                continue;
            }

            if (! $this->capabilityResolver->isMember($initiator, $tenant)) {
                continue;
            }

            if (! $this->capabilityResolver->can($initiator, $tenant, \App\Support\Auth\Capabilities::TENANT_VIEW)) {
                continue;
            }

            if (! $this->capabilityResolver->can($initiator, $tenant, \App\Support\Auth\Capabilities::TENANT_SYNC)) {
                $blockedCount++;
                $targets[] = [
                    'tenantId' => (int) $tenant->getKey(),
                    'runId' => null,
                    'launchState' => 'blocked',
                    'reasonCode' => 'tenant_sync_required',
                ];

                continue;
            }

            $result = $this->startCompareForProfile($profile, $tenant, $initiator);
            $run = $result['run'] ?? null;
            $reasonCode = is_string($result['reason_code'] ?? null) ? (string) $result['reason_code'] : null;

            if (! ($result['ok'] ?? false) || ! $run instanceof OperationRun) {
                $blockedCount++;
                $targets[] = [
                    'tenantId' => (int) $tenant->getKey(),
                    'runId' => null,
                    'launchState' => 'blocked',
                    'reasonCode' => $reasonCode,
                ];

                continue;
            }

            if ($run->wasRecentlyCreated) {
                $queuedCount++;
                $targets[] = [
                    'tenantId' => (int) $tenant->getKey(),
                    'runId' => (int) $run->getKey(),
                    'launchState' => 'queued',
                    'reasonCode' => null,
                ];

                continue;
            }

            $alreadyQueuedCount++;
            $targets[] = [
                'tenantId' => (int) $tenant->getKey(),
                'runId' => (int) $run->getKey(),
                'launchState' => 'already_queued',
                'reasonCode' => null,
            ];
        }

        return [
            'baselineProfileId' => (int) $profile->getKey(),
            'visibleAssignedTenantCount' => count($targets),
            'queuedCount' => $queuedCount,
            'alreadyQueuedCount' => $alreadyQueuedCount,
            'blockedCount' => $blockedCount,
            'targets' => $targets,
        ];
    }

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

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

        return null;
    }

    private function selectionFailureReasonCode(CompareStrategySelection $selection): string
    {
        return match ($selection->selectionState) {
            StrategySelectionState::Mixed => BaselineReasonCodes::COMPARE_MIXED_SCOPE,
            default => BaselineReasonCodes::COMPARE_UNSUPPORTED_SCOPE,
        };
    }

    /**
     * @return array{
     *   key: ?string,
     *   selection_state: string,
     *   matched_scope_entries: list<array<string, mixed>>,
     *   rejected_scope_entries: list<array<string, mixed>>,
     *   operator_reason: string,
     *   diagnostics: array<string, mixed>
     * }
     */
    private function strategyContext(CompareStrategySelection $selection): array
    {
        return [
            'key' => $selection->strategyKey?->value,
            'selection_state' => $selection->selectionState->value,
            'matched_scope_entries' => $selection->matchedScopeEntries,
            'rejected_scope_entries' => $selection->rejectedScopeEntries,
            'operator_reason' => $selection->operatorReason,
            'diagnostics' => $selection->diagnostics,
        ];
    }

    /**
     * @return array{ok: false, reason_code: string, reason_translation?: array<string, mixed>}
     */
    private function failedStart(string $reasonCode): array
    {
        $translation = app(ReasonPresenter::class)->forArtifactTruth($reasonCode, 'baseline_compare');

        return array_filter([
            'ok' => false,
            'reason_code' => $reasonCode,
            'reason_translation' => $translation?->toArray(),
        ], static fn (mixed $value): bool => $value !== null);
    }
}
