<?php

namespace WPSN\Modules\Redirects;

use WPSN\Core\Cache;
use WPSN\Core\Logger;

defined('ABSPATH') || exit;

final class RedirectRepository
{
    private const REGEX_CACHE_KEY = 'redirects_enabled_regex_rules';

    private Cache $cache;

    private Logger $logger;

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

    private function tableName(): string
    {
        global $wpdb;
        return $wpdb->prefix . 'wpsn_redirects';
    }

    /**
     * Find the best matching rule for a given request path.
     *
     * Order of precedence:
     *  1) exact
     *  2) prefix (longest prefix wins)
     *  3) regex (longest stored pattern wins, then newest)
     *
     * @return array<string, mixed>|null
     */
    public function findMatch(string $path, bool $allowRegex = true): ?array
    {
        global $wpdb;

        $path = RedirectsUrl::normalizeFrom($path);
        if ($path === '') {
            return null;
        }

        // Exact match first.
        $table = $this->tableName();
        $sql = $wpdb->prepare(
            "SELECT * FROM {$table} WHERE enabled = 1 AND match_type = 'exact' AND from_url = %s ORDER BY id DESC LIMIT 1",
            $path
        );
        $row = $wpdb->get_row($sql, ARRAY_A);
        if (is_array($row)) {
            return $row;
        }

        // Prefix match: choose the longest from_url.
        $sql = $wpdb->prepare(
            "SELECT * FROM {$table} WHERE enabled = 1 AND match_type = 'prefix' AND %s LIKE CONCAT(from_url, '%%') ORDER BY LENGTH(from_url) DESC, id DESC LIMIT 1",
            $path
        );

        $row = $wpdb->get_row($sql, ARRAY_A);
        if (is_array($row)) {
            return $row;
        }

        if (!$allowRegex) {
            return null;
        }

        foreach ($this->getEnabledRegexRules() as $rule) {
            $pattern = isset($rule['from_url']) ? (string) $rule['from_url'] : '';
            if ($pattern === '') {
                continue;
            }

            if (RedirectRegex::matches($pattern, $path)) {
                return $rule;
            }
        }

        return null;
    }

    /**
     * Find an existing rule by from_url + match_type (for upsert/migration).
     *
     * @return array<string, mixed>|null
     */
    public function findByFromAndType(string $fromUrl, string $matchType = 'exact'): ?array
    {
        global $wpdb;

        $matchType = $this->sanitizeMatchType($matchType);
        $fromUrl = $this->normalizeStoredFrom($fromUrl, $matchType);

        if ($fromUrl === '') {
            return null;
        }

        $table = $this->tableName();
        $sql = $wpdb->prepare(
            "SELECT * FROM {$table} WHERE from_url = %s AND match_type = %s ORDER BY id DESC LIMIT 1",
            $fromUrl,
            $matchType
        );

        $row = $wpdb->get_row($sql, ARRAY_A);
        return is_array($row) ? $row : null;
    }

    /**
     * Insert/update by unique key (from_url + match_type).
     *
     * @param array<string, mixed> $data
     * @return array{action: string, id: int}
     */
    public function upsertByFromAndType(array $data, bool $overwriteExisting = false): array
    {
        $from = (string) ($data['from_url'] ?? '');
        $type = (string) ($data['match_type'] ?? 'exact');

        $existing = $this->findByFromAndType($from, $type);
        if (is_array($existing) && !empty($existing['id'])) {
            $id = (int) $existing['id'];
            if (!$overwriteExisting) {
                return ['action' => 'skipped', 'id' => $id];
            }

            $ok = $this->update($id, $data);
            return ['action' => $ok ? 'updated' : 'error', 'id' => $id];
        }

        $id = $this->insert($data);
        return ['action' => $id > 0 ? 'inserted' : 'error', 'id' => $id];
    }

    /**
     * @param array<string, mixed> $data
     */
    public function insert(array $data): int
    {
        global $wpdb;

        $table = $this->tableName();
        $now = current_time('mysql');
        $matchType = $this->sanitizeMatchType((string) ($data['match_type'] ?? 'exact'));
        $fromUrl = $this->normalizeStoredFrom((string) ($data['from_url'] ?? ''), $matchType);
        $toUrl = isset($data['to_url']) ? RedirectsUrl::normalizeTo((string) $data['to_url']) : null;
        $statusCode = $this->sanitizeStatusCode((int) ($data['status_code'] ?? 301));

        if ($fromUrl === '') {
            return 0;
        }
        if (!in_array($statusCode, [410, 451], true) && empty($toUrl)) {
            return 0;
        }
        if ($matchType === 'regex') {
            $validation = RedirectRegex::validateRule($fromUrl, (string) $toUrl, $statusCode);
            if (!$validation['ok']) {
                return 0;
            }
        }

        $insert = [
            'from_url'    => $fromUrl,
            'to_url'      => $toUrl,
            'match_type'  => $matchType,
            'status_code' => $statusCode,
            'enabled'     => !empty($data['enabled']) ? 1 : 0,
            'hits'        => 0,
            'last_hit'    => null,
            'created_at'  => $now,
            'updated_at'  => $now,
        ];

        $ok = $wpdb->insert($table, $insert);
        if ($ok) {
            $this->bustMatchCaches();
        }

        return $ok ? (int) $wpdb->insert_id : 0;
    }

    /**
     * @param array<string, mixed> $data
     */
    public function update(int $id, array $data): bool
    {
        global $wpdb;

        if ($id <= 0) {
            return false;
        }

        $table = $this->tableName();
        $now = current_time('mysql');
        $matchType = $this->sanitizeMatchType((string) ($data['match_type'] ?? 'exact'));
        $fromUrl = $this->normalizeStoredFrom((string) ($data['from_url'] ?? ''), $matchType);
        $toUrl = isset($data['to_url']) ? RedirectsUrl::normalizeTo((string) $data['to_url']) : null;
        $statusCode = $this->sanitizeStatusCode((int) ($data['status_code'] ?? 301));

        if ($fromUrl === '') {
            return false;
        }
        if (!in_array($statusCode, [410, 451], true) && empty($toUrl)) {
            return false;
        }
        if ($matchType === 'regex') {
            $validation = RedirectRegex::validateRule($fromUrl, (string) $toUrl, $statusCode);
            if (!$validation['ok']) {
                return false;
            }
        }

        $update = [
            'from_url'    => $fromUrl,
            'to_url'      => $toUrl,
            'match_type'  => $matchType,
            'status_code' => $statusCode,
            'enabled'     => !empty($data['enabled']) ? 1 : 0,
            'updated_at'  => $now,
        ];

        $ok = $wpdb->update($table, $update, ['id' => $id]);
        if ($ok !== false) {
            $this->bustMatchCaches();
        }

        return $ok !== false;
    }

    public function delete(int $id): bool
    {
        global $wpdb;
        if ($id <= 0) {
            return false;
        }
        $table = $this->tableName();
        $ok = $wpdb->delete($table, ['id' => $id]);
        if ($ok !== false) {
            $this->bustMatchCaches();
        }
        return $ok !== false;
    }

    public function toggle(int $id, bool $enabled): bool
    {
        global $wpdb;
        if ($id <= 0) {
            return false;
        }
        $table = $this->tableName();
        $ok = $wpdb->update(
            $table,
            ['enabled' => $enabled ? 1 : 0, 'updated_at' => current_time('mysql')],
            ['id' => $id]
        );
        if ($ok !== false) {
            $this->bustMatchCaches();
        }
        return $ok !== false;
    }

    public function incrementHit(int $id): void
    {
        global $wpdb;
        if ($id <= 0) {
            return;
        }
        $table = $this->tableName();
        $wpdb->query(
            $wpdb->prepare(
                "UPDATE {$table} SET hits = hits + 1, last_hit = %s WHERE id = %d",
                current_time('mysql'),
                $id
            )
        );
    }

    /**
     * @return array<string, mixed>|null
     */
    public function getById(int $id): ?array
    {
        global $wpdb;
        if ($id <= 0) {
            return null;
        }
        $table = $this->tableName();
        $sql = $wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $id);
        $row = $wpdb->get_row($sql, ARRAY_A);
        return is_array($row) ? $row : null;
    }

    /**
     * @return array{items: array<int, array<string, mixed>>, total: int}
     */
    public function list(int $page, int $perPage, string $search = ''): array
    {
        global $wpdb;
        $table = $this->tableName();

        if ($page < 1) {
            $page = 1;
        }
        if ($perPage < 1) {
            $perPage = 20;
        }
        if ($perPage > 200) {
            $perPage = 200;
        }

        $where = '1=1';
        $params = [];

        $search = trim($search);
        if ($search !== '') {
            $like = '%' . $wpdb->esc_like($search) . '%';
            $where .= ' AND (from_url LIKE %s OR to_url LIKE %s)';
            $params[] = $like;
            $params[] = $like;
        }

        $offset = ($page - 1) * $perPage;

        // Total.
        $sqlTotal = "SELECT COUNT(*) FROM {$table} WHERE {$where}";
        if (!empty($params)) {
            $sqlTotal = $wpdb->prepare($sqlTotal, $params);
        }
        $total = (int) $wpdb->get_var($sqlTotal);

        // Items.
        $sqlItems = "SELECT * FROM {$table} WHERE {$where} ORDER BY updated_at DESC, id DESC LIMIT %d OFFSET %d";
        $paramsItems = $params;
        $paramsItems[] = $perPage;
        $paramsItems[] = $offset;
        $sqlItems = $wpdb->prepare($sqlItems, $paramsItems);
        $items = $wpdb->get_results($sqlItems, ARRAY_A);
        if (!is_array($items)) {
            $items = [];
        }

        return [
            'items' => $items,
            'total' => $total,
        ];
    }

    private function sanitizeMatchType(string $type): string
    {
        $type = sanitize_key($type);
        if (!in_array($type, ['exact', 'prefix', 'regex'], true)) {
            $type = 'exact';
        }
        return $type;
    }

    private function sanitizeStatusCode(int $code): int
    {
        // Supported: common redirects + 410/451 (non-redirect terminal responses).
        if (!in_array($code, [301, 302, 307, 308, 410, 451], true)) {
            return 301;
        }
        return $code;
    }

    private function normalizeStoredFrom(string $fromUrl, string $matchType): string
    {
        if ($matchType === 'regex') {
            return RedirectRegex::normalizeStoredPattern($fromUrl);
        }

        return RedirectsUrl::normalizeFrom($fromUrl);
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function getEnabledRegexRules(): array
    {
        $cached = $this->cache->get(self::REGEX_CACHE_KEY);
        if (is_array($cached)) {
            return $cached;
        }

        global $wpdb;
        $table = $this->tableName();
        $sql = "SELECT * FROM {$table} WHERE enabled = 1 AND match_type = 'regex' ORDER BY LENGTH(from_url) DESC, id DESC LIMIT 500";
        $rows = $wpdb->get_results($sql, ARRAY_A);
        if (!is_array($rows)) {
            $rows = [];
        }

        $this->cache->set(self::REGEX_CACHE_KEY, $rows, 300);
        return $rows;
    }

    private function bustMatchCaches(): void
    {
        $this->cache->delete(self::REGEX_CACHE_KEY);
    }
}
