<?php

namespace MintyPHP\Service\Scheduler;

use MintyPHP\Repository\Scheduler\ScheduledJobRepositoryInterface;
use MintyPHP\Repository\Scheduler\ScheduledJobRunRepositoryInterface;
use MintyPHP\Service\I18n\TranslatesServiceText;

class ScheduledJobService
{
    use TranslatesServiceText;

    private const RUN_RETENTION_DAYS = 90;

    public function __construct(
        private readonly ScheduledJobRepositoryInterface $scheduledJobRepository,
        private readonly ScheduledJobRunRepositoryInterface $scheduledJobRunRepository,
        private readonly ScheduledJobRegistry $scheduledJobRegistry,
        private readonly ScheduleCalculator $scheduleCalculator
    ) {
    }

    // Syncs the registry definitions to the DB on every request that needs job data —
    // new jobs are inserted automatically without a separate migration step.
    public function ensureSystemJobs(): void
    {
        $definitions = $this->scheduledJobRegistry->definitions();
        $nowUtc = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));

        foreach ($definitions as $jobKey => $definition) {
            $existing = $this->scheduledJobRepository->findByKey($jobKey);
            $job = $this->buildDefaultJob($definition, $jobKey, $nowUtc);

            if (!$existing) {
                $this->scheduledJobRepository->create($job);
                continue;
            }

            $normalized = $this->normalizeStoredJob($existing, $definition);
            if (!$this->jobsDiffer($existing, $normalized)) {
                continue;
            }
            $this->scheduledJobRepository->updateJobMeta((int) $existing['id'], $normalized);
        }

        $this->scheduledJobRepository->deleteOrphanedJobs(array_keys($definitions));
    }

    public function isRegistered(string $jobKey): bool
    {
        return $this->scheduledJobRegistry->get($jobKey) !== null;
    }

    public function listPaged(array $filters): array
    {
        $this->ensureSystemJobs();
        return $this->scheduledJobRepository->listPaged($filters);
    }

    public function find(int $id): ?array
    {
        $this->ensureSystemJobs();
        return $this->scheduledJobRepository->find($id);
    }

    public function updateFromAdmin(int $id, array $input): array
    {
        $job = $this->find($id);
        if (!$job) {
            return ['ok' => false, 'errors' => [$this->translate('Scheduled job not found')]];
        }

        $definition = $this->scheduledJobRegistry->get((string) ($job['job_key'] ?? ''));
        if (!$definition) {
            return ['ok' => false, 'errors' => [$this->translate('Scheduled job is not registered')]];
        }

        $form = [
            'enabled' => isset($input['enabled']) ? 1 : 0,
            'timezone' => $this->scheduleCalculator->normalizeTimezone((string) ($input['timezone'] ?? ($job['timezone'] ?? ''))),
            'schedule_type' => $this->scheduleCalculator->normalizeScheduleType((string) ($input['schedule_type'] ?? ($job['schedule_type'] ?? 'daily'))),
            'schedule_interval' => (int) ($input['schedule_interval'] ?? ($job['schedule_interval'] ?? 1)),
            'schedule_time' => $this->scheduleCalculator->normalizeTime((string) ($input['schedule_time'] ?? ($job['schedule_time'] ?? ''))),
            'schedule_weekdays_csv' => $this->scheduleCalculator->normalizeWeekdaysCsv(
                array_key_exists('schedule_weekdays', $input)
                    ? $input['schedule_weekdays']
                    : ($job['schedule_weekdays_csv'] ?? '')
            ),
            'catchup_once' => isset($input['catchup_once']) ? 1 : 0,
        ];

        $errors = $this->validateForm($form, $definition);
        if ($errors) {
            return ['ok' => false, 'errors' => $errors, 'form' => $form, 'job' => $job];
        }

        $nextRunAt = null;
        if ((int) $form['enabled'] === 1) {
            $nextRun = $this->scheduleCalculator->calculateNextRunUtc($form, new \DateTimeImmutable('now', new \DateTimeZone('UTC')));
            $nextRunAt = $nextRun ? $nextRun->format('Y-m-d H:i:s') : null;
        }

        $updated = $this->scheduledJobRepository->updateJobMeta((int) $job['id'], [
            'label' => (string) ($definition['label'] ?? (string) ($job['label'] ?? '')),
            'description' => (string) ($definition['description'] ?? (string) ($job['description'] ?? '')),
            'enabled' => (int) $form['enabled'],
            'timezone' => (string) $form['timezone'],
            'schedule_type' => (string) $form['schedule_type'],
            'schedule_interval' => (int) $form['schedule_interval'],
            'schedule_time' => $form['schedule_time'],
            'schedule_weekdays_csv' => $form['schedule_weekdays_csv'],
            'catchup_once' => (int) $form['catchup_once'],
            'next_run_at' => $nextRunAt,
        ]);

        if (!$updated) {
            return ['ok' => false, 'errors' => [$this->translate('Scheduled job could not be saved')], 'form' => $form, 'job' => $job];
        }

        $fresh = $this->find((int) $job['id']);
        return ['ok' => true, 'job' => $fresh, 'form' => $form];
    }

    public function listRunsByJobId(int $jobId, array $filters): array
    {
        return $this->scheduledJobRunRepository->listPagedByJobId($jobId, $filters);
    }

    public function purgeRunsExpired(): int
    {
        return $this->scheduledJobRunRepository->purgeOlderThanDays(self::RUN_RETENTION_DAYS);
    }

    public function scheduleSummary(array $job): string
    {
        $type = $this->scheduleCalculator->normalizeScheduleType((string) ($job['schedule_type'] ?? 'daily'));
        $interval = (int) ($job['schedule_interval'] ?? 1);
        $time = (string) ($job['schedule_time'] ?? '');

        if ($type === 'hourly') {
            return sprintf($this->translate('Every %d hour(s)'), max(1, $interval));
        }
        if ($type === 'daily') {
            return sprintf($this->translate('Every %d day(s) at %s'), max(1, $interval), $time !== '' ? $time : '--:--');
        }

        $weekdays = $this->scheduleCalculator->parseWeekdays((string) ($job['schedule_weekdays_csv'] ?? ''));
        $labels = [];
        foreach ($weekdays as $day) {
            $labels[] = $this->weekdayLabel($day);
        }
        $weekdayText = $labels ? implode(', ', $labels) : $this->translate('No weekdays');
        return sprintf(
            $this->translate('Every %d week(s) on %s at %s'),
            max(1, $interval),
            $weekdayText,
            $time !== '' ? $time : '--:--'
        );
    }

    public function weekdaysFromCsv(?string $csv): array
    {
        return $this->scheduleCalculator->parseWeekdays((string) ($csv ?? ''));
    }

    private function validateForm(array $form, array $definition): array
    {
        $errors = [];
        $allowedTypes = $definition['allowed_schedule_types'] ?? ['hourly', 'daily', 'weekly'];
        $type = (string) ($form['schedule_type'] ?? 'daily');
        if (!in_array($type, $allowedTypes, true)) {
            $errors[] = $this->translate('Schedule type is invalid');
            return $errors;
        }

        $interval = (int) ($form['schedule_interval'] ?? 0);
        if ($type === 'hourly' && ($interval < 1 || $interval > 24)) {
            $errors[] = $this->translate('Hourly interval must be between 1 and 24');
        } elseif ($type === 'daily' && ($interval < 1 || $interval > 365)) {
            $errors[] = $this->translate('Daily interval must be between 1 and 365');
        } elseif ($type === 'weekly' && ($interval < 1 || $interval > 52)) {
            $errors[] = $this->translate('Weekly interval must be between 1 and 52');
        }

        $timezone = trim((string) ($form['timezone'] ?? ''));
        try {
            new \DateTimeZone($timezone);
        } catch (\Throwable $exception) {
            $errors[] = $this->translate('Timezone is invalid');
        }

        if (in_array($type, ['daily', 'weekly'], true) && $form['schedule_time'] === null) {
            $errors[] = $this->translate('Schedule time is required');
        }
        if ($type === 'weekly') {
            $weekdays = $this->scheduleCalculator->parseWeekdays((string) ($form['schedule_weekdays_csv'] ?? ''));
            if (!$weekdays) {
                $errors[] = $this->translate('At least one weekday is required for weekly schedule');
            }
        }

        return $errors;
    }

    private function buildDefaultJob(array $definition, string $jobKey, \DateTimeImmutable $nowUtc): array
    {
        $type = (string) ($definition['default_schedule_type'] ?? 'daily');
        $job = [
            'job_key' => $jobKey,
            'label' => (string) ($definition['label'] ?? $jobKey),
            'description' => (string) ($definition['description'] ?? ''),
            'enabled' => (int) ($definition['default_enabled'] ?? 1),
            'timezone' => $this->scheduleCalculator->normalizeTimezone((string) ($definition['default_timezone'] ?? '')),
            'schedule_type' => $this->scheduleCalculator->normalizeScheduleType($type),
            'schedule_interval' => $this->scheduleCalculator->normalizeInterval($type, (int) ($definition['default_schedule_interval'] ?? 1)),
            'schedule_time' => $this->scheduleCalculator->normalizeTime((string) ($definition['default_schedule_time'] ?? '')),
            'schedule_weekdays_csv' => $this->scheduleCalculator->normalizeWeekdaysCsv($definition['default_schedule_weekdays_csv'] ?? null),
            'catchup_once' => (int) ($definition['default_catchup_once'] ?? 1),
            'next_run_at' => null,
        ];
        if ((int) $job['enabled'] === 1) {
            $nextRun = $this->scheduleCalculator->calculateNextRunUtc($job, $nowUtc);
            $job['next_run_at'] = $nextRun ? $nextRun->format('Y-m-d H:i:s') : null;
        }
        return $job;
    }

    private function normalizeStoredJob(array $existing, array $definition): array
    {
        $type = $this->scheduleCalculator->normalizeScheduleType((string) ($existing['schedule_type'] ?? ($definition['default_schedule_type'] ?? 'daily')));
        $normalized = [
            // label and description always come from the registry — admin edits to those fields are not preserved.
            'label' => (string) ($definition['label'] ?? (string) ($existing['label'] ?? '')),
            'description' => (string) ($definition['description'] ?? (string) ($existing['description'] ?? '')),
            'enabled' => (int) ($existing['enabled'] ?? ($definition['default_enabled'] ?? 1)),
            'timezone' => $this->scheduleCalculator->normalizeTimezone((string) ($existing['timezone'] ?? ($definition['default_timezone'] ?? ''))),
            'schedule_type' => $type,
            'schedule_interval' => $this->scheduleCalculator->normalizeInterval($type, (int) ($existing['schedule_interval'] ?? ($definition['default_schedule_interval'] ?? 1))),
            'schedule_time' => $this->scheduleCalculator->normalizeTime((string) ($existing['schedule_time'] ?? ($definition['default_schedule_time'] ?? ''))),
            'schedule_weekdays_csv' => $this->scheduleCalculator->normalizeWeekdaysCsv(
                (string) ($existing['schedule_weekdays_csv'] ?? ($definition['default_schedule_weekdays_csv'] ?? ''))
            ),
            'catchup_once' => (int) ($existing['catchup_once'] ?? ($definition['default_catchup_once'] ?? 1)),
            'next_run_at' => (string) ($existing['next_run_at'] ?? ''),
        ];
        if ($normalized['next_run_at'] === '' && (int) $normalized['enabled'] === 1) {
            $nextRun = $this->scheduleCalculator->calculateNextRunUtc($normalized, new \DateTimeImmutable('now', new \DateTimeZone('UTC')));
            $normalized['next_run_at'] = $nextRun ? $nextRun->format('Y-m-d H:i:s') : null;
        } else {
            $normalized['next_run_at'] = $normalized['next_run_at'] !== '' ? $normalized['next_run_at'] : null;
        }
        return $normalized;
    }

    private function jobsDiffer(array $existing, array $normalized): bool
    {
        $keys = [
            'label',
            'description',
            'enabled',
            'timezone',
            'schedule_type',
            'schedule_interval',
            'schedule_time',
            'schedule_weekdays_csv',
            'catchup_once',
            'next_run_at',
        ];
        foreach ($keys as $key) {
            $a = (string) ($existing[$key] ?? '');
            $b = (string) ($normalized[$key] ?? '');
            if ($a !== $b) {
                return true;
            }
        }
        return false;
    }

    private function weekdayLabel(int $weekday): string
    {
        return match ($weekday) {
            1 => $this->translate('Mon'),
            2 => $this->translate('Tue'),
            3 => $this->translate('Wed'),
            4 => $this->translate('Thu'),
            5 => $this->translate('Fri'),
            6 => $this->translate('Sat'),
            7 => $this->translate('Sun'),
            default => '-',
        };
    }

}
