<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\OperationRun;
use App\Models\RestoreRun;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\Operations\LifecycleReconciliationReason;
use App\Support\RestoreRunStatus;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;

final class AdapterRunReconciler
{
    /**
     * @return array<int, string>
     */
    public function supportedTypes(): array
    {
        return [
            'restore.execute',
        ];
    }

    /**
     * @param  array{type?: string|null, managed_environment_id?: int|null, older_than_minutes?: 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
    {
        $type = $options['type'] ?? null;
        $tenantId = $options['managed_environment_id'] ?? null;
        $olderThanMinutes = max(1, (int) ($options['older_than_minutes'] ?? 10));
        $limit = max(1, (int) ($options['limit'] ?? 50));
        $dryRun = (bool) ($options['dry_run'] ?? true);

        if ($type !== null && ! in_array($type, $this->supportedTypes(), true)) {
            throw new \InvalidArgumentException('Unsupported adapter run type: '.$type);
        }

        $cutoff = CarbonImmutable::now()->subMinutes($olderThanMinutes);

        $query = OperationRun::query()
            ->whereIn('type', $type ? [$type] : $this->supportedTypes())
            ->whereIn('status', [OperationRunStatus::Queued->value, OperationRunStatus::Running->value])
            ->whereNotNull('context->restore_run_id')
            ->where(function (Builder $q) use ($cutoff): void {
                $q
                    ->where(function (Builder $q2) use ($cutoff): void {
                        $q2->whereNull('started_at')->where('created_at', '<', $cutoff);
                    })
                    ->orWhere(function (Builder $q2) use ($cutoff): void {
                        $q2->whereNotNull('started_at')->where('started_at', '<', $cutoff);
                    });
            })
            ->orderBy('id')
            ->limit($limit);

        if (is_int($tenantId) && $tenantId > 0) {
            $query->where('managed_environment_id', $tenantId);
        }

        $candidates = $query->get();

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

        foreach ($candidates as $run) {
            $change = $this->reconcileOne($run, $dryRun);

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

                continue;
            }

            $changes[] = $change;

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

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

    /**
     * @return array<string, mixed>|null
     */
    private function reconcileOne(OperationRun $run, bool $dryRun): ?array
    {
        if ($run->type !== 'restore.execute') {
            return null;
        }

        $context = is_array($run->context) ? $run->context : [];
        $restoreRunId = $context['restore_run_id'] ?? null;

        if (! is_numeric($restoreRunId)) {
            return null;
        }

        $restoreRun = RestoreRun::query()
            ->where('managed_environment_id', $run->managed_environment_id)
            ->whereKey((int) $restoreRunId)
            ->first();

        if (! $restoreRun instanceof RestoreRun) {
            return null;
        }

        $restoreStatus = RestoreRunStatus::fromString($restoreRun->status);

        if (! $this->isTerminalRestoreStatus($restoreStatus)) {
            return null;
        }

        [$opStatus, $opOutcome, $failures] = $this->mapRestoreToOperationRun($restoreRun, $restoreStatus);

        $summaryCounts = $this->buildSummaryCounts($restoreRun);

        $before = [
            'status' => (string) $run->status,
            'outcome' => (string) $run->outcome,
        ];

        $after = [
            'status' => $opStatus,
            'outcome' => $opOutcome,
        ];

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

        /** @var OperationRunService $runs */
        $runs = app(OperationRunService::class);

        $runs->updateRunWithReconciliation(
            run: $run,
            status: $opStatus,
            outcome: $opOutcome,
            summaryCounts: $summaryCounts,
            failures: $failures,
            reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value,
            reasonMessage: LifecycleReconciliationReason::AdapterOutOfSync->defaultMessage(),
            source: 'adapter_reconciler',
            evidence: [
                'restore_run_id' => (int) $restoreRun->getKey(),
                'restore_status' => $restoreStatus?->value,
            ],
        );

        $run->refresh();

        if ($run->started_at === null && $restoreRun->started_at !== null) {
            $run->started_at = $restoreRun->started_at;
        }

        if ($run->completed_at === null && $restoreRun->completed_at !== null) {
            $run->completed_at = $restoreRun->completed_at;
        }

        $run->save();

        return [
            'applied' => true,
            'operation_run_id' => (int) $run->getKey(),
            'type' => (string) $run->type,
            'restore_run_id' => (int) $restoreRun->getKey(),
            'before' => $before,
            'after' => $after,
        ];
    }

    private function isTerminalRestoreStatus(?RestoreRunStatus $status): bool
    {
        if (! $status instanceof RestoreRunStatus) {
            return false;
        }

        return in_array($status, [
            RestoreRunStatus::Previewed,
            RestoreRunStatus::Completed,
            RestoreRunStatus::Partial,
            RestoreRunStatus::Failed,
            RestoreRunStatus::Cancelled,
            RestoreRunStatus::Aborted,
            RestoreRunStatus::CompletedWithErrors,
        ], true);
    }

    /**
     * @return array{0:string,1:string,2:array<int,array{code:string,message:string}>}
     */
    private function mapRestoreToOperationRun(RestoreRun $restoreRun, RestoreRunStatus $status): array
    {
        $failureReason = is_string($restoreRun->failure_reason ?? null) ? (string) $restoreRun->failure_reason : '';

        return match ($status) {
            RestoreRunStatus::Previewed => [OperationRunStatus::Completed->value, OperationRunOutcome::Succeeded->value, []],
            RestoreRunStatus::Completed => [OperationRunStatus::Completed->value, OperationRunOutcome::Succeeded->value, []],
            RestoreRunStatus::Partial, RestoreRunStatus::CompletedWithErrors => [
                OperationRunStatus::Completed->value,
                OperationRunOutcome::PartiallySucceeded->value,
                [[
                    'code' => 'restore.completed_with_warnings',
                    'message' => $failureReason !== '' ? $failureReason : 'Restore completed with warnings.',
                ]],
            ],
            RestoreRunStatus::Failed, RestoreRunStatus::Aborted => [
                OperationRunStatus::Completed->value,
                OperationRunOutcome::Failed->value,
                [[
                    'code' => 'restore.failed',
                    'message' => $failureReason !== '' ? $failureReason : 'Restore failed.',
                ]],
            ],
            RestoreRunStatus::Cancelled => [
                OperationRunStatus::Completed->value,
                OperationRunOutcome::Failed->value,
                [[
                    'code' => 'restore.cancelled',
                    'message' => 'Restore run was cancelled.',
                ]],
            ],
            default => [OperationRunStatus::Running->value, OperationRunOutcome::Pending->value, []],
        };
    }

    /**
     * @return array<string, int>
     */
    private function buildSummaryCounts(RestoreRun $restoreRun): array
    {
        $metadata = is_array($restoreRun->metadata) ? $restoreRun->metadata : [];

        $counts = [];

        foreach (['total', 'processed', 'succeeded', 'failed', 'skipped'] as $key) {
            if (array_key_exists($key, $metadata) && is_numeric($metadata[$key])) {
                $counts[$key] = (int) $metadata[$key];
            }
        }

        if (! isset($counts['processed'])) {
            $processed = (int) ($counts['succeeded'] ?? 0) + (int) ($counts['failed'] ?? 0) + (int) ($counts['skipped'] ?? 0);

            if ($processed > 0) {
                $counts['processed'] = $processed;
            }
        }

        return $counts;
    }
}
