<?php

namespace App\Services\Domain;

use App\Models\ClientDomain;
use App\Models\Setting;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Log;

/**
 * PRC-318 / D-11 — cycle-period sync scheduler.
 *
 * Replaces fixed-frequency schedule (every 1h/3h/6h/12h/daily) with a
 * "complete a full sync of N domains every X days" semantic. Driver:
 *
 *   total_domains          = ClientDomain::where('status', 'active')->count()
 *   batch_limit            = Setting::getValue('domain_sync_batch_limit', 100)
 *   batch_count            = ceil(total_domains / batch_limit)
 *   inter_batch_minutes    = (cycle_days × 24 × 60) / batch_count
 *
 * The `domains:tick` command fires every 5 minutes (Kernel.php). Each
 * tick consults this scheduler to decide whether the next batch of each
 * sync type (full/expiring/transfer) is due. Per-cycle state is kept in
 * 3 Setting keys (`domain_sync_last_batch_at`, etc.) plus a per-cycle
 * "cycle_started_at" tracker.
 *
 * Why state-less batch math: total_domains can change mid-cycle (new
 * domains added, others terminated). Recomputing per tick gives a
 * smoother distribution without needing a snapshot.
 */
class DomainSyncScheduler
{
    /**
     * Decide which sync commands should fire on this tick.
     *
     * @return list<array{type: string, command: string, reason: string}>
     */
    public function dueCommands(): array
    {
        $due = [];

        // Full-list sync
        $fullCycle = (int) Setting::getValue('domain_sync_cycle_days', 7);
        $batchLimit = (int) Setting::getValue('domain_sync_batch_limit', 100);
        if ($this->isBatchDue('full', $fullCycle, $batchLimit)) {
            $due[] = [
                'type' => 'full',
                'command' => "domains:sync --all --queue --limit={$batchLimit}",
                'reason' => "Full-list cycle {$fullCycle}d, batch due",
            ];
        }

        // Expiring sync
        $expCycle = (int) Setting::getValue('domain_expiring_sync_cycle_days', 1);
        if ($this->isBatchDue('expiring', $expCycle, $batchLimit)) {
            $days = (int) Setting::getValue('domain_expiring_sync_days', 30);
            $due[] = [
                'type' => 'expiring',
                'command' => "domains:sync --expiring --days={$days} --queue",
                'reason' => "Expiring cycle {$expCycle}d, batch due (threshold {$days}d)",
            ];
        }

        // Transfer sync
        $tCycle = (int) Setting::getValue('domain_transfer_sync_cycle_days', 1);
        if ($this->isBatchDue('transfer', $tCycle, $batchLimit, scope: 'pending_transfer')) {
            $due[] = [
                'type' => 'transfer',
                'command' => 'domains:sync-transfers',
                'reason' => "Transfer cycle {$tCycle}d, batch due",
            ];
        }

        return $due;
    }

    /**
     * Run one tick: fire any due commands + update state.
     */
    public function tick(): array
    {
        $fired = [];
        foreach ($this->dueCommands() as $batch) {
            Artisan::call($batch['command']);
            $this->markBatchFired($batch['type']);
            Log::info('DomainSyncScheduler fired batch', $batch);
            $fired[] = $batch;
        }
        return $fired;
    }

    /**
     * Is the next batch for `$type` due?
     *
     * Algorithm:
     *  - Get total domains in scope.
     *  - If no domains, no batch is due (no work).
     *  - Compute inter_batch_minutes from cycle + batch_count.
     *  - If now() − last_batch_at >= inter_batch_minutes, it's due.
     */
    protected function isBatchDue(string $type, int $cycleDays, int $batchLimit, string $scope = 'active'): bool
    {
        $totalDomains = $scope === 'pending_transfer'
            ? ClientDomain::where('status', 'pending_transfer')->count()
            : ClientDomain::where('status', 'active')->count();

        if ($totalDomains === 0) {
            return false;
        }

        $batchCount = max(1, (int) ceil($totalDomains / max(1, $batchLimit)));
        $cycleMinutes = $cycleDays * 24 * 60;
        $interBatchMinutes = max(5, (int) floor($cycleMinutes / $batchCount));

        $lastBatchKey = "domain_sync_last_batch_{$type}_at";
        $lastBatchAt = Setting::getValue($lastBatchKey);

        if (! $lastBatchAt) {
            return true; // first-ever batch
        }

        $elapsed = Carbon::parse($lastBatchAt)->diffInMinutes(now());

        return $elapsed >= $interBatchMinutes;
    }

    protected function markBatchFired(string $type): void
    {
        Setting::set("domain_sync_last_batch_{$type}_at", now()->toDateTimeString());
    }
}
