<?php

namespace WPSN\Modules\Redirects;

use WPSN\Core\Logger;
use WPSN\Modules\Revisions\RevisionsInstaller;
use WPSN\Modules\Revisions\RevisionsRepository;

defined('ABSPATH') || exit;

/**
 * Shared undo logic for Redirects Migration.
 *
 * This is used by:
 * - RedirectsMigrationAdmin (one-click undo)
 * - UndoRunsAdmin (unified undo runs framework)
 */
final class RedirectMigrationUndoService
{
    private RedirectRepository $redirects;
    private Logger $logger;

    public function __construct(RedirectRepository $redirects, Logger $logger)
    {
        $this->redirects = $redirects;
        $this->logger = $logger;
    }

    /**
     * Bulk undo for redirects migration (one-click). Safe by default.
     *
     * @return array{deleted:int,restored:int,skipped:int,already:int}
     */
    public function undoJob(string $jobId, bool $force = false): array
    {
        global $wpdb;

        $jobId = sanitize_text_field((string) $jobId);
        if ($jobId === '') {
            return ['deleted' => 0, 'restored' => 0, 'skipped' => 0, 'already' => 0];
        }

        $out = [
            'deleted' => 0,
            'restored' => 0,
            'skipped' => 0,
            'already' => 0,
        ];

        try {
            RevisionsInstaller::maybeInstall();
        } catch (\Throwable $e) {
            // ignore
        }

        $revTable = $wpdb->prefix . 'wpsn_revisions';
        // Find revisions that belong to this migration job.
        $needle = '"migration_job_id":"' . $jobId . '"';
        $like = '%' . $wpdb->esc_like($needle) . '%';

        $sql = $wpdb->prepare(
            "SELECT id, object_id, before_json, after_json, rolled_back_at FROM {$revTable} WHERE source = %s AND object_type = %s AND meta_json LIKE %s ORDER BY id ASC",
            'redirects_migration',
            'redirect',
            $like
        );

        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
        $rows = $wpdb->get_results($sql, ARRAY_A);
        if (!is_array($rows) || empty($rows)) {
            return $out;
        }

        // Group revisions by redirect id so we can restore the state BEFORE the first migration change.
        $groups = [];
        foreach ($rows as $r) {
            if (!is_array($r)) {
                continue;
            }
            $rid = (int) ($r['id'] ?? 0);
            $oid = (int) ($r['object_id'] ?? 0);
            if ($rid <= 0 || $oid <= 0) {
                continue;
            }

            $before = [];
            $after  = [];
            $b = isset($r['before_json']) ? (string) $r['before_json'] : '';
            $a = isset($r['after_json']) ? (string) $r['after_json'] : '';
            if ($b !== '') {
                $tmp = json_decode($b, true);
                if (is_array($tmp)) {
                    $before = $tmp;
                }
            }
            if ($a !== '') {
                $tmp = json_decode($a, true);
                if (is_array($tmp)) {
                    $after = $tmp;
                }
            }

            $rolled = !empty($r['rolled_back_at']);

            if (!isset($groups[$oid])) {
                $groups[$oid] = [
                    'rev_ids' => [$rid],
                    'first_id' => $rid,
                    'last_id' => $rid,
                    'before' => $before['redirect'] ?? null,
                    'after' => $after['redirect'] ?? null,
                    'all_rolled' => $rolled,
                ];
            } else {
                $groups[$oid]['rev_ids'][] = $rid;
                $groups[$oid]['last_id'] = $rid;
                $groups[$oid]['after'] = $after['redirect'] ?? ($groups[$oid]['after'] ?? null);
                $groups[$oid]['all_rolled'] = (bool) ($groups[$oid]['all_rolled'] && $rolled);
            }
        }

        if (empty($groups)) {
            return $out;
        }

        // Apply undo per redirect.
        foreach ($groups as $redirectId => $g) {
            $redirectId = (int) $redirectId;
            if ($redirectId <= 0 || !is_array($g)) {
                continue;
            }

            if (!empty($g['all_rolled'])) {
                $out['already']++;
                continue;
            }

            $desired = isset($g['before']) ? self::sanitizeRedirectRow(is_array($g['before']) ? $g['before'] : null) : null;
            $expected = isset($g['after']) ? self::sanitizeRedirectRow(is_array($g['after']) ? $g['after'] : null) : null;

            $currentRow = $this->redirects->getById($redirectId);
            $current = self::sanitizeRedirectRow(is_array($currentRow) ? $currentRow : null);

            if (!$force) {
                $curCmp = self::redirectComparable($current);
                $expCmp = self::redirectComparable($expected);
                if ($curCmp !== $expCmp) {
                    $out['skipped']++;
                    continue;
                }
            }

            // Snapshot before applying undo for rollback revision entry.
            $beforeUndo = ['redirect' => $current];

            $didChange = false;

            // If desired is null => it didn't exist before migration: delete.
            if ($desired === null) {
                if (is_array($currentRow) && !empty($currentRow['id'])) {
                    $this->redirects->delete((int) $currentRow['id']);
                    $out['deleted']++;
                    $didChange = true;
                } else {
                    // Already absent.
                    $out['already']++;
                }
            } else {
                if (is_array($currentRow) && !empty($currentRow['id'])) {
                    $this->redirects->update($redirectId, $desired);
                    $out['restored']++;
                    $didChange = true;
                } else {
                    // Redirect was deleted after migration — re-insert with the same id.
                    $table = $wpdb->prefix . 'wpsn_redirects';
                    $data = [
                        'id' => $redirectId,
                        'from_url' => isset($desired['from_url']) ? (string) $desired['from_url'] : '',
                        'to_url' => array_key_exists('to_url', $desired) ? $desired['to_url'] : null,
                        'match_type' => isset($desired['match_type']) ? (string) $desired['match_type'] : 'exact',
                        'status_code' => isset($desired['status_code']) ? (int) $desired['status_code'] : 301,
                        'enabled' => !empty($desired['enabled']) ? 1 : 0,
                        'hits' => isset($desired['hits']) ? (int) $desired['hits'] : 0,
                        'last_hit' => isset($desired['last_hit']) && is_string($desired['last_hit']) ? (string) $desired['last_hit'] : null,
                        'created_at' => isset($desired['created_at']) && is_string($desired['created_at']) && $desired['created_at'] !== '' ? (string) $desired['created_at'] : current_time('mysql', true),
                        'updated_at' => isset($desired['updated_at']) && is_string($desired['updated_at']) && $desired['updated_at'] !== '' ? (string) $desired['updated_at'] : current_time('mysql', true),
                    ];

                    // phpcs:ignore WordPress.DB.DirectDatabaseQuery
                    $wpdb->insert($table, $data, ['%d', '%s', '%s', '%s', '%d', '%d', '%d', '%s', '%s', '%s']);
                    $out['restored']++;
                    $didChange = true;
                }
            }

            // Create a rollback revision (even if no-op), then mark all migration revisions as rolled back.
            $afterUndo = ['redirect' => $desired];

            try {
                $revRepo = new RevisionsRepository($this->logger);
                $rollbackId = $revRepo->insert([
                    'object_type' => 'redirect',
                    'object_id' => $redirectId,
                    'object_subtype' => is_array($desired) && !empty($desired['match_type']) ? (string) $desired['match_type'] : '',
                    'action' => 'rollback',
                    'source' => 'revisions',
                    'actor_user_id' => (int) get_current_user_id(),
                    'before' => $beforeUndo,
                    'after' => $afterUndo,
                    'meta' => [
                        'rollback_of' => (int) ($g['last_id'] ?? 0),
                        'bulk_rollback_of' => isset($g['rev_ids']) ? array_values(array_map('intval', (array) $g['rev_ids'])) : [],
                        'migration_job_id' => $jobId,
                        'reason' => 'redirects_migration_undo',
                        'force' => $force ? 1 : 0,
                        'did_change' => $didChange ? 1 : 0,
                    ],
                ]);

                if ($rollbackId > 0 && !empty($g['rev_ids']) && is_array($g['rev_ids'])) {
                    foreach ($g['rev_ids'] as $rid) {
                        $rid = (int) $rid;
                        if ($rid > 0) {
                            $revRepo->markRolledBack($rid, $rollbackId);
                        }
                    }
                }
            } catch (\Throwable $e) {
                // ignore
            }
        }

        return $out;
    }

    /**
     * @param array<string,mixed>|null $row
     * @return array<string,mixed>|null
     */
    private static function sanitizeRedirectRow(?array $row): ?array
    {
        if (!is_array($row)) {
            return null;
        }

        $to = null;
        if (array_key_exists('to_url', $row)) {
            $to = $row['to_url'];
            if (!is_string($to) && $to !== null) {
                $to = null;
            }
        }

        $lastHit = null;
        if (array_key_exists('last_hit', $row)) {
            $lastHit = $row['last_hit'];
            if (!is_string($lastHit) && $lastHit !== null) {
                $lastHit = null;
            }
        }

        return [
            'id' => (int) ($row['id'] ?? 0),
            'from_url' => isset($row['from_url']) ? (string) $row['from_url'] : '',
            'to_url' => $to,
            'match_type' => isset($row['match_type']) ? (string) $row['match_type'] : 'exact',
            'status_code' => isset($row['status_code']) ? (int) $row['status_code'] : 301,
            'enabled' => !empty($row['enabled']),
            'hits' => isset($row['hits']) ? (int) $row['hits'] : 0,
            'last_hit' => $lastHit,
            'created_at' => isset($row['created_at']) ? (string) $row['created_at'] : '',
            'updated_at' => isset($row['updated_at']) ? (string) $row['updated_at'] : '',
        ];
    }

    /**
     * Normalize a redirect row to comparable stable fields.
     *
     * @param array<string,mixed>|null $row
     * @return array<string,mixed>|null
     */
    private static function redirectComparable(?array $row): ?array
    {
        if ($row === null) {
            return null;
        }

        $to = null;
        if (array_key_exists('to_url', $row)) {
            $to = $row['to_url'];
            if ($to !== null) {
                $to = (string) $to;
            }
        }

        return [
            'from_url' => isset($row['from_url']) ? (string) $row['from_url'] : '',
            'to_url' => $to,
            'match_type' => isset($row['match_type']) ? (string) $row['match_type'] : 'exact',
            'status_code' => isset($row['status_code']) ? (int) $row['status_code'] : 301,
            'enabled' => !empty($row['enabled']) ? 1 : 0,
        ];
    }
}
