<?php

declare(strict_types=1);

namespace Instinctor\Core\Queue;


if (!defined('ABSPATH')) exit;
use Instinctor\Core\DB;

/**
 * Database-backed queue driver.
 *
 * Shared by both WordPress and Standalone environments — both use the
 * instinctor_jobs table. The only difference is how the runner is triggered:
 *
 *   WordPress:  WordPressDriver extends this and adds WP-cron scheduling.
 *   Standalone: StandaloneDriver extends this; runner called by real system cron.
 *
 * Retry policy:
 *   - Max 3 attempts.
 *   - Backoff: 60s × (attempts²) — so 60s, 240s, 540s between retries.
 *   - After attempt 3, status → 'failed'.
 *
 * Duplicate-pickup prevention:
 *   - reserved_at is set when a job is claimed.
 *   - Jobs with reserved_at older than 5 minutes are considered stale and re-queued.
 */
class DatabaseDriver implements DriverInterface
{
    protected const TABLE       = 'instinctor_jobs';
    protected const MAX_ATTEMPTS = 3;
    protected const RESERVE_TTL  = 300;  // seconds before a reserved job is considered stale

    // -------------------------------------------------------------------------
    // Dispatch
    // -------------------------------------------------------------------------

    public function dispatch(string $type, array $payload, int $delay = 0): int
    {
        $now          = gmdate('Y-m-d H:i:s');
        $availableAt  = $delay > 0
            ? gmdate('Y-m-d H:i:s', time() + $delay)
            : $now;

        DB::insert(DB::table(self::TABLE), [
            'type'         => $type,
            'payload'      => json_encode($payload),
            'status'       => 'pending',
            'attempts'     => 0,
            'run_at'       => $now,
            'available_at' => $availableAt,
            'created_at'   => $now,
        ]);

        return DB::insert_id();
    }

    // -------------------------------------------------------------------------
    // Runner
    // -------------------------------------------------------------------------

    public function run(int $limit = 50): int
    {
        $processed = 0;
        $staleAt   = gmdate('Y-m-d H:i:s', time() - self::RESERVE_TTL);

        // Release stale reserved jobs back to pending
        DB::query(DB::prepare(
            "UPDATE " . DB::table(self::TABLE) . "
             SET status = 'pending', reserved_at = NULL
             WHERE status = 'running'
               AND reserved_at < %s
               AND attempts < %d",
            $staleAt,
            self::MAX_ATTEMPTS
        ));

        $now = gmdate('Y-m-d H:i:s');

        // Claim a batch of pending jobs
        $jobs = DB::get_results(DB::prepare(
            "SELECT * FROM " . DB::table(self::TABLE) . "
             WHERE status = 'pending'
               AND available_at <= %s
             ORDER BY id ASC
             LIMIT %d",
            $now,
            $limit
        ));

        foreach ($jobs as $job) {
            // Mark as running (claim it)
            $claimed = DB::query(DB::prepare(
                "UPDATE " . DB::table(self::TABLE) . "
                 SET status = 'running', reserved_at = %s, attempts = attempts + 1
                 WHERE id = %d AND status = 'pending'",
                $now,
                $job->id
            ));

            if (!$claimed) {
                continue; // Another runner claimed it first
            }

            $success = false;
            $error   = null;

            try {
                $payload = json_decode($job->payload, true) ?? [];
                $success = $this->handle($job->type, $payload);
            } catch (\Throwable $e) {
                $error = $e->getMessage();
            }

            $attempts = (int) $job->attempts + 1;

            if ($success) {
                DB::query(DB::prepare(
                    "UPDATE " . DB::table(self::TABLE) . "
                     SET status = 'completed', reserved_at = NULL, error = NULL
                     WHERE id = %d",
                    $job->id
                ));
            } elseif ($attempts >= self::MAX_ATTEMPTS) {
                DB::query(DB::prepare(
                    "UPDATE " . DB::table(self::TABLE) . "
                     SET status = 'failed', reserved_at = NULL, error = %s
                     WHERE id = %d",
                    $error ?? 'Job handler returned false',
                    $job->id
                ));
            } else {
                // Schedule retry with exponential backoff
                $backoff     = 60 * ($attempts ** 2);
                $availableAt = gmdate('Y-m-d H:i:s', time() + $backoff);

                DB::query(DB::prepare(
                    "UPDATE " . DB::table(self::TABLE) . "
                     SET status = 'pending', reserved_at = NULL,
                         available_at = %s, error = %s
                     WHERE id = %d",
                    $availableAt,
                    $error ?? 'Job handler returned false',
                    $job->id
                ));
            }

            $processed++;
        }

        return $processed;
    }

    // -------------------------------------------------------------------------
    // Job Handler Registry
    // -------------------------------------------------------------------------

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

    /**
     * Register a handler for a job type.
     * Called once at boot — not per-request overhead.
     *
     * Queue::register('email', fn($payload) => Mailer::send(...));
     */
    public static function register(string $type, callable $handler): void
    {
        self::$handlers[$type] = $handler;
    }

    /**
     * Execute the registered handler for a job type.
     * Returns true on success, false on failure.
     */
    protected function handle(string $type, array $payload): bool
    {
        if (isset(self::$handlers[$type])) {
            return (bool) call_user_func(self::$handlers[$type], $payload);
        }

        // Built-in handlers
        return match ($type) {
            'email'         => $this->handleEmail($payload),
            'sms'           => $this->handleSms($payload),
            'webhook'       => $this->handleWebhook($payload),
            default         => throw new \RuntimeException('Unknown job type: ' . esc_html($type)),
        };
    }

    // -------------------------------------------------------------------------
    // Built-in job handlers
    // -------------------------------------------------------------------------

    private function handleEmail(array $payload): bool
    {
        $to      = (string)($payload['to']      ?? '');
        $subject = (string)($payload['subject'] ?? '');
        $body    = (string)($payload['body']    ?? '');
        $options = is_array($payload['options'] ?? []) ? ($payload['options'] ?? []) : [];

        if (!$to || !$subject || !$body) {
            return false;
        }

        return \Instinctor\Core\Mailer::send($to, $subject, $body, $options);
    }

    private function handleSms(array $payload): bool
    {
        $to      = (string)($payload['to']      ?? '');
        $message = (string)($payload['message'] ?? '');
        $channel = (string)($payload['channel'] ?? 'sms');

        if (!$to || !$message) {
            return false;
        }

        // Delegate to relay (same as ChatService SMS sending)
        $relayUrl   = 'https://platform.instinctor.com/api/relay/sms';
        $licenseKey = \Instinctor\Core\Config::get('instinctor_license_key', '');

        $response = $this->httpPost($relayUrl, [
            'license_key' => $licenseKey,
            'to'          => $to,
            'message'     => $message,
            'channel'     => $channel,
        ]);

        return !empty($response['success']);
    }

    private function handleWebhook(array $payload): bool
    {
        $url     = (string)($payload['url']     ?? '');
        $data    = is_array($payload['data'] ?? []) ? ($payload['data'] ?? []) : [];
        $method  = strtoupper((string)($payload['method'] ?? 'POST'));
        $headers = is_array($payload['headers'] ?? []) ? ($payload['headers'] ?? []) : [];

        if (!$url) {
            return false;
        }

        $hdrs = ['Content-Type' => 'application/json'];
        foreach ($headers as $h) {
            if (strpos($h, ':') !== false) {
                [$k, $v] = explode(':', $h, 2);
                $hdrs[trim($k)] = trim($v);
            }
        }
        $response = wp_remote_request($url, [
            'method'  => $method,
            'timeout' => 10,
            'headers' => $hdrs,
            'body'    => json_encode($data),
        ]);
        if (is_wp_error($response)) return false;
        $code = (int) wp_remote_retrieve_response_code($response);
        return $code >= 200 && $code < 300;
    }

    // -------------------------------------------------------------------------
    // Stats & Maintenance
    // -------------------------------------------------------------------------

    public function stats(): array
    {
        $rows = DB::get_results(
            "SELECT status, COUNT(*) AS cnt
             FROM " . DB::table(self::TABLE) . "
             GROUP BY status"
        );

        $stats = ['pending' => 0, 'running' => 0, 'completed' => 0, 'failed' => 0];
        foreach ($rows as $row) {
            if (isset($stats[$row->status])) {
                $stats[$row->status] = (int) $row->cnt;
            }
        }

        return $stats;
    }

    public function prune(int $days = 7): int
    {
        $before = gmdate('Y-m-d H:i:s', time() - ($days * 86400));

        return (int) DB::query(DB::prepare(
            "DELETE FROM " . DB::table(self::TABLE) . "
             WHERE status IN ('completed', 'failed')
               AND created_at < %s",
            $before
        ));
    }

    // -------------------------------------------------------------------------
    // HTTP helper (standalone-safe, no wp_remote_post)
    // -------------------------------------------------------------------------

    protected function httpPost(string $url, array $data): array
    {
        // WP.org guideline 7/9 — SMS/queue relay is gated on opt-in.
        if (!\Instinctor\Core\PlatformConsent::granted()) {
            return [];
        }
        $response = wp_remote_post($url, ['body' => $data, 'timeout' => 10]);
        if (is_wp_error($response)) {
            return [];
        }
        return json_decode(wp_remote_retrieve_body($response), true) ?? [];
    }
}
