<?php

namespace App\Domain\Project\Models;

use App\Domain\Project\Enums\OverlapPolicy;
use App\Domain\Project\Enums\ScheduleFrequency;
use Cron\CronExpression;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;

/**
 * @property string $id
 * @property string $project_id
 * @property ScheduleFrequency $frequency
 * @property string|null $cron_expression
 * @property int|null $interval_minutes
 * @property string $timezone
 * @property OverlapPolicy $overlap_policy
 * @property int $max_consecutive_failures
 * @property bool $catchup_missed
 * @property bool $run_immediately
 * @property array<string, mixed>|null $overrides
 * @property bool $enabled
 * @property Carbon|null $last_run_at
 * @property Carbon|null $next_run_at
 * @property Carbon|null $queued_run_at
 * @property bool $heartbeat_enabled
 * @property int|null $heartbeat_interval_minutes
 * @property int|null $heartbeat_budget_cap
 * @property array<string, mixed>|null $heartbeat_context_sources
 * @property Carbon|null $created_at
 * @property Carbon|null $updated_at
 * @property-read Project $project
 */
class ProjectSchedule extends Model
{
    use HasUuids;

    protected $fillable = [
        'project_id',
        'frequency',
        'cron_expression',
        'interval_minutes',
        'timezone',
        'overlap_policy',
        'max_consecutive_failures',
        'catchup_missed',
        'run_immediately',
        'overrides',
        'enabled',
        'last_run_at',
        'next_run_at',
        'queued_run_at',
        'heartbeat_enabled',
        'heartbeat_interval_minutes',
        'heartbeat_budget_cap',
        'heartbeat_context_sources',
    ];

    protected function casts(): array
    {
        return [
            'frequency' => ScheduleFrequency::class,
            'overlap_policy' => OverlapPolicy::class,
            'interval_minutes' => 'integer',
            'max_consecutive_failures' => 'integer',
            'catchup_missed' => 'boolean',
            'run_immediately' => 'boolean',
            'overrides' => 'array',
            'enabled' => 'boolean',
            'last_run_at' => 'datetime',
            'next_run_at' => 'datetime',
            'queued_run_at' => 'datetime',
            'heartbeat_enabled' => 'boolean',
            'heartbeat_interval_minutes' => 'integer',
            'heartbeat_budget_cap' => 'integer',
            'heartbeat_context_sources' => 'array',
        ];
    }

    public function getOverride(string $key, mixed $default = null): mixed
    {
        return data_get($this->overrides, $key, $default);
    }

    public function project(): BelongsTo
    {
        return $this->belongsTo(Project::class);
    }

    public function resolvedCronExpression(): ?string
    {
        if ($this->cron_expression) {
            return $this->cron_expression;
        }

        return $this->frequency->toCronExpression();
    }

    public function isDue(): bool
    {
        if (! $this->enabled || ! $this->next_run_at) {
            return false;
        }

        return $this->next_run_at->lte(now());
    }

    public function calculateNextRunAt(?Carbon $from = null): ?Carbon
    {
        $expression = $this->resolvedCronExpression();
        if (! $expression) {
            return null;
        }

        $from = $from ?? now();
        $cron = new CronExpression($expression);

        $nextRun = Carbon::instance(
            $cron->getNextRunDate($from->setTimezone($this->timezone)),
        )->setTimezone('UTC');

        return $nextRun;
    }

    public function getNextRunTimes(int $count = 5): array
    {
        $expression = $this->resolvedCronExpression();
        if (! $expression) {
            return [];
        }

        $cron = new CronExpression($expression);
        $times = [];
        $from = now()->setTimezone($this->timezone);

        for ($i = 0; $i < $count; $i++) {
            $nextRun = Carbon::instance($cron->getNextRunDate($from));
            $times[] = $nextRun->copy()->setTimezone('UTC');
            $from = $nextRun->addSecond();
        }

        return $times;
    }

    public function isOverlapping(): bool
    {
        if ($this->overlap_policy === OverlapPolicy::Allow) {
            return false;
        }

        return $this->project->activeRun() !== null;
    }
}
