<?php
declare(strict_types=1);

namespace App\Questions;

use App\Support\TextNormalizer;
use PDO;

final class SearchService
{
    private readonly TextNormalizer $normalizer;

    public function __construct(
        private readonly PDO $pdo,
        ?TextNormalizer $normalizer = null,
    ) {
        $this->normalizer = $normalizer ?? new TextNormalizer();
    }

    /**
     * @param array<string,mixed> $filters
     * @return array{items:list<array<string,mixed>>,total:int,page:int,per_page:int,filters:array<string,string>}
     */
    public function search(array $filters, int $page = 1, int $perPage = 20): array
    {
        $page = max(1, $page);
        $perPage = max(1, min(50, $perPage));
        $normalized = $this->normalizeFilters($filters);
        $where = ["q.status = 'published'"];
        $params = [];

        if ($normalized['q'] !== '') {
            $where[] = "(COALESCE(q.title_normalized,q.title) LIKE :q_title ESCAPE '|' OR COALESCE(q.body_normalized,q.body) LIKE :q_body ESCAPE '|')";
            $needle = '%' . str_replace(['|', '%', '_'], ['||', '|%', '|_'], $this->normalizer->normalize($normalized['q'], $normalized['locale'] !== '' ? $normalized['locale'] : null)) . '%';
            $params['q_title'] = $needle;
            $params['q_body'] = $needle;
        }
        if ($normalized['category'] !== '') {
            $where[] = 'c.slug = :category';
            $params['category'] = $normalized['category'];
        }
        if ($normalized['tag'] !== '') {
            $where[] = 'EXISTS (SELECT 1 FROM question_tags qtf INNER JOIN tags tf ON tf.id=qtf.tag_id WHERE qtf.question_id=q.id AND tf.slug=:tag)';
            $params['tag'] = $normalized['tag'];
        }
        if ($normalized['answered'] === 'answered') {
            $where[] = 'q.answers_count > 0';
        } elseif ($normalized['answered'] === 'unanswered') {
            $where[] = 'q.answers_count = 0';
        }
        if ($normalized['date'] !== '') {
            $days = match ($normalized['date']) { '7d' => 7, '30d' => 30, '365d' => 365, default => 0 };
            if ($days > 0) {
                $where[] = 'q.created_at >= DATE_SUB(NOW(), INTERVAL ' . $days . ' DAY)';
            }
        }
        if ($normalized['member'] !== '') {
            $where[] = 'u.username = :member';
            $params['member'] = $normalized['member'];
        }
        if ($normalized['locale'] !== '') {
            $where[] = 'q.content_locale = :locale';
            $params['locale'] = $normalized['locale'];
        }

        $whereSql = implode(' AND ', $where);
        $count = $this->pdo->prepare(
            'SELECT COUNT(DISTINCT q.id) FROM questions q '
            . 'LEFT JOIN categories c ON c.id=q.category_id '
            . 'LEFT JOIN users u ON u.id=q.user_id '
            . 'WHERE ' . $whereSql
        );
        $count->execute($params);
        $total = (int) $count->fetchColumn();

        $offset = ($page - 1) * $perPage;
        $statement = $this->pdo->prepare(
            'SELECT q.id,q.slug,q.title,q.body,q.content_locale,q.votes_count,q.answers_count,q.comments_count,q.views_count,q.created_at, '
            . "COALESCE(c.name,'') AS category_name, COALESCE(c.slug,'') AS category_slug, "
            . "CASE WHEN q.is_anonymous=1 OR u.id IS NULL THEN '' ELSE COALESCE(u.name,'') END AS author_name, "
            . "CASE WHEN q.is_anonymous=1 OR u.id IS NULL THEN '' ELSE COALESCE(u.username,'') END AS author_username "
            . 'FROM questions q '
            . 'LEFT JOIN categories c ON c.id=q.category_id '
            . 'LEFT JOIN users u ON u.id=q.user_id '
            . 'WHERE ' . $whereSql . ' '
            . 'ORDER BY q.created_at DESC,q.id DESC LIMIT ' . $perPage . ' OFFSET ' . $offset
        );
        $statement->execute($params);

        return [
            'items' => $statement->fetchAll(PDO::FETCH_ASSOC),
            'total' => $total,
            'page' => $page,
            'per_page' => $perPage,
            'filters' => $normalized,
        ];
    }

    /** @return array{categories:list<array<string,mixed>>,tags:list<array<string,mixed>>} */
    public function options(): array
    {
        $categories = $this->pdo->query("SELECT id,name,slug,content_locale FROM categories ORDER BY sort_order ASC,name ASC,id ASC")->fetchAll(PDO::FETCH_ASSOC);
        $tags = $this->pdo->query("SELECT id,name,slug,content_locale FROM tags ORDER BY name ASC,id ASC")->fetchAll(PDO::FETCH_ASSOC);
        return ['categories' => $categories, 'tags' => $tags];
    }

    /** @param array<string,mixed> $filters @return array<string,string> */
    private function normalizeFilters(array $filters): array
    {
        $text = static function (mixed $value, int $max): string {
            $value = trim((string) $value);
            if ($value === '') return '';
            return function_exists('mb_substr') ? mb_substr($value, 0, $max, 'UTF-8') : substr($value, 0, $max);
        };
        $slug = static function (mixed $value) use ($text): string {
            $value = strtolower($text($value, 191));
            return preg_match('/^[a-z0-9%][a-z0-9_%-]*$/Di', $value) === 1 ? $value : '';
        };
        $locale = strtolower(str_replace('_', '-', $text($filters['locale'] ?? '', 16)));
        if ($locale !== '' && preg_match('/^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$/D', $locale) !== 1) $locale = '';
        $answered = strtolower($text($filters['answered'] ?? '', 16));
        if (!in_array($answered, ['answered', 'unanswered'], true)) $answered = '';
        $date = strtolower($text($filters['date'] ?? '', 16));
        if (!in_array($date, ['7d', '30d', '365d'], true)) $date = '';
        return [
            'q' => $text($filters['q'] ?? '', 200),
            'category' => $slug($filters['category'] ?? ''),
            'tag' => $slug($filters['tag'] ?? ''),
            'answered' => $answered,
            'date' => $date,
            'member' => (static function (string $value): string { return preg_match('/^[a-z0-9][a-z0-9._-]{1,79}$/D', strtolower($value)) === 1 ? strtolower($value) : ''; })($text($filters['member'] ?? '', 80)),
            'locale' => $locale,
        ];
    }
}
