<?php

namespace WPSN\Modules\Diagnostics;

use WPSN\Core\AdminFootprintGuard;
use WPSN\Core\FrontendFootprintGuard;
use WPSN\Core\Container;
use WPSN\Modules\Stability\HeadSnapshotService;

defined('ABSPATH') || exit;

/**
 * Builds a "support bundle" payload (single JSON) for troubleshooting.
 *
 * This is intentionally read-only and must NOT include secrets (API keys, tokens).
 */
final class SupportBundleBuilder
{
    private Container $container;

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

    /**
     * @return array<string,mixed>
     */
    public function build(): array
    {
        $settings = $this->container->get('settings');
        $modules  = $this->container->get('modules');
        $queue    = $this->container->get('queue');

        $diagnostics = (new DiagnosticsReportBuilder($settings, $modules, $queue))->build();

        $head = null;
        try {
            $hs = new HeadSnapshotService($this->container);
            $cases = $hs->buildDefaultCases();
            $head = $hs->buildSnapshotPayload($cases);
        } catch (\Throwable $e) {
            $head = [
                'error' => 'head_snapshot_failed',
                'message' => $e->getMessage(),
            ];
        }

        $releaseRepo = new ReleaseSmokeReportRepository();
        $platformRepo = new PlatformOpsReportRepository();
        $platformLast = $platformRepo->getLast();
        $platformPinned = $platformRepo->getPinned();
        $platformPinnedDiff = [];
        if (!empty($platformPinned) && !empty($platformLast) && (string) ($platformPinned['id'] ?? '') !== (string) ($platformLast['id'] ?? '')) {
            $platformPinnedDiff = (new PlatformOpsDiffBuilder())->compare($platformPinned, $platformLast);
        }

        return [
            'generated_at_utc' => gmdate('c'),
            'plugin_version' => defined('WPSN_VERSION') ? (string) WPSN_VERSION : '',
            'build' => defined('WPSN_BUILD') ? (string) WPSN_BUILD : '',
            'diagnostics' => $diagnostics,
            'release_smoke_last' => $releaseRepo->getLast(),
            'release_smoke_history' => $releaseRepo->getHistory(10),
            'platform_ops_last' => $platformLast,
            'platform_ops_history' => $platformRepo->getHistory(10),
            'platform_ops_pinned' => $platformPinned,
            'platform_ops_latest_vs_pinned' => $platformPinnedDiff,
            'head_snapshot' => $head,
            'performance_history_recent' => array_slice(\WPSN\Core\PerfHistory::all(), 0, 30),
            'fatal_errors_recent' => $this->recentFatalErrors(),
            'notifications_recent' => $this->recentNotifications(),
            'queue_jobs_recent' => $this->recentQueueJobs(),
            'activity_log_recent' => $this->recentActivityLog(),
            'external_requests_recent' => $this->recentExternalRequests(),
            // Mostly useful for verifying "Minimize admin footprint" behavior.
            'admin_assets_snapshot' => AdminFootprintGuard::snapshot(),
            // Mostly useful for verifying "Frontend performance contract" behavior.
            'frontend_assets_snapshot' => FrontendFootprintGuard::snapshot(),
        ];
    }

    /**
     * Best-effort recent fatal errors captured by ErrorTracker.
     *
     * @return array<string,mixed>
     */
    private function recentFatalErrors(): array
    {
        if (!function_exists('get_option')) {
            return [
                'available' => false,
                'reason' => 'no_get_option',
            ];
        }

        $log = get_option('wpsn_fatal_log', []);
        if (!is_array($log)) {
            $log = [];
        }

        // Scrub to safe fields only.
        $out = [];
        $i = 0;
        foreach ($log as $row) {
            if (!is_array($row)) {
                continue;
            }
            $out[] = [
                'at_utc' => isset($row['at_utc']) ? sanitize_text_field((string) $row['at_utc']) : '',
                'type' => isset($row['type']) ? (int) $row['type'] : 0,
                'message' => isset($row['message']) ? sanitize_text_field((string) $row['message']) : '',
                'file' => isset($row['file']) ? sanitize_text_field((string) $row['file']) : '',
                'line' => isset($row['line']) ? (int) $row['line'] : 0,
                'plugin_version' => isset($row['plugin_version']) ? sanitize_text_field((string) $row['plugin_version']) : '',
                'build' => isset($row['build']) ? sanitize_text_field((string) $row['build']) : '',
                'php' => isset($row['php']) ? sanitize_text_field((string) $row['php']) : '',
            ];
            $i++;
            if ($i >= 30) {
                break;
            }
        }

        // Truncate messages.
        foreach ($out as &$r) {
            if (!is_array($r)) {
                continue;
            }
            if (isset($r['message']) && is_string($r['message']) && strlen($r['message']) > 500) {
                $r['message'] = substr($r['message'], 0, 500) . '…';
            }
        }
        unset($r);

        return [
            'available' => true,
            'count' => count($out),
            'items' => $out,
        ];
    }

    /**
     * Recent notifications/events (safe; does not include secrets).
     *
     * @return array<string,mixed>
     */
    private function recentNotifications(): array
    {
        global $wpdb;

        $table = $wpdb->prefix . 'wpsn_notifications';
        $exists = $this->tableExists($table);
        if (!$exists) {
            return [
                'available' => false,
                'reason' => 'table_missing',
            ];
        }

        $limit = 80;
        $sql = "SELECT id, created_at, source, type, severity, title, message, url, acknowledged_at, muted_until, sent_at, send_attempts, last_error FROM {$table} ORDER BY id DESC LIMIT %d";
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
        $rows = $wpdb->get_results($wpdb->prepare($sql, $limit), ARRAY_A);
        if (!is_array($rows)) {
            $rows = [];
        }

        foreach ($rows as &$r) {
            if (!is_array($r)) {
                continue;
            }
            $r['source'] = isset($r['source']) ? sanitize_key((string) $r['source']) : '';
            $r['type'] = isset($r['type']) ? sanitize_key((string) $r['type']) : '';
            $r['severity'] = isset($r['severity']) ? sanitize_key((string) $r['severity']) : '';
            $r['title'] = isset($r['title']) ? sanitize_text_field((string) $r['title']) : '';
            $r['message'] = isset($r['message']) ? sanitize_text_field((string) $r['message']) : '';
            if (strlen($r['message']) > 500) {
                $r['message'] = substr($r['message'], 0, 500) . '…';
            }
            $r['url'] = isset($r['url']) ? $this->sanitizeUrlForSupport((string) $r['url']) : '';
            $r['last_error'] = isset($r['last_error']) ? sanitize_text_field((string) $r['last_error']) : '';
            if (strlen($r['last_error']) > 500) {
                $r['last_error'] = substr($r['last_error'], 0, 500) . '…';
            }
        }
        unset($r);

        return [
            'available' => true,
            'items' => $rows,
        ];
    }

    /**
     * @return array<string,mixed>
     */
    private function recentQueueJobs(): array
    {
        global $wpdb;

        $table = $wpdb->prefix . 'wpsn_queue_jobs';
        $exists = $this->tableExists($table);
        if (!$exists) {
            return [
                'available' => false,
                'reason' => 'table_missing',
            ];
        }

        $limit = 80;

        // Exclude payload to avoid leaking anything sensitive.
        $sql = "SELECT id, job_type, priority, status, attempts, run_at, locked_at, locked_by, last_error, created_at, updated_at FROM {$table} ORDER BY id DESC LIMIT %d";
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
        $rows = $wpdb->get_results($wpdb->prepare($sql, $limit), ARRAY_A);
        if (!is_array($rows)) {
            $rows = [];
        }

        // Light scrubbing.
        foreach ($rows as &$r) {
            if (!is_array($r)) {
                continue;
            }
            $r['job_type'] = isset($r['job_type']) ? sanitize_key((string) $r['job_type']) : '';
            $r['status'] = isset($r['status']) ? sanitize_key((string) $r['status']) : '';
            $r['last_error'] = isset($r['last_error']) ? sanitize_text_field((string) $r['last_error']) : '';
            if (strlen($r['last_error']) > 500) {
                $r['last_error'] = substr($r['last_error'], 0, 500) . '…';
            }
            // locked_by is opaque; keep only first 12 chars.
            if (!empty($r['locked_by']) && is_string($r['locked_by'])) {
                $r['locked_by'] = substr((string) $r['locked_by'], 0, 12) . '…';
            }
        }
        unset($r);

        $counts = [];
        try {
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
            $prev = $wpdb->suppress_errors(true);
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
            $crows = $wpdb->get_results("SELECT status, COUNT(*) as c FROM {$table} GROUP BY status", ARRAY_A);
            $err = $wpdb->last_error;
            $wpdb->suppress_errors($prev);
            if ($err) {
                $crows = [];
            }
            if (is_array($crows)) {
                foreach ($crows as $cr) {
                    if (!is_array($cr)) {
                        continue;
                    }
                    $st = isset($cr['status']) ? sanitize_key((string) $cr['status']) : '';
                    $c = isset($cr['c']) ? (int) $cr['c'] : 0;
                    if ($st !== '') {
                        $counts[$st] = $c;
                    }
                }
            }
        } catch (\Throwable $e) {
            // ignore
        }

        return [
            'available' => true,
            'counts' => $counts,
            'items' => $rows,
        ];
    }

    /**
     * @return array<string,mixed>
     */
    private function recentActivityLog(): array
    {
        global $wpdb;

        $table = $wpdb->prefix . 'wpsn_activity_log';
        $exists = $this->tableExists($table);
        if (!$exists) {
            return [
                'available' => false,
                'reason' => 'table_missing',
            ];
        }

        $limit = 80;
        $sql = "SELECT id, created_at, user_id, action, context, object_type, object_id, object_key, message FROM {$table} ORDER BY id DESC LIMIT %d";
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
        $rows = $wpdb->get_results($wpdb->prepare($sql, $limit), ARRAY_A);
        if (!is_array($rows)) {
            $rows = [];
        }

        foreach ($rows as &$r) {
            if (!is_array($r)) {
                continue;
            }
            $r['action'] = isset($r['action']) ? sanitize_key((string) $r['action']) : '';
            $r['context'] = isset($r['context']) ? sanitize_key((string) $r['context']) : '';
            $r['object_type'] = isset($r['object_type']) ? sanitize_key((string) $r['object_type']) : '';
            $r['object_key'] = isset($r['object_key']) ? sanitize_text_field((string) $r['object_key']) : '';
            $r['message'] = isset($r['message']) ? sanitize_text_field((string) $r['message']) : '';
            if (strlen($r['message']) > 500) {
                $r['message'] = substr($r['message'], 0, 500) . '…';
            }
        }
        unset($r);

        return [
            'available' => true,
            'items' => $rows,
        ];
    }

    /**
     * @return array<string,mixed>
     */
    private function recentExternalRequests(): array
    {
        global $wpdb;

        $table = $wpdb->prefix . 'wpsn_external_log';
        $exists = $this->tableExists($table);
        if (!$exists) {
            return [
                'available' => false,
                'reason' => 'table_missing',
            ];
        }

        $limit = 80;
        $sql = "SELECT id, service, context, method, url, response_code, error, request_bytes, created_at FROM {$table} ORDER BY id DESC LIMIT %d";
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
        $rows = $wpdb->get_results($wpdb->prepare($sql, $limit), ARRAY_A);
        if (!is_array($rows)) {
            $rows = [];
        }

        foreach ($rows as &$r) {
            if (!is_array($r)) {
                continue;
            }
            $r['service'] = isset($r['service']) ? sanitize_key((string) $r['service']) : '';
            $r['context'] = isset($r['context']) ? sanitize_key((string) $r['context']) : '';
            $r['method'] = isset($r['method']) ? strtoupper(sanitize_key((string) $r['method'])) : '';
            $r['response_code'] = isset($r['response_code']) ? (int) $r['response_code'] : 0;
            $r['request_bytes'] = isset($r['request_bytes']) ? (int) $r['request_bytes'] : 0;

            $url = isset($r['url']) ? (string) $r['url'] : '';
            $r['url'] = $this->sanitizeUrlForSupport($url);

            $err = isset($r['error']) ? (string) $r['error'] : '';
            $err = sanitize_text_field($err);
            if (strlen($err) > 500) {
                $err = substr($err, 0, 500) . '…';
            }
            $r['error'] = $err;
        }
        unset($r);

        return [
            'available' => true,
            'items' => $rows,
        ];
    }

    private function sanitizeUrlForSupport(string $url): string
    {
        $url = trim($url);
        if ($url === '') {
            return '';
        }

        // Keep scheme+host+path only (strip query/fragment to avoid leaking tokens).
        $p = wp_parse_url($url);
        if (!is_array($p)) {
            return '';
        }

        $scheme = isset($p['scheme']) ? (string) $p['scheme'] : '';
        $host = isset($p['host']) ? (string) $p['host'] : '';
        $path = isset($p['path']) ? (string) $p['path'] : '';

        if ($host === '') {
            return '';
        }

        $out = ($scheme !== '' ? $scheme : 'https') . '://' . $host;
        if ($path !== '') {
            $out .= $path;
        }

        return esc_url_raw($out);
    }

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

        $tableName = (string) $tableName;
        if ($tableName === '') {
            return false;
        }

        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
        $found = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $tableName));
        return is_string($found) && $found === $tableName;
    }
}
