<?php

namespace App\Jobs;

use App\Jobs\Middleware\EnsureQueuedExecutionLegitimate;
use App\Jobs\Middleware\TrackOperationRun;
use App\Models\BackupSchedule;
use App\Models\OperationRun;
use App\Models\ManagedEnvironment;
use App\Services\BackupScheduling\PolicyTypeResolver;
use App\Services\BackupScheduling\RunErrorMapper;
use App\Services\BackupScheduling\ScheduleTimeService;
use App\Services\Intune\AuditLogger;
use App\Services\Intune\BackupService;
use App\Services\Intune\PolicySyncService;
use App\Services\OperationRunService;
use App\Support\OperationRunOutcome;
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 Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Cache;

class RunBackupScheduleJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    private const string STATUS_RUNNING = 'running';

    private const string STATUS_SUCCESS = 'success';

    private const string STATUS_PARTIAL = 'partial';

    private const string STATUS_FAILED = 'failed';

    private const string STATUS_SKIPPED = 'skipped';

    public int $tries = 3;

    public int $timeout = 300;

    public bool $failOnTimeout = true;

    /**
     * Compatibility-only legacy field.
     *
     * Kept as an uninitialized typed property so old queued payloads can still
     * deserialize safely, while new payloads omit the field.
     */
    public int $backupScheduleRunId;

    public ?OperationRun $operationRun = null;

    public function __construct(
        ?OperationRun $operationRun = null,
        public ?int $backupScheduleId = null,
    ) {
        $this->operationRun = $operationRun;
    }

    public function middleware(): array
    {
        return [new EnsureQueuedExecutionLegitimate, new TrackOperationRun];
    }

    public function handle(
        PolicySyncService $policySyncService,
        BackupService $backupService,
        PolicyTypeResolver $policyTypeResolver,
        ScheduleTimeService $scheduleTimeService,
        AuditLogger $auditLogger,
        RunErrorMapper $errorMapper,
    ): void {
        if (! $this->operationRun) {
            $this->fail(new \RuntimeException('OperationRun context is required for RunBackupScheduleJob.'));

            return;
        }

        $backupScheduleId = $this->resolveBackupScheduleId();

        if ($backupScheduleId <= 0) {
            $this->markOperationRunFailed(
                run: $this->operationRun,
                summaryCounts: [],
                reasonCode: 'schedule_not_provided',
                reason: 'No backup schedule was provided for this run.',
            );

            return;
        }

        $schedule = BackupSchedule::query()
            ->withTrashed()
            ->with('tenant')
            ->find($backupScheduleId);

        if (! $schedule instanceof BackupSchedule) {
            $this->markOperationRunFailed(
                run: $this->operationRun,
                summaryCounts: [],
                reasonCode: 'schedule_not_found',
                reason: 'Schedule not found.',
            );

            return;
        }

        $tenant = $schedule->tenant;

        if (! $tenant instanceof ManagedEnvironment) {
            $this->markOperationRunFailed(
                run: $this->operationRun,
                summaryCounts: [],
                reasonCode: 'tenant_not_found',
                reason: 'ManagedEnvironment not found.',
            );

            return;
        }

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

        $this->operationRun->update([
            'context' => array_merge($this->operationRun->context ?? [], [
                'backup_schedule_id' => (int) $schedule->getKey(),
            ]),
        ]);

        if ($this->operationRun->status === 'queued') {
            $operationRunService->updateRun($this->operationRun, 'running');
        }

        if ($schedule->trashed()) {
            $nowUtc = CarbonImmutable::now('UTC');

            $this->finishSchedule(
                schedule: $schedule,
                status: self::STATUS_SKIPPED,
                scheduleTimeService: $scheduleTimeService,
                nowUtc: $nowUtc,
            );

            $operationRunService->updateRun(
                $this->operationRun,
                status: 'completed',
                outcome: OperationRunOutcome::Blocked->value,
                summaryCounts: [
                    'total' => 0,
                    'processed' => 0,
                    'failed' => 0,
                    'skipped' => 1,
                ],
                failures: [
                    [
                        'code' => 'schedule_archived',
                        'message' => 'Schedule is archived; run will not execute.',
                    ],
                ],
            );

            $auditLogger->log(
                tenant: $tenant,
                action: 'backup_schedule.run_skipped',
                context: [
                    'metadata' => [
                        'backup_schedule_id' => $schedule->id,
                        'operation_run_id' => $this->operationRun->getKey(),
                        'reason' => 'schedule_archived',
                    ],
                ],
                resourceType: 'operation_run',
                resourceId: (string) $this->operationRun->getKey(),
                status: 'partial'
            );

            return;
        }

        $lock = Cache::lock("backup_schedule:{$schedule->id}", 900);

        if (! $lock->get()) {
            $nowUtc = CarbonImmutable::now('UTC');

            $this->finishSchedule(
                schedule: $schedule,
                status: self::STATUS_SKIPPED,
                scheduleTimeService: $scheduleTimeService,
                nowUtc: $nowUtc,
            );

            $operationRunService->updateRun(
                $this->operationRun,
                status: 'completed',
                outcome: OperationRunOutcome::Blocked->value,
                summaryCounts: [
                    'total' => 0,
                    'processed' => 0,
                    'failed' => 0,
                    'skipped' => 1,
                ],
                failures: [
                    [
                        'code' => 'concurrent_run',
                        'message' => 'Another run is already in progress for this schedule.',
                    ],
                ],
            );

            $auditLogger->log(
                tenant: $tenant,
                action: 'backup_schedule.run_skipped',
                context: [
                    'metadata' => [
                        'backup_schedule_id' => $schedule->id,
                        'operation_run_id' => $this->operationRun->getKey(),
                        'reason' => 'concurrent_run',
                    ],
                ],
                resourceType: 'operation_run',
                resourceId: (string) $this->operationRun->getKey(),
                status: 'partial'
            );

            return;
        }

        try {
            $nowUtc = CarbonImmutable::now('UTC');

            $auditLogger->log(
                tenant: $tenant,
                action: 'backup_schedule.run_started',
                context: [
                    'metadata' => [
                        'backup_schedule_id' => $schedule->id,
                        'operation_run_id' => $this->operationRun->getKey(),
                    ],
                ],
                resourceType: 'operation_run',
                resourceId: (string) $this->operationRun->getKey(),
                status: 'success'
            );

            $runtime = $policyTypeResolver->resolveRuntime((array) ($schedule->policy_types ?? []));
            $validTypes = $runtime['valid'];
            $unknownTypes = $runtime['unknown'];

            if (empty($validTypes)) {
                $this->finishSchedule(
                    schedule: $schedule,
                    status: self::STATUS_SKIPPED,
                    scheduleTimeService: $scheduleTimeService,
                    nowUtc: $nowUtc,
                );

                $operationRunService->updateRun(
                    $this->operationRun,
                    status: 'completed',
                    outcome: OperationRunOutcome::Blocked->value,
                    summaryCounts: [
                        'total' => 0,
                        'processed' => 0,
                        'failed' => 0,
                        'skipped' => 1,
                    ],
                    failures: [
                        [
                            'code' => 'unknown_policy_type',
                            'message' => 'All configured policy types are unknown.',
                        ],
                    ],
                );

                return;
            }

            $supported = array_values(array_filter(
                config('tenantpilot.supported_policy_types', []),
                fn (array $typeConfig): bool => in_array($typeConfig['type'] ?? null, $validTypes, true),
            ));

            $syncReport = $policySyncService->syncPoliciesWithReport($tenant, $supported);

            $policyIds = $syncReport['synced'] ?? [];
            $syncFailures = $syncReport['failures'] ?? [];

            $backupSet = $backupService->createBackupSet(
                tenant: $tenant,
                policyIds: $policyIds,
                actorEmail: null,
                actorName: null,
                name: 'Scheduled backup: '.$schedule->name,
                includeAssignments: false,
                includeScopeTags: false,
                includeFoundations: (bool) ($schedule->include_foundations ?? false),
            );

            $status = match ($backupSet->status) {
                'completed' => self::STATUS_SUCCESS,
                'partial' => self::STATUS_PARTIAL,
                'failed' => self::STATUS_FAILED,
                default => self::STATUS_SUCCESS,
            };

            $errorCode = null;
            $errorMessage = null;

            if (! empty($unknownTypes)) {
                $status = self::STATUS_PARTIAL;
                $errorCode = 'UNKNOWN_POLICY_TYPE';
                $errorMessage = 'Some configured policy types are unknown and were skipped.';
            }

            $policiesTotal = count($policyIds);
            $policiesBackedUp = (int) ($backupSet->item_count ?? 0);
            $failedCount = max(0, $policiesTotal - $policiesBackedUp);

            $summaryCounts = [
                'total' => $policiesTotal,
                'processed' => $policiesTotal,
                'succeeded' => $policiesBackedUp,
                'failed' => $failedCount,
                'skipped' => 0,
                'created' => 1,
                'updated' => $policiesBackedUp,
                'items' => $policiesTotal,
            ];

            $failures = [];

            if (is_string($errorMessage) && $errorMessage !== '') {
                $failures[] = [
                    'code' => strtolower((string) ($errorCode ?: 'backup_schedule_error')),
                    'message' => $errorMessage,
                ];
            }

            if (is_array($syncFailures)) {
                foreach ($syncFailures as $failure) {
                    if (! is_array($failure)) {
                        continue;
                    }

                    $policyType = (string) ($failure['policy_type'] ?? 'unknown');
                    $httpStatus = is_numeric($failure['status'] ?? null) ? (int) $failure['status'] : null;
                    $errors = $failure['errors'] ?? null;

                    $firstErrorMessage = null;
                    if (is_array($errors) && isset($errors[0]) && is_array($errors[0])) {
                        $firstErrorMessage = $errors[0]['message'] ?? null;
                    }

                    $message = $httpStatus !== null
                        ? "{$policyType}: Graph returned {$httpStatus}"
                        : "{$policyType}: Graph request failed";

                    if (is_string($firstErrorMessage) && $firstErrorMessage !== '') {
                        $message .= ' - '.trim($firstErrorMessage);
                    }

                    $failures[] = [
                        'code' => $httpStatus !== null ? 'graph_http_'.(string) $httpStatus : 'graph_error',
                        'message' => $message,
                    ];
                }
            }

            $this->operationRun->update([
                'context' => array_merge($this->operationRun->context ?? [], [
                    'backup_schedule_id' => (int) $schedule->getKey(),
                    'backup_set_id' => (int) $backupSet->getKey(),
                ]),
            ]);

            $outcome = match ($status) {
                self::STATUS_SUCCESS => OperationRunOutcome::Succeeded->value,
                self::STATUS_PARTIAL => OperationRunOutcome::PartiallySucceeded->value,
                self::STATUS_SKIPPED => OperationRunOutcome::Blocked->value,
                default => OperationRunOutcome::Failed->value,
            };

            $operationRunService->updateRun(
                $this->operationRun,
                status: 'completed',
                outcome: $outcome,
                summaryCounts: $summaryCounts,
                failures: $failures,
            );

            $this->finishSchedule(
                schedule: $schedule,
                status: $status,
                scheduleTimeService: $scheduleTimeService,
                nowUtc: $nowUtc,
            );

            if (in_array($status, [self::STATUS_SUCCESS, self::STATUS_PARTIAL], true)) {
                Bus::dispatch(new ApplyBackupScheduleRetentionJob((int) $schedule->getKey()));
            }

            $auditLogger->log(
                tenant: $tenant,
                action: 'backup_schedule.run_finished',
                context: [
                    'metadata' => [
                        'backup_schedule_id' => $schedule->id,
                        'operation_run_id' => $this->operationRun->getKey(),
                        'status' => $status,
                        'error_code' => $errorCode,
                    ],
                ],
                resourceType: 'operation_run',
                resourceId: (string) $this->operationRun->getKey(),
                status: in_array($status, [self::STATUS_SUCCESS], true) ? 'success' : 'partial'
            );
        } catch (\Throwable $throwable) {
            $attempt = (int) method_exists($this, 'attempts') ? $this->attempts() : 1;
            $mapped = $errorMapper->map($throwable, $attempt, $this->tries);

            if ($mapped['shouldRetry']) {
                $operationRunService->updateRun($this->operationRun, 'running', OperationRunOutcome::Pending->value);

                $this->release($mapped['delay']);

                return;
            }

            $nowUtc = CarbonImmutable::now('UTC');

            $this->finishSchedule(
                schedule: $schedule,
                status: self::STATUS_FAILED,
                scheduleTimeService: $scheduleTimeService,
                nowUtc: $nowUtc,
            );

            $operationRunService->updateRun(
                $this->operationRun,
                status: 'completed',
                outcome: OperationRunOutcome::Failed->value,
                summaryCounts: [
                    'total' => 0,
                    'processed' => 0,
                    'failed' => 1,
                ],
                failures: [
                    [
                        'code' => strtolower((string) $mapped['error_code']),
                        'message' => (string) $mapped['error_message'],
                    ],
                ],
            );

            $auditLogger->log(
                tenant: $tenant,
                action: 'backup_schedule.run_failed',
                context: [
                    'metadata' => [
                        'backup_schedule_id' => $schedule->id,
                        'operation_run_id' => $this->operationRun->getKey(),
                        'error_code' => $mapped['error_code'],
                    ],
                ],
                resourceType: 'operation_run',
                resourceId: (string) $this->operationRun->getKey(),
                status: 'failed'
            );
        } finally {
            optional($lock)->release();
        }
    }

    private function resolveBackupScheduleId(): int
    {
        if ($this->backupScheduleId !== null && $this->backupScheduleId > 0) {
            return $this->backupScheduleId;
        }

        $contextScheduleId = data_get($this->operationRun?->context, 'backup_schedule_id');

        return is_numeric($contextScheduleId) ? (int) $contextScheduleId : 0;
    }

    private function finishSchedule(
        BackupSchedule $schedule,
        string $status,
        ScheduleTimeService $scheduleTimeService,
        CarbonImmutable $nowUtc,
    ): void {
        $schedule->forceFill([
            'last_run_at' => $nowUtc,
            'last_run_status' => $status,
            'next_run_at' => $scheduleTimeService->nextRunFor($schedule, $nowUtc),
        ])->saveQuietly();
    }

    private function markOperationRunFailed(
        OperationRun $run,
        array $summaryCounts,
        string $reasonCode,
        string $reason,
    ): void {
        /** @var OperationRunService $operationRunService */
        $operationRunService = app(OperationRunService::class);

        $operationRunService->updateRun(
            $run,
            status: 'completed',
            outcome: OperationRunOutcome::Failed->value,
            summaryCounts: $summaryCounts,
            failures: [
                [
                    'code' => $reasonCode,
                    'message' => $reason,
                ],
            ],
        );
    }
}
