<?php

namespace WPSN\SEO;

use WPSN\Core\Settings;
use WPSN\Domain\SeoDocumentContext;
use WPSN\Domain\SeoMeta;

defined('ABSPATH') || exit;

/**
 * Shared builder for "headless" head tag output.
 *
 * Used by:
 * - Automation → Public Head API
 * - Stability snapshots (head output snapshots + diffs)
 */
final class PublicHeadBuilder
{
    private Settings $settings;
    private SeoMetaRepository $repo;
    private SchemaGraphBuilder $schemaBuilder;
    private HreflangResolver $hreflangResolver;

    public function __construct(Settings $settings, SeoMetaRepository $repo, SchemaGraphBuilder $schemaBuilder, HreflangResolver $hreflangResolver)
    {
        $this->settings = $settings;
        $this->repo = $repo;
        $this->schemaBuilder = $schemaBuilder;
        $this->hreflangResolver = $hreflangResolver;
    }

    /**
     * Build head payload (resolved meta, schema graph, tags + HTML).
     *
     * @param array<string,bool>|null $modules
     * @return array<string,mixed>
     */
    public function build(SeoDocumentContext $ctx, ?array $modules = null): array
    {
        if ($modules === null) {
            $modules = [
                'meta_tags' => $this->settings->moduleEnabled('meta_tags', true),
                'social'    => $this->settings->moduleEnabled('social', true),
                'schema'    => $this->settings->moduleEnabled('schema', true),
            ];
        }

        $meta = $this->repo->forContext($ctx);

        $schemaGraph = null;
        if (!empty($modules['schema'])) {
            $schemaGraph = $this->schemaBuilder->build($ctx, $meta);
        }

        $extraSocialHtml = '';
        if (!empty($modules['social'])) {
            ob_start();
            /**
             * Allow modules to output extra social tags (WooCommerce, etc.).
             *
             * Matches SocialModule::render() behavior.
             *
             * @since 0.47.0
             */
            do_action('wpsn/social/after', $ctx, $meta);
            $extraSocialHtml = (string) ob_get_clean();
        }

        $head = $this->buildHeadTags($ctx, $meta, $schemaGraph, $modules);
        $headHtml = $this->buildHeadHtml($head, $extraSocialHtml);

        $resolved = $this->formatResolved($meta);
        if (!empty($modules['meta_tags'])) {
            $resolved['hreflang'] = $this->hreflangResolver->resolve($ctx, $meta);
        }

        return [
            'modules' => $modules,
            'resolved' => $resolved,
            'schema_graph' => $schemaGraph,
            'head' => [
                'tags' => $head,
                'html' => $headHtml,
                'extra_social_html' => $extraSocialHtml,
            ],
            // Internal: meta object (not intended for JSON serialization).
            '__meta' => $meta,
        ];
    }

    /**
     * Build structured head tags (arrays) for headless usage.
     *
     * @param array<string,mixed>|null $schemaGraph
     * @param array<string,bool> $modules
     * @return array<string,mixed>
     */
    public function buildHeadTags(SeoDocumentContext $ctx, SeoMeta $meta, ?array $schemaGraph, array $modules): array
    {
        $tags = [
            'title' => '',
            'meta' => [],
            'link' => [],
            'script' => [],
        ];

        if (!empty($modules['meta_tags'])) {
            $title = is_string($meta->title) ? trim($meta->title) : '';
            if ($title !== '') {
                $tags['title'] = $title;
            }

            $desc = is_string($meta->description) ? trim($meta->description) : '';
            if ($desc !== '') {
                $tags['meta'][] = ['name' => 'description', 'content' => $desc];
            }

            $canonical = is_string($meta->canonical) ? trim($meta->canonical) : '';
            if ($canonical === '') {
                $canonical = $ctx->url;
            }
            if ($canonical !== '') {
                $tags['link'][] = ['rel' => 'canonical', 'href' => $canonical];
            }

            foreach ($this->hreflangResolver->resolve($ctx, $meta) as $alternate) {
                $tags['link'][] = [
                    'rel' => 'alternate',
                    'hreflang' => (string) ($alternate['hreflang'] ?? ''),
                    'href' => (string) ($alternate['href'] ?? ''),
                ];
            }

            // Robots
            $robots = is_array($meta->robots) ? $meta->robots : [];
            $robotsContent = $this->formatRobotsContent($robots);
            if ($robotsContent !== '') {
                $tags['meta'][] = ['name' => 'robots', 'content' => $robotsContent];
            }
        }

        if (!empty($modules['social'])) {
            $siteName = get_bloginfo('name');
            $canonical = is_string($meta->canonical) && trim($meta->canonical) !== '' ? trim($meta->canonical) : $ctx->url;

            $locale = get_locale();
            $locale = is_string($locale) ? str_replace('-', '_', $locale) : '';
            if ($locale !== '') {
                $tags['meta'][] = ['property' => 'og:locale', 'content' => $locale];
            }

            $ogType = $ctx->isSingular() ? 'article' : 'website';
            $ogType = apply_filters('wpsn/social/og_type', $ogType, $ctx, $meta);
            $ogType = is_string($ogType) ? trim($ogType) : '';
            if ($ogType === '') {
                $ogType = $ctx->isSingular() ? 'article' : 'website';
            }
            $tags['meta'][] = ['property' => 'og:type', 'content' => $ogType];

            $ogTitle = is_string($meta->ogTitle) ? trim($meta->ogTitle) : '';
            $ogDesc  = is_string($meta->ogDescription) ? trim($meta->ogDescription) : '';
            $ogImage = is_string($meta->ogImage) ? trim($meta->ogImage) : '';

            if ($ogTitle !== '') {
                $tags['meta'][] = ['property' => 'og:title', 'content' => $ogTitle];
            }
            if ($ogDesc !== '') {
                $tags['meta'][] = ['property' => 'og:description', 'content' => $ogDesc];
            }
            if ($canonical !== '') {
                $tags['meta'][] = ['property' => 'og:url', 'content' => $canonical];
            }
            if (is_string($siteName) && $siteName !== '') {
                $tags['meta'][] = ['property' => 'og:site_name', 'content' => $siteName];
            }
            if ($ogImage !== '') {
                $tags['meta'][] = ['property' => 'og:image', 'content' => $ogImage];
            }

            // Twitter overrides (fallback to OG in SeoMetaRepository).
            $twTitle = is_string($meta->twitterTitle) ? trim($meta->twitterTitle) : '';
            $twDesc  = is_string($meta->twitterDescription) ? trim($meta->twitterDescription) : '';
            $twImage = is_string($meta->twitterImage) ? trim($meta->twitterImage) : '';

            $card = $twImage !== '' ? 'summary_large_image' : 'summary';
            $tags['meta'][] = ['name' => 'twitter:card', 'content' => $card];
            if ($twTitle !== '') {
                $tags['meta'][] = ['name' => 'twitter:title', 'content' => $twTitle];
            }
            if ($twDesc !== '') {
                $tags['meta'][] = ['name' => 'twitter:description', 'content' => $twDesc];
            }
            if ($twImage !== '') {
                $tags['meta'][] = ['name' => 'twitter:image', 'content' => $twImage];
            }
        }

        if (!empty($modules['schema']) && is_array($schemaGraph) && !empty($schemaGraph)) {
            $tags['script'][] = [
                'type' => 'application/ld+json',
                'json' => $schemaGraph,
            ];
        }

        return $tags;
    }

    /**
     * Render head tags as a HTML string for headless front-ends.
     *
     * @param array<string,mixed> $head
     */
    public function buildHeadHtml(array $head, string $extraSocialHtml = ''): string
    {
        $lines = [];

        $title = isset($head['title']) && is_string($head['title']) ? trim($head['title']) : '';
        if ($title !== '') {
            $lines[] = '<title>' . esc_html($title) . '</title>';
        }

        $metaTags = isset($head['meta']) && is_array($head['meta']) ? $head['meta'] : [];
        foreach ($metaTags as $m) {
            if (!is_array($m)) {
                continue;
            }
            $content = isset($m['content']) && is_string($m['content']) ? $m['content'] : '';
            if ($content === '') {
                continue;
            }

            if (isset($m['name']) && is_string($m['name']) && $m['name'] !== '') {
                $name = (string) $m['name'];
                // Some meta values are URLs (og:image); but esc_attr is fine for content.
                $lines[] = '<meta name="' . esc_attr($name) . '" content="' . esc_attr($content) . '" />';
            } elseif (isset($m['property']) && is_string($m['property']) && $m['property'] !== '') {
                $prop = (string) $m['property'];
                $isUrl = in_array($prop, ['og:url', 'og:image'], true);
                $val = $isUrl ? esc_url($content) : esc_attr($content);
                $lines[] = '<meta property="' . esc_attr($prop) . '" content="' . $val . '" />';
            }
        }

        $links = isset($head['link']) && is_array($head['link']) ? $head['link'] : [];
        foreach ($links as $l) {
            if (!is_array($l)) {
                continue;
            }
            $rel = isset($l['rel']) && is_string($l['rel']) ? $l['rel'] : '';
            $href = isset($l['href']) && is_string($l['href']) ? $l['href'] : '';
            if ($rel === '' || $href === '') {
                continue;
            }

            $attrs = [
                'rel="' . esc_attr($rel) . '"',
                'href="' . esc_url($href) . '"',
            ];

            if (isset($l['hreflang']) && is_string($l['hreflang']) && trim($l['hreflang']) !== '') {
                $attrs[] = 'hreflang="' . esc_attr((string) $l['hreflang']) . '"';
            }

            $lines[] = '<link ' . implode(' ', $attrs) . ' />';
        }

        $scripts = isset($head['script']) && is_array($head['script']) ? $head['script'] : [];
        foreach ($scripts as $s) {
            if (!is_array($s)) {
                continue;
            }
            $type = isset($s['type']) && is_string($s['type']) ? $s['type'] : '';
            if ($type === '') {
                continue;
            }
            if ($type === 'application/ld+json' && isset($s['json']) && (is_array($s['json']) || is_object($s['json']))) {
                $json = wp_json_encode($s['json'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
                if (is_string($json) && $json !== '') {
                    $lines[] = '<script type="application/ld+json">' . $json . '</script>';
                }
            }
        }

        $extra = trim($extraSocialHtml);
        if ($extra !== '') {
            $lines[] = $extra;
        }

        return implode("\n", $lines) . "\n";
    }

    /**
     * Convert robots array to a meta robots content string (index/follow + directives).
     *
     * @param array<string,mixed> $robots
     */
    public function formatRobotsContent(array $robots): string
    {
        $noindex = !empty($robots['noindex']);
        $nofollow = !empty($robots['nofollow']);

        $parts = [];
        $parts[] = $noindex ? 'noindex' : 'index';
        $parts[] = $nofollow ? 'nofollow' : 'follow';

        foreach (['noarchive', 'nosnippet'] as $k) {
            if (!empty($robots[$k])) {
                $parts[] = $k;
            }
        }

        foreach (['max-image-preview', 'max-snippet', 'max-video-preview'] as $k) {
            if (isset($robots[$k]) && is_string($robots[$k]) && trim($robots[$k]) !== '') {
                $parts[] = $k . ':' . trim($robots[$k]);
            }
        }

        return implode(', ', $parts);
    }

    /**
     * @return array<string,mixed>
     */
    public function formatResolved(SeoMeta $meta): array
    {
        return [
            'title' => (string) ($meta->title ?? ''),
            'description' => (string) ($meta->description ?? ''),
            'canonical' => (string) ($meta->canonical ?? ''),
            'robots' => is_array($meta->robots ?? null) ? $meta->robots : [],
            'og_title' => (string) ($meta->ogTitle ?? ''),
            'og_description' => (string) ($meta->ogDescription ?? ''),
            'og_image' => (string) ($meta->ogImage ?? ''),
            'twitter_title' => (string) ($meta->twitterTitle ?? ''),
            'twitter_description' => (string) ($meta->twitterDescription ?? ''),
            'twitter_image' => (string) ($meta->twitterImage ?? ''),
        ];
    }
}
