<?php
/**
 * Template Parser for BoostBot.
 *
 * Parses AI responses and replaces template tags with rendered HTML.
 *
 * @package BoostBot
 * @since 1.1.0
 */

// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Class BoostBot_Template_Parser
 *
 * Parses AI response content for template tags and replaces them with
 * rendered template HTML.
 *
 * @since 1.1.0
 */
class BoostBot_Template_Parser {

    /**
     * Template manager instance.
     *
     * @var BoostBot_Template_Manager
     */
    private $template_manager;

    /**
     * Constructor.
     */
    public function __construct() {
        $this->template_manager = BoostBot_Template_Manager::get_instance();
    }

    /**
     * Parse AI response content for template tags.
     *
     * Template tags have the format: {template:TEMPLATE_ID|JSON_DATA}
     *
     * @param string $content The AI response content.
     * @return string Content with template tags replaced by rendered HTML.
     */
    public function parse( string $content ): string {
        $tags = $this->find_template_tags( $content );

        if ( empty( $tags ) ) {
            return $content;
        }

        // Replace from last to first so earlier offsets stay valid.
        foreach ( array_reverse( $tags ) as $tag ) {
            $replacement = $this->replace_template_tag(
                array(
                    1 => $tag['template_id'],
                    2 => $tag['json'],
                )
            );
            $content = substr_replace( $content, $replacement, $tag['start'], $tag['length'] );
        }

        return $content;
    }

    /**
     * Locate every template tag in the content using brace-aware scanning.
     *
     * A lazy regex such as `\{template:id\|(\{.+?\})\}` stops at the first `}`,
     * which truncates any JSON payload whose final value is itself an object or
     * array (e.g. `{"a":1,"b":{"c":2}}`), silently breaking valid templates.
     * Instead we match the opening marker up to the `|`, then scan forward from
     * the JSON's opening `{` counting braces (while respecting quoted strings and
     * escapes) to find the matching close, then require the tag's own closing `}`.
     *
     * Note: brace/quote/escape characters are ASCII, and UTF-8 continuation bytes
     * never collide with ASCII, so byte-wise scanning is multibyte-safe.
     *
     * @param string $content The content to scan.
     * @return array List of [ 'template_id', 'json', 'start', 'length' ].
     */
    private function find_template_tags( string $content ): array {
        $tags   = array();
        $offset = 0;

        while ( preg_match( '/\{template:([a-z0-9-]+)\|/', $content, $m, PREG_OFFSET_CAPTURE, $offset ) ) {
            $match_start = $m[0][1];
            $template_id = $m[1][0];
            $json_start  = $match_start + strlen( $m[0][0] ); // First char after the "|".

            // Find the closing brace of the JSON object that follows the "|".
            $json_end = $this->scan_json_end( $content, $json_start );

            if ( -1 === $json_end ) {
                // No balanced JSON object — skip past this marker and keep scanning.
                $offset = $json_start;
                continue;
            }

            // The tag's own closing "}" must immediately follow the JSON object.
            $tag_close = $json_end + 1;
            if ( ! isset( $content[ $tag_close ] ) || '}' !== $content[ $tag_close ] ) {
                $offset = $json_start;
                continue;
            }

            $tags[] = array(
                'template_id' => $template_id,
                'json'        => substr( $content, $json_start, $json_end - $json_start + 1 ),
                'start'       => $match_start,
                'length'      => $tag_close - $match_start + 1,
            );

            $offset = $tag_close + 1;
        }

        return $tags;
    }

    /**
     * Scan a JSON object starting at $start and return the index of its matching
     * closing brace, or -1 if the content at $start is not a balanced object.
     *
     * Respects double-quoted strings and backslash escapes so braces inside
     * string values are not counted.
     *
     * @param string $content The full content.
     * @param int    $start   Index that must point at the opening "{".
     * @return int Index of the matching "}", or -1 if unbalanced/not an object.
     */
    private function scan_json_end( string $content, int $start ): int {
        $len = strlen( $content );

        if ( $start >= $len || '{' !== $content[ $start ] ) {
            return -1;
        }

        $depth     = 0;
        $in_string = false;
        $escaped   = false;

        for ( $i = $start; $i < $len; $i++ ) {
            $ch = $content[ $i ];

            if ( $in_string ) {
                if ( $escaped ) {
                    $escaped = false;
                } elseif ( '\\' === $ch ) {
                    $escaped = true;
                } elseif ( '"' === $ch ) {
                    $in_string = false;
                }
                continue;
            }

            if ( '"' === $ch ) {
                $in_string = true;
            } elseif ( '{' === $ch ) {
                $depth++;
            } elseif ( '}' === $ch ) {
                $depth--;
                if ( 0 === $depth ) {
                    return $i;
                }
            }
        }

        return -1;
    }

    /**
     * Callback for preg_replace_callback to replace a single template tag.
     *
     * @param array $matches Regex matches.
     * @return string Rendered template HTML or error comment.
     */
    private function replace_template_tag( array $matches ): string {
        $template_id = $matches[1];
        $json_string = $matches[2];

        // Try to decode the JSON
        $data = json_decode( $json_string, true );

        if ( json_last_error() !== JSON_ERROR_NONE ) {
            BoostBot_Logger::warning( 'Template JSON parse error', [
                'template'    => $template_id,
                'json'        => substr( $json_string, 0, 200 ),
                'error'       => json_last_error_msg(),
            ]);
            return $this->render_error( $template_id, 'Invalid JSON: ' . json_last_error_msg() );
        }

        // Check if template exists
        if ( ! $this->template_manager->template_exists( $template_id ) ) {
            BoostBot_Logger::warning( 'Unknown template requested', [
                'template' => $template_id,
            ]);
            return $this->render_error( $template_id, 'Unknown template' );
        }

        // Render the template (access check is done in template_manager->render)
        try {
            return $this->template_manager->render( $template_id, $data );
        } catch ( \Exception $e ) {
            BoostBot_Logger::error( 'Template render error', [
                'template' => $template_id,
                'error'    => $e->getMessage(),
            ]);
            return $this->render_error( $template_id, 'Render error' );
        }
    }

    /**
     * Check if content contains any template tags.
     *
     * @param string $content The content to check.
     * @return bool True if content has template tags.
     */
    public function has_templates( string $content ): bool {
        return preg_match( '/\{template:[a-z0-9-]+\|/', $content ) === 1;
    }

    /**
     * Extract all template tags from content (without rendering).
     *
     * Useful for analytics or debugging.
     *
     * @param string $content The content to parse.
     * @return array Array of [ 'template_id' => string, 'data' => array ].
     */
    public function extract_templates( string $content ): array {
        $templates = [];

        foreach ( $this->find_template_tags( $content ) as $tag ) {
            $data = json_decode( $tag['json'], true );
            $templates[] = [
                'template_id' => $tag['template_id'],
                'data'        => $data ?? [],
                'valid_json'  => $data !== null,
            ];
        }

        return $templates;
    }

    /**
     * Count templates in content.
     *
     * @param string $content The content to check.
     * @return int Number of template tags found.
     */
    public function count_templates( string $content ): int {
        $pattern = '/\{template:[a-z0-9-]+\|/';
        return preg_match_all( $pattern, $content );
    }

    /**
     * Render an error message for failed template parsing.
     *
     * @param string $template_id The template that failed.
     * @param string $error       Error message.
     * @return string HTML error comment (invisible in rendered output).
     */
    private function render_error( string $template_id, string $error ): string {
        // Return an HTML comment for debugging (invisible to users)
        return '<!-- Template error [' . esc_html( $template_id ) . ']: ' . esc_html( $error ) . ' -->';
    }

    /**
     * Validate template JSON structure before rendering.
     *
     * @param string $template_id The template identifier.
     * @param array  $data        The parsed JSON data.
     * @return bool|string True if valid, error message if not.
     */
    public function validate_template_data( string $template_id, array $data ) {
        $template = $this->template_manager->get_template( $template_id );

        if ( ! $template ) {
            return 'Unknown template';
        }

        $schema = $template['schema'] ?? [];
        $missing_required = [];

        foreach ( $schema as $field => $rules ) {
            if ( ! empty( $rules['required'] ) && ! isset( $data[ $field ] ) ) {
                $missing_required[] = $field;
            }
        }

        if ( ! empty( $missing_required ) ) {
            return 'Missing required fields: ' . implode( ', ', $missing_required );
        }

        return true;
    }
}
