<?php

namespace WPSN\SEO;

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

defined('ABSPATH') || exit;

final class TemplateEngine
{
    private Settings $settings;

    private VariableResolver $variableResolver;

    public function __construct(Settings $settings, ?VariableResolver $variableResolver = null)
    {
        $this->settings = $settings;

        // Variable providers (dynamic tokens) are pluggable.
        $this->variableResolver = $variableResolver ?: new VariableResolver($settings);
    }

    public function render(string $template, SeoDocumentContext $ctx): string
    {
        $result = $this->renderInternal($template, $ctx, false);
        return $result['output'] ?? '';
    }

    /**
     * Render with debug info (token resolution).
     *
     * @return array{output:string,tokens?:array<int,array{token:string,expr:string,value:string,selected_branch:string,branches:array<int,array{expr:string,value:string}>}>}
     */
    public function renderWithDebug(string $template, SeoDocumentContext $ctx): array
    {
        return $this->renderInternal($template, $ctx, true);
    }

    /**
     * @return array{output:string,tokens?:array<int,array{token:string,expr:string,value:string,selected_branch:string,branches:array<int,array{expr:string,value:string}>}>}
     */
    private function renderInternal(string $template, SeoDocumentContext $ctx, bool $debug): array
    {
        $vars = $this->defaultVariables($ctx);

        /**
         * Filter template variables used by TemplateEngine.
         *
         * @since 0.1.0
         *
         * @param array<string, string> $vars
         * @param SeoDocumentContext $ctx
         */
        $vars = apply_filters('wpsn/template_variables', $vars, $ctx);

        // Find all %...% tokens in the template.
        $tokens = [];
        if (preg_match_all('/%([^%]+)%/u', $template, $m)) {
            $tokens = array_values(array_unique(array_map('strval', $m[1] ?? [])));
        }

        $replacements = [];
        $debugTokens = [];

        foreach ($tokens as $expr) {
            $expr = trim((string) $expr);
            if ($expr === '') {
                continue;
            }

            $placeholder = '%' . $expr . '%';
            $info = $this->evaluateExpression($expr, $vars, $ctx, $debug);
            $replacements[$placeholder] = (string) ($info['value'] ?? '');

            if ($debug) {
                $debugTokens[] = [
                    'token' => $placeholder,
                    'expr' => $expr,
                    'value' => (string) ($info['value'] ?? ''),
                    'selected_branch' => (string) ($info['selected_branch'] ?? ''),
                    'branches' => (array) ($info['branches'] ?? []),
                ];
            }
        }

        $out = strtr($template, $replacements);

        // Cleanup: collapse whitespace.
        $out = trim(preg_replace('/\s+/u', ' ', $out) ?? '');

        $res = ['output' => $out];
        if ($debug) {
            $res['tokens'] = $debugTokens;
        }
        return $res;
    }

    /**
     * Evaluate a token expression.
     *
     * Supported:
     * - Fallback: a??b??c
     * - Transforms: a|strip|truncate(160)
     *
     * @param array<string,string> $vars
     * @return array{value:string,selected_branch:string,branches:array<int,array{expr:string,value:string}>}
     */
    private function evaluateExpression(string $expr, array $vars, SeoDocumentContext $ctx, bool $debug): array
    {
        $branches = TemplateExpression::splitOutside($expr, '??');
        $branchInfos = [];
        $selected = '';
        $selectedValue = '';

        foreach ($branches as $branch) {
            $branch = trim((string) $branch);
            if ($branch === '') {
                continue;
            }

            $v = $this->evaluatePipeline($branch, $vars, $ctx);
            $branchInfos[] = [
                'expr' => $branch,
                'value' => $v,
            ];

            if ($selectedValue === '' && trim($v) !== '') {
                $selected = $branch;
                $selectedValue = $v;
                // Continue collecting branch info when debug is on.
                if (!$debug) {
                    break;
                }
            }
        }

        return [
            'value' => $selectedValue,
            'selected_branch' => $selected,
            'branches' => $branchInfos,
        ];
    }

    /**
     * Evaluate a single pipeline: var|transform(...)|...
     *
     * @param array<string,string> $vars
     */
    private function evaluatePipeline(string $pipeline, array $vars, SeoDocumentContext $ctx): string
    {
        $parts = TemplateExpression::splitOutside($pipeline, '|');
        if (empty($parts)) {
            return '';
        }

        $varExpr = trim((string) array_shift($parts));
        $value = $this->resolveVariable($varExpr, $vars, $ctx);

        foreach ($parts as $t) {
            $parsed = TemplateExpression::parseTransform((string) $t);
            $name = strtolower((string) ($parsed['name'] ?? ''));
            $args = (array) ($parsed['args'] ?? []);

            if ($name === '') {
                continue;
            }

            $value = $this->applyTransform($value, $name, $args, $ctx);
        }

        return $value;
    }

    /**
     * @param array<string,string> $vars
     */
    private function resolveVariable(string $var, array $vars, SeoDocumentContext $ctx): string
    {
        $var = trim($var);
        if ($var === '') {
            return '';
        }

        if (array_key_exists($var, $vars)) {
            return (string) $vars[$var];
        }

        // Dynamic variables: allow placeholders like %acf:field_name% or %meta:my_key%.
        if (strpos($var, ':') !== false) {
            [$prefix, $key] = explode(':', $var, 2);
            $prefix = strtolower(trim((string) $prefix));
            $key = trim((string) $key);

            if ($prefix !== '' && $key !== '') {
                $res = $this->variableResolver->resolve($prefix, $key, $ctx);
                if (!empty($res['handled'])) {
                    return trim((string) ($res['value'] ?? ''));
                }
                return '';
            }
        }

        return '';
    }

    /**
     * Apply a transform to a value.
     *
     * Built-ins:
     * - lower, upper
     * - trim
     * - strip (strip tags)
     * - truncate(n, ellipsis?)
     * - prefix(text), suffix(text)
     *
     * @param string[] $args
     */
    private function applyTransform(string $value, string $name, array $args, SeoDocumentContext $ctx): string
    {
        $value = (string) $value;

        switch ($name) {
            case 'lower':
                if (function_exists('mb_strtolower')) {
                    return (string) mb_strtolower($value, 'UTF-8');
                }
                return strtolower($value);

            case 'upper':
                if (function_exists('mb_strtoupper')) {
                    return (string) mb_strtoupper($value, 'UTF-8');
                }
                return strtoupper($value);

            case 'trim':
                return trim($value);

            case 'strip':
            case 'strip_tags':
                return trim(wp_strip_all_tags($value));

            case 'truncate':
                $n = isset($args[0]) ? (int) $args[0] : 0;
                $ellipsis = isset($args[1]) ? (string) $args[1] : '…';
                return $this->truncate($value, $n, $ellipsis);

            case 'prefix':
                $p = isset($args[0]) ? (string) $args[0] : '';
                if ($p === '') {
                    return $value;
                }
                return $value !== '' ? ($p . $value) : '';

            case 'suffix':
                $s = isset($args[0]) ? (string) $args[0] : '';
                if ($s === '') {
                    return $value;
                }
                return $value !== '' ? ($value . $s) : '';

            case 'json':
                return wp_json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);

            default:
                /**
                 * Custom transform hook.
                 *
                 * @since 0.66.24
                 *
                 * @param string $value
                 * @param string $name
                 * @param string[] $args
                 * @param SeoDocumentContext $ctx
                 */
                return (string) apply_filters('wpsn/template_transform', $value, $name, $args, $ctx);
        }
    }

    private function truncate(string $value, int $limit, string $ellipsis = '…'): string
    {
        $value = trim($value);
        if ($limit <= 0) {
            return $value;
        }

        $len = function_exists('mb_strlen') ? (int) mb_strlen($value, 'UTF-8') : strlen($value);
        if ($len <= $limit) {
            return $value;
        }

        $ell = (string) $ellipsis;
        $ellLen = $ell !== '' ? (function_exists('mb_strlen') ? (int) mb_strlen($ell, 'UTF-8') : strlen($ell)) : 0;
        if ($ellLen >= $limit) {
            // Keep output <= limit.
            $ell = '';
            $ellLen = 0;
        }
        $sliceLen = max(0, $limit - $ellLen);

        $cut = '';
        if (function_exists('mb_substr')) {
            $cut = (string) mb_substr($value, 0, $sliceLen, 'UTF-8');
        } else {
            $cut = substr($value, 0, $sliceLen);
        }

        $cut = rtrim($cut);
        return $ell !== '' ? ($cut . $ell) : $cut;
    }

    /**
     * @return array<string, string>
     */
    private function defaultVariables(SeoDocumentContext $ctx): array
    {
        $siteName = get_bloginfo('name');
        $siteDesc = get_bloginfo('description');
        $sep      = $this->settings->separator();

        $title = $this->defaultTitle($ctx);
        $excerpt = $this->defaultExcerpt($ctx);

        $page = '';
        $pagenum = '';
        if ($ctx->isPaged()) {
            $pagenum = (string) $ctx->paged;
            /* translators: %s: page number */
            $page = sprintf(__('Page %s', 'wp-seo-next'), $pagenum);
        }

        $vars = [
            'title'    => $title,
            'sitename' => $siteName,
            'sitedesc' => is_string($siteDesc) ? trim($siteDesc) : '',
            'tagline'  => is_string($siteDesc) ? trim($siteDesc) : '',
            'sep'      => $sep,
            'excerpt'  => $excerpt,
            'url'      => $ctx->url,
            'page'     => $page,
            'pagenum'  => $pagenum,
        ];

        // Context-aware variables.
        if ($ctx->isSingular()) {
            $post = get_post((int) $ctx->objectId);
            if ($post instanceof \WP_Post) {
                $vars['id'] = (string) $post->ID;
                $vars['post_id'] = (string) $post->ID;
                $vars['slug'] = (string) $post->post_name;

                $pt = (string) $post->post_type;
                $vars['post_type'] = $pt;
                $ptObj = get_post_type_object($pt);
                if ($ptObj && isset($ptObj->labels)) {
                    $vars['post_type_singular'] = isset($ptObj->labels->singular_name) ? (string) $ptObj->labels->singular_name : $pt;
                    $vars['post_type_plural'] = isset($ptObj->labels->name) ? (string) $ptObj->labels->name : $vars['post_type_singular'];
                } else {
                    $vars['post_type_singular'] = $pt;
                    $vars['post_type_plural'] = $pt;
                }

                $permalink = get_permalink($post);
                $vars['permalink'] = is_string($permalink) ? $permalink : '';

                // Dates.
                $df = get_option('date_format');
                $vars['date'] = (string) get_the_date(is_string($df) ? $df : 'Y-m-d', $post);
                $vars['year'] = (string) get_the_date('Y', $post);
                $vars['month'] = (string) get_the_date('F', $post);
                $vars['monthnum'] = (string) get_the_date('m', $post);
                $vars['day'] = (string) get_the_date('d', $post);

                // Author.
                $authorId = (int) $post->post_author;
                if ($authorId > 0) {
                    $vars += $this->authorVariables($authorId);
                }
            }
        }

        if ($ctx->isTerm()) {
            $term = get_term((int) $ctx->objectId, (string) ($ctx->objectSubType ?: ''));
            if ($term && !is_wp_error($term) && isset($term->term_id)) {
                $vars['id'] = (string) $term->term_id;
                $vars['term_id'] = (string) $term->term_id;
                $vars['term_name'] = isset($term->name) ? (string) $term->name : '';
                $vars['term_slug'] = isset($term->slug) ? (string) $term->slug : '';
                $vars['slug'] = $vars['term_slug'];
                $vars['term_description'] = isset($term->description) ? trim(wp_strip_all_tags((string) $term->description)) : '';

                $tax = isset($term->taxonomy) ? (string) $term->taxonomy : (string) ($ctx->objectSubType ?: '');
                $vars['taxonomy'] = $tax;
                $taxObj = $tax !== '' ? get_taxonomy($tax) : null;
                if ($taxObj && isset($taxObj->labels)) {
                    $vars['taxonomy_singular'] = isset($taxObj->labels->singular_name) ? (string) $taxObj->labels->singular_name : $tax;
                    $vars['taxonomy_plural'] = isset($taxObj->labels->name) ? (string) $taxObj->labels->name : $vars['taxonomy_singular'];
                } else {
                    $vars['taxonomy_singular'] = $tax;
                    $vars['taxonomy_plural'] = $tax;
                }

                $link = get_term_link($term);
                $vars['permalink'] = (!is_wp_error($link) && is_string($link)) ? $link : '';
            }
        }

        // Author archive context.
        if ($ctx->type === SeoDocumentContext::TYPE_ARCHIVE && $ctx->objectSubType === 'author' && $ctx->objectId !== null) {
            $authorId = (int) $ctx->objectId;
            if ($authorId > 0) {
                $vars += $this->authorVariables($authorId);
            }
        }

        // Search context.
        if ($ctx->type === SeoDocumentContext::TYPE_SEARCH) {
            $q = get_search_query();
            $vars['search_query'] = is_string($q) ? (string) $q : '';
        }

        return $vars;
    }

    /**
     * @return array<string,string>
     */
    private function authorVariables(int $authorId): array
    {
        $dn = get_the_author_meta('display_name', $authorId);
        $fn = get_the_author_meta('first_name', $authorId);
        $ln = get_the_author_meta('last_name', $authorId);
        $bio = get_the_author_meta('description', $authorId);

        return [
            'author_id' => (string) $authorId,
            'author_name' => is_string($dn) ? trim($dn) : '',
            'author_first_name' => is_string($fn) ? trim($fn) : '',
            'author_last_name' => is_string($ln) ? trim($ln) : '',
            'author_bio' => is_string($bio) ? trim(wp_strip_all_tags($bio)) : '',
        ];
    }

    private function defaultTitle(SeoDocumentContext $ctx): string
    {
        if ($ctx->isSingular()) {
            return (string) get_the_title($ctx->objectId);
        }

        if ($ctx->isTerm()) {
            $term = get_term($ctx->objectId, $ctx->objectSubType ?: '');
            if ($term && !is_wp_error($term) && isset($term->name)) {
                return (string) $term->name;
            }
        }

        if ($ctx->type === SeoDocumentContext::TYPE_HOME) {
            return (string) get_bloginfo('name');
        }

        if ($ctx->type === SeoDocumentContext::TYPE_SEARCH) {
            /* translators: %s: search query */
            return sprintf(__('Search results for “%s”', 'wp-seo-next'), get_search_query());
        }

        if ($ctx->type === SeoDocumentContext::TYPE_404) {
            return (string) __('Page not found', 'wp-seo-next');
        }

        if ($ctx->type === SeoDocumentContext::TYPE_ARCHIVE) {
            $t = get_the_archive_title();
            $t = is_string($t) ? trim(wp_strip_all_tags($t)) : '';
            if ($t !== '') {
                return $t;
            }
        }

        $name = get_bloginfo('name');
        $name = is_string($name) ? trim($name) : '';

        return $name !== '' ? $name : (string) __('Archive', 'wp-seo-next');

    }

    private function defaultExcerpt(SeoDocumentContext $ctx): string
    {
        if ($ctx->isSingular()) {
            $excerpt = get_the_excerpt($ctx->objectId);
            if (is_string($excerpt) && trim($excerpt) !== '') {
                return trim(wp_strip_all_tags($excerpt));
            }

            // Fallback: derive excerpt from extracted content.
            // This is important for page builders (Elementor) and custom fields.
            $extractor = new ContentExtractor();
            $derived = $extractor->getPostExcerptText((int) $ctx->objectId, 28);
            if ($derived !== '') {
                return $derived;
            }
        }

        // Fallback: site description for non-singular pages.
        $tagline = get_bloginfo('description');
        return is_string($tagline) ? trim($tagline) : '';
    }
}
