<?php

namespace WPSN\Modules\Stability;

use WPSN\Core\Container;

defined('ABSPATH') || exit;

final class HeadDiffService
{
    public const OPTION_NOTICE = 'wpsn_head_diff_notice';

    private Container $container;

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

    /**
     * Compare baseline head snapshot with current output after a plugin version update.
     *
     * Stores a notice payload (OPTION_NOTICE) if changes are detected.
     */
    public function checkDiffOnUpdate(string $fromVersion, string $toVersion): void
    {
        $fromVersion = (string) $fromVersion;
        $toVersion = (string) $toVersion;

        $head = new HeadSnapshotService($this->container);
        $baselineId = $head->getBaselineId();
        if ($baselineId <= 0) {
            return;
        }

        $repo = new SnapshotRepository();
        $baselineRow = $repo->get($baselineId);
        if (!is_array($baselineRow) || empty($baselineRow['type']) || (string) $baselineRow['type'] !== SnapshotRepository::TYPE_HEAD) {
            return;
        }

        $baselinePayload = isset($baselineRow['payload_decoded']) && is_array($baselineRow['payload_decoded']) ? $baselineRow['payload_decoded'] : [];
        $baselineCases = isset($baselinePayload['cases']) && is_array($baselinePayload['cases']) ? $baselinePayload['cases'] : [];
        if (empty($baselineCases)) {
            return;
        }

        // Build a fresh snapshot for the same contexts.
        $cases = [];
        foreach ($baselineCases as $bc) {
            if (!is_array($bc)) {
                continue;
            }
            $cases[] = [
                'key' => $bc['key'] ?? '',
                'label' => $bc['label'] ?? '',
                'context' => $bc['context'] ?? [],
            ];
        }

        $currentPayload = $head->buildSnapshotPayload($cases);
        $currentCases = isset($currentPayload['cases']) && is_array($currentPayload['cases']) ? $currentPayload['cases'] : [];

        // Index current by key.
        $currentByKey = [];
        foreach ($currentCases as $cc) {
            if (!is_array($cc)) {
                continue;
            }
            $k = isset($cc['key']) ? (string) $cc['key'] : '';
            if ($k !== '') {
                $currentByKey[$k] = $cc;
            }
        }

        $changed = [];
        foreach ($baselineCases as $bc) {
            if (!is_array($bc)) {
                continue;
            }
            $k = isset($bc['key']) ? (string) $bc['key'] : '';
            if ($k === '' || !isset($currentByKey[$k])) {
                continue;
            }
            $cc = $currentByKey[$k];

            // Prefer stable structured hash when available.
            $oldHash = isset($bc['struct_hash']) ? (string) $bc['struct_hash'] : (isset($bc['head_hash']) ? (string) $bc['head_hash'] : '');
            $newHash = isset($cc['struct_hash']) ? (string) $cc['struct_hash'] : (isset($cc['head_hash']) ? (string) $cc['head_hash'] : '');

            if ($oldHash !== '' && $newHash !== '' && hash_equals($oldHash, $newHash)) {
                continue;
            }

            $oldStruct = isset($bc['head_struct']) && is_array($bc['head_struct']) ? $bc['head_struct'] : null;
            $newStruct = isset($cc['head_struct']) && is_array($cc['head_struct']) ? $cc['head_struct'] : null;
            $summary = null;
            if (is_array($oldStruct) && is_array($newStruct)) {
                $summary = $this->summarizeStructChanges($oldStruct, $newStruct);
            }

            $changed[] = [
                'key' => $k,
                'label' => isset($bc['label']) ? (string) $bc['label'] : $k,
                'baseline_hash' => $oldHash,
                'current_hash' => $newHash,
                'diff_summary' => $summary,
                'baseline_resolved' => isset($bc['resolved']) && is_array($bc['resolved']) ? $bc['resolved'] : [],
                'current_resolved' => isset($cc['resolved']) && is_array($cc['resolved']) ? $cc['resolved'] : [],
            ];
        }

        if (empty($changed)) {
            // Clear any previous notice.
            delete_option(self::OPTION_NOTICE);
            return;
        }

        $notice = [
            'from_version' => $fromVersion,
            'to_version' => $toVersion,
            'created_at' => gmdate('Y-m-d H:i:s'),
            'baseline_snapshot_id' => $baselineId,
            'changed' => $changed,
        ];

        update_option(self::OPTION_NOTICE, $notice, false);

        /**
         * Fired when head output diff is detected after update.
         *
         * @param array<string,mixed> $notice
         */
        do_action('wpsn/head_diff/detected', $notice);
    }

    /**
     * Create a compact "what changed" summary for head structs.
     *
     * @param array<string,mixed> $old
     * @param array<string,mixed> $new
     * @return array<string,mixed>
     */
    private function summarizeStructChanges(array $old, array $new): array
    {
        $changedKeys = [];

        $oldTitle = isset($old['title']) && is_string($old['title']) ? (string) $old['title'] : '';
        $newTitle = isset($new['title']) && is_string($new['title']) ? (string) $new['title'] : '';
        if ($oldTitle !== $newTitle) {
            $changedKeys[] = 'title';
        }

        $oldSchemaHash = isset($old['schema_hash']) && is_string($old['schema_hash']) ? (string) $old['schema_hash'] : '';
        $newSchemaHash = isset($new['schema_hash']) && is_string($new['schema_hash']) ? (string) $new['schema_hash'] : '';
        if ($oldSchemaHash !== $newSchemaHash) {
            $changedKeys[] = 'schema_hash';
        }

        $oldTypes = isset($old['schema_types']) && is_array($old['schema_types']) ? $old['schema_types'] : [];
        $newTypes = isset($new['schema_types']) && is_array($new['schema_types']) ? $new['schema_types'] : [];
        if (wp_json_encode($oldTypes) !== wp_json_encode($newTypes)) {
            $changedKeys[] = 'schema_types';
        }

        $oldMeta = isset($old['meta']) && is_array($old['meta']) ? $old['meta'] : [];
        $newMeta = isset($new['meta']) && is_array($new['meta']) ? $new['meta'] : [];
        $changedKeys = array_merge($changedKeys, $this->diffMapKeys($oldMeta, $newMeta, 'meta.'));

        $oldLink = isset($old['link']) && is_array($old['link']) ? $old['link'] : [];
        $newLink = isset($new['link']) && is_array($new['link']) ? $new['link'] : [];
        $changedKeys = array_merge($changedKeys, $this->diffMapKeys($oldLink, $newLink, 'link.'));

        $changedKeys = array_values(array_unique(array_filter(array_map('strval', $changedKeys), static function ($x) {
            return $x !== '';
        })));
        sort($changedKeys);

        // Keep it small for the option payload.
        $max = 25;
        $total = count($changedKeys);
        $sample = $changedKeys;
        if ($total > $max) {
            $sample = array_slice($changedKeys, 0, $max);
        }

        return [
            'count' => $total,
            'sample' => $sample,
        ];
    }

    /**
     * @param array<string,mixed> $old
     * @param array<string,mixed> $new
     * @return array<int,string>
     */
    private function diffMapKeys(array $old, array $new, string $prefix): array
    {
        $out = [];
        $keys = array_unique(array_merge(array_keys($old), array_keys($new)));
        foreach ($keys as $k) {
            $ov = $old[$k] ?? '';
            $nv = $new[$k] ?? '';
            if (is_array($ov) || is_object($ov)) {
                $ov = wp_json_encode($ov);
            }
            if (is_array($nv) || is_object($nv)) {
                $nv = wp_json_encode($nv);
            }
            $ov = is_string($ov) ? $ov : (string) $ov;
            $nv = is_string($nv) ? $nv : (string) $nv;
            if ($ov !== $nv) {
                $out[] = $prefix . (string) $k;
            }
        }
        return $out;
    }
}
