<?php

namespace WPSN\Modules\Diagnostics;

use WPSN\Core\Container;
use WPSN\Core\Settings;
use WPSN\Core\UndoRuns;
use WPSN\Modules\Stability\HeadSnapshotService;

defined('ABSPATH') || exit;

/**
 * Release smoke / regression matrix.
 *
 * Goal:
 * - Give agencies/site owners a "release readiness" view before/after updates.
 * - Keep the checks runtime-safe and read-only.
 * - Reuse Quality Gate where possible, then add rollback/editor readiness checks.
 */
final class ReleaseSmokeMatrixService
{
    private Container $container;
    private Settings $settings;
    private ReleaseSmokeReportRepository $reports;

    public function __construct(Container $container)
    {
        $this->container = $container;
        $this->settings = $container->get('settings');
        $this->reports = new ReleaseSmokeReportRepository();
    }

    /**
     * @param array<string,mixed> $context
     * @return array<string,mixed>
     */
    public function run(array $context = []): array
    {
        $qg = new QualityGateService($this->container);
        $quality = $qg->runAllChecks(array_merge(['source' => 'release_smoke'], $context));

        $checks = [
            'frontend_matrix' => $this->checkFrontendMatrix(),
            'robots_txt' => $this->wrapQualityCheck(
                $quality['checks']['robots'] ?? [],
                __('robots.txt readiness looks OK.', 'wp-seo-next')
            ),
            'sitemap_index' => $this->wrapQualityCheck(
                $quality['checks']['sitemap'] ?? [],
                __('Sitemap index looks OK.', 'wp-seo-next')
            ),
            'queue_bulk_undo' => $this->checkQueueBulkUndo($quality['checks']['queue'] ?? []),
            'revisions_rollback' => $this->checkRevisionsRollback(),
            'import_export_rollback' => $this->checkImportExportRollback(),
            'admin_lists_quick_edit' => $this->checkAdminListsQuickEdit(),
        ];

        $counts = ['ok' => 0, 'info' => 0, 'warn' => 0, 'critical' => 0];
        $worst = 'ok';
        foreach ($checks as $check) {
            if (!is_array($check)) {
                continue;
            }
            $sev = isset($check['severity']) ? (string) $check['severity'] : 'info';
            if (!isset($counts[$sev])) {
                $sev = 'info';
            }
            $counts[$sev]++;
            $worst = $this->worseSeverity($worst, $sev);
        }

        return [
            'id' => function_exists('wp_generate_uuid4') ? (string) wp_generate_uuid4() : uniqid('wpsn_release_', true),
            'created_at_utc' => gmdate('c'),
            'ts' => time(),
            'plugin_version' => (string) (defined('WPSN_VERSION') ? WPSN_VERSION : ''),
            'severity' => $worst,
            'counts' => $counts,
            'checks' => $checks,
            'quality_gate' => $quality,
            'context' => $this->sanitizeContext($context),
        ];
    }

    /**
     * @param array<string,mixed> $report
     */
    public function save(array $report): void
    {
        $this->reports->save($report);
    }

    /**
     * @return array<string,mixed>
     */
    public function getLast(): array
    {
        return $this->reports->getLast();
    }

    /**
     * @return array<int,array<string,mixed>>
     */
    public function getHistory(int $limit = 10): array
    {
        return $this->reports->getHistory($limit);
    }

    /**
     * @return array<string,mixed>
     */
    public function getRun(string $id): array
    {
        return $this->reports->get($id);
    }

    /**
     * @return array<string,mixed>
     */
    public function getPinned(): array
    {
        return $this->reports->getPinned();
    }

    public function getPinnedId(): string
    {
        return $this->reports->getPinnedId();
    }

    public function pinRun(string $id): bool
    {
        $id = trim($id);
        if ($id === '') {
            return false;
        }

        $report = $this->getRun($id);
        if (empty($report)) {
            return false;
        }

        $this->reports->pin($report);
        return true;
    }

    public function unpin(): void
    {
        $this->reports->unpin();
    }

    public function clear(): void
    {
        $this->reports->clear();
    }

    /**
     * @return array{ok:bool,severity:string,summary:string,details:array<string,mixed>}
     */
    private function checkFrontendMatrix(): array
    {
        $details = [
            'cases' => [],
            'present_cases' => 0,
            'expected' => ['home', 'single', 'term', 'product'],
            'social_enabled' => (bool) $this->settings->moduleEnabled('social', true),
            'schema_enabled' => (bool) $this->settings->moduleEnabled('schema', true),
            'disable_output_effective' => (new QualityGateService($this->container))->disableOutputEffective(),
        ];

        try {
            $hs = new HeadSnapshotService($this->container);
            $payload = $hs->buildSnapshotPayload($hs->buildDefaultCases());
        } catch (\Throwable $e) {
            return [
                'ok' => false,
                'severity' => 'critical',
                'summary' => __('Could not build frontend smoke matrix.', 'wp-seo-next'),
                'details' => array_merge($details, [
                    'error' => $e->getMessage(),
                ]),
            ];
        }

        $payloadCases = isset($payload['cases']) && is_array($payload['cases']) ? $payload['cases'] : [];
        $slots = [
            'home' => null,
            'single' => null,
            'term' => null,
            'product' => null,
        ];

        foreach ($payloadCases as $case) {
            if (!is_array($case)) {
                continue;
            }
            $slot = $this->caseSlot($case);
            if ($slot === '' || !array_key_exists($slot, $slots) || $slots[$slot] !== null) {
                continue;
            }
            $slots[$slot] = $this->analyzeFrontCase($slot, $case);
        }

        $present = 0;
        $worst = 'ok';
        $problemCount = 0;
        foreach ($slots as $slot => $row) {
            if (is_array($row)) {
                $details['cases'][$slot] = $row;
                $present++;
                $sev = isset($row['severity']) ? (string) $row['severity'] : 'info';
                $worst = $this->worseSeverity($worst, $sev);
                if (!empty($row['problems']) && is_array($row['problems'])) {
                    $problemCount += count($row['problems']);
                }
                continue;
            }

            $details['cases'][$slot] = [
                'slot' => $slot,
                'ok' => true,
                'severity' => 'info',
                'summary' => sprintf(
                    /* translators: %s is slot label */
                    __('No published %s case was available for this smoke run.', 'wp-seo-next'),
                    $this->slotLabel($slot)
                ),
                'problems' => [],
                'details' => [
                    'missing_case' => true,
                ],
            ];
        }

        $details['present_cases'] = $present;

        if ($present === 0) {
            return [
                'ok' => false,
                'severity' => 'warn',
                'summary' => __('Frontend smoke matrix could not find any public cases to inspect.', 'wp-seo-next'),
                'details' => $details,
            ];
        }

        if ($problemCount === 0) {
            return [
                'ok' => true,
                'severity' => 'ok',
                'summary' => sprintf(
                    /* translators: 1: present cases 2: expected cases */
                    __('Frontend smoke matrix looks OK (%1$d/%2$d cases inspected).', 'wp-seo-next'),
                    $present,
                    count($slots)
                ),
                'details' => $details,
            ];
        }

        return [
            'ok' => $worst !== 'critical',
            'severity' => $worst,
            'summary' => sprintf(
                /* translators: 1: problem count 2: inspected case count */
                __('Frontend smoke matrix found %1$d issue(s) across %2$d inspected case(s).', 'wp-seo-next'),
                $problemCount,
                $present
            ),
            'details' => $details,
        ];
    }

    /**
     * @param array<string,mixed> $case
     * @return array<string,mixed>
     */
    private function analyzeFrontCase(string $slot, array $case): array
    {
        $struct = isset($case['head_struct']) && is_array($case['head_struct']) ? $case['head_struct'] : [];
        $meta = isset($struct['meta']) && is_array($struct['meta']) ? $struct['meta'] : [];
        $link = isset($struct['link']) && is_array($struct['link']) ? $struct['link'] : [];
        $title = isset($struct['title']) ? trim((string) $struct['title']) : '';

        $canonical = '';
        if (isset($link['rel:canonical'])) {
            if (is_string($link['rel:canonical'])) {
                $canonical = trim($link['rel:canonical']);
            } elseif (is_array($link['rel:canonical']) && !empty($link['rel:canonical'][0])) {
                $canonical = trim((string) $link['rel:canonical'][0]);
            }
        }

        $description = '';
        if (isset($meta['name:description'])) {
            if (is_string($meta['name:description'])) {
                $description = trim($meta['name:description']);
            } elseif (is_array($meta['name:description']) && !empty($meta['name:description'][0])) {
                $description = trim((string) $meta['name:description'][0]);
            }
        }

        $ogTitle = '';
        if (isset($meta['property:og:title'])) {
            if (is_string($meta['property:og:title'])) {
                $ogTitle = trim($meta['property:og:title']);
            } elseif (is_array($meta['property:og:title']) && !empty($meta['property:og:title'][0])) {
                $ogTitle = trim((string) $meta['property:og:title'][0]);
            }
        }

        $schemaTypes = isset($struct['schema_types']) && is_array($struct['schema_types']) ? array_values(array_filter(array_map('strval', $struct['schema_types']))) : [];

        $problems = [];
        $severity = 'ok';
        $siteHost = wp_parse_url(home_url('/'), PHP_URL_HOST);
        $canonHost = $canonical !== '' ? wp_parse_url($canonical, PHP_URL_HOST) : '';
        $siteHost = is_string($siteHost) ? $siteHost : '';
        $canonHost = is_string($canonHost) ? $canonHost : '';

        if ($title === '') {
            $problems[] = __('Missing title tag.', 'wp-seo-next');
            $severity = $this->worseSeverity($severity, 'critical');
        }
        if ($canonical === '') {
            $problems[] = __('Missing canonical URL.', 'wp-seo-next');
            $severity = $this->worseSeverity($severity, 'critical');
        } elseif ($siteHost !== '' && $canonHost !== '' && strcasecmp($siteHost, $canonHost) !== 0) {
            $problems[] = __('Canonical host does not match the site host.', 'wp-seo-next');
            $severity = $this->worseSeverity($severity, 'warn');
        }
        if ($description === '') {
            $problems[] = __('Missing meta description.', 'wp-seo-next');
            $severity = $this->worseSeverity($severity, 'warn');
        }

        $socialEnabled = (bool) $this->settings->moduleEnabled('social', true);
        if ($socialEnabled && $ogTitle === '') {
            $problems[] = __('Missing Open Graph title.', 'wp-seo-next');
            $severity = $this->worseSeverity($severity, 'warn');
        }

        $schemaEnabled = (bool) $this->settings->moduleEnabled('schema', true);
        if ($schemaEnabled && empty($schemaTypes)) {
            $problems[] = __('No schema types detected in the generated head snapshot.', 'wp-seo-next');
            $severity = $this->worseSeverity($severity, 'warn');
        }

        $summary = empty($problems)
            ? sprintf(__('Frontend case OK: %s.', 'wp-seo-next'), $this->slotLabel($slot))
            : sprintf(__('Frontend case needs attention: %s.', 'wp-seo-next'), $this->slotLabel($slot));

        return [
            'slot' => $slot,
            'ok' => empty($problems),
            'severity' => $severity,
            'summary' => $summary,
            'problems' => $problems,
            'details' => [
                'label' => isset($case['label']) ? (string) $case['label'] : $this->slotLabel($slot),
                'url' => isset($case['context']['url']) ? (string) $case['context']['url'] : '',
                'title' => $title,
                'canonical' => $canonical,
                'description_present' => $description !== '',
                'og_title_present' => $ogTitle !== '',
                'schema_types' => $schemaTypes,
            ],
        ];
    }

    /**
     * @param array<string,mixed> $qualityCheck
     * @return array{ok:bool,severity:string,summary:string,details:array<string,mixed>}
     */
    private function wrapQualityCheck(array $qualityCheck, string $fallbackSummary): array
    {
        $severity = isset($qualityCheck['severity']) ? (string) $qualityCheck['severity'] : 'info';
        $summary = isset($qualityCheck['summary']) ? (string) $qualityCheck['summary'] : $fallbackSummary;
        $details = isset($qualityCheck['details']) && is_array($qualityCheck['details']) ? $qualityCheck['details'] : [];

        return [
            'ok' => !empty($qualityCheck['ok']),
            'severity' => $severity,
            'summary' => $summary,
            'details' => $details,
        ];
    }

    /**
     * @param array<string,mixed> $qualityQueue
     * @return array{ok:bool,severity:string,summary:string,details:array<string,mixed>}
     */
    private function checkQueueBulkUndo(array $qualityQueue): array
    {
        $queue = $this->wrapQualityCheck($qualityQueue, __('Queue looks healthy.', 'wp-seo-next'));

        $details = [
            'queue' => $queue,
            'bulk_module_enabled' => (bool) $this->settings->moduleEnabled('bulk', true),
            'undo_runs_available' => class_exists(UndoRuns::class),
            'undo_recent_count' => 0,
            'queue_mode' => (string) $this->settings->get('performance.queue.mode', 'manual'),
            'direct_max_jobs' => (int) $this->settings->get('performance.queue.direct.max_jobs', 200),
            'direct_max_seconds' => (int) $this->settings->get('performance.queue.direct.max_seconds', 20),
        ];

        if ($details['undo_runs_available']) {
            try {
                $details['undo_recent_count'] = count(UndoRuns::list(10));
            } catch (\Throwable $e) {
                $details['undo_recent_count'] = 0;
            }
        }

        $severity = isset($queue['severity']) ? (string) $queue['severity'] : 'ok';
        $problems = [];

        if (empty($details['bulk_module_enabled'])) {
            $problems[] = __('Bulk module is disabled.', 'wp-seo-next');
            $severity = $this->worseSeverity($severity, 'warn');
        }
        if (empty($details['undo_runs_available'])) {
            $problems[] = __('Undo registry is not available.', 'wp-seo-next');
            $severity = $this->worseSeverity($severity, 'warn');
        }
        if ((int) $details['direct_max_jobs'] <= 0) {
            $problems[] = __('Direct queue batch size is not configured.', 'wp-seo-next');
            $severity = $this->worseSeverity($severity, 'warn');
        }

        if (empty($problems)) {
            return [
                'ok' => $severity !== 'critical',
                'severity' => $severity,
                'summary' => __('Queue/Bulk/Undo readiness looks OK.', 'wp-seo-next'),
                'details' => $details,
            ];
        }

        return [
            'ok' => $severity !== 'critical',
            'severity' => $severity,
            'summary' => implode(' ', $problems),
            'details' => $details,
        ];
    }

    /**
     * @return array{ok:bool,severity:string,summary:string,details:array<string,mixed>}
     */
    private function checkRevisionsRollback(): array
    {
        global $wpdb;

        $table = isset($wpdb->prefix) ? $wpdb->prefix . 'wpsn_revisions' : 'wpsn_revisions';
        $moduleEnabled = (bool) $this->settings->moduleEnabled('revisions', true);
        $tableExists = $this->tableExists($table);
        $dbVersion = (string) get_option('wpsn_revisions_db_version', '');
        $recentCount = 0;

        if ($tableExists) {
            try {
                // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
                $recentCount = (int) $wpdb->get_var('SELECT COUNT(*) FROM ' . $table);
            } catch (\Throwable $e) {
                $recentCount = 0;
            }
        }

        $details = [
            'module_enabled' => $moduleEnabled,
            'table' => $table,
            'table_exists' => $tableExists,
            'db_version' => $dbVersion,
            'recent_count' => $recentCount,
        ];

        if (!$moduleEnabled) {
            return [
                'ok' => false,
                'severity' => 'warn',
                'summary' => __('Revisions module is disabled, so rollback readiness is reduced.', 'wp-seo-next'),
                'details' => $details,
            ];
        }

        if (!$tableExists) {
            return [
                'ok' => false,
                'severity' => 'critical',
                'summary' => __('Revisions rollback table is missing.', 'wp-seo-next'),
                'details' => $details,
            ];
        }

        return [
            'ok' => true,
            'severity' => 'ok',
            'summary' => __('Revisions rollback path looks ready.', 'wp-seo-next'),
            'details' => $details,
        ];
    }

    /**
     * @return array{ok:bool,severity:string,summary:string,details:array<string,mixed>}
     */
    private function checkImportExportRollback(): array
    {
        global $wpdb;

        $table = isset($wpdb->prefix) ? $wpdb->prefix . 'wpsn_snapshots' : 'wpsn_snapshots';
        $moduleEnabled = (bool) $this->settings->moduleEnabled('import_export', true);
        $stabilityEnabled = (bool) $this->settings->moduleEnabled('stability', true);
        $tableExists = $this->tableExists($table);
        $dbVersion = (int) get_option('wpsn_snapshots_db_version', 0);
        $undoRunsAvailable = class_exists(UndoRuns::class);

        $details = [
            'import_export_enabled' => $moduleEnabled,
            'stability_enabled' => $stabilityEnabled,
            'snapshot_table' => $table,
            'snapshot_table_exists' => $tableExists,
            'snapshot_db_version' => $dbVersion,
            'undo_runs_available' => $undoRunsAvailable,
        ];

        if (!$moduleEnabled) {
            return [
                'ok' => false,
                'severity' => 'warn',
                'summary' => __('Import/Export module is disabled.', 'wp-seo-next'),
                'details' => $details,
            ];
        }

        if (!$stabilityEnabled || !$tableExists || !$undoRunsAvailable) {
            return [
                'ok' => false,
                'severity' => !$tableExists ? 'critical' : 'warn',
                'summary' => __('Import/Export rollback path needs attention (snapshots/undo).', 'wp-seo-next'),
                'details' => $details,
            ];
        }

        return [
            'ok' => true,
            'severity' => 'ok',
            'summary' => __('Import/Export rollback path looks ready.', 'wp-seo-next'),
            'details' => $details,
        ];
    }

    /**
     * @return array{ok:bool,severity:string,summary:string,details:array<string,mixed>}
     */
    private function checkAdminListsQuickEdit(): array
    {
        $postTypes = function_exists('get_post_types')
            ? get_post_types(['public' => true, 'show_ui' => true], 'names')
            : [];
        $taxonomies = function_exists('get_taxonomies')
            ? get_taxonomies(['public' => true, 'show_ui' => true], 'names')
            : [];

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

        $taxonomies = array_values(array_filter(array_map('strval', $taxonomies), static function (string $tx): bool {
            return $tx !== '' && $tx !== 'post_format';
        }));

        $details = [
            'module_enabled' => (bool) $this->settings->moduleEnabled('admin_lists', true),
            'asset_exists' => defined('WPSN_PLUGIN_DIR') ? is_readable(WPSN_PLUGIN_DIR . 'assets/admin/admin-lists.js') : false,
            'post_types_count' => count($postTypes),
            'taxonomies_count' => count($taxonomies),
            'revisions_module_enabled' => (bool) $this->settings->moduleEnabled('revisions', true),
        ];

        if (!$details['module_enabled']) {
            return [
                'ok' => false,
                'severity' => 'warn',
                'summary' => __('Admin Lists module is disabled.', 'wp-seo-next'),
                'details' => $details,
            ];
        }

        if (!$details['asset_exists']) {
            return [
                'ok' => false,
                'severity' => 'critical',
                'summary' => __('Admin Lists Quick Edit asset is missing.', 'wp-seo-next'),
                'details' => $details,
            ];
        }

        if ((int) $details['post_types_count'] === 0) {
            return [
                'ok' => false,
                'severity' => 'warn',
                'summary' => __('No public post types are available for Admin Lists Quick Edit.', 'wp-seo-next'),
                'details' => $details,
            ];
        }

        return [
            'ok' => true,
            'severity' => 'ok',
            'summary' => __('Admin Lists Quick Edit path looks ready.', 'wp-seo-next'),
            'details' => $details,
        ];
    }

    /**
     * @param array<string,mixed> $case
     */
    private function caseSlot(array $case): string
    {
        $ctx = isset($case['context']) && is_array($case['context']) ? $case['context'] : [];
        $type = isset($ctx['type']) ? (string) $ctx['type'] : '';
        $subType = isset($ctx['object_sub_type']) ? (string) $ctx['object_sub_type'] : '';

        if ($type === 'home') {
            return 'home';
        }
        if ($type === 'term') {
            return 'term';
        }
        if ($type === 'singular' && $subType === 'product') {
            return 'product';
        }
        if ($type === 'singular') {
            return 'single';
        }

        return '';
    }

    private function slotLabel(string $slot): string
    {
        $map = [
            'home' => __('home', 'wp-seo-next'),
            'single' => __('single', 'wp-seo-next'),
            'term' => __('term', 'wp-seo-next'),
            'product' => __('product', 'wp-seo-next'),
        ];

        return isset($map[$slot]) ? $map[$slot] : $slot;
    }

    private function worseSeverity(string $a, string $b): string
    {
        $order = ['ok' => 0, 'info' => 1, 'warn' => 2, 'critical' => 3];
        return ($order[$b] ?? 1) > ($order[$a] ?? 0) ? $b : $a;
    }

    private function tableExists(string $table): bool
    {
        global $wpdb;

        if ($table === '') {
            return false;
        }

        try {
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
            $sql = $wpdb->prepare('SHOW TABLES LIKE %s', $table);
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
            $found = $wpdb->get_var($sql);
            return is_string($found) && $found === $table;
        } catch (\Throwable $e) {
            return false;
        }
    }

    /**
     * @param array<string,mixed> $context
     * @return array<string,mixed>
     */
    private function sanitizeContext(array $context): array
    {
        $out = [];
        foreach ($context as $k => $v) {
            $key = sanitize_key((string) $k);
            if ($key === '') {
                continue;
            }
            if (is_scalar($v) || $v === null) {
                $out[$key] = $v;
            }
        }
        return $out;
    }
}
