<?php

namespace WPSN\Modules\Bulk;

use WPSN\Core\Logger;

defined('ABSPATH') || exit;

final class BulkQueueSessionRepository
{
    private const INDEX_OPTION = 'wpsn_bulk_queue_sessions_index';
    private const SESSION_OPTION_PREFIX = 'wpsn_bulk_queue_session_';
    private const MAX_SESSIONS = 50;

    private Logger $logger;

    public function __construct(Logger $logger)
    {
        $this->logger = $logger;
    }

    /**
     * @param array<string,mixed> $meta
     */
    public function create(array $meta): string
    {
        $sessionId = $this->generateSessionId();
        $now = time();

        $data = [
            'session_id' => $sessionId,
            'action' => isset($meta['action']) ? sanitize_key((string) $meta['action']) : '',
            'source' => isset($meta['source']) ? sanitize_key((string) $meta['source']) : 'audit',
            'scan_id' => isset($meta['scan_id']) ? (int) $meta['scan_id'] : 0,
            'created_at' => $now,
            'created_by' => isset($meta['created_by']) ? (int) $meta['created_by'] : 0,
            'targets' => isset($meta['targets']) ? (int) $meta['targets'] : 0,
            'eligible_targets' => isset($meta['eligible_targets']) ? (int) $meta['eligible_targets'] : 0,
            'queued_jobs' => isset($meta['queued_jobs']) ? (int) $meta['queued_jobs'] : 0,
            'skipped' => isset($meta['skipped']) ? (int) $meta['skipped'] : 0,
            'preferred_profile' => isset($meta['preferred_profile']) ? sanitize_key((string) $meta['preferred_profile']) : '',
            'return_url' => isset($meta['return_url']) ? esc_url_raw((string) $meta['return_url']) : '',
            'jobs' => [],
            'updated_at' => $now,
        ];

        update_option($this->sessionOption($sessionId), $data, false);

        $index = $this->index();
        $index[$sessionId] = [
            'session_id' => $sessionId,
            'action' => (string) $data['action'],
            'source' => (string) $data['source'],
            'scan_id' => (int) $data['scan_id'],
            'created_at' => $now,
            'updated_at' => $now,
            'targets' => (int) $data['targets'],
            'eligible_targets' => (int) $data['eligible_targets'],
            'queued_jobs' => (int) $data['queued_jobs'],
            'skipped' => (int) $data['skipped'],
            'preferred_profile' => (string) ($data['preferred_profile'] ?? ''),
            'return_url' => (string) $data['return_url'],
        ];
        $this->storeIndex($index);

        return $sessionId;
    }

    /**
     * @param array<string,mixed> $fields
     */
    public function updateMeta(string $sessionId, array $fields): void
    {
        $sessionId = sanitize_key($sessionId);
        if ($sessionId === '') {
            return;
        }

        $session = $this->get($sessionId);
        if (empty($session)) {
            return;
        }

        foreach ($fields as $key => $value) {
            if (in_array($key, ['session_id', 'jobs'], true)) {
                continue;
            }
            if (in_array($key, ['scan_id', 'created_by', 'targets', 'eligible_targets', 'queued_jobs', 'skipped'], true)) {
                $session[$key] = (int) $value;
                continue;
            }
            if (in_array($key, ['action', 'source', 'preferred_profile'], true)) {
                $session[$key] = sanitize_key((string) $value);
                continue;
            }
            if ($key === 'return_url') {
                $session[$key] = esc_url_raw((string) $value);
                continue;
            }
            $session[$key] = is_scalar($value) ? (string) $value : $value;
        }

        $session['updated_at'] = time();
        update_option($this->sessionOption($sessionId), $session, false);

        $index = $this->index();
        if (isset($index[$sessionId]) && is_array($index[$sessionId])) {
            foreach (['action', 'source', 'scan_id', 'targets', 'eligible_targets', 'queued_jobs', 'skipped', 'preferred_profile', 'return_url'] as $key) {
                if (array_key_exists($key, $session)) {
                    $index[$sessionId][$key] = $session[$key];
                }
            }
            $index[$sessionId]['updated_at'] = (int) $session['updated_at'];
            $this->storeIndex($index);
        }
    }

    /**
     * @param array<string,mixed> $payload
     */
    public function addJob(string $sessionId, int $jobId, string $jobType, array $payload = []): void
    {
        $sessionId = sanitize_key($sessionId);
        $jobId = (int) $jobId;
        $jobType = sanitize_key($jobType);

        if ($sessionId === '' || $jobId <= 0 || $jobType === '') {
            return;
        }

        $session = $this->get($sessionId);
        if (empty($session)) {
            return;
        }

        $jobs = isset($session['jobs']) && is_array($session['jobs']) ? $session['jobs'] : [];
        $jobKey = (string) $jobId;
        if (isset($jobs[$jobKey]) && is_array($jobs[$jobKey])) {
            return;
        }

        $objectType = isset($payload['object_type']) ? sanitize_key((string) $payload['object_type']) : '';
        $objectId = isset($payload['object_id']) ? (int) $payload['object_id'] : 0;
        if ($objectId <= 0 && isset($payload['post_id'])) {
            $objectType = 'post';
            $objectId = (int) $payload['post_id'];
        }

        $jobs[$jobKey] = [
            'job_id' => $jobId,
            'job_type' => $jobType,
            'stage' => $this->stageForJobType($jobType),
            'object_type' => $objectType,
            'object_id' => $objectId,
        ];

        $session['jobs'] = $jobs;
        $session['queued_jobs'] = count($jobs);
        $session['updated_at'] = time();
        update_option($this->sessionOption($sessionId), $session, false);

        $index = $this->index();
        if (isset($index[$sessionId]) && is_array($index[$sessionId])) {
            $index[$sessionId]['queued_jobs'] = (int) $session['queued_jobs'];
            $index[$sessionId]['updated_at'] = (int) $session['updated_at'];
            $this->storeIndex($index);
        }
    }

    /**
     * @return array<string,mixed>
     */
    public function get(string $sessionId): array
    {
        $sessionId = sanitize_key($sessionId);
        if ($sessionId === '') {
            return [];
        }

        $data = get_option($this->sessionOption($sessionId), []);
        return is_array($data) ? $data : [];
    }

    /**
     * @return array<int,array<string,mixed>>
     */
    public function listRecent(int $limit = 8): array
    {
        $limit = max(1, min(self::MAX_SESSIONS, $limit));
        $index = $this->index();
        if (empty($index)) {
            return [];
        }

        usort($index, static function (array $a, array $b): int {
            return ((int) ($b['updated_at'] ?? 0)) <=> ((int) ($a['updated_at'] ?? 0));
        });

        $out = [];
        foreach ($index as $meta) {
            if (!is_array($meta)) {
                continue;
            }
            $sessionId = isset($meta['session_id']) ? sanitize_key((string) $meta['session_id']) : '';
            if ($sessionId === '') {
                continue;
            }
            $session = $this->get($sessionId);
            if (empty($session)) {
                continue;
            }
            $out[] = $session;
            if (count($out) >= $limit) {
                break;
            }
        }

        return $out;
    }

    /**
     * @return array<int,int>
     */
    public function jobIds(string $sessionId): array
    {
        $session = $this->get($sessionId);
        if (empty($session)) {
            return [];
        }

        $jobs = isset($session['jobs']) && is_array($session['jobs']) ? $session['jobs'] : [];
        $ids = [];
        foreach ($jobs as $job) {
            if (!is_array($job)) {
                continue;
            }
            $id = isset($job['job_id']) ? (int) $job['job_id'] : 0;
            if ($id > 0) {
                $ids[] = $id;
            }
        }

        return array_values(array_unique($ids));
    }

    /**
     * @return array<string,mixed>
     */
    public function progress(string $sessionId): array
    {
        $session = $this->get($sessionId);
        if (empty($session)) {
            return [];
        }

        $jobs = isset($session['jobs']) && is_array($session['jobs']) ? $session['jobs'] : [];
        $jobIds = $this->jobIds($sessionId);

        $summary = [
            'session_id' => (string) ($session['session_id'] ?? ''),
            'action' => (string) ($session['action'] ?? ''),
            'targets' => (int) ($session['targets'] ?? 0),
            'eligible_targets' => (int) ($session['eligible_targets'] ?? 0),
            'queued_jobs' => count($jobIds),
            'pending' => 0,
            'running' => 0,
            'done' => 0,
            'failed' => 0,
            'missing' => 0,
            'terminal' => 0,
            'active' => false,
            'percent' => 0,
            'last_error' => '',
            'stages' => [
                'generate' => ['label' => __('Generate', 'wp-seo-next'), 'total' => 0, 'pending' => 0, 'running' => 0, 'done' => 0, 'failed' => 0],
                'apply' => ['label' => __('Apply', 'wp-seo-next'), 'total' => 0, 'pending' => 0, 'running' => 0, 'done' => 0, 'failed' => 0],
                'fix' => ['label' => __('Fix', 'wp-seo-next'), 'total' => 0, 'pending' => 0, 'running' => 0, 'done' => 0, 'failed' => 0],
                'refresh' => ['label' => __('Refresh issues', 'wp-seo-next'), 'total' => 0, 'pending' => 0, 'running' => 0, 'done' => 0, 'failed' => 0],
                'other' => ['label' => __('Other', 'wp-seo-next'), 'total' => 0, 'pending' => 0, 'running' => 0, 'done' => 0, 'failed' => 0],
            ],
        ];

        if (empty($jobIds)) {
            return $summary;
        }

        global $wpdb;
        if (!isset($wpdb) || !is_object($wpdb) || !property_exists($wpdb, 'prefix')) {
            return $summary;
        }

        $table = $wpdb->prefix . 'wpsn_queue_jobs';
        $placeholders = implode(', ', array_fill(0, count($jobIds), '%d'));
        $rows = [];

        try {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
            $rows = $wpdb->get_results($wpdb->prepare(
                "SELECT id, status, last_error FROM {$table} WHERE id IN ({$placeholders})",
                $jobIds
            ), ARRAY_A);
        } catch (\Throwable $e) {
            $this->logger->warning('Bulk session progress query failed: ' . $e->getMessage(), ['session_id' => $sessionId]);
            return $summary;
        }

        if (!is_array($rows)) {
            $rows = [];
        }

        $statusById = [];
        foreach ($rows as $row) {
            if (!is_array($row)) {
                continue;
            }
            $id = isset($row['id']) ? (int) $row['id'] : 0;
            if ($id <= 0) {
                continue;
            }
            $statusById[(string) $id] = [
                'status' => isset($row['status']) ? sanitize_key((string) $row['status']) : '',
                'last_error' => isset($row['last_error']) ? (string) $row['last_error'] : '',
            ];
        }

        foreach ($jobs as $job) {
            if (!is_array($job)) {
                continue;
            }
            $id = isset($job['job_id']) ? (int) $job['job_id'] : 0;
            if ($id <= 0) {
                continue;
            }
            $jobKey = (string) $id;
            $stage = isset($job['stage']) ? sanitize_key((string) $job['stage']) : 'other';
            if (!isset($summary['stages'][$stage])) {
                $stage = 'other';
            }

            $summary['stages'][$stage]['total']++;

            $statusInfo = $statusById[$jobKey] ?? ['status' => 'missing', 'last_error' => ''];
            $status = isset($statusInfo['status']) ? sanitize_key((string) $statusInfo['status']) : 'missing';
            if ($status === '') {
                $status = 'missing';
            }

            if ($status === 'pending') {
                $summary['pending']++;
                $summary['stages'][$stage]['pending']++;
            } elseif ($status === 'running') {
                $summary['running']++;
                $summary['stages'][$stage]['running']++;
            } elseif ($status === 'failed') {
                $summary['failed']++;
                $summary['stages'][$stage]['failed']++;
            } elseif ($status === 'done') {
                $summary['done']++;
                $summary['stages'][$stage]['done']++;
            } else {
                $summary['missing']++;
            }

            if ($summary['last_error'] === '' && !empty($statusInfo['last_error'])) {
                $summary['last_error'] = (string) $statusInfo['last_error'];
            }
        }

        $summary['terminal'] = (int) $summary['done'] + (int) $summary['failed'] + (int) $summary['missing'];
        $summary['active'] = ((int) $summary['pending'] + (int) $summary['running']) > 0;
        $total = max(0, count($jobIds));
        $summary['percent'] = $total > 0 ? (int) round(((int) $summary['terminal'] / $total) * 100) : 0;

        return $summary;
    }

    public function actionLabel(string $action): string
    {
        $action = sanitize_key($action);

        $labels = [
            'generate_local' => __('Generate local meta suggestions', 'wp-seo-next'),
            'generate_apply_local' => __('Generate + apply local meta suggestions', 'wp-seo-next'),
            'clear_canonical' => __('Clear canonical override', 'wp-seo-next'),
            'clear_noindex' => __('Clear noindex override', 'wp-seo-next'),
        ];

        return $labels[$action] ?? ($action !== '' ? $action : __('Bulk meta fixes', 'wp-seo-next'));
    }

    private function stageForJobType(string $jobType): string
    {
        $jobType = sanitize_key($jobType);
        if ($jobType === 'bulk_generate') {
            return 'generate';
        }
        if ($jobType === 'bulk_apply') {
            return 'apply';
        }
        if ($jobType === 'audit_bulk_fix') {
            return 'fix';
        }
        if ($jobType === 'audit_refresh_object_issues') {
            return 'refresh';
        }

        return 'other';
    }

    /**
     * @return array<string,array<string,mixed>>
     */
    private function index(): array
    {
        $raw = get_option(self::INDEX_OPTION, []);
        return is_array($raw) ? $raw : [];
    }

    /**
     * @param array<string,array<string,mixed>> $index
     */
    private function storeIndex(array $index): void
    {
        uasort($index, static function (array $a, array $b): int {
            return ((int) ($b['updated_at'] ?? 0)) <=> ((int) ($a['updated_at'] ?? 0));
        });

        $trimmed = [];
        $count = 0;
        foreach ($index as $sessionId => $meta) {
            $sessionId = sanitize_key((string) $sessionId);
            if ($sessionId === '' || !is_array($meta)) {
                continue;
            }
            $trimmed[$sessionId] = $meta;
            $count++;
            if ($count >= self::MAX_SESSIONS) {
                break;
            }
        }

        $existingIds = array_keys($index);
        $keptIds = array_keys($trimmed);
        foreach ($existingIds as $sessionId) {
            $sessionId = sanitize_key((string) $sessionId);
            if ($sessionId === '' || in_array($sessionId, $keptIds, true)) {
                continue;
            }
            delete_option($this->sessionOption($sessionId));
        }

        update_option(self::INDEX_OPTION, $trimmed, false);
    }

    private function sessionOption(string $sessionId): string
    {
        return self::SESSION_OPTION_PREFIX . sanitize_key($sessionId);
    }

    private function generateSessionId(): string
    {
        try {
            $rand = wp_generate_password(10, false, false);
        } catch (\Throwable $e) {
            $rand = (string) wp_rand(100000, 999999999);
        }

        $rand = strtolower(preg_replace('/[^a-zA-Z0-9]/', '', (string) $rand));
        if ($rand === '') {
            $rand = strtolower((string) wp_rand(100000, 999999999));
        }

        return 'bq' . substr($rand, 0, 10);
    }
}
