<?php

declare(strict_types=1);

namespace PpPhp\Core\Seo;

/**
 * Per-record SEO meta for generated view pages (platform feature A2).
 *
 * Given a page's SEO config + the loaded record, produces the document title and
 * the <meta>/<link> block (description, canonical, robots, Open Graph, Twitter).
 * The canonical URL is the A1 slug URL, so every record has one canonical address
 * search engines can consolidate on.
 *
 * Pure and side-effect-free (no globals, no I/O) → fully unit-testable. The
 * generated view page calls build() at runtime with $row + the request's base URL;
 * an empty/disabled config yields the default title and NO extra tags, so a page
 * that hasn't opted in is unchanged.
 *
 * Config shape (a page's settings.seo, shared with A1):
 *   [ 'prefix'=>'q', 'slug_column'=>'slug', 'title_column'=>'title',
 *     'title_template'=>'{title} — PPPHP Community',
 *     'description_field'=>'body_md'  (or 'description_template'=>'{excerpt}'),
 *     'og_image_field'=>'cover_url', 'og_image_default'=>'https://…/og.png',
 *     'og_type'=>'article', 'robots'=>'index'|'noindex' ]
 *
 * @return array{title:string, tags:string}  title is HTML-escaped for <title>.
 */
final class MetaTags
{
    /**
     * @param array<string,mixed> $cfg
     * @param array<string,mixed> $row
     * @param array{base?:string,id?:int|string,default_title?:string} $ctx
     * @return array{title:string, tags:string}
     */
    public static function build(array $cfg, array $row, array $ctx): array
    {
        $e = static fn (mixed $s): string => htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8');

        // ── title ──
        $title = self::expand((string) ($cfg['title_template'] ?? ''), $row);
        if (trim($title) === '') {
            $title = (string) ($ctx['default_title'] ?? '');
        }
        $title = self::clip($title, 70);

        // ── description ──
        $desc = '';
        if (!empty($cfg['description_template'])) {
            $desc = self::expand((string) $cfg['description_template'], $row);
        } elseif (!empty($cfg['description_field'])) {
            $desc = (string) ($row[(string) $cfg['description_field']] ?? '');
        }
        $desc = self::clip(self::stripMarkup($desc), 160);

        // ── canonical (the A1 slug URL) ──
        $canonical = '';
        $prefix = (string) ($cfg['prefix'] ?? '');
        $id = (string) ($ctx['id'] ?? '');
        if ($prefix !== '' && $id !== '') {
            $slug = SlugRouter::canonicalSlug($row, (string) ($cfg['slug_column'] ?? ''), (string) ($cfg['title_column'] ?? ''), $id);
            $canonical = rtrim((string) ($ctx['base'] ?? ''), '/') . SlugRouter::canonicalPath($prefix, $id, $slug);
        }

        // ── og:image ──
        $ogImage = '';
        if (!empty($cfg['og_image_field'])) {
            $ogImage = (string) ($row[(string) $cfg['og_image_field']] ?? '');
        }
        if ($ogImage === '') {
            $ogImage = (string) ($cfg['og_image_default'] ?? '');
        }

        $robots = ((string) ($cfg['robots'] ?? 'index')) === 'noindex'
            ? 'noindex, follow'
            : 'index, follow, max-image-preview:large';
        $ogType = (string) ($cfg['og_type'] ?? 'article');

        // ── assemble ──
        $t = '<meta name="robots" content="' . $e($robots) . '">';
        if ($desc !== '')      { $t .= '<meta name="description" content="' . $e($desc) . '">'; }
        if ($canonical !== '') { $t .= '<link rel="canonical" href="' . $e($canonical) . '">'; }
        $t .= '<meta property="og:type" content="' . $e($ogType) . '">';
        $t .= '<meta property="og:title" content="' . $e($title) . '">';
        if ($desc !== '')      { $t .= '<meta property="og:description" content="' . $e($desc) . '">'; }
        if ($canonical !== '') { $t .= '<meta property="og:url" content="' . $e($canonical) . '">'; }
        if ($ogImage !== '')   { $t .= '<meta property="og:image" content="' . $e($ogImage) . '">'; }
        $t .= '<meta name="twitter:card" content="' . ($ogImage !== '' ? 'summary_large_image' : 'summary') . '">';
        $t .= '<meta name="twitter:title" content="' . $e($title) . '">';
        if ($desc !== '')      { $t .= '<meta name="twitter:description" content="' . $e($desc) . '">'; }
        if ($ogImage !== '')   { $t .= '<meta name="twitter:image" content="' . $e($ogImage) . '">'; }

        return ['title' => $e($title), 'tags' => $t];
    }

    /** Replace {field} placeholders with record values. */
    private static function expand(string $tpl, array $row): string
    {
        if ($tpl === '' || !str_contains($tpl, '{')) {
            return $tpl;
        }
        return preg_replace_callback(
            '/\{([A-Za-z0-9_]+)\}/',
            static fn (array $m): string => (string) ($row[$m[1]] ?? ''),
            $tpl
        ) ?? $tpl;
    }

    /** Reduce HTML/Markdown-ish text to plain words for a meta description. */
    private static function stripMarkup(string $s): string
    {
        $s = strip_tags($s);
        $s = preg_replace('/[#*`>_~\[\]()]+/', ' ', $s) ?? $s;
        return trim(preg_replace('/\s+/', ' ', $s) ?? $s);
    }

    /** Collapse whitespace and clip to a length with an ellipsis. */
    private static function clip(string $s, int $max): string
    {
        $s = trim(preg_replace('/\s+/', ' ', $s) ?? $s);
        if ($max > 0 && mb_strlen($s) > $max) {
            $s = rtrim(mb_substr($s, 0, $max - 1)) . '…';
        }
        return $s;
    }
}
