<?php

namespace WPSN\Modules\Diagnostics;

defined('ABSPATH') || exit;

/**
 * Builds a human-readable diff between two saved platform probe runs.
 */
final class PlatformOpsDiffBuilder
{
    /** @var array<string,int> */
    private const SEVERITY_WEIGHT = [
        'ok' => 0,
        'warn' => 2,
        'critical' => 3,
    ];

    /**
     * @param array<string,mixed> $baseline
     * @param array<string,mixed> $candidate
     * @return array<string,mixed>
     */
    public function compare(array $baseline, array $candidate): array
    {
        $baselineChecks = isset($baseline['checks']) && is_array($baseline['checks']) ? $baseline['checks'] : [];
        $candidateChecks = isset($candidate['checks']) && is_array($candidate['checks']) ? $candidate['checks'] : [];

        $keys = array_values(array_unique(array_merge(array_keys($baselineChecks), array_keys($candidateChecks))));
        sort($keys);

        $checkDiffs = [];
        $regressions = 0;
        $improvements = 0;
        $changed = 0;
        $unchanged = 0;

        foreach ($keys as $key) {
            $diff = $this->compareCheck(
                (string) $key,
                isset($baselineChecks[$key]) && is_array($baselineChecks[$key]) ? $baselineChecks[$key] : [],
                isset($candidateChecks[$key]) && is_array($candidateChecks[$key]) ? $candidateChecks[$key] : []
            );

            $status = (string) ($diff['status'] ?? 'changed');
            if ($status === 'regressed') {
                $regressions++;
            } elseif ($status === 'improved') {
                $improvements++;
            } elseif ($status === 'unchanged') {
                $unchanged++;
            } else {
                $changed++;
            }

            $checkDiffs[$key] = $diff;
        }

        $baselineSeverity = $this->normalizeSeverity(isset($baseline['severity']) ? (string) $baseline['severity'] : 'info');
        $candidateSeverity = $this->normalizeSeverity(isset($candidate['severity']) ? (string) $candidate['severity'] : 'info');
        $baselineLocalOnly = !empty($baseline['local_only']);
        $candidateLocalOnly = !empty($candidate['local_only']);

        return [
            'generated_at_utc' => gmdate('c'),
            'baseline' => $this->runHeader($baseline),
            'candidate' => $this->runHeader($candidate),
            'summary' => [
                'baseline_severity' => $baselineSeverity,
                'candidate_severity' => $candidateSeverity,
                'severity_status' => $this->compareSeverityStatus($baselineSeverity, $candidateSeverity),
                'count_deltas' => $this->countDeltas($baseline, $candidate),
                'regressions' => $regressions,
                'improvements' => $improvements,
                'changed' => $changed,
                'unchanged' => $unchanged,
                'checks_total' => count($checkDiffs),
                'local_only_status' => $baselineLocalOnly === $candidateLocalOnly ? 'unchanged' : 'changed',
                'local_only_baseline' => $baselineLocalOnly,
                'local_only_candidate' => $candidateLocalOnly,
            ],
            'checks' => $checkDiffs,
        ];
    }

    /**
     * @param array<string,mixed> $run
     * @return array<string,mixed>
     */
    private function runHeader(array $run): array
    {
        $counts = isset($run['counts']) && is_array($run['counts']) ? $run['counts'] : [];

        return [
            'id' => isset($run['id']) ? (string) $run['id'] : '',
            'created_at_utc' => isset($run['created_at_utc']) ? (string) $run['created_at_utc'] : '',
            'severity' => $this->normalizeSeverity(isset($run['severity']) ? (string) $run['severity'] : 'info'),
            'plugin_version' => isset($run['plugin_version']) ? (string) $run['plugin_version'] : '',
            'local_only' => !empty($run['local_only']),
            'counts' => [
                'ok' => (int) ($counts['ok'] ?? 0),
                'warn' => (int) ($counts['warn'] ?? 0),
                'critical' => (int) ($counts['critical'] ?? 0),
                'skipped' => (int) ($counts['skipped'] ?? 0),
            ],
        ];
    }

    /**
     * @param array<string,mixed> $baseline
     * @param array<string,mixed> $candidate
     * @return array<string,int>
     */
    private function countDeltas(array $baseline, array $candidate): array
    {
        $baseCounts = isset($baseline['counts']) && is_array($baseline['counts']) ? $baseline['counts'] : [];
        $candidateCounts = isset($candidate['counts']) && is_array($candidate['counts']) ? $candidate['counts'] : [];
        $keys = ['ok', 'warn', 'critical', 'skipped'];

        $out = [];
        foreach ($keys as $key) {
            $out[$key] = (int) ($candidateCounts[$key] ?? 0) - (int) ($baseCounts[$key] ?? 0);
        }

        return $out;
    }

    /**
     * @param array<string,mixed> $baseline
     * @param array<string,mixed> $candidate
     * @return array<string,mixed>
     */
    private function compareCheck(string $key, array $baseline, array $candidate): array
    {
        $label = isset($candidate['label']) && (string) $candidate['label'] !== ''
            ? (string) $candidate['label']
            : (isset($baseline['label']) && (string) $baseline['label'] !== '' ? (string) $baseline['label'] : $key);

        $baselinePresent = !empty($baseline);
        $candidatePresent = !empty($candidate);
        $detailChanges = [];

        if (!$baselinePresent && $candidatePresent) {
            $status = 'added';
        } elseif ($baselinePresent && !$candidatePresent) {
            $status = 'removed';
        } else {
            $baselineSeverity = $this->normalizeSeverity(isset($baseline['severity']) ? (string) $baseline['severity'] : 'info');
            $candidateSeverity = $this->normalizeSeverity(isset($candidate['severity']) ? (string) $candidate['severity'] : 'info');
            $status = $this->compareSeverityStatus($baselineSeverity, $candidateSeverity);

            $detailChanges = $this->compareDetails(
                isset($baseline['details']) && is_array($baseline['details']) ? $baseline['details'] : [],
                isset($candidate['details']) && is_array($candidate['details']) ? $candidate['details'] : []
            );

            if (
                $status === 'unchanged'
                && (
                    trim((string) ($baseline['summary'] ?? '')) !== trim((string) ($candidate['summary'] ?? ''))
                    || (string) ($baseline['mode'] ?? '') !== (string) ($candidate['mode'] ?? '')
                    || (int) ($baseline['http_status'] ?? 0) !== (int) ($candidate['http_status'] ?? 0)
                    || !empty($detailChanges)
                )
            ) {
                $status = 'changed';
            }
        }

        return [
            'key' => $key,
            'label' => $label,
            'status' => $status,
            'baseline' => $this->checkHeader($baseline),
            'candidate' => $this->checkHeader($candidate),
            'detail_changes' => $detailChanges,
        ];
    }

    /**
     * @param array<string,mixed> $check
     * @return array<string,mixed>
     */
    private function checkHeader(array $check): array
    {
        return [
            'severity' => $this->normalizeSeverity(isset($check['severity']) ? (string) $check['severity'] : 'info'),
            'status' => isset($check['status']) ? (string) $check['status'] : '',
            'summary' => isset($check['summary']) ? (string) $check['summary'] : '',
            'ok' => !empty($check['ok']),
            'mode' => isset($check['mode']) ? (string) $check['mode'] : '',
            'http_status' => (int) ($check['http_status'] ?? 0),
        ];
    }

    /**
     * @param array<string,mixed> $baseline
     * @param array<string,mixed> $candidate
     * @return array<int,array<string,string>>
     */
    private function compareDetails(array $baseline, array $candidate): array
    {
        $keys = array_values(array_unique(array_merge(array_keys($baseline), array_keys($candidate))));
        sort($keys);

        $out = [];
        foreach ($keys as $key) {
            $baseValue = $this->normalizeDetailValue($baseline[$key] ?? null);
            $candidateValue = $this->normalizeDetailValue($candidate[$key] ?? null);
            if ($baseValue === $candidateValue) {
                continue;
            }
            $out[] = [
                'key' => (string) $key,
                'baseline' => $baseValue,
                'candidate' => $candidateValue,
            ];
            if (count($out) >= 8) {
                break;
            }
        }

        return $out;
    }

    /**
     * @param mixed $value
     */
    private function normalizeDetailValue($value): string
    {
        if ($value === null || $value === '') {
            return '';
        }

        if (is_bool($value)) {
            return $value ? 'yes' : 'no';
        }

        if (is_scalar($value)) {
            $text = (string) $value;
            if (strlen($text) > 240) {
                $text = substr($text, 0, 240) . '…';
            }
            return $text;
        }

        if (is_array($value)) {
            $parts = [];
            $i = 0;
            foreach ($value as $k => $v) {
                $text = $this->normalizeDetailValue($v);
                if ($text === '') {
                    continue;
                }
                $parts[] = is_string($k) ? ($k . ': ' . $text) : $text;
                $i++;
                if ($i >= 8) {
                    break;
                }
            }
            $joined = implode(', ', $parts);
            if ($joined === '') {
                return '';
            }
            if (count($value) > 8) {
                $joined .= '…';
            }
            if (strlen($joined) > 240) {
                $joined = substr($joined, 0, 240) . '…';
            }
            return $joined;
        }

        return '';
    }

    private function compareSeverityStatus(string $baseline, string $candidate): string
    {
        $baseline = $this->normalizeSeverity($baseline);
        $candidate = $this->normalizeSeverity($candidate);

        if ($baseline === $candidate) {
            return 'unchanged';
        }

        if (in_array($baseline, ['info', 'skipped'], true) || in_array($candidate, ['info', 'skipped'], true)) {
            return 'changed';
        }

        $baselineWeight = self::SEVERITY_WEIGHT[$baseline] ?? 1;
        $candidateWeight = self::SEVERITY_WEIGHT[$candidate] ?? 1;
        return $candidateWeight > $baselineWeight ? 'regressed' : 'improved';
    }

    private function normalizeSeverity(string $severity): string
    {
        $severity = trim(strtolower($severity));
        if (!in_array($severity, ['ok', 'warn', 'critical', 'skipped', 'info'], true)) {
            return 'info';
        }

        return $severity;
    }
}
