<?php

namespace PloverCRM\Modules\ContactEnrichment\Services;

use PloverCRM\Core\Container;
use PloverCRM\Modules\ContactEnrichment\Database\PostCreateJobSchema;

/**
 * Manages background post-create jobs for enrichment, notifications, and automations.
 */
class PostCreateJobManager
{
    private const STATUS_PENDING = 'pending';
    private const STATUS_QUEUED = 'queued';
    private const STATUS_RUNNING = 'running';
    private const STATUS_COMPLETED = 'completed';
    private const MAX_INLINE_RERUNS = 3;

    private Container $container;

    public function __construct(Container $container)
    {
        $this->container = $container;
    }

    public function schedule(array $payload): void
    {
        $payload = $this->normalizePayload($payload);
        if (!$payload) {
            return;
        }

        $jobKey = self::buildJobKey($payload['provider_id'], (int) $payload['subscriber_id']);
        $job = $this->getByJobKey($jobKey);
        $now = current_time('mysql');
        $encodedPayload = wp_json_encode($payload);

        if ($encodedPayload === false) {
            return;
        }

        $shouldEnqueue = false;

        if (!$job) {
            $jobId = wp_generate_uuid4();
            $dispatchToken = wp_generate_password(64, false, false);

            $this->insert([
                'job_id' => $jobId,
                'job_key' => $jobKey,
                'status' => self::STATUS_PENDING,
                'dispatch_token' => $dispatchToken,
                'payload_json' => $encodedPayload,
                'rerun_requested' => 0,
                'attempt_count' => 0,
                'last_error' => null,
                'queued_at' => null,
                'started_at' => null,
                'completed_at' => null,
                'created_at' => $now,
                'updated_at' => $now,
            ]);

            $job = $this->getByJobKey($jobKey);
            $shouldEnqueue = true;
        } elseif (($job['status'] ?? '') === self::STATUS_RUNNING) {
            $this->updateById((int) $job['id'], [
                'payload_json' => $encodedPayload,
                'rerun_requested' => 1,
                'updated_at' => $now,
            ]);
            return;
        } elseif (($job['status'] ?? '') === self::STATUS_QUEUED) {
            $this->updateById((int) $job['id'], [
                'payload_json' => $encodedPayload,
                'updated_at' => $now,
                'last_error' => null,
            ]);
            return;
        } else {
            $dispatchToken = wp_generate_password(64, false, false);

            $this->updateById((int) $job['id'], [
                'status' => self::STATUS_PENDING,
                'dispatch_token' => $dispatchToken,
                'payload_json' => $encodedPayload,
                'rerun_requested' => 0,
                'last_error' => null,
                'queued_at' => null,
                'started_at' => null,
                'completed_at' => null,
                'updated_at' => $now,
            ]);

            $job = $this->getById((int) $job['id']);
            $shouldEnqueue = true;
        }

        if (!$shouldEnqueue || !$job) {
            return;
        }

        $result = PostCreateJobRelay::enqueue((string) $job['job_id'], (string) $job['dispatch_token']);

        if (is_wp_error($result)) {
            $this->updateById((int) $job['id'], [
                'status' => self::STATUS_PENDING,
                'last_error' => $result->get_error_message(),
                'updated_at' => current_time('mysql'),
            ]);
            return;
        }

        $this->updateById((int) $job['id'], [
            'status' => self::STATUS_QUEUED,
            'queued_at' => current_time('mysql'),
            'last_error' => null,
            'updated_at' => current_time('mysql'),
        ]);
    }

    public function run(string $jobId, string $dispatchToken): array
    {
        $job = $this->getByJobId($jobId);
        if (!$job) {
            return ['status' => 'ignored', 'reason' => 'missing'];
        }

        if (!hash_equals((string) $job['dispatch_token'], $dispatchToken)) {
            return ['status' => 'ignored', 'reason' => 'stale'];
        }

        if (($job['status'] ?? '') === self::STATUS_COMPLETED) {
            return ['status' => 'ignored', 'reason' => 'completed'];
        }

        if (($job['status'] ?? '') === self::STATUS_RUNNING) {
            return ['status' => 'ignored', 'reason' => 'running'];
        }

        if (!$this->claim($jobId, $dispatchToken)) {
            return ['status' => 'ignored', 'reason' => 'already_claimed'];
        }

        $runs = 0;

        try {
            while ($runs < self::MAX_INLINE_RERUNS) {
                $runs++;
                $currentJob = $this->getByJobId($jobId);
                if (!$currentJob || !hash_equals((string) $currentJob['dispatch_token'], $dispatchToken)) {
                    return ['status' => 'ignored', 'reason' => 'stale_after_claim'];
                }

                $payload = $this->decodePayload((string) $currentJob['payload_json']);
                if (!$payload) {
                    throw new \RuntimeException('Background lead job payload is invalid.');
                }

                do_action(
                    'plover_crm_after_contact_created_actions',
                    (int) $payload['subscriber_id'],
                    $payload['user_ip'],
                    (string) $payload['user_agent'],
                    (string) $payload['current_url'],
                    $payload['tracking_context'],
                    (string) $payload['provider_id']
                );

                if ($this->consumeRerunRequest($jobId, $dispatchToken)) {
                    continue;
                }

                if ($this->markCompleted($jobId, $dispatchToken)) {
                    return ['status' => 'completed', 'runs' => $runs];
                }
            }

            if ($this->markCompleted($jobId, $dispatchToken)) {
                return ['status' => 'completed', 'runs' => $runs];
            }

            return ['status' => 'ignored', 'reason' => 'state_changed'];
        } catch (\Throwable $throwable) {
            $this->updateByJobIdAndToken($jobId, $dispatchToken, [
                'status' => self::STATUS_PENDING,
                'last_error' => $throwable->getMessage(),
                'updated_at' => current_time('mysql'),
            ]);

            throw $throwable;
        }
    }

    public static function buildJobKey(string $providerId, int $subscriberId): string
    {
        return $providerId . ':' . $subscriberId;
    }

    private function normalizePayload(array $payload): array
    {
        $subscriberId = (int) ($payload['subscriber_id'] ?? 0);
        $providerId = sanitize_key((string) ($payload['provider_id'] ?? ''));

        if ($subscriberId <= 0 || $providerId === '') {
            return [];
        }

        return [
            'provider_id' => $providerId,
            'subscriber_id' => $subscriberId,
            'user_ip' => $payload['user_ip'] ?? null,
            'user_agent' => (string) ($payload['user_agent'] ?? ''),
            'current_url' => (string) ($payload['current_url'] ?? ''),
            'tracking_context' => is_array($payload['tracking_context'] ?? null) ? $payload['tracking_context'] : [],
        ];
    }

    private function decodePayload(string $payloadJson): array
    {
        $payload = json_decode($payloadJson, true);

        return is_array($payload) ? $payload : [];
    }

    private function claim(string $jobId, string $dispatchToken): bool
    {
        global $wpdb;

        $table = PostCreateJobSchema::getTableName();
        $updated = $wpdb->query($wpdb->prepare(
            "UPDATE $table
            SET status = %s,
                started_at = %s,
                updated_at = %s,
                rerun_requested = 0,
                last_error = NULL,
                attempt_count = attempt_count + 1
            WHERE job_id = %s
              AND dispatch_token = %s
              AND status IN (%s, %s)",
            self::STATUS_RUNNING,
            current_time('mysql'),
            current_time('mysql'),
            $jobId,
            $dispatchToken,
            self::STATUS_PENDING,
            self::STATUS_QUEUED
        ));

        return $updated === 1;
    }

    private function consumeRerunRequest(string $jobId, string $dispatchToken): bool
    {
        global $wpdb;

        $table = PostCreateJobSchema::getTableName();
        $updated = $wpdb->query($wpdb->prepare(
            "UPDATE $table
            SET rerun_requested = 0,
                updated_at = %s
            WHERE job_id = %s
              AND dispatch_token = %s
              AND status = %s
              AND rerun_requested = 1",
            current_time('mysql'),
            $jobId,
            $dispatchToken,
            self::STATUS_RUNNING
        ));

        return $updated === 1;
    }

    private function markCompleted(string $jobId, string $dispatchToken): bool
    {
        global $wpdb;

        $table = PostCreateJobSchema::getTableName();
        $updated = $wpdb->query($wpdb->prepare(
            "UPDATE $table
            SET status = %s,
                completed_at = %s,
                updated_at = %s,
                last_error = NULL,
                rerun_requested = 0
            WHERE job_id = %s
              AND dispatch_token = %s
              AND status = %s
              AND rerun_requested = 0",
            self::STATUS_COMPLETED,
            current_time('mysql'),
            current_time('mysql'),
            $jobId,
            $dispatchToken,
            self::STATUS_RUNNING
        ));

        return $updated === 1;
    }

    private function getByJobKey(string $jobKey): ?array
    {
        global $wpdb;

        $table = PostCreateJobSchema::getTableName();
        $job = $wpdb->get_row(
            $wpdb->prepare("SELECT * FROM $table WHERE job_key = %s LIMIT 1", $jobKey),
            ARRAY_A
        );

        return is_array($job) ? $job : null;
    }

    private function getByJobId(string $jobId): ?array
    {
        global $wpdb;

        $table = PostCreateJobSchema::getTableName();
        $job = $wpdb->get_row(
            $wpdb->prepare("SELECT * FROM $table WHERE job_id = %s LIMIT 1", $jobId),
            ARRAY_A
        );

        return is_array($job) ? $job : null;
    }

    private function getById(int $id): ?array
    {
        global $wpdb;

        $table = PostCreateJobSchema::getTableName();
        $job = $wpdb->get_row(
            $wpdb->prepare("SELECT * FROM $table WHERE id = %d LIMIT 1", $id),
            ARRAY_A
        );

        return is_array($job) ? $job : null;
    }

    private function insert(array $data): void
    {
        global $wpdb;

        $table = PostCreateJobSchema::getTableName();
        $wpdb->insert($table, $data, [
            '%s',
            '%s',
            '%s',
            '%s',
            '%s',
            '%d',
            '%d',
            '%s',
            '%s',
            '%s',
            '%s',
            '%s',
            '%s',
        ]);
    }

    private function updateById(int $id, array $data): void
    {
        global $wpdb;

        $table = PostCreateJobSchema::getTableName();
        $wpdb->update($table, $data, ['id' => $id]);
    }

    private function updateByJobIdAndToken(string $jobId, string $dispatchToken, array $data): void
    {
        global $wpdb;

        $table = PostCreateJobSchema::getTableName();
        $wpdb->update($table, $data, [
            'job_id' => $jobId,
            'dispatch_token' => $dispatchToken,
        ]);
    }
}
