<?php

declare(strict_types=1);

namespace App\Services\Operations;

use App\Models\OperationRun;
use App\Services\OperationRunService;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\Operations\LifecycleReconciliationReason;
use App\Support\Operations\OperationLifecyclePolicy;
use App\Support\Operations\OperationRunFreshnessState;
use Illuminate\Database\Eloquent\Builder;

final class OperationLifecycleReconciler
{
    public function __construct(
        private readonly OperationLifecyclePolicy $policy,
        private readonly OperationRunService $operationRunService,
        private readonly QueuedExecutionLegitimacyGate $queuedExecutionLegitimacyGate,
    ) {}

    /**
     * @param  array{
     *     types?: array<int, string>,
     *     tenant_ids?: array<int, int>,
     *     workspace_ids?: array<int, int>,
     *     limit?: int,
     *     dry_run?: bool
     * }  $options
     * @return array{candidates:int,reconciled:int,skipped:int,changes:array<int, array<string, mixed>>}
     */
    public function reconcile(array $options = []): array
    {
        $types = array_values(array_filter(
            $options['types'] ?? $this->policy->coveredTypeNames(),
            static fn (mixed $type): bool => is_string($type) && trim($type) !== '',
        ));
        $tenantIds = array_values(array_filter(
            $options['tenant_ids'] ?? [],
            static fn (mixed $tenantId): bool => is_int($tenantId) && $tenantId > 0,
        ));
        $workspaceIds = array_values(array_filter(
            $options['workspace_ids'] ?? [],
            static fn (mixed $workspaceId): bool => is_int($workspaceId) && $workspaceId > 0,
        ));
        $limit = min(max(1, (int) ($options['limit'] ?? $this->policy->reconciliationBatchLimit())), 500);
        $dryRun = (bool) ($options['dry_run'] ?? false);

        $runs = OperationRun::query()
            ->with(['tenant', 'user'])
            ->whereIn('type', $types)
            ->whereIn('status', [
                OperationRunStatus::Queued->value,
                OperationRunStatus::Running->value,
            ])
            ->when(
                $tenantIds !== [],
                fn (Builder $query): Builder => $query->whereIn('managed_environment_id', $tenantIds),
            )
            ->when(
                $workspaceIds !== [],
                fn (Builder $query): Builder => $query->whereIn('workspace_id', $workspaceIds),
            )
            ->orderBy('id')
            ->limit($limit)
            ->get();

        $changes = [];
        $reconciled = 0;
        $skipped = 0;

        foreach ($runs as $run) {
            $change = $this->reconcileRun($run, $dryRun);

            if ($change === null) {
                $skipped++;

                continue;
            }

            $changes[] = $change;

            if (($change['applied'] ?? false) === true) {
                $reconciled++;
            }
        }

        return [
            'candidates' => $runs->count(),
            'reconciled' => $reconciled,
            'skipped' => $skipped,
            'changes' => $changes,
        ];
    }

    /**
     * @return array<string, mixed>|null
     */
    public function reconcileRun(OperationRun $run, bool $dryRun = false): ?array
    {
        $assessment = $this->assessment($run);

        if ($assessment === null || ($assessment['should_reconcile'] ?? false) !== true) {
            return null;
        }

        $before = [
            'status' => (string) $run->status,
            'outcome' => (string) $run->outcome,
            'freshness_state' => OperationRunFreshnessState::forRun($run, $this->policy)->value,
        ];
        $after = [
            'status' => OperationRunStatus::Completed->value,
            'outcome' => OperationRunOutcome::Failed->value,
            'freshness_state' => OperationRunFreshnessState::ReconciledFailed->value,
        ];

        if ($dryRun) {
            return [
                'applied' => false,
                'operation_run_id' => (int) $run->getKey(),
                'type' => (string) $run->type,
                'before' => $before,
                'after' => $after,
                'reason_code' => $assessment['reason_code'],
                'reason_message' => $assessment['reason_message'],
                'evidence' => $assessment['evidence'],
            ];
        }

        $updated = $this->operationRunService->forceFailNonTerminalRun(
            run: $run,
            reasonCode: (string) $assessment['reason_code'],
            message: (string) $assessment['reason_message'],
            source: 'scheduled_reconciler',
            evidence: is_array($assessment['evidence'] ?? null) ? $assessment['evidence'] : [],
        );

        return [
            'applied' => true,
            'operation_run_id' => (int) $updated->getKey(),
            'type' => (string) $updated->type,
            'before' => $before,
            'after' => $after,
            'reason_code' => $assessment['reason_code'],
            'reason_message' => $assessment['reason_message'],
            'evidence' => $assessment['evidence'],
        ];
    }

    /**
     * @return array{should_reconcile:bool,reason_code:string,reason_message:string,evidence:array<string, mixed>}|null
     */
    public function assessment(OperationRun $run): ?array
    {
        if ((string) $run->status === OperationRunStatus::Completed->value) {
            return null;
        }

        if (! $this->policy->supports((string) $run->type) || ! $this->policy->supportsScheduledReconciliation((string) $run->type)) {
            return null;
        }

        $freshnessState = OperationRunFreshnessState::forRun($run, $this->policy);

        if (! $freshnessState->isLikelyStale()) {
            return null;
        }

        $reason = (string) $run->status === OperationRunStatus::Queued->value
            ? LifecycleReconciliationReason::StaleQueued
            : LifecycleReconciliationReason::StaleRunning;
        $referenceTime = (string) $run->status === OperationRunStatus::Queued->value
            ? $run->created_at
            : ($run->started_at ?? $run->created_at);
        $thresholdSeconds = (string) $run->status === OperationRunStatus::Queued->value
            ? $this->policy->queuedStaleAfterSeconds((string) $run->type)
            : $this->policy->runningStaleAfterSeconds((string) $run->type);
        $legitimacy = $this->queuedExecutionLegitimacyGate->evaluate($run)->toArray();

        return [
            'should_reconcile' => true,
            'reason_code' => $reason->value,
            'reason_message' => $reason->defaultMessage(),
            'evidence' => [
                'evaluated_at' => now()->toIso8601String(),
                'freshness_state' => $freshnessState->value,
                'threshold_seconds' => $thresholdSeconds,
                'reference_time' => $referenceTime?->toIso8601String(),
                'status' => (string) $run->status,
                'execution_legitimacy' => $legitimacy,
                'terminal_truth_path' => $this->policy->requiresDirectFailedBridge((string) $run->type)
                    ? 'direct_and_scheduled'
                    : 'scheduled_only',
            ],
        ];
    }
}
