<?php

namespace Openguru\OpenCartFramework\Scheduler;

use DateTime;
use Openguru\OpenCartFramework\Cache\CacheInterface;
use Openguru\OpenCartFramework\Config\Settings;
use Openguru\OpenCartFramework\Container\Container;
use Psr\Log\LoggerInterface;
use Throwable;

class SchedulerService
{
    private LoggerInterface $logger;
    private CacheInterface $cache;
    private Container $container;
    private Settings $settings;
    private ?ScheduleJobRegistry $registry = null;

    private const GLOBAL_LOCK_KEY = 'scheduler.global_lock';
    private const GLOBAL_LOCK_TTL = 300; // 5 minutes

    public function __construct(
        LoggerInterface $logger,
        CacheInterface $cache,
        Container $container,
        Settings $settings
    ) {
        $this->logger = $logger;
        $this->cache = $cache;
        $this->container = $container;
        $this->settings = $settings;
    }

    // For testing purposes
    public function setRegistry(ScheduleJobRegistry $registry): void
    {
        $this->registry = $registry;
    }

    public function run(): SchedulerResult
    {
        $result = new SchedulerResult();

        $mode = $this->settings->get('cron.mode', 'disabled');
        if ($mode === 'disabled') {
            $result->addSkipped('Global', 'Scheduler is disabled');

            return $result;
        }

        if ($this->isGlobalLocked()) {
            $result->addSkipped('Global', 'Global scheduler lock active');

            return $result;
        }

        $this->acquireGlobalLock();
        // Since we want to run every 5 minutes, running it more frequently won't trigger jobs earlier than due,
        // but locking might prevent overlap if previous run takes > 5 mins.
        // However, updating global last run on every attempt might be useful for diagnostics,
        // but strictly speaking, we only care if tasks were processed.
        $this->updateGlobalLastRun();

        try {
            $scheduler = $this->registry ?: new ScheduleJobRegistry($this->container);

            // Only load config file if registry was not injected (for production use)
            if (! $this->registry) {
                $configFile = BP_PHAR_BASE_PATH . '/configs/schedule.php';
                if (file_exists($configFile)) {
                    require $configFile;
                } else {
                    $this->logger->warning('Scheduler config file not found: ' . $configFile);
                }
            }

            foreach ($scheduler->getJobs() as $job) {
                $this->processJob($job, $result);
            }
        } catch (Throwable $e) {
            $this->logger->error('Scheduler run failed: ' . $e->getMessage(), ['exception' => $e]);
            $result->addFailed('Scheduler', $e->getMessage());
        } finally {
            $this->releaseGlobalLock();
        }

        return $result;
    }

    private function processJob(Job $job, SchedulerResult $result): void
    {
        $name = $job->getName();
        $id = $job->getId();

        try {
            // 1. Check if due by Cron expression
            if (! $job->isDue()) {
                $result->addSkipped($name, 'Not due');

                return;
            }

            // 2. Check Last Run (Prevent running multiple times in the same minute)
            if ($this->hasRanRecently($id)) {
                $result->addSkipped($name, 'Already ran recently');

                return;
            }

            // 3. Check Lock (Prevent parallel execution)
            if ($this->isJobLocked($id)) {
                $result->addSkipped($name, 'Job is locked (running)');

                return;
            }

            // Lock and Run
            $this->lockJob($id);
            $startTime = microtime(true);

            try {
                $job->run();

                $duration = microtime(true) - $startTime;
                $this->updateLastRun($id);

                $this->logger->debug("Job executed: {$name}", ['duration' => $duration]);
                $result->addExecuted($name, $duration);
            } catch (Throwable $e) {
                $this->updateLastFailure($id, $e->getMessage());
                $this->logger->error("Job failed: {$name}", ['exception' => $e]);
                $result->addFailed($name, $e->getMessage());
            } finally {
                $this->unlockJob($id);
            }
        } catch (Throwable $e) {
            $this->logger->error("Error processing job {$name}: " . $e->getMessage());
            $result->addFailed($name, 'Processing error: ' . $e->getMessage());
        }
    }

    private function isGlobalLocked(): bool
    {
        return (bool) $this->cache->get(self::GLOBAL_LOCK_KEY);
    }

    private function acquireGlobalLock(): void
    {
        $this->cache->set(self::GLOBAL_LOCK_KEY, 1, self::GLOBAL_LOCK_TTL);
    }

    private function releaseGlobalLock(): void
    {
        $this->cache->delete(self::GLOBAL_LOCK_KEY);
    }

    private function isJobLocked(string $id): bool
    {
        return (bool) $this->cache->get("scheduler.lock.{$id}");
    }

    private function lockJob(string $id): void
    {
        // 30 minutes max execution time for a single job safe-guard
        $this->cache->set("scheduler.lock.{$id}", 1, 1800);
    }

    private function unlockJob(string $id): void
    {
        $this->cache->delete("scheduler.lock.{$id}");
    }

    private function hasRanRecently(string $id): bool
    {
        $lastRun = $this->getLastRun($id);
        if (! $lastRun) {
            return false;
        }

        $lastRunDate = (new DateTime())->setTimestamp((int) $lastRun);
        $now = new DateTime();

        return $lastRunDate->format('Y-m-d H:i') === $now->format('Y-m-d H:i');
    }

    private function updateLastRun(string $id): void
    {
        $this->cache->set("scheduler.last_run.{$id}", time());
    }

    private function updateGlobalLastRun(): void
    {
        $this->cache->set("scheduler.global_last_run", time());
    }

    public function getGlobalLastRun(): ?int
    {
        $time = $this->cache->get("scheduler.global_last_run");

        return $time ? (int) $time : null;
    }

    private function updateLastFailure(string $id, string $message): void
    {
        $this->cache->set("scheduler.last_failure.{$id}", time());
        $this->cache->set("scheduler.last_failure_msg.{$id}", $message);
    }

    public function getLastRun(string $id): ?int
    {
        return $this->cache->get("scheduler.last_run.{$id}");
    }

    public function getLastFailure(string $id): ?array
    {
        $time = $this->cache->get("scheduler.last_failure.{$id}");
        if (! $time) {
            return null;
        }

        return [
            'time' => (int) $time,
            'message' => $this->cache->get("scheduler.last_failure_msg.{$id}"),
        ];
    }
}
