<?php

namespace MintyPHP\Service\Scheduler;

use MintyPHP\App\AppContainer;
use MintyPHP\App\Module\ModuleRegistry;
use MintyPHP\Service\Scheduler\Handler\ScheduledJobHandlerInterface;
use MintyPHP\Service\Scheduler\Handler\UserLifecycleJobHandler;
use MintyPHP\Service\User\UserLifecycleService;
use RuntimeException;

class ScheduledJobRegistry
{
    public const USER_LIFECYCLE_RUN = 'user_lifecycle_run';

    /** @var array<string, ScheduledJobHandlerInterface> */
    private array $handlers;

    /**
     * @var array<string, array{
     *     handler: string,
     *     label: string,
     *     description: string,
     *     default_enabled: int,
     *     default_timezone: string,
     *     default_schedule_type: string,
     *     default_schedule_interval: int,
     *     default_schedule_time: ?string,
     *     default_schedule_weekdays_csv: ?string,
     *     default_catchup_once: int,
     *     allowed_schedule_types: list<string>
     * }>
     */
    private array $moduleJobs = [];

    public function __construct(
        UserLifecycleService $userLifecycleService,
        private readonly AppContainer $container
    ) {
        $this->handlers = [
            self::USER_LIFECYCLE_RUN => new UserLifecycleJobHandler($userLifecycleService),
        ];

        $this->loadModuleJobs();
    }

    /**
     * Returns all job definitions keyed by job_key.
     * Consumed by ScheduledJobService->ensureSystemJobs() to sync registry with the database.
     */
    public function definitions(): array
    {
        $result = [];
        foreach ($this->handlers as $jobKey => $handler) {
            $result[$jobKey] = $handler->definition();
        }
        foreach ($this->moduleJobs as $jobKey => $definition) {
            $result[$jobKey] = $this->stripHandlerFromDefinition($definition);
        }
        return $result;
    }

    /**
     * Returns the definition for a single job key, or null if not registered.
     */
    public function get(string $jobKey): ?array
    {
        $jobKey = trim($jobKey);
        if ($jobKey === '') {
            return null;
        }
        if (isset($this->handlers[$jobKey])) {
            return $this->handlers[$jobKey]->definition();
        }
        if (isset($this->moduleJobs[$jobKey])) {
            return $this->stripHandlerFromDefinition($this->moduleJobs[$jobKey]);
        }
        return null;
    }

    /**
     * Dispatches execution to the registered handler for the given job key and returns
     * a normalized result envelope. Unknown job keys return status 'failed' with
     * error_code 'job_not_supported' rather than throwing an exception.
     *
     * @param string   $jobKey      Registered job identifier (see class constants).
     * @param int|null $actorUserId Null for scheduler-triggered runs; user ID for manual runs.
     * @return array{status:string,error_code:?string,error_message:?string,result:array}
     */
    public function execute(string $jobKey, ?int $actorUserId): array
    {
        if (isset($this->handlers[$jobKey])) {
            return $this->handlers[$jobKey]->execute($actorUserId);
        }

        $moduleDefinition = $this->moduleJobs[$jobKey] ?? null;
        if (!is_array($moduleDefinition)) {
            return [
                'status'        => 'failed',
                'error_code'    => 'job_not_supported',
                'error_message' => null,
                'result'        => [],
            ];
        }

        $handlerClass = trim((string) $moduleDefinition['handler']);
        if ($handlerClass === '') {
            return [
                'status' => 'failed',
                'error_code' => 'job_handler_missing',
                'error_message' => 'Scheduler job handler is not configured.',
                'result' => [],
            ];
        }

        try {
            $handler = $this->resolveHandler($handlerClass);
            return $handler->execute($actorUserId);
        } catch (\Throwable $exception) {
            return [
                'status' => 'failed',
                'error_code' => 'job_handler_resolution_failed',
                'error_message' => $exception->getMessage(),
                'result' => [],
            ];
        }
    }

    private function loadModuleJobs(): void
    {
        $registry = $this->resolveModuleRegistry();
        if (!$registry instanceof ModuleRegistry) {
            return;
        }

        foreach ($registry->getSchedulerJobs() as $jobDefinition) {
            $jobKey = trim((string) $jobDefinition['job_key']);
            if ($jobKey === '') {
                continue;
            }

            if (isset($this->handlers[$jobKey])) {
                throw new RuntimeException(
                    "Module scheduler job '{$jobKey}' collides with core scheduler job key."
                );
            }
            if (isset($this->moduleJobs[$jobKey])) {
                throw new RuntimeException(
                    "Duplicate module scheduler job key '{$jobKey}'."
                );
            }

            $this->moduleJobs[$jobKey] = [
                'handler' => trim((string) $jobDefinition['handler']),
                'label' => trim((string) $jobDefinition['label']),
                'description' => trim((string) $jobDefinition['description']),
                'default_enabled' => (int) $jobDefinition['default_enabled'],
                'default_timezone' => trim((string) $jobDefinition['default_timezone']),
                'default_schedule_type' => trim((string) $jobDefinition['default_schedule_type']),
                'default_schedule_interval' => (int) $jobDefinition['default_schedule_interval'],
                'default_schedule_time' => $jobDefinition['default_schedule_time'],
                'default_schedule_weekdays_csv' => $jobDefinition['default_schedule_weekdays_csv'],
                'default_catchup_once' => (int) $jobDefinition['default_catchup_once'],
                'allowed_schedule_types' => $jobDefinition['allowed_schedule_types'],
            ];
        }
    }

    /**
     * @param array{
     *     handler: string,
     *     label: string,
     *     description: string,
     *     default_enabled: int,
     *     default_timezone: string,
     *     default_schedule_type: string,
     *     default_schedule_interval: int,
     *     default_schedule_time: ?string,
     *     default_schedule_weekdays_csv: ?string,
     *     default_catchup_once: int,
     *     allowed_schedule_types: list<string>
     * } $definition
     * @return array{
     *     label: string,
     *     description: string,
     *     default_enabled: int,
     *     default_timezone: string,
     *     default_schedule_type: string,
     *     default_schedule_interval: int,
     *     default_schedule_time: ?string,
     *     default_schedule_weekdays_csv: ?string,
     *     default_catchup_once: int,
     *     allowed_schedule_types: list<string>
     * }
     */
    private function stripHandlerFromDefinition(array $definition): array
    {
        unset($definition['handler']);
        return $definition;
    }

    private function resolveModuleRegistry(): ?ModuleRegistry
    {
        try {
            $registry = $this->container->get(ModuleRegistry::class);
        } catch (\Throwable) {
            return null;
        }
        return $registry instanceof ModuleRegistry ? $registry : null;
    }

    private function resolveHandler(string $handlerClass): ScheduledJobHandlerInterface
    {
        $handler = null;
        if ($this->container->has($handlerClass)) {
            $handler = $this->container->get($handlerClass);
        } elseif (class_exists($handlerClass)) {
            $handler = new $handlerClass();
        }

        if (!$handler instanceof ScheduledJobHandlerInterface) {
            throw new RuntimeException(
                "Scheduler job handler '{$handlerClass}' must implement " . ScheduledJobHandlerInterface::class . '.'
            );
        }

        return $handler;
    }
}
