<?php

namespace SEOAIC\cron;

class MissedScheduleCron
{
    public const HOOK        = 'seoaic_missed_schedule_cron';
    public const LAST_RUN_OPTION = 'seoaic_missed_scheduled_last_run';
    public const KEY_LOCK_OPTION     = 'seoaic_missed_schedule_lock';

    public const DEFAULT_FREQ  = 3600;
    public const DEFAULT_LIMIT = 20;

    protected \SEOAIC\SEOAIC $seoiac;

    private ?string $lock_token = null;

    public function __construct(\SEOAIC\SEOAIC $seoiac)
    {
        $this->seoiac = $seoiac;
        add_filter('cron_schedules', [$this, 'register_interval']);
        add_action(self::HOOK, [$this, 'run']);
    }

    public function init(): void
    {
        add_action('init', [$this, 'maybe_schedule'], 20);
    }

    public function register_interval(array $schedules): array
    {
        $freq = (int) apply_filters('seoaic_missed_schedule_frequency', self::DEFAULT_FREQ);
        $freq = $freq > 0 ? $freq : self::DEFAULT_FREQ;

        $schedules['seoaic_dynamic'] = [
            'interval' => $freq,
            'display'  => 'SEOAIC Missed Schedule',
        ];

        return $schedules;
    }

    public function maybe_schedule(): void
    {
        if (!wp_next_scheduled(self::HOOK)) {
            wp_schedule_event(time() + 60, 'seoaic_dynamic', self::HOOK);
            //error_log('SEOAIC Cron: scheduled');
        }
    }

    public function run(): void
    {
        //error_log('SEOAIC Cron: run() start');
        $lock_ttl = (int) apply_filters('seoaic_missed_schedule_lock_ttl', 180);
        $lock_ttl = $lock_ttl > 0 ? $lock_ttl : 180;

        if (!$this->acquire_lock($lock_ttl)) {
            //error_log('SEOAIC Cron: lock busy, exit');
            return;
        }
        try {
            $result = $this->do_publish_cycle();
            //error_log('SEOAIC Cron: run() end count=' . ($result['count'] ?? 0));
        } finally {
            $this->release_lock();
        }
    }

    public function run_once(int $limit_override = 0): array
    {
        $lock_ttl = (int) apply_filters('seoaic_missed_schedule_lock_ttl', 180);
        $lock_ttl = $lock_ttl > 0 ? $lock_ttl : 180;

        if (!$this->acquire_lock($lock_ttl)) {
            return [
                'status'  => 'ok',
                'message' => 'Lock busy',
                'count'   => 0,
                'ts'      => time(),
            ];
        }

        try {
            return $this->do_publish_cycle($limit_override);
        } finally {
            $this->release_lock();
        }
    }

    private function do_publish_cycle(int $limit_override = 0): array
    {
        $last_run = (int) $this->get_option_value(self::LAST_RUN_OPTION, 0);
        $freq     = (int) apply_filters('seoaic_missed_schedule_frequency', self::DEFAULT_FREQ);
        $freq     = $freq > 0 ? $freq : self::DEFAULT_FREQ;

        if ($last_run >= (time() - $freq)) {
            return [
                'status'   => 'ok',
                'message'  => 'Already ran recently',
                'last_run' => $last_run,
                'count'    => 0,
                'ts'       => time(),
            ];
        }

        $limit     = $limit_override > 0 ? $limit_override : (int) apply_filters('seoaic_missed_schedule_limit', self::DEFAULT_LIMIT);
        $limit     = $limit > 0 ? $limit : self::DEFAULT_LIMIT;
        $max_loops = (int) apply_filters('seoaic_missed_schedule_max_loops', 20);
        $max_loops = $max_loops > 0 ? $max_loops : 1;

        $all_published = [];

        for ($i = 0; $i < $max_loops; $i++) {
            $batch = $this->publish_missed_posts($limit);
            $cnt   = is_array($batch) ? count($batch) : 0;

            //error_log('SEOAIC Cron: loop=' . $i . ' published=' . $cnt);

            if ($cnt === 0) {
                break;
            }

            $all_published = array_merge($all_published, $batch);

            if ($cnt < $limit) {
                break;
            }
        }

        return [
            'status'    => 'ok',
            'published' => $all_published,
            'count'     => count($all_published),
            'ts'        => time(),
        ];
    }

    private function acquire_lock(int $ttl): bool
    {
        $token   = wp_generate_password(12, false, false);
        $expires = time() + $ttl;

        for ($attempt = 0; $attempt < 2; $attempt++) {
            $opts = $this->get_all_options_fresh();
            $lock = isset($opts[self::KEY_LOCK_OPTION]) && is_array($opts[self::KEY_LOCK_OPTION]) ? $opts[self::KEY_LOCK_OPTION] : [];
            $exp  = isset($lock['expires']) ? (int) $lock['expires'] : 0;

            if ($exp && $exp >= time()) {
                return false;
            }

            $opts[self::KEY_LOCK_OPTION] = [
                'token'   => $token,
                'expires' => $expires,
            ];

            $updated = update_option('seoaic_options', $opts);

            $verify = get_option('seoaic_options');
            if (is_array($verify)
                && isset($verify[self::KEY_LOCK_OPTION]['token'])
                && $verify[self::KEY_LOCK_OPTION]['token'] === $token
            ) {
                $this->sync_global_options($verify);
                $this->lock_token = $token;
                return true;
            }
        }

        return false;
    }

    private function release_lock(): void
    {
        $opts = $this->get_all_options_fresh();
        $cur  = isset($opts[self::KEY_LOCK_OPTION]) && is_array($opts[self::KEY_LOCK_OPTION]) ? $opts[self::KEY_LOCK_OPTION] : [];

        if (isset($cur['token']) && $cur['token'] === $this->lock_token) {
            unset($opts[self::KEY_LOCK_OPTION]);
            $updated = update_option('seoaic_options', $opts);

            $verify = get_option('seoaic_options');
            if (is_array($verify)) {
                $this->sync_global_options($verify);
            }
        }

        $this->lock_token = null;
    }

    private function publish_missed_posts(int $limit): array
    {
        global $wpdb;

        $this->set_option_value(self::LAST_RUN_OPTION, time());

        $now_gmt = gmdate('Y-m-d H:i:s');
        $limit   = max(1, (int) $limit);

        $meta_key = apply_filters('seoaic_missed_schedule_meta_key', 'seoaic_posted');

        $ids = $wpdb->get_col(
            $wpdb->prepare(
                "SELECT DISTINCT p.ID
             FROM {$wpdb->posts} AS p
             INNER JOIN {$wpdb->postmeta} AS pm
                     ON pm.post_id = p.ID
                    AND pm.meta_key = %s
                    AND LENGTH(pm.meta_value) > 0
             WHERE p.post_status = 'future'
               AND p.post_date_gmt <= %s
             ORDER BY p.post_date_gmt ASC
             LIMIT %d",
                $meta_key,
                $now_gmt,
                $limit
            )
        );

        if (empty($ids)) {
            return [];
        }

        if (count($ids) === $limit) {
            $this->set_option_value(self::LAST_RUN_OPTION, 0);
        }

        $published = [];
        foreach ($ids as $post_id) {
            $pid = (int) $post_id;

            if (get_post_status($pid) !== 'future') {
                continue;
            }

            $val = get_post_meta($pid, $meta_key, true);
            if ($val === '' || $val === null) {
                continue;
            }

            wp_publish_post($pid);

            if (get_post_status($pid) === 'publish') {
                $published[] = $pid;
            }
        }

        //error_log('SEOAIC Cron: batch_ids=' . implode(',', array_map('intval', $ids)));

        return $published;
    }

    private function get_all_options_fresh(): array
    {
        $opts = get_option('seoaic_options');
        return is_array($opts) ? $opts : [];
    }

    private function sync_global_options(array $opts): void
    {
        global $SEOAIC_OPTIONS;
        $SEOAIC_OPTIONS = $opts;
    }

    private function get_option_value(string $key, $default = null)
    {
        $opts = $this->get_all_options_fresh();
        return array_key_exists($key, $opts) ? $opts[$key] : $default;
    }

    private function set_option_value(string $key, $value): void
    {
        $opts = $this->get_all_options_fresh();
        $opts[$key] = $value;

        update_option('seoaic_options', $opts);

        $this->sync_global_options($opts);
    }
}
