<?php

namespace ZenoChatbot\Services\Scheduler;

use ZenoChatbot\Support\Hooks;

if (!defined('ABSPATH')) {
    exit;
}

class Scheduler
{
    public const OPTION_REGISTRY = 'zeno_chatbot_ai_scheduler_registry';
    public const OPTION_MIGRATION = 'zeno_chatbot_ai_scheduler_migrated_v1';
    public const CLEANUP_JOB = 'zeno_chatbot_ai/scheduler/cleanup';
    public const DEFAULT_GROUP = 'zeno-chatbot-ai';
    public const HOOK_PREFIX = 'zeno_chatbot_ai/scheduler/job/';

    /** @var bool */
    private static $booted = false;

    /** @var array<string,bool> */
    private static $registeredHooks = [];

    /** @var bool|null */
    private static $actionSchedulerTablesReady = null;

    public static function schedule($job, $action): PendingSchedule
    {
        static::boot();
        return PendingSchedule::create((string) $job, $action);
    }

    public static function sync($job, $action): PendingSchedule
    {
        return static::schedule($job, $action)->sync();
    }

    public static function boot(): void
    {
        if (static::$booted) {
            return;
        }

        static::loadActionScheduler();
        static::registerPersistedJobs();
        static::runLegacyCronMigrationOnce();

        // Mark as booted before cleanup registration because cleanup scheduling
        // re-enters the facade (`sync()` -> `schedule()` -> `boot()`).
        static::$booted = true;

        if (did_action('action_scheduler_init') > 0) {
            if (static::canUseActionScheduler()) {
                static::registerCleanupTask();
            }
        } else {
            add_action('action_scheduler_init', function() {
                if (static::canUseActionScheduler()) {
                    static::registerCleanupTask();
                }
            }, 20, 0);
        }

    }

    public static function defaultGroup(): string
    {
        return static::DEFAULT_GROUP;
    }

    public static function supportsEnsureRecurringActionsHook(): bool
    {
        if (!function_exists('as_supports')) {
            return false;
        }

        try {
            return (bool) as_supports('ensure_recurring_actions_hook');
        } catch (\Throwable $e) {
            return false;
        }
    }

    public static function ensureCleanupTaskScheduled(): void
    {
        static::boot();

        if (did_action('action_scheduler_init') <= 0 || !static::canUseActionScheduler()) {
            return;
        }

        static::registerCleanupTask();
    }

    public static function rememberAction($job, $action): bool
    {
        $job = trim((string) $job);
        $spec = static::normalizeAction($action);
        if ($job === '' || $spec === '') {
            return false;
        }

        $registry = static::getRegistry();
        if (($registry[$job] ?? '') !== $spec) {
            $registry[$job] = $spec;
            update_option(static::OPTION_REGISTRY, $registry, false);
        }

        static::registerJobHook($job);
        return true;
    }

    public static function normalizeAction($action): string
    {
        if (is_array($action) && count($action) === 2) {
            $class = $action[0] ?? '';
            $method = $action[1] ?? '';

            if (is_object($class)) {
                return '';
            }

            if (is_string($class) && is_string($method) && $class !== '' && $method !== '') {
                return $class . '::' . $method;
            }

            return '';
        }

        if (is_string($action)) {
            $action = trim($action);
            return $action !== '' ? $action : '';
        }

        return '';
    }

    public static function resolveAction($job)
    {
        $job = (string) $job;
        $registry = static::getRegistry();
        $spec = (string) ($registry[$job] ?? '');

        if ($spec === '') {
            return null;
        }

        if (strpos($spec, '::') !== false) {
            [$class, $method] = explode('::', $spec, 2);
            if ($class === '' || $method === '') {
                return null;
            }

            $callable = [$class, $method];
            return is_callable($callable) ? $callable : null;
        }

        return is_callable($spec) ? $spec : null;
    }

    public static function dispatchJob($job, $payload = []): void
    {
        $callable = static::resolveAction($job);
        if (!is_callable($callable)) {
            Hooks::doAction(Hooks::SCHEDULER_JOB_FAILED, [
                'job' => (string) $job,
                'reason' => 'missing_callable',
                'payload' => is_array($payload) ? $payload : [],
                'timestamp' => gmdate('c'),
            ]);
            return;
        }

        try {
            call_user_func($callable, is_array($payload) ? $payload : []);
            Hooks::doAction(Hooks::SCHEDULER_JOB_PROCESSED, [
                'job' => (string) $job,
                'timestamp' => gmdate('c'),
            ]);
        } catch (\Throwable $e) {
            Hooks::doAction(Hooks::SCHEDULER_JOB_FAILED, [
                'job' => (string) $job,
                'reason' => 'exception',
                'error' => $e->getMessage(),
                'payload' => is_array($payload) ? $payload : [],
                'timestamp' => gmdate('c'),
            ]);
        }
    }

    public static function jobHook($job): string
    {
        $job = strtolower(trim((string) $job));
        $job = preg_replace('/[^a-z0-9_\/-]/', '_', $job) ?? '';
        $job = trim($job, '/');

        if ($job === '') {
            $job = 'unknown';
        }

        return static::HOOK_PREFIX . $job;
    }

    public static function jobFromHook($hook): string
    {
        $hook = (string) $hook;
        if (strpos($hook, static::HOOK_PREFIX) === 0) {
            return substr($hook, strlen(static::HOOK_PREFIX));
        }

        return $hook;
    }

    public static function hasScheduled($job, $args = [], $group = ''): bool
    {
        static::boot();

        $hook = static::jobHook($job);
        $group = (string) $group;

        if (static::canUseActionScheduler() && function_exists('as_has_scheduled_action')) {
            return (bool) as_has_scheduled_action($hook, is_array($args) ? $args : [], $group);
        }

        $next = wp_next_scheduled($hook, is_array($args) ? $args : []);
        return is_int($next) || is_numeric($next);
    }

    public static function enqueueAsync($job, $args = [], $group = '', $unique = false, $priority = 10): int
    {
        static::boot();

        $hook = static::jobHook($job);
        $args = is_array($args) ? $args : [];
        $group = (string) $group;

        if (static::canUseActionScheduler() && function_exists('as_enqueue_async_action')) {
            return (int) as_enqueue_async_action($hook, $args, $group, (bool) $unique, (int) $priority);
        }

        return wp_schedule_single_event(time() + 1, $hook, $args) ? 1 : 0;
    }

    public static function scheduleSingle($job, $timestamp, $args = [], $group = '', $unique = false, $priority = 10): int
    {
        static::boot();

        $hook = static::jobHook($job);
        $args = is_array($args) ? $args : [];
        $group = (string) $group;
        $timestamp = max(time(), (int) $timestamp);

        if (static::canUseActionScheduler() && function_exists('as_schedule_single_action')) {
            return (int) as_schedule_single_action($timestamp, $hook, $args, $group, (bool) $unique, (int) $priority);
        }

        return wp_schedule_single_event($timestamp, $hook, $args) ? 1 : 0;
    }

    public static function scheduleRecurring($job, $timestamp, $intervalSeconds, $args = [], $group = '', $unique = false, $priority = 10): int
    {
        static::boot();

        $hook = static::jobHook($job);
        $args = is_array($args) ? $args : [];
        $group = (string) $group;
        $timestamp = max(time(), (int) $timestamp);
        $intervalSeconds = max(1, (int) $intervalSeconds);

        if (static::canUseActionScheduler() && function_exists('as_schedule_recurring_action')) {
            return (int) as_schedule_recurring_action($timestamp, $intervalSeconds, $hook, $args, $group, (bool) $unique, (int) $priority);
        }

        return 0;
    }

    public static function scheduleCron($job, $timestamp, $pattern, $args = [], $group = '', $unique = false, $priority = 10): int
    {
        static::boot();

        $hook = static::jobHook($job);
        $args = is_array($args) ? $args : [];
        $group = (string) $group;
        $timestamp = max(time(), (int) $timestamp);
        $pattern = trim((string) $pattern);

        if ($pattern === '') {
            return 0;
        }

        if (static::canUseActionScheduler() && function_exists('as_schedule_cron_action')) {
            return (int) as_schedule_cron_action($timestamp, $pattern, $hook, $args, $group, (bool) $unique, (int) $priority);
        }

        return 0;
    }

    public static function cancel($job, $args = null, $group = ''): int
    {
        static::boot();

        $hook = static::jobHook($job);
        $group = (string) $group;

        if (!static::canUseActionScheduler() || !function_exists('as_get_scheduled_actions') || !class_exists('ActionScheduler')) {
            return 0;
        }

        $query = [
            'hook' => $hook,
            'status' => [\ActionScheduler_Store::STATUS_PENDING, \ActionScheduler_Store::STATUS_RUNNING],
            'group' => $group,
            'orderby' => 'date',
            'order' => 'ASC',
            'per_page' => -1,
        ];

        if (is_array($args)) {
            $query['args'] = $args;
        }

        $ids = as_get_scheduled_actions($query, 'ids');
        if (!is_array($ids)) {
            return 0;
        }

        $store = \ActionScheduler::store();
        $cancelled = 0;

        foreach ($ids as $id) {
            try {
                $store->cancel_action((int) $id);
                $cancelled++;
            } catch (\Throwable $e) {
                // ignore
            }
        }

        return $cancelled;
    }

    public static function list($filters = []): array
    {
        static::boot();

        if (!static::canUseActionScheduler() || !function_exists('as_get_scheduled_actions') || !class_exists('ActionScheduler')) {
            return [
                'items' => [],
                'page' => 1,
                'per_page' => 0,
                'count' => 0,
            ];
        }

        $filters = is_array($filters) ? $filters : [];
        $page = max(1, (int) ($filters['page'] ?? 1));
        $perPage = max(1, min(200, (int) ($filters['per_page'] ?? 20)));
        $offset = ($page - 1) * $perPage;

        $query = [
            'per_page' => $perPage,
            'offset' => $offset,
            'orderby' => sanitize_key((string) ($filters['orderby'] ?? 'date')),
            'order' => strtoupper((string) ($filters['order'] ?? 'DESC')) === 'ASC' ? 'ASC' : 'DESC',
        ];

        if (!empty($filters['status'])) {
            $query['status'] = $filters['status'];
        }

        if (!empty($filters['group'])) {
            $query['group'] = sanitize_text_field((string) $filters['group']);
        }

        if (!empty($filters['job'])) {
            $query['hook'] = static::jobHook((string) $filters['job']);
        }

        if (!empty($filters['hook'])) {
            $query['hook'] = sanitize_text_field((string) $filters['hook']);
        }

        $ids = as_get_scheduled_actions($query, 'ids');
        if (!is_array($ids)) {
            $ids = [];
        }

        $store = \ActionScheduler::store();
        $items = [];

        foreach ($ids as $id) {
            try {
                $action = $store->fetch_action((int) $id);
                $status = (string) $store->get_status((int) $id);
                $schedule = $action->get_schedule();
                $date = $schedule ? $schedule->get_date() : null;

                $rawArgs = $action->get_args();
                $payload = $rawArgs;
                if (is_array($rawArgs) && count($rawArgs) === 1 && isset($rawArgs[0]) && is_array($rawArgs[0])) {
                    $payload = $rawArgs[0];
                }

                $recurrence = '';
                if (is_object($schedule) && method_exists($schedule, 'get_recurrence')) {
                    $recurrence = (string) $schedule->get_recurrence();
                }

                $items[] = [
                    'id' => (int) $id,
                    'job' => static::jobFromHook((string) $action->get_hook()),
                    'hook' => (string) $action->get_hook(),
                    'group' => (string) $action->get_group(),
                    'status' => $status,
                    'scheduled_at_gmt' => $date instanceof \DateTimeInterface ? gmdate('Y-m-d H:i:s', $date->getTimestamp()) : null,
                    'scheduled_at_local' => $date instanceof \DateTimeInterface ? wp_date('Y-m-d H:i:s', $date->getTimestamp()) : null,
                    'args' => is_array($payload) ? $payload : [],
                    'is_recurring' => is_object($schedule) && method_exists($schedule, 'is_recurring') ? (bool) $schedule->is_recurring() : false,
                    'interval' => $recurrence,
                    'attempts' => null,
                    'last_error' => null,
                ];
            } catch (\Throwable $e) {
                // ignore malformed row
            }
        }

        return [
            'items' => $items,
            'page' => $page,
            'per_page' => $perPage,
            'count' => count($items),
        ];
    }

    public static function cleanup($policy = []): array
    {
        static::boot();

        if (!static::canUseActionScheduler() || !function_exists('as_get_scheduled_actions') || !class_exists('ActionScheduler')) {
            return [
                'deleted' => 0,
                'retention_days' => 0,
            ];
        }

        $policy = is_array($policy) ? $policy : [];
        $retentionDays = max(1, (int) ($policy['retention_days'] ?? 14));
        $before = gmdate('Y-m-d H:i:s', time() - ($retentionDays * DAY_IN_SECONDS));

        $statuses = [
            \ActionScheduler_Store::STATUS_COMPLETE,
            \ActionScheduler_Store::STATUS_FAILED,
            \ActionScheduler_Store::STATUS_CANCELED,
        ];

        $store = \ActionScheduler::store();
        $deleted = 0;

        foreach ($statuses as $status) {
            $ids = as_get_scheduled_actions([
                'status' => $status,
                'modified' => $before,
                'modified_compare' => '<=',
                'orderby' => 'date',
                'order' => 'ASC',
                'per_page' => 200,
            ], 'ids');

            if (!is_array($ids)) {
                continue;
            }

            foreach ($ids as $id) {
                try {
                    $store->delete_action((int) $id);
                    $deleted++;
                } catch (\Throwable $e) {
                    // ignore cleanup failures
                }
            }
        }

        return [
            'deleted' => $deleted,
            'retention_days' => $retentionDays,
        ];
    }

    public static function clearLegacyWpCronHook($hook): int
    {
        $hook = (string) $hook;
        if ($hook === '' || !function_exists('_get_cron_array')) {
            return 0;
        }

        $crons = _get_cron_array();
        if (!is_array($crons)) {
            return 0;
        }

        $removed = 0;

        foreach ($crons as $timestamp => $cronHooks) {
            if (!is_array($cronHooks) || !isset($cronHooks[$hook]) || !is_array($cronHooks[$hook])) {
                continue;
            }

            foreach ($cronHooks[$hook] as $eventData) {
                $args = is_array($eventData['args'] ?? null) ? $eventData['args'] : [];
                if (wp_unschedule_event((int) $timestamp, $hook, $args)) {
                    $removed++;
                }
            }
        }

        return $removed;
    }

    public static function toSeconds($value): int
    {
        if (is_int($value) || is_float($value) || (is_string($value) && ctype_digit(trim($value)))) {
            return max(0, (int) $value);
        }

        if (!is_string($value)) {
            return 0;
        }

        $value = trim($value);
        if ($value === '') {
            return 0;
        }

        if (preg_match('/^P/i', $value) === 1) {
            try {
                $interval = new \DateInterval($value);
                if ((int) $interval->y > 0 || (int) $interval->m > 0) {
                    return 0;
                }

                $days = (int) $interval->d;
                if ($interval->days !== false) {
                    $days = (int) $interval->days;
                }

                $seconds = ($days * DAY_IN_SECONDS)
                    + ((int) $interval->h * HOUR_IN_SECONDS)
                    + ((int) $interval->i * MINUTE_IN_SECONDS)
                    + (int) $interval->s;

                return max(0, $seconds);
            } catch (\Throwable $e) {
                return 0;
            }
        }

        $future = strtotime('+' . $value);
        if (is_int($future) && $future > time()) {
            return $future - time();
        }

        return 0;
    }

    private static function loadActionScheduler(): void
    {
        if (function_exists('as_schedule_single_action')) {
            return;
        }

        $bootstrap = __DIR__ . '/scoped-vendor/action-scheduler/action-scheduler.php';
        if (!file_exists($bootstrap)) {
            return;
        }

        // We bundle a fresh Action Scheduler — no legacy wp_post data to migrate.
        // Mark migration complete so AS uses ActionScheduler_DBStore directly,
        // skipping the HybridStore/migration controller entirely.
        if (false === get_option('action_scheduler_migration_status')) {
            update_option('action_scheduler_migration_status', 'complete');
        }

        // Create AS tables BEFORE requiring the bootstrap. During activation,
        // plugins_loaded and init have already fired, so AS initializes
        // immediately inside require_once and queries tables right away.
        // Tables must exist before that happens.
        // Buffer output: dbDelta/schema setup can echo during activation
        // sandbox scrape, triggering WordPress "unexpected output" warnings.
        ob_start();
        try {
            static::ensureActionSchedulerTables();
            require_once $bootstrap;
        } finally {
            ob_end_clean();
        }
    }

    private static function ensureActionSchedulerTables(): void
    {
        $base = __DIR__ . '/scoped-vendor/action-scheduler/classes/';

        $files = [
            'abstracts/ActionScheduler_Abstract_Schema.php',
            'schema/ActionScheduler_StoreSchema.php',
            'schema/ActionScheduler_LoggerSchema.php',
        ];

        foreach ($files as $file) {
            $path = $base . $file;
            if (!file_exists($path)) {
                return;
            }
            require_once $path;
        }

        $storeSchema = new \ActionScheduler_StoreSchema();
        $loggerSchema = new \ActionScheduler_LoggerSchema();

        if (!$storeSchema->tables_exist()) {
            $storeSchema->register_tables(true);
            $loggerSchema->register_tables(true);
        }
    }

    private static function canUseActionScheduler(): bool
    {
        if (!class_exists('ActionScheduler') || !function_exists('as_schedule_single_action')) {
            return false;
        }

        if (static::$actionSchedulerTablesReady === true) {
            return static::$actionSchedulerTablesReady;
        }

        if (static::$actionSchedulerTablesReady === false && did_action('action_scheduler_init') > 0) {
            return false;
        }

        $tablesReady = false;

        if (class_exists('ActionScheduler_StoreSchema')) {
            try {
                $schema = new \ActionScheduler_StoreSchema();
                if (method_exists($schema, 'tables_exist')) {
                    $tablesReady = (bool) $schema->tables_exist();
                    if ($tablesReady || did_action('action_scheduler_init') > 0) {
                        static::$actionSchedulerTablesReady = $tablesReady;
                    }

                    return $tablesReady;
                }
            } catch (\Throwable $e) {
                // Fallback to direct table checks below.
            }
        }

        global $wpdb;
        if (!isset($wpdb) || !is_object($wpdb)) {
            if (did_action('action_scheduler_init') > 0) {
                static::$actionSchedulerTablesReady = false;
            }
            return false;
        }

        $actionTable = static::actionSchedulerTableName($wpdb, 'actionscheduler_actions');
        $groupTable = static::actionSchedulerTableName($wpdb, 'actionscheduler_groups');

        $tablesReady = static::tableExists($actionTable) && static::tableExists($groupTable);

        if ($tablesReady || did_action('action_scheduler_init') > 0) {
            static::$actionSchedulerTablesReady = $tablesReady;
        }

        return $tablesReady;
    }

    private static function actionSchedulerTableName($wpdb, $property): string
    {
        $property = (string) $property;
        $value = isset($wpdb->$property) ? (string) $wpdb->$property : '';
        if ($value !== '') {
            return $value;
        }

        $prefix = isset($wpdb->prefix) ? (string) $wpdb->prefix : '';
        return $prefix . $property;
    }

    private static function tableExists($table): bool
    {
        $table = (string) $table;
        if ($table === '') {
            return false;
        }

        global $wpdb;
        if (!isset($wpdb) || !is_object($wpdb) || !method_exists($wpdb, 'prepare')) {
            return false;
        }

        $cacheKey = 'as_table_exists:' . md5(strtolower($table));
        $cacheGroup = 'zeno_chatbot_ai_scheduler';
        if (function_exists('wp_cache_get')) {
            $cacheFound = false;
            $cached = wp_cache_get($cacheKey, $cacheGroup, false, $cacheFound);
            if ($cacheFound) {
                return $cached === 'yes';
            }
        }

        $pattern = str_replace('_', '\\_', $table);
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Fallback schema probe; result is cached above/below via wp_cache_*.
        $found = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $pattern));
        $exists = is_string($found) && strtolower($found) === strtolower($table);

        if (function_exists('wp_cache_set')) {
            wp_cache_set($cacheKey, $exists ? 'yes' : 'no', $cacheGroup, 60);
        }

        return $exists;
    }

    /**
     * @return array<string,string>
     */
    private static function getRegistry(): array
    {
        $registry = get_option(static::OPTION_REGISTRY, []);
        return is_array($registry) ? $registry : [];
    }

    private static function registerPersistedJobs(): void
    {
        $registry = static::getRegistry();
        foreach ($registry as $job => $spec) {
            if (!is_string($job) || !is_string($spec)) {
                continue;
            }
            static::registerJobHook($job);
        }
    }

    private static function registerJobHook($job): void
    {
        $job = (string) $job;
        $hook = static::jobHook($job);

        if (isset(static::$registeredHooks[$hook])) {
            return;
        }

        add_action($hook, function($payload = []) use ($job) {
            static::dispatchJob($job, $payload);
        }, 10, 1);

        static::$registeredHooks[$hook] = true;
    }

    private static function registerCleanupTask(): void
    {
        // Daily cleanup of completed/failed/cancelled old actions.
        static::sync(static::CLEANUP_JOB, [static::class, 'runCleanupTask'])
            ->group(static::DEFAULT_GROUP)
            ->repeat()
            ->daily()
            ->unique()
            ->dispatch();
    }

    public static function runCleanupTask($payload = []): void
    {
        static::cleanup();
    }

    private static function runLegacyCronMigrationOnce(): void
    {
        $migrated = get_option(static::OPTION_MIGRATION, null);
        if (is_array($migrated) && isset($migrated['done']) && $migrated['done'] === 'yes') {
            return;
        }

        $hooks = [
            'zeno_chatbot_ai/process_conversation_summary',
            'zeno_chatbot_ai/conversation_maintenance',
            'zeno_chatbot_ai/sitemap_sync_async',
        ];

        $removed = 0;
        foreach ($hooks as $hook) {
            $removed += static::clearLegacyWpCronHook($hook);
        }

        update_option(static::OPTION_MIGRATION, [
            'done' => 'yes',
            'removed' => $removed,
            'migrated_at' => gmdate('c'),
        ], false);
    }
}
