<?php

namespace WPSN\Core;

defined('ABSPATH') || exit;

/**
 * A tiny "undo runs" registry.
 *
 * Goal:
 * - Provide a unified way for migrations/imports to register a rollback handle.
 * - Keep implementation lightweight (ring buffer in wp_options).
 *
 * A "run" is a small record that points to a rollback strategy:
 * - config_snapshot: restore a config snapshot
 * - redirects_migration: undo a redirects migration job
 */
final class UndoRuns
{
    private const OPTION = 'wpsn_undo_runs';
    private const MAX_ITEMS = 50;

    /**
     * Record a run (prepends it to the list).
     *
     * @param array<string,mixed> $run
     */
    public static function record(array $run): string
    {
        if (!function_exists('get_option') || !function_exists('update_option')) {
            return '';
        }

        $id = isset($run['id']) && is_string($run['id']) ? trim($run['id']) : '';
        if ($id === '') {
            $id = self::uuid();
            $run['id'] = $id;
        }

        if (!isset($run['created_at'])) {
            $run['created_at'] = gmdate('c');
        }
        if (!isset($run['status'])) {
            $run['status'] = 'active';
        }

        try {
            $all = get_option(self::OPTION, []);
            $all = is_array($all) ? $all : [];

            // Remove any existing run with same id.
            $filtered = [];
            foreach ($all as $r) {
                if (!is_array($r)) {
                    continue;
                }
                $rid = isset($r['id']) ? (string) $r['id'] : '';
                if ($rid === $id) {
                    continue;
                }
                $filtered[] = $r;
            }

            array_unshift($filtered, $run);
            if (count($filtered) > self::MAX_ITEMS) {
                $filtered = array_slice($filtered, 0, self::MAX_ITEMS);
            }

            update_option(self::OPTION, $filtered, false);
        } catch (\Throwable $e) {
            return '';
        }

        return $id;
    }

    /**
     * @return array<string,mixed>|null
     */
    public static function get(string $runId): ?array
    {
        $runId = trim((string) $runId);
        if ($runId === '' || !function_exists('get_option')) {
            return null;
        }

        $all = get_option(self::OPTION, []);
        $all = is_array($all) ? $all : [];
        foreach ($all as $r) {
            if (!is_array($r)) {
                continue;
            }
            if ((string) ($r['id'] ?? '') === $runId) {
                return $r;
            }
        }
        return null;
    }

    /**
     * Patch a run in-place.
     *
     * @param array<string,mixed> $patch
     */
    public static function update(string $runId, array $patch): bool
    {
        $runId = trim((string) $runId);
        if ($runId === '' || !function_exists('get_option') || !function_exists('update_option')) {
            return false;
        }

        try {
            $all = get_option(self::OPTION, []);
            $all = is_array($all) ? $all : [];
            $changed = false;

            foreach ($all as $i => $r) {
                if (!is_array($r)) {
                    continue;
                }
                if ((string) ($r['id'] ?? '') !== $runId) {
                    continue;
                }
                $all[$i] = array_merge($r, $patch);
                $changed = true;
                break;
            }

            if ($changed) {
                update_option(self::OPTION, $all, false);
            }

            return $changed;
        } catch (\Throwable $e) {
            return false;
        }
    }

    /**
     * @return array<int,array<string,mixed>>
     */
    public static function list(int $limit = 30): array
    {
        $limit = max(1, min(200, (int) $limit));
        if (!function_exists('get_option')) {
            return [];
        }

        $all = get_option(self::OPTION, []);
        $all = is_array($all) ? $all : [];

        $out = [];
        foreach ($all as $r) {
            if (!is_array($r)) {
                continue;
            }
            $out[] = $r;
            if (count($out) >= $limit) {
                break;
            }
        }
        return $out;
    }

    /**
     * Find the most recent run matching a type and meta key/value.
     *
     * @return array<string,mixed>|null
     */
    public static function findLatest(string $type, string $metaKey, string $metaValue): ?array
    {
        $type = sanitize_key((string) $type);
        $metaKey = sanitize_key((string) $metaKey);
        $metaValue = (string) $metaValue;

        if ($type === '' || $metaKey === '' || !function_exists('get_option')) {
            return null;
        }

        $all = get_option(self::OPTION, []);
        $all = is_array($all) ? $all : [];

        foreach ($all as $r) {
            if (!is_array($r)) {
                continue;
            }
            if ((string) ($r['type'] ?? '') !== $type) {
                continue;
            }
            $meta = isset($r['meta']) && is_array($r['meta']) ? $r['meta'] : [];
            if ((string) ($meta[$metaKey] ?? '') === $metaValue) {
                return $r;
            }
        }

        return null;
    }

    private static function uuid(): string
    {
        if (function_exists('wp_generate_uuid4')) {
            return (string) wp_generate_uuid4();
        }
        return uniqid('wpsn_run_', true);
    }
}
