<?php

declare(strict_types=1);

namespace App\Jobs\Operations;

use App\Jobs\Middleware\EnsureQueuedExecutionLegitimate;
use App\Jobs\Middleware\TrackOperationRun;
use App\Models\BackupItem;
use App\Models\BackupSet;
use App\Models\OperationRun;
use App\Models\Policy;
use App\Models\PolicyVersion;
use App\Models\RestoreRun;
use App\Models\ManagedEnvironment;
use App\Services\Audit\WorkspaceAuditLogger;
use App\Services\Intune\RestoreService;
use App\Services\OperationRunService;
use App\Services\Operations\TargetScopeConcurrencyLimiter;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use Carbon\CarbonImmutable;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use RuntimeException;
use Throwable;

final class CrossEnvironmentPromotionExecutionJob implements ShouldQueue
{
    use Dispatchable;
    use InteractsWithQueue;
    use Queueable;
    use SerializesModels;

    public int $timeout = 420;

    public bool $failOnTimeout = true;

    public ?OperationRun $operationRun = null;

    public function __construct(OperationRun $operationRun)
    {
        $this->operationRun = $operationRun;
    }

    /**
     * @return array<int, object>
     */
    public function middleware(): array
    {
        return [
            new EnsureQueuedExecutionLegitimate,
            new TrackOperationRun,
        ];
    }

    public function handle(
        OperationRunService $operationRuns,
        RestoreService $restoreService,
        TargetScopeConcurrencyLimiter $limiter,
        WorkspaceAuditLogger $auditLogger,
    ): void {
        if (! $this->operationRun instanceof OperationRun) {
            throw new RuntimeException('OperationRun is required for promotion execution.');
        }

        $this->operationRun->refresh();

        if ($this->operationRun->status === OperationRunStatus::Completed->value) {
            return;
        }

        $targetEnvironment = $this->operationRun->tenant;

        if (! $targetEnvironment instanceof ManagedEnvironment) {
            throw new RuntimeException('Promotion execution target environment is missing.');
        }

        $context = is_array($this->operationRun->context) ? $this->operationRun->context : [];
        $targetScope = is_array($context['target_scope'] ?? null) ? $context['target_scope'] : [];

        $lock = $limiter->acquireSlot((int) $targetEnvironment->getKey(), $targetScope);

        if (! $lock) {
            $this->release(max(1, (int) config('tenantpilot.bulk_operations.poll_interval_seconds', 3)));

            return;
        }

        try {
            $plan = is_array(data_get($context, 'promotion_execution.plan'))
                ? data_get($context, 'promotion_execution.plan')
                : null;

            if (! is_array($plan)) {
                throw new RuntimeException('Promotion execution plan is missing from operation context.');
            }

            $summary = [
                'total' => 0,
                'processed' => 0,
                'succeeded' => 0,
                'failed' => 0,
                'skipped' => 0,
                'created' => 0,
                'updated' => 0,
            ];
            $failures = [];

            $items = is_array($plan['items'] ?? null) ? array_values(array_filter($plan['items'], 'is_array')) : [];
            $summary['total'] = count($items);

            [$backupSet, $selectedItemIds, $preRestoreSummary, $preRestoreFailures] = $this->buildRestoreInputs(
                targetEnvironment: $targetEnvironment,
                operationRun: $this->operationRun,
                items: $items,
            );

            $summary = array_replace($summary, $preRestoreSummary);
            $failures = array_merge($failures, $preRestoreFailures);

            $restoreRun = null;

            if ($selectedItemIds !== []) {
                $restoreRun = RestoreRun::withoutEvents(fn (): RestoreRun => $restoreService->execute(
                    tenant: $targetEnvironment,
                    backupSet: $backupSet,
                    selectedItemIds: $selectedItemIds,
                    dryRun: false,
                    actorEmail: $this->operationRun->user?->email,
                    actorName: $this->operationRun->initiator_name,
                    providerConnectionId: is_numeric($context['provider_connection_id'] ?? null) ? (int) $context['provider_connection_id'] : null,
                ));

                RestoreRun::withoutEvents(function () use ($restoreRun): void {
                    $restoreRun->forceFill(['operation_run_id' => (int) $this->operationRun?->getKey()])->save();
                });

                [$restoreSummary, $restoreFailures] = $this->summaryFromRestoreRun($restoreRun, $items);
                $summary = $this->mergeSummary($summary, $restoreSummary);
                $failures = array_merge($failures, $restoreFailures);

                $context['restore_run_id'] = (int) $restoreRun->getKey();
                $context['backup_set_id'] = (int) $backupSet->getKey();
                $this->operationRun->forceFill(['context' => $context])->save();
            } else {
                $backupSet?->delete();
            }

            $outcome = $this->outcome($summary);

            $updated = $operationRuns->updateRun(
                run: $this->operationRun,
                status: OperationRunStatus::Completed->value,
                outcome: $outcome,
                summaryCounts: $summary,
                failures: $failures,
            );

            $auditLogger->logCrossEnvironmentPromotionExecutionCompleted(
                operationRun: $updated,
                sourceEnvironmentId: is_numeric($context['source_environment_id'] ?? null) ? (int) $context['source_environment_id'] : null,
                targetEnvironment: $targetEnvironment,
                summaryCounts: $summary,
                restoreRun: $restoreRun,
            );
        } catch (Throwable $exception) {
            throw $exception;
        } finally {
            $lock->release();
        }
    }

    public function getOperationRun(): ?OperationRun
    {
        return $this->operationRun;
    }

    /**
     * @param  list<array<string, mixed>>  $items
     * @return array{0: ?BackupSet, 1: list<int>, 2: array<string, int>, 3: list<array{code: string, message: string}>}
     */
    private function buildRestoreInputs(ManagedEnvironment $targetEnvironment, OperationRun $operationRun, array $items): array
    {
        $summary = [
            'processed' => 0,
            'succeeded' => 0,
            'failed' => 0,
            'skipped' => 0,
            'created' => 0,
            'updated' => 0,
        ];
        $failures = [];
        $backupSet = BackupSet::query()->create([
            'managed_environment_id' => (int) $targetEnvironment->getKey(),
            'name' => 'Cross-environment promotion • Operation #'.$operationRun->getKey(),
            'created_by' => $operationRun->user?->email,
            'status' => 'completed',
            'item_count' => 0,
            'completed_at' => CarbonImmutable::now(),
            'metadata' => [
                'source' => 'cross_environment_promotion',
                'operation_run_id' => (int) $operationRun->getKey(),
            ],
        ]);
        $selectedItemIds = [];

        foreach ($items as $item) {
            $action = (string) ($item['execution_action'] ?? '');

            if ($action === 'skip_aligned') {
                $summary['processed']++;
                $summary['skipped']++;

                continue;
            }

            $versionId = data_get($item, 'source.policy_version_id');
            $sourceEnvironmentId = data_get($item, 'source.managed_environment_id');

            $version = is_numeric($versionId) && is_numeric($sourceEnvironmentId)
                ? PolicyVersion::query()
                    ->with('policy')
                    ->whereKey((int) $versionId)
                    ->where('managed_environment_id', (int) $sourceEnvironmentId)
                    ->first()
                : null;

            if (! $version instanceof PolicyVersion || ! $version->policy instanceof Policy) {
                $summary['processed']++;
                $summary['failed']++;
                $failures[] = [
                    'code' => 'promotion.source_version_missing',
                    'message' => 'Source policy version for '.$this->itemLabel($item).' was not found.',
                ];

                continue;
            }

            $sourcePolicy = $version->policy;
            $targetExternalId = data_get($item, 'target.subject_external_id');
            $sourceExternalId = data_get($item, 'source.subject_external_id');
            $policyIdentifier = is_string($targetExternalId) && trim($targetExternalId) !== ''
                ? trim($targetExternalId)
                : (is_string($sourceExternalId) && trim($sourceExternalId) !== '' ? trim($sourceExternalId) : (string) $sourcePolicy->external_id);

            $targetPolicy = Policy::query()
                ->where('managed_environment_id', (int) $targetEnvironment->getKey())
                ->where('policy_type', (string) $sourcePolicy->policy_type)
                ->where('external_id', $policyIdentifier)
                ->first();

            $backupItem = BackupItem::query()->create([
                'managed_environment_id' => (int) $targetEnvironment->getKey(),
                'backup_set_id' => (int) $backupSet->getKey(),
                'policy_id' => $targetPolicy?->getKey(),
                'policy_identifier' => $policyIdentifier,
                'policy_type' => (string) $sourcePolicy->policy_type,
                'platform' => (string) $sourcePolicy->platform,
                'captured_at' => $version->captured_at ?? CarbonImmutable::now(),
                'payload' => is_array($version->snapshot) ? $version->snapshot : [],
                'metadata' => [
                    'source' => 'cross_environment_promotion',
                    'display_name' => (string) $sourcePolicy->display_name,
                    'operation_run_id' => (int) $operationRun->getKey(),
                    'source_environment_id' => (int) $sourcePolicy->managed_environment_id,
                    'source_policy_id' => (int) $sourcePolicy->getKey(),
                    'source_policy_version_id' => (int) $version->getKey(),
                    'source_subject_key' => (string) ($item['subject_key'] ?? ''),
                    'execution_action' => $action,
                    'target_subject_external_id' => is_string($targetExternalId) ? $targetExternalId : null,
                ],
                'assignments' => is_array($version->assignments) ? $version->assignments : [],
            ]);

            $selectedItemIds[] = (int) $backupItem->getKey();
        }

        $backupSet->forceFill(['item_count' => count($selectedItemIds)])->save();

        return [$backupSet, $selectedItemIds, $summary, $failures];
    }

    /**
     * @param  list<array<string, mixed>>  $items
     * @return array{0: array<string, int>, 1: list<array{code: string, message: string}>}
     */
    private function summaryFromRestoreRun(RestoreRun $restoreRun, array $items): array
    {
        $metadata = is_array($restoreRun->metadata) ? $restoreRun->metadata : [];
        $results = is_array($restoreRun->results) ? $restoreRun->results : [];
        $resultItems = is_array($results['items'] ?? null) ? $results['items'] : [];
        $succeeded = (int) ($metadata['succeeded'] ?? 0);
        $failed = (int) ($metadata['failed'] ?? 0) + (int) ($metadata['partial'] ?? 0);
        $skipped = (int) ($metadata['skipped'] ?? 0);
        $processed = $succeeded + $failed + $skipped;
        $created = 0;
        $updated = 0;
        $failures = [];

        foreach ($items as $item) {
            $action = (string) ($item['execution_action'] ?? '');

            if ($action === 'create_missing') {
                $created++;
            } elseif ($action === 'update_existing') {
                $updated++;
            }
        }

        foreach ($resultItems as $result) {
            if (! is_array($result)) {
                continue;
            }

            $status = (string) ($result['status'] ?? '');

            if (in_array($status, ['applied', 'dry_run'], true)) {
                continue;
            }

            $failures[] = [
                'code' => 'promotion.restore_item_not_applied',
                'message' => (string) ($result['reason'] ?? 'Promotion restore item did not apply.'),
            ];
        }

        return [[
            'processed' => $processed,
            'succeeded' => $succeeded,
            'failed' => $failed,
            'skipped' => $skipped,
            'created' => min($created, $succeeded),
            'updated' => min($updated, max(0, $succeeded - min($created, $succeeded))),
        ], $failures];
    }

    /**
     * @param  array<string, int>  $left
     * @param  array<string, int>  $right
     * @return array<string, int>
     */
    private function mergeSummary(array $left, array $right): array
    {
        foreach ($right as $key => $value) {
            $left[$key] = (int) ($left[$key] ?? 0) + (int) $value;
        }

        return $left;
    }

    /**
     * @param  array<string, int>  $summary
     */
    private function outcome(array $summary): string
    {
        $total = (int) ($summary['total'] ?? 0);
        $failed = (int) ($summary['failed'] ?? 0);
        $succeeded = (int) ($summary['succeeded'] ?? 0);
        $skipped = (int) ($summary['skipped'] ?? 0);

        if ($total > 0 && $failed >= $total) {
            return OperationRunOutcome::Failed->value;
        }

        if ($failed > 0) {
            return OperationRunOutcome::PartiallySucceeded->value;
        }

        if ($succeeded > 0 || $skipped > 0) {
            return OperationRunOutcome::Succeeded->value;
        }

        return OperationRunOutcome::Failed->value;
    }

    /**
     * @param  array<string, mixed>  $item
     */
    private function itemLabel(array $item): string
    {
        $displayName = (string) ($item['display_name'] ?? '');

        if ($displayName !== '') {
            return $displayName;
        }

        return (string) ($item['subject_key'] ?? 'unknown subject');
    }
}
