<?php
declare(strict_types=1);

namespace App\Seo;

use PDO;
use InvalidArgumentException;
use App\Support\CoreHooks;
use App\Support\FileLogger;
use App\Support\Hooks;

final class SitemapService
{
    public const PAGE_SIZE = 20000;

    public function __construct(private readonly PDO $pdo, private readonly SeoBuilder $seo) {}

    /** @return list<array{loc:string,lastmod?:string}> */
    public function indexEntries(int $pageSize = self::PAGE_SIZE): array
    {
        $pageSize = max(1000, min(45000, $pageSize));
        $entries = [];
        foreach (['questions', 'topics', 'profiles'] as $type) {
            $count = $this->countHighVolume($type);
            for ($page = 1, $pages = max(1, (int) ceil($count / $pageSize)); $page <= $pages; $page++) {
                if ($count === 0) break;
                $entries[] = ['loc' => $this->seo->canonicalUrl('/sitemaps/' . $type . '-' . $page . '.xml')];
            }
        }
        if ($this->countCategories() > 0) $entries[] = ['loc' => $this->seo->canonicalUrl('/sitemaps/categories.xml')];
        if ($this->countTags() > 0) $entries[] = ['loc' => $this->seo->canonicalUrl('/sitemaps/tags.xml')];
        return $this->filterEntries($entries, 'index');
    }

    /** @return list<array{loc:string,lastmod?:string}> */
    public function childEntries(string $name, int $pageSize = self::PAGE_SIZE): array
    {
        $pageSize = max(1000, min(45000, $pageSize));
        if (preg_match('/^(questions|topics|profiles)-(\d+)$/', $name, $m) === 1) {
            $page = max(1, (int) $m[2]);
            $entries = match ($m[1]) {
                'questions' => $this->questions($page, $pageSize),
                'topics' => $this->topics($page, $pageSize),
                'profiles' => $this->profiles($page, $pageSize),
            };
            return $this->filterEntries($entries, $name);
        }
        $entries = match ($name) {
            'categories' => $this->categories(),
            'tags' => $this->tags(),
            default => throw new InvalidArgumentException('Unknown sitemap child.'),
        };
        return $this->filterEntries($entries, $name);
    }

    /** @return list<array{loc:string,lastmod?:string}> */
    private function questions(int $page, int $pageSize): array
    {
        $offset = ($page - 1) * $pageSize;
        $stmt = $this->pdo->prepare("SELECT id,slug,status,published_at,updated_at,seo_index,canonical_url FROM questions WHERE status='published' AND slug IS NOT NULL AND slug<>'' ORDER BY COALESCE(published_at,updated_at) DESC,id DESC LIMIT :limit OFFSET :offset");
        $stmt->bindValue(':limit', $pageSize, PDO::PARAM_INT); $stmt->bindValue(':offset', $offset, PDO::PARAM_INT); $stmt->execute();
        return $this->seo->sitemapEntries($stmt->fetchAll(PDO::FETCH_ASSOC));
    }

    /** @return list<array{loc:string,lastmod?:string}> */
    private function topics(int $page, int $pageSize): array
    {
        $offset = ($page - 1) * $pageSize;
        $stmt = $this->pdo->prepare("SELECT slug,created_at,updated_at,COALESCE(last_activity_at,updated_at,created_at) AS last_activity_at FROM topics WHERE status='published' AND slug IS NOT NULL AND slug<>'' ORDER BY COALESCE(last_activity_at,updated_at,created_at) DESC,id DESC LIMIT :limit OFFSET :offset");
        $stmt->bindValue(':limit', $pageSize, PDO::PARAM_INT); $stmt->bindValue(':offset', $offset, PDO::PARAM_INT); $stmt->execute();
        $entries = [];
        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $topic) {
            $slug = trim((string) ($topic['slug'] ?? '')); if ($slug === '') continue;
            $path = function_exists('quaeriva_topic_path') ? quaeriva_topic_path($slug) : 'd/' . $slug;
            $entry = ['loc' => $this->seo->canonicalUrl('/' . $path)];
            if (($last = $this->lastmod($topic['last_activity_at'] ?? null)) !== null) $entry['lastmod'] = $last;
            $entries[] = $entry;
        }
        return $entries;
    }

    /** @return list<array{loc:string,lastmod?:string}> */

    /** Taxonomy sitemaps are capped rather than paged, so warn instead of silently dropping URLs. */
    private function warnIfTruncated(string $kind, int $count): void
    {
        if ($count < self::PAGE_SIZE) return;
        (new FileLogger())->warning('Sitemap URL limit reached for ' . $kind . ' (' . self::PAGE_SIZE . '); extra URLs were truncated.');
    }

    private function categories(): array
    {
        $stmt = $this->pdo->query("SELECT c.slug,c.updated_at,c.seo_index,c.canonical_url FROM categories c WHERE c.slug IS NOT NULL AND c.slug<>'' AND (EXISTS (SELECT 1 FROM questions q WHERE q.category_id=c.id AND q.status='published') OR EXISTS (SELECT 1 FROM topics t WHERE t.category_id=c.id AND t.status='published')) ORDER BY c.updated_at DESC,c.id DESC LIMIT " . self::PAGE_SIZE);
        $entries=[];
        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
            if (strtolower(trim((string) ($row['seo_index'] ?? 'inherit'))) === 'noindex') continue;
            $slug=trim((string)($row['slug']??'')); if($slug==='')continue;
            $canonical=trim((string)($row['canonical_url']??'')); if($canonical!==''&&!str_starts_with($canonical,'/'))continue;
            $entry=['loc'=>$canonical!==''?$this->seo->canonicalUrl($canonical):$this->seo->canonicalUrl('/c/'.rawurlencode($slug))];
            if(($last=$this->lastmod($row['updated_at']??null))!==null)$entry['lastmod']=$last; $entries[]=$entry;
        }
        $this->warnIfTruncated('taxonomy', count($entries));
        return $entries;
    }

    /** @return list<array{loc:string,lastmod?:string}> */
    private function tags(): array
    {
        $stmt=$this->pdo->query("SELECT t.slug,t.updated_at,t.seo_index,t.canonical_url FROM tags t WHERE t.slug IS NOT NULL AND t.slug<>'' AND EXISTS (SELECT 1 FROM question_tags qt INNER JOIN questions q ON q.id=qt.question_id WHERE qt.tag_id=t.id AND q.status='published') ORDER BY t.updated_at DESC,t.id DESC LIMIT " . self::PAGE_SIZE);
        $entries=[];
        foreach($stmt->fetchAll(PDO::FETCH_ASSOC) as $row){if(strtolower(trim((string)($row['seo_index']??'inherit')))==='noindex')continue;$slug=trim((string)($row['slug']??''));if($slug==='')continue;$canonical=trim((string)($row['canonical_url']??''));if($canonical!==''&&!str_starts_with($canonical,'/'))continue;$entry=['loc'=>$canonical!==''?$this->seo->canonicalUrl($canonical):$this->seo->canonicalUrl('/t/'.rawurlencode($slug))];if(($last=$this->lastmod($row['updated_at']??null))!==null)$entry['lastmod']=$last;$entries[]=$entry;}
        $this->warnIfTruncated('taxonomy', count($entries));
        return $entries;
    }

    /** @return list<array{loc:string,lastmod?:string}> */
    private function profiles(int $page, int $pageSize): array
    {
        $offset=($page-1)*$pageSize;
        $stmt=$this->pdo->prepare("SELECT u.username,u.updated_at FROM users u WHERE u.username IS NOT NULL AND u.username<>'' AND u.status='active' AND COALESCE(u.account_type,'member')='member' AND (COALESCE(TRIM(u.bio),'')<>'' OR EXISTS(SELECT 1 FROM questions q WHERE q.user_id=u.id AND q.status='published' AND q.is_anonymous=0) OR EXISTS(SELECT 1 FROM answers a INNER JOIN questions q2 ON q2.id=a.question_id WHERE a.user_id=u.id AND a.status='published' AND q2.status='published') OR EXISTS(SELECT 1 FROM topics t WHERE t.user_id=u.id AND t.status='published')) ORDER BY u.updated_at DESC,u.id DESC LIMIT :limit OFFSET :offset");
        $stmt->bindValue(':limit',$pageSize,PDO::PARAM_INT);$stmt->bindValue(':offset',$offset,PDO::PARAM_INT);$stmt->execute();$entries=[];
        foreach($stmt->fetchAll(PDO::FETCH_ASSOC) as $row){$username=trim((string)($row['username']??''));if($username==='')continue;$entry=['loc'=>$this->seo->canonicalUrl('/u/'.rawurlencode($username))];if(($last=$this->lastmod($row['updated_at']??null))!==null)$entry['lastmod']=$last;$entries[]=$entry;}
        return $entries;
    }

    private function countHighVolume(string $type): int
    {
        $sql=match($type){
            'questions'=>"SELECT COUNT(*) FROM questions WHERE status='published' AND slug IS NOT NULL AND slug<>'' AND COALESCE(seo_index,'inherit')<>'noindex'",
            'topics'=>"SELECT COUNT(*) FROM topics WHERE status='published' AND slug IS NOT NULL AND slug<>''",
            'profiles'=>"SELECT COUNT(*) FROM users u WHERE u.username IS NOT NULL AND u.username<>'' AND u.status='active' AND COALESCE(u.account_type,'member')='member' AND (COALESCE(TRIM(u.bio),'')<>'' OR EXISTS(SELECT 1 FROM questions q WHERE q.user_id=u.id AND q.status='published' AND q.is_anonymous=0) OR EXISTS(SELECT 1 FROM answers a INNER JOIN questions q2 ON q2.id=a.question_id WHERE a.user_id=u.id AND a.status='published' AND q2.status='published') OR EXISTS(SELECT 1 FROM topics t WHERE t.user_id=u.id AND t.status='published'))",
            default=>throw new InvalidArgumentException('Unknown sitemap type.'),
        };
        return (int)$this->pdo->query($sql)->fetchColumn();
    }

    private function countCategories(): int{return (int)$this->pdo->query("SELECT COUNT(*) FROM categories c WHERE c.slug IS NOT NULL AND c.slug<>'' AND COALESCE(c.seo_index,'inherit')<>'noindex' AND (EXISTS (SELECT 1 FROM questions q WHERE q.category_id=c.id AND q.status='published') OR EXISTS (SELECT 1 FROM topics t WHERE t.category_id=c.id AND t.status='published'))")->fetchColumn();}
    private function countTags(): int{return (int)$this->pdo->query("SELECT COUNT(*) FROM tags t WHERE t.slug IS NOT NULL AND t.slug<>'' AND COALESCE(t.seo_index,'inherit')<>'noindex' AND EXISTS (SELECT 1 FROM question_tags qt INNER JOIN questions q ON q.id=qt.question_id WHERE qt.tag_id=t.id AND q.status='published')")->fetchColumn();}

    /** @param list<array{loc:string,lastmod?:string}> $entries @return list<array{loc:string,lastmod?:string}> */
    private function filterEntries(array $entries, string $scope): array
    {
        $filtered = Hooks::filter(CoreHooks::SITEMAP_ENTRIES, $entries, ['scope' => $scope]);
        if (!is_array($filtered)) return $entries;
        $safe = [];
        foreach ($filtered as $entry) {
            if (!is_array($entry)) continue;
            $loc = trim((string) ($entry['loc'] ?? ''));
            if ($loc === '' || filter_var($loc, FILTER_VALIDATE_URL) === false) continue;
            $safeEntry = ['loc' => $loc];
            $lastmod = trim((string) ($entry['lastmod'] ?? ''));
            if ($lastmod !== '' && strtotime($lastmod) !== false) $safeEntry['lastmod'] = $lastmod;
            $safe[] = $safeEntry;
        }
        return $safe;
    }

    private function lastmod(mixed $value): ?string{$value=trim((string)$value);if($value===''||($time=strtotime($value))===false)return null;return gmdate('c',$time);}
}
