<?php
declare(strict_types=1);

namespace App\Http;

use App\Seo\SeoBuilder;
use App\Support\Translator;
use PDO;

final class QuestionApiController
{
    private const DEFAULT_PER_PAGE = 20;
    private const MAX_PER_PAGE = 100;

    public function __construct(
        private readonly PDO $pdo,
        string $canonicalBaseUrl = '',
        private readonly QuestionSerializer $serializer = new QuestionSerializer(),
        private readonly string $locale = 'en',
        private readonly ?Translator $translator = null,
        private readonly string $fallbackLocale = 'ar',
    ) {
        $this->seo = new SeoBuilder($canonicalBaseUrl);
    }

    private readonly SeoBuilder $seo;

    public function listQuestions(array $query): JsonResponse
    {
        $page = $this->boundedInt($query['page'] ?? null, 1, 1000000, 1);
        $perPage = $this->boundedInt($query['per_page'] ?? null, 1, self::MAX_PER_PAGE, self::DEFAULT_PER_PAGE);
        [$where, $params, $filters] = $this->filters($query);

        $from = ' FROM questions q
                  LEFT JOIN users u ON u.id = q.user_id
                  LEFT JOIN categories c ON c.id = q.category_id';
        $whereSql = ' WHERE q.status = :published_status' . $where;
        $params['published_status'] = 'published';

        $count = $this->pdo->prepare('SELECT COUNT(*)' . $from . $whereSql);
        $count->execute($params);
        $total = (int) $count->fetchColumn();

        $totalPages = max(1, (int) ceil($total / $perPage));
        $page = min($page, $totalPages);
        $offset = ($page - 1) * $perPage;
        $select = $this->pdo->prepare(
            'SELECT q.id, q.user_id, q.category_id, q.content_locale, q.title, q.body, q.slug,
                    q.status, q.duplicate_of_id, q.is_anonymous, q.views_count,
                    q.answers_count, q.votes_count, q.comments_count, q.best_answer_id,
                    q.meta_description, q.published_at, q.created_at, q.updated_at,
                    u.name AS author_name, u.email AS author_email,
                    c.name AS category_name, c.slug AS category_slug' .
            $from .
            $whereSql .
            ' ORDER BY COALESCE(q.published_at, q.created_at) DESC, q.id DESC
              LIMIT ' . $perPage . ' OFFSET ' . $offset
        );
        $select->execute($params);

        $questions = [];

        foreach ($select->fetchAll(PDO::FETCH_ASSOC) as $row) {
            $questions[] = $this->serializer->question($row);
        }

        return JsonResponse::success($questions, [
            'page' => $page,
            'per_page' => $perPage,
            'total' => $total,
            'total_pages' => $totalPages,
            'filters' => $filters,
        ]);
    }

    public function showQuestion(string $identifier, array $query = []): JsonResponse
    {
        $identifier = trim($identifier);

        if ($identifier === '' || strlen($identifier) > 191) {
            return JsonResponse::failure(404, 'question_not_found', $this->questionNotFoundMessage());
        }
        $byId = ctype_digit($identifier);
        $lookupSql = $byId ? 'q.id = :identifier' : 'q.slug = :identifier';
        $statement = $this->pdo->prepare(
            'SELECT q.id, q.user_id, q.category_id, q.content_locale, q.title, q.body, q.slug,
                    q.status, q.duplicate_of_id, q.is_anonymous, q.views_count,
                    q.answers_count, q.votes_count, q.comments_count, q.best_answer_id,
                    q.meta_description, q.published_at, q.created_at, q.updated_at,
                    u.name AS author_name, u.email AS author_email,
                    c.name AS category_name, c.slug AS category_slug
             FROM questions q
             LEFT JOIN users u ON u.id = q.user_id
             LEFT JOIN categories c ON c.id = q.category_id
             WHERE ' . $lookupSql . '
               AND q.status = :status
             LIMIT 1'
        );
        $statement->execute(['identifier' => $byId ? (int)$identifier : $identifier, 'status' => 'published']);
        $question = $statement->fetch(PDO::FETCH_ASSOC);

        if (!is_array($question)) {
            return JsonResponse::failure(404, 'question_not_found', $this->questionNotFoundMessage());
        }

        $answersPage = $this->boundedInt($query['answers_page'] ?? null, 1, 1000000, 1);
        $answersPerPage = $this->boundedInt($query['answers_per_page'] ?? null, 1, 100, 50);
        $answersCount = $this->pdo->prepare("SELECT COUNT(*) FROM answers WHERE question_id=:question_id AND status='published'");
        $answersCount->execute(['question_id' => (int) $question['id']]);
        $answersTotal = (int) $answersCount->fetchColumn();
        $answersPages = max(1, (int) ceil($answersTotal / $answersPerPage));
        $answersPage = min($answersPage, $answersPages);
        $answers = $this->answersFor((int) $question['id'], $answersPage, $answersPerPage);
        $data = $this->serializer->question($question, $answers);
        $questionSlug = trim((string)($question['slug'] ?? ''));
        $questionPath = '/' . (function_exists('quaeriva_question_path') ? quaeriva_question_path($questionSlug !== '' ? $questionSlug : (string)$question['id'], (int)($question['id'] ?? 0)) : 'q/' . rawurlencode($questionSlug));
        $data['seo'] = [
            'canonical_url' => $this->seo->canonicalUrl($questionPath),
            'json_ld' => $this->seo->jsonLdForQuestion($question, $answers),
        ];

        return JsonResponse::success($data, ['answers' => [
            'page' => $answersPage,
            'per_page' => $answersPerPage,
            'total' => $answersTotal,
            'total_pages' => $answersPages,
        ]]);
    }

    /**
     * @return array{0: string, 1: array<string, mixed>, 2: array<string, string>}
     */
    private function filters(array $query): array
    {
        $where = '';
        $params = [];
        $filters = [];

        $category = $this->queryText($query['category'] ?? null);

        if ($category !== null) {
            if (ctype_digit($category)) {
                $where .= ' AND c.id = :category_id';
                $params['category_id'] = (int) $category;
            } else {
                $where .= ' AND c.slug = :category_slug';
                $params['category_slug'] = $category;
            }

            $filters['category'] = $category;
        }

        $tag = $this->queryText($query['tag'] ?? null);

        if ($tag !== null) {
            if (ctype_digit($tag)) {
                $where .= ' AND EXISTS (
                    SELECT 1 FROM question_tags qt
                    INNER JOIN tags t ON t.id = qt.tag_id
                    WHERE qt.question_id = q.id AND t.id = :tag_id
                )';
                $params['tag_id'] = (int) $tag;
            } else {
                $where .= ' AND EXISTS (
                    SELECT 1 FROM question_tags qt
                    INNER JOIN tags t ON t.id = qt.tag_id
                    WHERE qt.question_id = q.id AND t.slug = :tag_slug
                )';
                $params['tag_slug'] = $tag;
            }

            $filters['tag'] = $tag;
        }

        $search = $this->queryText($query['search'] ?? null);

        if ($search !== null) {
            $search = str_replace(['|', '%', '_'], ['||', '|%', '|_'], $search);
            $where .= " AND (q.title LIKE :search_title ESCAPE '|' OR q.body LIKE :search_body ESCAPE '|')";
            $params['search_title'] = '%' . $search . '%';
            $params['search_body'] = '%' . $search . '%';
            $filters['search'] = $search;
        }

        $answered = $this->queryText($query['answered'] ?? null);
        if ($answered === 'yes') { $where .= ' AND q.answers_count > 0'; $filters['answered']='yes'; }
        elseif ($answered === 'no') { $where .= ' AND q.answers_count = 0'; $filters['answered']='no'; }

        $locale = $this->queryText($query['locale'] ?? null);
        if ($locale !== null && preg_match('/^[a-z]{2,8}(?:-[a-z0-9]{2,8})?$/i', $locale) === 1) {
            $where .= ' AND q.content_locale = :content_locale'; $params['content_locale']=strtolower($locale); $filters['locale']=strtolower($locale);
        }

        $member = $this->queryText($query['member'] ?? null);
        if ($member !== null) { $where .= ' AND u.username = :member_username'; $params['member_username']=$member; $filters['member']=$member; }

        $date = $this->queryText($query['date'] ?? null);
        $days = match ($date) { '7d'=>7, '30d'=>30, '365d'=>365, default=>0 };
        if ($days > 0) {
            $where .= ' AND q.created_at >= DATE_SUB(NOW(), INTERVAL ' . $days . ' DAY)';
            $filters['date']=$date;
        }

        return [$where, $params, $filters];
    }

    /** @return array<int, array<string, mixed>> */
    private function answersFor(int $questionId, int $page = 1, int $perPage = 50): array
    {
        $page = max(1, $page);
        $perPage = max(1, min(100, $perPage));
        $offset = ($page - 1) * $perPage;
        $statement = $this->pdo->prepare(
            "SELECT a.id, a.question_id, a.user_id, a.body, a.votes_count, a.is_best,
                    a.created_at, a.updated_at, u.name AS user_name, u.email AS user_email
             FROM answers a
             LEFT JOIN users u ON u.id = a.user_id
             WHERE a.question_id = :question_id AND a.status='published'
             ORDER BY a.is_best DESC, a.created_at ASC, a.id ASC
             LIMIT " . $perPage . ' OFFSET ' . $offset
        );
        $statement->execute(['question_id' => $questionId]);

        return $statement->fetchAll(PDO::FETCH_ASSOC);
    }

    private function queryText(mixed $value): ?string
    {
        if (!is_scalar($value)) {
            return null;
        }

        $value = trim((string) $value);

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

        $value = preg_replace('/[\x00-\x1F\x7F]/u', '', $value) ?? $value;

        return function_exists('mb_substr') ? mb_substr($value, 0, 200, 'UTF-8') : substr($value, 0, 200);
    }

    private function questionNotFoundMessage(): string
    {
        $translator = $this->translator ?? new Translator($this->locale, $this->fallbackLocale);
        return $translator->get('api.question_not_found');
    }

    private function boundedInt(mixed $value, int $minimum, int $maximum, int $default): int
    {
        if (!is_scalar($value) || !is_numeric((string) $value)) {
            return $default;
        }

        return max($minimum, min($maximum, (int) $value));
    }
}
