<?php

namespace WPSN\Modules\BulkTable;

use WPSN\Modules\BulkTable\Admin\PermalinkRewrite;

defined('ABSPATH') || exit;

/**
 * Storage/reuse for Bulk Table permalink rewrite presets.
 *
 * These presets are used for staging → production mapping when importing CSV/JSON.
 * We expose them to agency tooling (Config Sync / Profiles) and WP-CLI.
 */
final class RewritePresetsRepository
{
    public const OPTION_NAME = 'wpsn_bulk_table_rewrite_presets';

    public const CONFLICT_UPDATE = 'update';
    public const CONFLICT_KEEP_BOTH = 'keep_both';

    /**
     * Get all presets (sanitized + sorted).
     *
     * @return array<string,array{name:string,rules:string}>
     */
    public function all(): array
    {
        $v = get_option(self::OPTION_NAME, []);
        if (!is_array($v)) {
            $v = [];
        }

        $presets = $this->sanitizePresets($v);

        uasort($presets, static function ($a, $b): int {
            $an = is_array($a) ? (string) ($a['name'] ?? '') : '';
            $bn = is_array($b) ? (string) ($b['name'] ?? '') : '';
            return strcmp($an, $bn);
        });

        return $presets;
    }

    /**
     * Replace all presets.
     *
     * @param array<string,mixed> $presets
     */
    public function replace(array $presets): void
    {
        $clean = $this->sanitizePresets($presets);
        update_option(self::OPTION_NAME, $clean, false);
    }

    /**
     * Diff incoming presets against current site.
     *
     * This is used for Config Sync / Profiles preview, so agencies can see what will change.
     *
     * @param array<string,mixed> $incoming
     * @param bool $replaceExisting
     * @param string $onConflict One of: update|keep_both
     * @return array{incoming:int,existing:int,added:int,updated:int,skipped:int,renamed:int,total:int}
     */
    public function diff(array $incoming, bool $replaceExisting = false, string $onConflict = self::CONFLICT_UPDATE): array
    {
        $res = $this->simulateImport($incoming, $replaceExisting, $onConflict, false);
        unset($res['renamed_sample'], $res['_final']);
        return $res;
    }

    /**
     * Import presets (merge/update, merge/keep-both, or replace).
     *
     * @param array<string,mixed> $incoming
     * @param bool $replaceExisting
     * @param string $onConflict One of: update|keep_both
     * @return array{incoming:int,existing:int,added:int,updated:int,skipped:int,renamed:int,total:int,renamed_sample:array<int,array{from:string,to:string,name:string}>}
     */
    public function import(array $incoming, bool $replaceExisting = false, string $onConflict = self::CONFLICT_UPDATE): array
    {
        $res = $this->simulateImport($incoming, $replaceExisting, $onConflict, true);

        // Persist (sanitize + clamp).
        if (isset($res['_final']) && is_array($res['_final'])) {
            $this->replace($res['_final']);
        }
        unset($res['_final']);

        return $res;
    }

    /**
     * @param array<string,mixed> $incoming
     * @return array<string,mixed>
     */
    private function simulateImport(array $incoming, bool $replaceExisting, string $onConflict, bool $mutate): array
    {
        $onConflict = $onConflict === self::CONFLICT_KEEP_BOTH ? self::CONFLICT_KEEP_BOTH : self::CONFLICT_UPDATE;

        /** @var array<string,array{name:string,rules:string}> $existing */
        $existing = $replaceExisting ? [] : $this->all();
        $existingCount = count($existing);

        $added = 0;
        $updated = 0;
        $skipped = 0;
        $renamed = 0;
        $renamedSample = [];

        $incomingCount = 0;

        foreach ($incoming as $id => $p) {
            $incomingCount++;
            $norm = $this->normalizeIncomingPreset($id, $p);
            if ($norm === null) {
                $skipped++;
                continue;
            }

            $pid = $norm['id'];
            $name = $norm['name'];
            $rules = $norm['rules'];

            // New preset.
            if (!isset($existing[$pid])) {
                if (count($existing) >= 50) {
                    $skipped++;
                    continue;
                }
                $added++;
                $existing[$pid] = [
                    'name' => $name,
                    'rules' => $rules,
                ];
                continue;
            }

            // Same id exists.
            if ($this->isSamePreset($existing[$pid], ['name' => $name, 'rules' => $rules])) {
                $skipped++;
                continue;
            }

            // Conflict.
            if ($onConflict === self::CONFLICT_KEEP_BOTH && !$replaceExisting) {
                if (count($existing) >= 50) {
                    $skipped++;
                    continue;
                }

                $newId = $this->generateUniqueId($pid, $name, $rules, $existing);
                if ($newId === '') {
                    $skipped++;
                    continue;
                }

                $importName = $name;
                $existingName = (string) ($existing[$pid]['name'] ?? '');
                if ($existingName !== '' && $existingName === $name) {
                    $importName = $name . ' ' . __('(imported)', 'wp-seo-next');
                }

                $existing[$newId] = [
                    'name' => $importName,
                    'rules' => $rules,
                ];

                $added++;
                $renamed++;
                if (count($renamedSample) < 20) {
                    $renamedSample[] = [
                        'from' => $pid,
                        'to' => $newId,
                        'name' => $importName,
                    ];
                }
                continue;
            }

            // Default: update existing.
            $updated++;
            $existing[$pid] = [
                'name' => $name,
                'rules' => $rules,
            ];
        }

        $out = [
            'incoming' => $incomingCount,
            'existing' => $existingCount,
            'added' => $added,
            'updated' => $updated,
            'skipped' => $skipped,
            'renamed' => $renamed,
            'total' => $added + $updated,
            'renamed_sample' => $renamedSample,
        ];

        if ($mutate) {
            $out['_final'] = $existing;
        }

        return $out;
    }

    /**
     * Normalize a single incoming preset.
     *
     * @param mixed $id
     * @param mixed $p
     * @return array{id:string,name:string,rules:string}|null
     */
    private function normalizeIncomingPreset($id, $p): ?array
    {
        if (!is_array($p)) {
            return null;
        }

        $pid = sanitize_key(is_string($id) ? $id : '');
        $name = isset($p['name']) ? sanitize_text_field((string) $p['name']) : '';
        $rules = isset($p['rules']) ? (string) $p['rules'] : '';
        $rules = str_replace(["\r\n", "\r"], "\n", $rules);
        $rules = trim($rules);

        if ($name === '' || $rules === '') {
            return null;
        }

        if (empty(PermalinkRewrite::parseRules($rules))) {
            return null;
        }

        if ($pid === '') {
            $pid = substr(sha1($name), 0, 12);
        }

        return [
            'id' => $pid,
            'name' => $name,
            'rules' => $rules,
        ];
    }

    /**
     * @param array{name:string,rules:string} $a
     * @param array{name:string,rules:string} $b
     */
    private function isSamePreset(array $a, array $b): bool
    {
        return (string) ($a['name'] ?? '') === (string) ($b['name'] ?? '')
            && (string) ($a['rules'] ?? '') === (string) ($b['rules'] ?? '');
    }

    /**
     * Generate a new unique id for keep-both import.
     *
     * @param array<string,array{name:string,rules:string}> $existing
     */
    private function generateUniqueId(string $conflictingId, string $name, string $rules, array $existing): string
    {
        $tries = 0;
        while ($tries < 10) {
            $tries++;
            $seed = function_exists('wp_generate_uuid4') ? wp_generate_uuid4() : uniqid('wpsn_rp_', true);
            $nid = substr(sha1($conflictingId . '|' . $name . '|' . $rules . '|' . $seed), 0, 12);
            if ($nid !== '' && !isset($existing[$nid])) {
                return $nid;
            }
        }
        return '';
    }

    /**
     * @param array<string,mixed> $presets
     * @return array<string,array{name:string,rules:string}>
     */
    private function sanitizePresets(array $presets): array
    {
        $clean = [];
        $count = 0;

        foreach ($presets as $id => $p) {
            if ($count >= 50) {
                break;
            }
            if (!is_array($p)) {
                continue;
            }

            $pid = sanitize_key(is_string($id) ? $id : '');
            $name = isset($p['name']) ? sanitize_text_field((string) $p['name']) : '';
            $rules = isset($p['rules']) ? (string) $p['rules'] : '';
            $rules = str_replace(["\r\n", "\r"], "\n", $rules);
            $rules = trim($rules);

            if ($pid === '' || $name === '' || $rules === '') {
                continue;
            }

            if (empty(PermalinkRewrite::parseRules($rules))) {
                continue;
            }

            $clean[$pid] = [
                'name' => $name,
                'rules' => $rules,
            ];
            $count++;
        }

        return $clean;
    }
}
