<?php
declare(strict_types=1);

namespace App\Seo;

final class SeoBuilder
{
    public function __construct(private readonly string $baseUrl = '')
    {
    }

    public function canonicalUrl(string $path): string
    {
        $path = trim($path);
        $parsedQuery = parse_url($path, PHP_URL_QUERY);
        $querySuffix = is_string($parsedQuery) && $parsedQuery !== '' ? '?' . $parsedQuery : '';
        $parsedPath = parse_url($path, PHP_URL_PATH);
        $path = is_string($parsedPath) ? $parsedPath : $path;
        $baseUrl = rtrim(trim($this->baseUrl), '/');
        $basePath = parse_url($baseUrl, PHP_URL_PATH);

        if (is_string($basePath) && $basePath !== '' && $basePath !== '/' && ($path === $basePath || str_starts_with($path, rtrim($basePath, '/') . '/'))) {
            $path = substr($path, strlen($basePath)) ?: '/';
        }

        $path = '/' . ltrim($path, '/');

        if ($baseUrl === '') {
            return $path . $querySuffix;
        }

        return $baseUrl . $path . $querySuffix;
    }

    public function jsonLdForQuestion(array $question, array $answers, ?int $totalAnswerCount = null): ?array
    {
        if (($question['status'] ?? null) !== 'published') {
            return null;
        }

        $serializedAnswers = [];

        foreach ($answers as $answer) {
            if (!is_array($answer) || trim((string) ($answer['body'] ?? '')) === '') {
                continue;
            }

            $answerId = (int) ($answer['id'] ?? 0);
            $answerData = [
                '@type' => 'Answer',
                'text' => trim((string) $answer['body']),
                'upvoteCount' => (int) ($answer['votes_count'] ?? 0),
                'datePublished' => $this->dateValue($answer['created_at'] ?? null),
                'dateModified' => $this->dateValue($answer['updated_at'] ?? $answer['created_at'] ?? null),
                'author' => $this->authorData($answer, false),
            ];

            if ($answerId > 0 && trim((string) ($question['slug'] ?? '')) !== '') {
                $answerData['url'] = $this->questionUrl((string) $question['slug'], (int) ($question['id'] ?? 0)) . '#answer-' . $answerId;
            }

            $serializedAnswers[] = [
                'data' => array_filter($answerData, static fn (mixed $value): bool => $value !== null && $value !== ''),
                'is_best' => (int) ($answer['is_best'] ?? 0) === 1,
            ];
        }

        // Google Q&A rich-result markup is only useful when the page has at least one answer.
        // Unanswered pages remain indexable through normal HTML/canonical/meta SEO without invalid QAPage markup.
        if ($serializedAnswers === []) {
            return null;
        }

        $mainEntity = [
            '@type' => 'Question',
            'name' => trim((string) ($question['title'] ?? '')),
            'text' => trim((string) ($question['body'] ?? '')),
            'answerCount' => max(count($serializedAnswers), (int) ($totalAnswerCount ?? count($serializedAnswers))),
            'upvoteCount' => (int) ($question['votes_count'] ?? 0),
            'datePublished' => $this->dateValue($question['published_at'] ?? $question['created_at'] ?? null),
            'dateModified' => $this->dateValue($question['updated_at'] ?? $question['published_at'] ?? $question['created_at'] ?? null),
            'author' => (int) ($question['is_anonymous'] ?? 0) === 1
                ? ['@type' => 'Person', 'name' => 'Anonymous']
                : $this->authorData($question, true),
        ];

        $contentLocale = trim((string) ($question['content_locale'] ?? ''));
        if ($contentLocale !== '') {
            $mainEntity['inLanguage'] = $contentLocale;
        }

        if (trim((string) ($question['slug'] ?? '')) !== '') {
            $mainEntity['url'] = $this->questionUrl((string) $question['slug'], (int) ($question['id'] ?? 0));
        }

        $suggestedAnswers = [];
        foreach ($serializedAnswers as $answer) {
            if ($answer['is_best']) {
                $mainEntity['acceptedAnswer'] = $answer['data'];
                continue;
            }
            $suggestedAnswers[] = $answer['data'];
        }

        if ($suggestedAnswers !== []) {
            $mainEntity['suggestedAnswer'] = $suggestedAnswers;
        }

        return [
            '@context' => 'https://schema.org',
            '@type' => 'QAPage',
            'mainEntity' => array_filter($mainEntity, static fn (mixed $value): bool => $value !== null && $value !== ''),
        ];
    }

    /** @return array<string,mixed>|null */
    public function jsonLdForTopic(array $topic): ?array
    {
        if (($topic['status'] ?? null) !== 'published') return null;
        $slug = trim((string) ($topic['slug'] ?? ''));
        $title = trim((string) ($topic['title'] ?? ''));
        if ($slug === '' || $title === '') return null;
        $path = function_exists('quaeriva_topic_path') ? quaeriva_topic_path($slug) : 'd/' . $slug;
        $authorName = trim((string) ($topic['author_name'] ?? $topic['user_name'] ?? ''));
        $author = ['@type' => 'Person', 'name' => $authorName !== '' ? $authorName : 'Quaeriva user'];
        $username = trim((string) ($topic['author_username'] ?? $topic['username'] ?? ''));
        if ($username !== '') $author['url'] = $this->canonicalUrl('/u/' . rawurlencode($username));
        $accountType = strtolower(trim((string) ($topic['author_account_type'] ?? $topic['account_type'] ?? 'member')));
        $isEditorial = in_array($accountType, ['editorial', 'system'], true);
        $data = [
            '@context' => 'https://schema.org',
            '@type' => $isEditorial ? 'WebPage' : 'DiscussionForumPosting',
            'name' => $isEditorial ? $title : null,
            'headline' => $isEditorial ? null : $title,
            'text' => trim((string) ($topic['body'] ?? '')),
            'author' => $author,
            'datePublished' => $this->dateValue($topic['created_at'] ?? null),
            'dateModified' => $this->dateValue($topic['updated_at'] ?? $topic['created_at'] ?? null),
            'url' => $this->canonicalUrl('/' . $path),
            'mainEntityOfPage' => $this->canonicalUrl('/' . $path),
            'commentCount' => max(0, (int) ($topic['comments_count'] ?? 0)),
            'interactionStatistic' => [
                '@type' => 'InteractionCounter',
                'interactionType' => 'https://schema.org/CommentAction',
                'userInteractionCount' => max(0, (int) ($topic['comments_count'] ?? 0)),
            ],
        ];
        $locale = trim((string) ($topic['content_locale'] ?? ''));
        if ($locale !== '') $data['inLanguage'] = $locale;
        return array_filter($data, static fn (mixed $value): bool => $value !== null && $value !== '');
    }

    /** @return array<string,mixed>|null */
    public function breadcrumbForQuestion(array $question, string $homeLabel = 'Home'): ?array
    {
        $slug = trim((string) ($question['slug'] ?? ''));
        $title = trim((string) ($question['title'] ?? ''));
        if (($question['status'] ?? null) !== 'published' || $slug === '' || $title === '') {
            return null;
        }

        $items = [
            [
                '@type' => 'ListItem',
                'position' => 1,
                'name' => $homeLabel,
                'item' => $this->canonicalUrl('/'),
            ],
        ];

        $categoryName = trim((string) ($question['category_name'] ?? ''));
        $categorySlug = trim((string) ($question['category_slug'] ?? ''));
        if ($categoryName !== '' && $categorySlug !== '') {
            $items[] = [
                '@type' => 'ListItem',
                'position' => count($items) + 1,
                'name' => $categoryName,
                'item' => $this->canonicalUrl('/c/' . rawurlencode($categorySlug)),
            ];
        }

        $items[] = [
            '@type' => 'ListItem',
            'position' => count($items) + 1,
            'name' => $title,
            'item' => $this->questionUrl($slug, (int) ($question['id'] ?? 0)),
        ];

        return [
            '@context' => 'https://schema.org',
            '@type' => 'BreadcrumbList',
            'itemListElement' => $items,
        ];
    }

    /**
     * @param array<int, array<string, mixed>> $questions
     * @return array<int, array{loc: string, lastmod?: string}>
     */
    public function sitemapEntries(array $questions): array
    {
        $entries = [];

        foreach ($questions as $question) {
            if (($question['status'] ?? null) !== 'published') {
                continue;
            }

            if (strtolower(trim((string) ($question['seo_index'] ?? 'inherit'))) === 'noindex') {
                continue;
            }

            $slug = trim((string) ($question['slug'] ?? ''));
            if ($slug === '') {
                continue;
            }

            $canonical = trim((string) ($question['canonical_url'] ?? ''));
            $entry = ['loc' => $this->sitemapLocation($slug, $canonical, (int) ($question['id'] ?? 0))];
            if ($entry['loc'] === '') {
                continue;
            }

            $lastModified = $question['updated_at'] ?? $question['published_at'] ?? null;
            if (is_string($lastModified) && trim($lastModified) !== '') {
                $timestamp = strtotime($lastModified);
                if ($timestamp !== false) {
                    $entry['lastmod'] = date(DATE_ATOM, $timestamp);
                }
            }

            $entries[] = $entry;
        }

        return $entries;
    }

    public function questionUrl(string $slug, int $id = 0): string
    {
        // Structured data, the sitemap and the canonical tag must agree with the
        // permalink the visitor is actually on, or Google indexes one URL and
        // finds another.
        $path = function_exists('quaeriva_question_path')
            ? quaeriva_question_path(trim($slug), $id)
            : 'q/' . trim($slug);

        return $this->canonicalUrl('/' . implode('/', array_map('rawurlencode', explode('/', $path))));
    }

    /** @return array<string,mixed> */
    private function authorData(array $row, bool $question): array
    {
        $name = $question ? $this->questionAuthorName($row) : $this->answerAuthorName($row);
        $author = [
            '@type' => 'Person',
            'name' => $name,
        ];

        $username = trim((string) ($row['author_username'] ?? $row['username'] ?? ''));
        if ($username !== '') {
            $author['url'] = $this->canonicalUrl('/u/' . rawurlencode($username));
        }

        return $author;
    }

    private function questionAuthorName(array $question): string
    {
        $name = trim((string) ($question['author_name'] ?? $question['user_name'] ?? ''));
        return $name !== '' ? $name : 'Quaeriva user';
    }

    private function answerAuthorName(array $answer): string
    {
        $name = trim((string) ($answer['user_name'] ?? $answer['author_name'] ?? ''));
        return $name !== '' ? $name : 'Quaeriva user';
    }

    private function sitemapLocation(string $slug, string $canonical, int $id = 0): string
    {
        if ($canonical === '') {
            return $this->questionUrl($slug, $id);
        }

        if (str_starts_with($canonical, '/')) {
            return $this->canonicalUrl($canonical);
        }

        $canonicalParts = parse_url($canonical);
        $baseParts = parse_url($this->baseUrl);
        if (!is_array($canonicalParts) || !is_array($baseParts)) {
            return '';
        }

        $canonicalHost = strtolower((string) ($canonicalParts['host'] ?? ''));
        $baseHost = strtolower((string) ($baseParts['host'] ?? ''));
        if ($canonicalHost === '' || $baseHost === '' || $canonicalHost !== $baseHost) {
            return '';
        }

        return $canonical;
    }

    /** @return array<string,mixed> */
    public function jsonLdForWebSite(string $siteName, string $siteDescription = ''): array
    {
        $siteUrl = $this->canonicalUrl('/');
        $data = [
            '@context' => 'https://schema.org',
            '@type' => 'WebSite',
            'name' => $siteName !== '' ? $siteName : 'Quaeriva',
            'url' => $siteUrl,
            'potentialAction' => [
                '@type' => 'SearchAction',
                'target' => [
                    '@type' => 'EntryPoint',
                    'urlTemplate' => $this->canonicalUrl('/search') . '?q={search_term_string}',
                ],
                'query-input' => 'required name=search_term_string',
            ],
        ];

        if ($siteDescription !== '') {
            $data['description'] = $siteDescription;
        }

        return $data;
    }

    private function dateValue(mixed $value): ?string
    {
        $value = trim((string) $value);
        return $value === '' ? null : $value;
    }
}
