<?php

namespace App\Services;

use App\Models\Campaign;
use Carbon\Carbon;

class RecurringScheduleCalculator
{
    /**
     * Compute the next timestamp a recurring campaign should fire, or null
     * if its schedule is incomplete or its end condition has been reached.
     */
    public static function nextRunAt(Campaign $campaign, ?Carbon $after = null): ?Carbon
    {
        if (!$campaign->frequency || !$campaign->recurring_time) {
            return null;
        }

        if (self::isExhausted($campaign)) {
            return null;
        }

        // Timezone is tenant-wide (Settings > General Settings), not per-campaign 
        // so changing it in Settings and recomputing next_run_at (see
        // SettingsController::update) reschedules every recurring campaign at once.
        $tz  = BusinessSettings::timezone($campaign->user_id);
        [$hour, $minute] = array_pad(explode(':', $campaign->recurring_time), 2, 0);

        $from = ($after ?? Carbon::now($tz))->copy()->setTimezone($tz);

        $earliest = $from->copy()->addMinute();
        if ($campaign->start_date) {
            $startOfDay = Carbon::parse($campaign->start_date, $tz)->startOfDay();
            if ($startOfDay->greaterThan($earliest)) {
                $earliest = $startOfDay;
            }
        }

        $candidate = match ($campaign->frequency) {
            'daily'   => self::nextDaily($earliest, (int) $hour, (int) $minute),
            'weekly'  => self::nextWeekly($earliest, $campaign->recurring_days ?? [], (int) $hour, (int) $minute),
            'monthly' => self::nextMonthly($earliest, $campaign->recurring_days ?? [], (int) $hour, (int) $minute),
            default   => null,
        };

        if (!$candidate) {
            return null;
        }

        if ($campaign->end_type === 'date' && $campaign->end_date) {
            $endOfDay = Carbon::parse($campaign->end_date, $tz)->endOfDay();
            if ($candidate->greaterThan($endOfDay)) {
                return null;
            }
        }

        return $candidate->setTimezone(config('app.timezone'));
    }

    private static function isExhausted(Campaign $campaign): bool
    {
        return $campaign->end_type === 'count'
            && $campaign->end_count !== null
            && $campaign->occurrences_sent >= $campaign->end_count;
    }

    private static function nextDaily(Carbon $earliest, int $hour, int $minute): Carbon
    {
        $candidate = $earliest->copy()->setTime($hour, $minute, 0);
        if ($candidate->lessThan($earliest)) {
            $candidate->addDay();
        }
        return $candidate;
    }

    private static function nextWeekly(Carbon $earliest, array $days, int $hour, int $minute): ?Carbon
    {
        $days = array_values(array_unique(array_map('intval', $days)));
        if (empty($days)) {
            return null;
        }
        sort($days);

        for ($i = 0; $i <= 7; $i++) {
            $candidate = $earliest->copy()->addDays($i)->setTime($hour, $minute, 0);
            if (in_array((int) $candidate->dayOfWeek, $days, true) && $candidate->greaterThanOrEqualTo($earliest)) {
                return $candidate;
            }
        }

        return null;
    }

    private static function nextMonthly(Carbon $earliest, array $days, int $hour, int $minute): ?Carbon
    {
        $days = array_values(array_unique(array_map('intval', $days)));
        if (empty($days)) {
            return null;
        }
        sort($days);

        for ($m = 0; $m <= 12; $m++) {
            $monthStart = $earliest->copy()->addMonthsNoOverflow($m)->startOfMonth();
            $daysInMonth = $monthStart->daysInMonth;

            foreach ($days as $day) {
                $dom = min($day, $daysInMonth);
                $candidate = $monthStart->copy()->addDays($dom - 1)->setTime($hour, $minute, 0);
                if ($candidate->greaterThanOrEqualTo($earliest)) {
                    return $candidate;
                }
            }
        }

        return null;
    }
}
