<?php

declare(strict_types=1);

namespace PpPhp\Core\Seo;

/**
 * Dynamic XML sitemap for generated apps (platform feature A4).
 *
 * Enumerates the records of every SEO-enabled page (feature A1/A2) as canonical
 * slug URLs, so search engines can discover and freshness-track every record.
 * Over 50 000 URLs it automatically emits a <sitemapindex> that fans out to
 * per-table chunk sitemaps (?t=<table>&p=<n>), staying within the sitemap spec.
 *
 * urlset() / index() are pure XML builders. respond() is the orchestrator: it is
 * pure too — the DB is injected as `$counter`/`$fetcher` callbacks, so the whole
 * thing is unit-testable with fakes and the generated `sitemap.php` stays a thin
 * wire-up. Everything is XML-escaped.
 */
final class SitemapBuilder
{
    public const MAX_PER_FILE = 50000;

    /**
     * @param iterable<array{loc:string,lastmod?:string,changefreq?:string,priority?:string}> $urls
     */
    public static function urlset(iterable $urls): string
    {
        $x = '<?xml version="1.0" encoding="UTF-8"?>' . "\n"
            . '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
        foreach ($urls as $u) {
            $loc = self::esc((string) ($u['loc'] ?? ''));
            if ($loc === '') {
                continue;
            }
            $x .= '  <url><loc>' . $loc . '</loc>';
            if (!empty($u['lastmod']))    { $x .= '<lastmod>' . self::esc(self::date((string) $u['lastmod'])) . '</lastmod>'; }
            if (!empty($u['changefreq'])) { $x .= '<changefreq>' . self::esc((string) $u['changefreq']) . '</changefreq>'; }
            if (isset($u['priority']) && $u['priority'] !== '') { $x .= '<priority>' . self::esc((string) $u['priority']) . '</priority>'; }
            $x .= '</url>' . "\n";
        }
        return $x . '</urlset>' . "\n";
    }

    /**
     * @param iterable<array{loc:string,lastmod?:string}> $sitemaps
     */
    public static function index(iterable $sitemaps): string
    {
        $x = '<?xml version="1.0" encoding="UTF-8"?>' . "\n"
            . '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
        foreach ($sitemaps as $s) {
            $loc = self::esc((string) ($s['loc'] ?? ''));
            if ($loc === '') {
                continue;
            }
            $x .= '  <sitemap><loc>' . $loc . '</loc>';
            if (!empty($s['lastmod'])) { $x .= '<lastmod>' . self::esc(self::date((string) $s['lastmod'])) . '</lastmod>'; }
            $x .= '</sitemap>' . "\n";
        }
        return $x . '</sitemapindex>' . "\n";
    }

    /**
     * Produce the sitemap body for the current request.
     *
     * @param array<int,array<string,mixed>> $tables  eligible SEO pages (prefix set, not noindex),
     *        each: table, prefix, id_column, slug_column, title_column, lastmod_column, changefreq, priority
     * @param string   $base    absolute base URL (scheme+host+APP_BASE), no trailing slash
     * @param array<string,mixed> $query  $_GET (reads 't' + 'p')
     * @param callable $counter fn(string $table): int
     * @param callable $fetcher fn(string $table, int $limit, int $offset): iterable<array<string,mixed>>
     */
    public static function respond(array $tables, string $base, array $query, callable $counter, callable $fetcher): string
    {
        $base = rtrim($base, '/');
        $byName = [];
        foreach ($tables as $t) {
            if (isset($t['table'])) {
                $byName[(string) $t['table']] = $t;
            }
        }

        // Chunk mode: one table's Nth 50k slice.
        $t = (string) ($query['t'] ?? '');
        if ($t !== '' && isset($byName[$t])) {
            $p = max(1, (int) ($query['p'] ?? 1));
            $rows = $fetcher($t, self::MAX_PER_FILE, ($p - 1) * self::MAX_PER_FILE);
            return self::urlset(self::entries($byName[$t], $base, $rows));
        }

        // Decide single vs index by total count.
        $total = 0;
        $counts = [];
        foreach ($byName as $name => $cfg) {
            $counts[$name] = max(0, (int) $counter($name));
            $total += $counts[$name];
        }

        if ($total <= self::MAX_PER_FILE) {
            $all = [];
            foreach ($byName as $name => $cfg) {
                foreach (self::entries($cfg, $base, $fetcher($name, self::MAX_PER_FILE, 0)) as $e) {
                    $all[] = $e;
                }
            }
            return self::urlset($all);
        }

        // Index: fan out to per-table chunks.
        $children = [];
        foreach ($byName as $name => $cfg) {
            $chunks = max(1, (int) ceil($counts[$name] / self::MAX_PER_FILE));
            for ($i = 1; $i <= $chunks; $i++) {
                $children[] = ['loc' => $base . '/sitemap.xml?t=' . rawurlencode($name) . '&p=' . $i];
            }
        }
        return self::index($children);
    }

    /**
     * Turn DB rows into sitemap URL entries for one page config.
     *
     * @param array<string,mixed> $cfg
     * @param iterable<array<string,mixed>> $rows
     * @return array<int,array{loc:string,lastmod?:string,changefreq?:string,priority?:string}>
     */
    private static function entries(array $cfg, string $base, iterable $rows): array
    {
        $prefix   = (string) ($cfg['prefix'] ?? '');
        $idCol    = (string) ($cfg['id_column'] ?? 'id');
        $slugCol  = (string) ($cfg['slug_column'] ?? '');
        $titleCol = (string) ($cfg['title_column'] ?? '');
        $lastCol  = (string) ($cfg['lastmod_column'] ?? '');
        $out = [];
        foreach ($rows as $row) {
            $id = (string) ($row[$idCol] ?? '');
            if ($id === '' || $prefix === '') {
                continue;
            }
            $slug = SlugRouter::canonicalSlug($row, $slugCol, $titleCol, $id);
            $entry = ['loc' => $base . SlugRouter::canonicalPath($prefix, $id, $slug)];
            if ($lastCol !== '' && !empty($row[$lastCol])) {
                $entry['lastmod'] = (string) $row[$lastCol];
            }
            if (!empty($cfg['changefreq'])) { $entry['changefreq'] = (string) $cfg['changefreq']; }
            if (isset($cfg['priority']) && $cfg['priority'] !== '') { $entry['priority'] = (string) $cfg['priority']; }
            $out[] = $entry;
        }
        return $out;
    }

    private static function esc(string $s): string
    {
        return htmlspecialchars($s, ENT_QUOTES | ENT_XML1, 'UTF-8');
    }

    private static function date(string $s): string
    {
        $ts = strtotime($s);
        return $ts ? date('Y-m-d', $ts) : $s;
    }
}
