<?php

namespace App\Services\BackupScheduling;

use App\Jobs\RunBackupScheduleJob;
use App\Models\BackupSchedule;
use App\Models\BackupScheduleRun;
use App\Models\Tenant;
use App\Services\Intune\AuditLogger;
use Carbon\CarbonImmutable;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Log;

class BackupScheduleDispatcher
{
    public function __construct(
        private readonly ScheduleTimeService $scheduleTimeService,
        private readonly AuditLogger $auditLogger,
    ) {}

    /**
     * Dispatch due schedules.
     *
     * No catch-up policy: we only dispatch if the current minute-slot is due.
     *
     * @return array{created_runs:int, skipped_runs:int, scanned_schedules:int}
     */
    public function dispatchDue(?array $tenantIdentifiers = null): array
    {
        $nowUtc = CarbonImmutable::now('UTC');

        $schedulesQuery = BackupSchedule::query()
            ->where('is_enabled', true)
            ->whereHas('tenant', fn ($query) => $query->where('status', 'active'))
            ->with('tenant');

        if (is_array($tenantIdentifiers) && ! empty($tenantIdentifiers)) {
            $schedulesQuery->whereIn('tenant_id', $this->resolveTenantIds($tenantIdentifiers));
        }

        $createdRuns = 0;
        $skippedRuns = 0;
        $scannedSchedules = 0;

        foreach ($schedulesQuery->cursor() as $schedule) {
            $scannedSchedules++;

            $slot = $this->scheduleTimeService->nextRunFor($schedule, $nowUtc->subMinute());

            if ($slot === null) {
                $schedule->forceFill(['next_run_at' => null])->saveQuietly();

                continue;
            }

            if ($slot->greaterThan($nowUtc)) {
                if (! $schedule->next_run_at || ! $schedule->next_run_at->equalTo($slot)) {
                    $schedule->forceFill(['next_run_at' => $slot])->saveQuietly();
                }

                continue;
            }

            $run = null;

            try {
                $run = BackupScheduleRun::create([
                    'backup_schedule_id' => $schedule->id,
                    'tenant_id' => $schedule->tenant_id,
                    'scheduled_for' => $slot->toDateTimeString(),
                    'status' => BackupScheduleRun::STATUS_RUNNING,
                    'summary' => null,
                ]);
            } catch (UniqueConstraintViolationException) {
                // Idempotency: unique (backup_schedule_id, scheduled_for)
                $skippedRuns++;

                Log::debug('Backup schedule run already dispatched for slot.', [
                    'schedule_id' => $schedule->id,
                    'slot' => $slot->toDateTimeString(),
                ]);

                $schedule->forceFill([
                    'next_run_at' => $this->scheduleTimeService->nextRunFor($schedule, $nowUtc),
                ])->saveQuietly();

                continue;
            }

            $createdRuns++;

            $this->auditLogger->log(
                tenant: $schedule->tenant,
                action: 'backup_schedule.run_dispatched',
                context: [
                    'metadata' => [
                        'backup_schedule_id' => $schedule->id,
                        'backup_schedule_run_id' => $run->id,
                        'scheduled_for' => $slot->toDateTimeString(),
                    ],
                ],
                resourceType: 'backup_schedule_run',
                resourceId: (string) $run->id,
                status: 'success'
            );

            $schedule->forceFill([
                'next_run_at' => $this->scheduleTimeService->nextRunFor($schedule, $nowUtc),
            ])->saveQuietly();

            Bus::dispatch(new RunBackupScheduleJob($run->id));
        }

        return [
            'created_runs' => $createdRuns,
            'skipped_runs' => $skippedRuns,
            'scanned_schedules' => $scannedSchedules,
        ];
    }

    /**
     * @param  array<int, string>  $tenantIdentifiers
     * @return array<int>
     */
    private function resolveTenantIds(array $tenantIdentifiers): array
    {
        $tenantIds = [];

        foreach ($tenantIdentifiers as $identifier) {
            $tenant = Tenant::query()
                ->where('status', 'active')
                ->forTenant($identifier)
                ->first();

            if ($tenant) {
                $tenantIds[] = $tenant->id;
            }
        }

        return array_values(array_unique($tenantIds));
    }
}
