<?php

namespace WPSN\Modules\Stability;

use WPSN\Admin\SettingsRepository;

defined('ABSPATH') || exit;

/**
 * Helpers for config snapshots (settings snapshots).
 *
 * v2 notes:
 * - Payload may be either a raw settings array (legacy) or a wrapped payload:
 *   [ 'settings' => <settings>, 'meta' => <small metadata> ].
 * - Provides grouped diff and selective restore utilities.
 */
final class ConfigSnapshotService
{
    /**
     * Create a config snapshot of the current settings.
     *
     * @param string $label
     * @param array<string,mixed> $meta
     */
    public static function createAutoSnapshot(string $label, array $meta = []): int
    {
        // Ensure table exists.
        SnapshotsInstaller::maybeInstall();

        $settings = get_option(SettingsRepository::OPTION_NAME, []);
        if (!is_array($settings)) {
            $settings = [];
        }

        $payload = [
            'settings' => $settings,
            'meta' => array_merge([
                'kind' => 'auto',
                'site' => [
                    'home_url' => function_exists('home_url') ? (string) home_url('/') : '',
                    'site_url' => function_exists('site_url') ? (string) site_url('/') : '',
                ],
                'wp' => [
                    'version' => (string) (function_exists('get_bloginfo') ? get_bloginfo('version') : ''),
                ],
                'created_at' => gmdate('c'),
            ], $meta),
        ];

        $repo = new SnapshotRepository();
        return $repo->create(SnapshotRepository::TYPE_CONFIG, $label, $payload);
    }

    /**
     * @param array<string,mixed> $payload
     * @return array<string,mixed>
     */
    public static function extractSettings(array $payload): array
    {
        if (isset($payload['settings']) && is_array($payload['settings'])) {
            return $payload['settings'];
        }
        return $payload;
    }

    /**
     * @return array<string,array{label:string,paths:array<int,string>,priority:int}>
     */
    public static function groups(): array
    {
        // NOTE: Order matters when classifying diffs (higher priority wins).
        return [
            'providers' => [
                'label' => __('Providers', 'wp-seo-next'),
                'paths' => [
                    // AI external provider/model.
                    'ai.external.provider',
                    'ai.external.model',
                    'ai.external.openai_org',
                    'ai.external.openai_project',
                    // SERP Gap providers.
                    'serp_gap.provider_id',
                    'serp_gap.serper',
                ],
                'priority' => 100,
            ],
            'saas_endpoints' => [
                'label' => __('SaaS endpoints', 'wp-seo-next'),
                'paths' => [
                    // Licensing endpoints.
                    'licensing.server_url',
                    'licensing.verify_path',
                    'licensing.update_server_url',
                    'licensing.update_path',
                    // SaaS Sync endpoints.
                    'saas_sync.server_url',
                    'saas_sync.sync_path',
                    'saas_sync.timeout',
                    'saas_sync.serp_path',
                    'saas_sync.serp_timeout',
                    'saas_sync.serp_page_path',
                    'saas_sync.serp_page_timeout',
                    'saas_sync.ai_path',
                    'saas_sync.ai_timeout',
                    'saas_sync.ai_credits_path',
                    'saas_sync.ai_credits_timeout',
                ],
                'priority' => 90,
            ],
            'templates' => [
                'label' => __('Templates', 'wp-seo-next'),
                'paths' => [
                    'separator',
                    'templates',
                ],
                'priority' => 80,
            ],
            'robots' => [
                'label' => __('Robots & robots.txt', 'wp-seo-next'),
                'paths' => [
                    'robots',
                    'robots_txt',
                ],
                'priority' => 70,
            ],
            'modules' => [
                'label' => __('Modules', 'wp-seo-next'),
                'paths' => [
                    'modules',
                ],
                'priority' => 60,
            ],
            'schema' => [
                'label' => __('Schema', 'wp-seo-next'),
                'paths' => [
                    'schema',
                    'local',
                    'woocommerce',
                    'breadcrumbs',
                ],
                'priority' => 50,
            ],
            'social' => [
                'label' => __('Social', 'wp-seo-next'),
                'paths' => [
                    'social',
                ],
                'priority' => 49,
            ],
            'sitemap' => [
                'label' => __('Sitemap', 'wp-seo-next'),
                'paths' => [
                    'sitemap',
                ],
                'priority' => 48,
            ],
            'ai' => [
                'label' => __('AI', 'wp-seo-next'),
                'paths' => [
                    'ai',
                ],
                'priority' => 40,
            ],
            'search_console' => [
                'label' => __('Search Console', 'wp-seo-next'),
                'paths' => [
                    'search_console',
                ],
                'priority' => 40,
            ],
            'serp_gap' => [
                'label' => __('SERP Gap', 'wp-seo-next'),
                'paths' => [
                    'serp_gap',
                ],
                'priority' => 35,
            ],
            'indexnow' => [
                'label' => __('IndexNow', 'wp-seo-next'),
                'paths' => [
                    'indexnow',
                ],
                'priority' => 35,
            ],
            'performance' => [
                'label' => __('Performance', 'wp-seo-next'),
                'paths' => [
                    'performance',
                ],
                'priority' => 30,
            ],
            'compatibility' => [
                'label' => __('Compatibility', 'wp-seo-next'),
                'paths' => [
                    'compatibility',
                ],
                'priority' => 30,
            ],
            'privacy' => [
                'label' => __('Privacy', 'wp-seo-next'),
                'paths' => [
                    'privacy',
                ],
                'priority' => 30,
            ],
            'automation' => [
                'label' => __('Automation', 'wp-seo-next'),
                'paths' => [
                    'automation',
                ],
                'priority' => 30,
            ],
            'white_label' => [
                'label' => __('White label', 'wp-seo-next'),
                'paths' => [
                    'white_label',
                ],
                'priority' => 20,
            ],
            'ui' => [
                'label' => __('UI', 'wp-seo-next'),
                'paths' => [
                    'ui',
                ],
                'priority' => 20,
            ],
            'licensing' => [
                'label' => __('Licensing', 'wp-seo-next'),
                'paths' => [
                    'licensing',
                ],
                'priority' => 10,
            ],
            'saas_sync' => [
                'label' => __('SaaS Sync', 'wp-seo-next'),
                'paths' => [
                    'saas_sync',
                ],
                'priority' => 10,
            ],
        ];
    }

    public static function isKnownGroup(string $groupId): bool
    {
        $groups = self::groups();
        return isset($groups[$groupId]);
    }

    /**
     * Classify a dot-path key into a diff group.
     */
    public static function classifyKey(string $dotKey): string
    {
        $dotKey = trim($dotKey);
        if ($dotKey === '') {
            return 'other';
        }

        $groups = self::groups();
        // Sort by priority (desc).
        uasort($groups, static function ($a, $b) {
            $pa = isset($a['priority']) ? (int) $a['priority'] : 0;
            $pb = isset($b['priority']) ? (int) $b['priority'] : 0;
            return $pb <=> $pa;
        });

        foreach ($groups as $gid => $g) {
            $paths = isset($g['paths']) && is_array($g['paths']) ? $g['paths'] : [];
            foreach ($paths as $p) {
                $p = (string) $p;
                if ($p === '') {
                    continue;
                }
                if ($dotKey === $p || str_starts_with($dotKey, $p . '.')) {
                    return (string) $gid;
                }
            }
        }

        return 'other';
    }

    /**
     * @param array<string,string> $flatSnapshot
     * @param array<string,string> $flatCurrent
     * @return array<string,array<int,array{key:string,snapshot:string,current:string}>>
     */
    public static function groupChanges(array $flatSnapshot, array $flatCurrent): array
    {
        $keys = array_unique(array_merge(array_keys($flatSnapshot), array_keys($flatCurrent)));
        sort($keys);

        $grouped = [];
        foreach ($keys as $k) {
            $va = $flatSnapshot[$k] ?? '';
            $vb = $flatCurrent[$k] ?? '';
            if ($va === $vb) {
                continue;
            }
            $gid = self::classifyKey((string) $k);
            if (!isset($grouped[$gid])) {
                $grouped[$gid] = [];
            }
            $grouped[$gid][] = [
                'key' => (string) $k,
                'snapshot' => (string) $va,
                'current' => (string) $vb,
            ];
        }

        // Sort groups by priority, keep 'other' last.
        $groups = self::groups();
        uksort($grouped, static function ($a, $b) use ($groups) {
            if ($a === 'other') {
                return 1;
            }
            if ($b === 'other') {
                return -1;
            }
            $pa = isset($groups[$a]['priority']) ? (int) $groups[$a]['priority'] : 0;
            $pb = isset($groups[$b]['priority']) ? (int) $groups[$b]['priority'] : 0;
            if ($pa === $pb) {
                return strcmp((string) $a, (string) $b);
            }
            return $pb <=> $pa;
        });

        return $grouped;
    }

    /**
     * Apply selected groups from snapshot settings into current settings.
     *
     * For associative arrays we do a recursive merge (preserves keys that did not exist when snapshot was taken).
     * For list arrays (numeric keys), we replace.
     *
     * @param array<string,mixed> $current
     * @param array<string,mixed> $snapshot
     * @param array<int,string> $groupIds
     * @return array<string,mixed>
     */
    public static function applyGroups(array $current, array $snapshot, array $groupIds): array
    {
        $defs = self::groups();
        foreach ($groupIds as $gid) {
            $gid = sanitize_key((string) $gid);
            if ($gid === '' || !isset($defs[$gid])) {
                continue;
            }
            $paths = $defs[$gid]['paths'] ?? [];
            if (!is_array($paths)) {
                continue;
            }
            foreach ($paths as $path) {
                $path = (string) $path;
                if ($path === '') {
                    continue;
                }
                if (!self::pathExists($snapshot, $path)) {
                    // Snapshot (older version) does not contain this path; skip to avoid deleting newer settings.
                    continue;
                }

                $sv = self::getPath($snapshot, $path);
                $cv = self::getPath($current, $path);

                if (is_array($sv) && is_array($cv)) {
                    if (self::isList($sv) || self::isList($cv)) {
                        self::setPath($current, $path, $sv);
                    } else {
                        $merged = self::mergeAssoc($cv, $sv);
                        self::setPath($current, $path, $merged);
                    }
                } else {
                    self::setPath($current, $path, $sv);
                }
            }
        }
        return $current;
    }

    /**
     * @param array<string,mixed> $arr
     */
    private static function pathExists(array $arr, string $path): bool
    {
        $parts = array_values(array_filter(explode('.', $path), static fn($p) => $p !== ''));
        if (empty($parts)) {
            return false;
        }
        $cur = $arr;
        foreach ($parts as $p) {
            if (!is_array($cur) || !array_key_exists($p, $cur)) {
                return false;
            }
            $cur = $cur[$p];
        }
        return true;
    }

    /**
     * @param array<string,mixed> $arr
     * @return mixed
     */
    private static function getPath(array $arr, string $path)
    {
        $parts = array_values(array_filter(explode('.', $path), static fn($p) => $p !== ''));
        $cur = $arr;
        foreach ($parts as $p) {
            if (!is_array($cur) || !array_key_exists($p, $cur)) {
                return null;
            }
            $cur = $cur[$p];
        }
        return $cur;
    }

    /**
     * @param array<string,mixed> $arr
     * @param mixed $value
     */
    private static function setPath(array &$arr, string $path, $value): void
    {
        $parts = array_values(array_filter(explode('.', $path), static fn($p) => $p !== ''));
        if (empty($parts)) {
            return;
        }
        $cur =& $arr;
        $last = array_pop($parts);
        foreach ($parts as $p) {
            if (!isset($cur[$p]) || !is_array($cur[$p])) {
                $cur[$p] = [];
            }
            $cur =& $cur[$p];
        }
        $cur[$last] = $value;
    }

    /**
     * @param array<mixed> $arr
     */
    private static function isList(array $arr): bool
    {
        if ($arr === []) {
            return true;
        }
        $keys = array_keys($arr);
        return $keys === range(0, count($arr) - 1);
    }

    /**
     * Merge associative arrays recursively: values from $incoming override $base.
     *
     * @param array<string,mixed> $base
     * @param array<string,mixed> $incoming
     * @return array<string,mixed>
     */
    private static function mergeAssoc(array $base, array $incoming): array
    {
        foreach ($incoming as $k => $v) {
            if (is_array($v) && isset($base[$k]) && is_array($base[$k]) && !self::isList($v) && !self::isList($base[$k])) {
                $base[$k] = self::mergeAssoc($base[$k], $v);
            } else {
                $base[$k] = $v;
            }
        }
        return $base;
    }
}
