"""YAML Template Engine with inline Jinja2 support (ported from v2)."""

from __future__ import annotations

import logging
from collections.abc import Callable, MutableMapping
from pathlib import Path
from typing import Any, cast

import yaml
from jinja2.sandbox import SandboxedEnvironment

logger = logging.getLogger(__name__)


class YAMLTemplateEngine:
    """Complete solution for inline Jinja2 templates in YAML (v2 parity)."""

    def __init__(self) -> None:
        # Configure Jinja2 for YAML-friendly output
        self.env = SandboxedEnvironment(
            block_start_string="{%",
            block_end_string="%}",
            variable_start_string="{{",
            variable_end_string="}}",
            comment_start_string="{#",
            comment_end_string="#}",
            trim_blocks=False,
            lstrip_blocks=False,
            keep_trailing_newline=True,
        )

        # Add custom filters for YAML
        self.env.filters["yaml"] = self._yaml_filter
        self.env.filters["indent"] = self._indent_filter
        self.env.filters["sum"] = self._sum_filter
        self.env.filters["selectattr"] = cast(
            Callable[..., Any], self._selectattr_filter
        )

        # Expose safe Python built-ins commonly used in templates (v2 parity)
        env_globals = cast(MutableMapping[str, Any], self.env.globals)
        env_globals.update(
            cast(
                dict[str, Any],
                {
                    "range": range,
                    "abs": abs,
                    "round": round,
                    "len": len,
                    "min": min,
                    "max": max,
                    "sum": sum,
                },
            )
        )

    def load_file(
        self, file_path: Path, context: dict[str, Any] | None = None
    ) -> dict[str, Any]:
        with open(file_path, encoding="utf-8") as f:
            content = f.read()
        return self.load_string(content, context)

    def load_string(
        self, yaml_content: str, context: dict[str, Any] | None = None
    ) -> dict[str, Any]:
        if "{%" not in yaml_content and "{{" not in yaml_content:
            loaded = yaml.safe_load(yaml_content)
            if not isinstance(loaded, dict):
                raise ValueError(
                    f"Expected YAML to load as dict, got {type(loaded).__name__}"
                )
            return cast(dict[str, Any], loaded)

        if context is not None:
            return self._render_and_parse(yaml_content, context)
        return self._prepare_for_deferred_rendering(yaml_content)

    def _render_and_parse(
        self, yaml_content: str, context: dict[str, Any]
    ) -> dict[str, Any]:
        processed_content = self._preprocess_for_rendering(yaml_content)
        full_context = self._create_render_context(context)
        template = self.env.from_string(processed_content)
        rendered = template.render(**full_context)
        fixed_yaml = self._postprocess_rendered_yaml(rendered)

        try:
            parsed = yaml.safe_load(fixed_yaml)
            if not isinstance(parsed, dict):
                raise ValueError(
                    f"Expected YAML to parse as dict, got {type(parsed).__name__}"
                )
            return cast(dict[str, Any], parsed)
        except yaml.YAMLError as exc:
            logger.error("Failed to parse rendered YAML: %s", exc)
            logger.debug("Rendered YAML:\n%s", fixed_yaml)
            fixed_yaml = self._fix_common_yaml_issues(fixed_yaml)
            parsed = yaml.safe_load(fixed_yaml)
            if not isinstance(parsed, dict):
                raise ValueError(
                    f"Expected YAML to parse as dict, got {type(parsed).__name__}"
                ) from exc
            return cast(dict[str, Any], parsed)

    def _preprocess_for_rendering(self, content: str) -> str:
        lines = content.split("\n")
        processed_lines: list[str] = []
        for line in lines:
            if "{%" in line and "for" in line:
                indent = len(line) - len(line.lstrip())
                processed_lines.append(line)
                processed_lines.append(f"{' ' * indent}{{# indent: {indent} #}}")
            else:
                processed_lines.append(line)
        return "\n".join(processed_lines)

    def _postprocess_rendered_yaml(self, content: str) -> str:
        lines = content.split("\n")
        fixed_lines: list[str] = []
        for i, line in enumerate(lines):
            if 0 < i < len(lines) - 1 and line.strip() == "":
                next_line = lines[i + 1] if i + 1 < len(lines) else ""
                if next_line and not next_line.startswith(" "):
                    continue

            if ":" in line and line.count(":") == 1:
                parts = line.split(":", 1)
                if len(parts) == 2:
                    key_part = parts[0]
                    value_part = parts[1].strip()
                    if value_part and " " in value_part and ":" not in value_part:
                        value_words = value_part.split()
                        if len(value_words) > 1 and any(
                            ":" in w for w in value_words[1:]
                        ):
                            fixed_lines.append(f"{key_part}: {value_words[0]}")
                            remaining = " ".join(value_words[1:])
                            indent = len(key_part) - len(key_part.lstrip())
                            fixed_lines.append(f"{' ' * indent}{remaining}")
                            continue
            fixed_lines.append(line)
        return "\n".join(fixed_lines)

    def _fix_common_yaml_issues(self, content: str) -> str:
        lines = content.split("\n")
        fixed_lines: list[str] = []
        for line in lines:
            if line.count(":") > 1:
                parts = line.split(":", 1)
                if len(parts) == 2:
                    indent = len(parts[0]) - len(parts[0].lstrip())
                    fixed_lines.append(f"{parts[0]}:")
                    remaining = parts[1].strip()
                    if remaining:
                        tokens = remaining.split()
                        current_line = ""
                        for token in tokens:
                            if ":" in token and token.endswith(":"):
                                if current_line:
                                    fixed_lines.append(
                                        f"{' ' * (indent + 2)}{current_line}"
                                    )
                                fixed_lines.append(f"{' ' * (indent + 2)}{token}")
                                current_line = ""
                            else:
                                current_line += f" {token}" if current_line else token
                        if current_line:
                            fixed_lines.append(f"{' ' * (indent + 2)}{current_line}")
                    continue
            fixed_lines.append(line)
        return "\n".join(fixed_lines)

    def _prepare_for_deferred_rendering(self, yaml_content: str) -> dict[str, Any]:
        return self._simple_template_extraction(yaml_content)

    def _analyze_yaml_structure(self, content: str) -> dict[str, Any]:
        lines = content.split("\n")
        structure: dict[str, Any] = {
            "template_blocks": [],
            "inline_templates": [],
            "hierarchy": [],
        }
        current_path: list[tuple[str, int]] = []
        for i, line in enumerate(lines):
            stripped = line.strip()
            if not stripped or stripped.startswith("#"):
                continue
            indent = len(line) - len(line.lstrip())
            while current_path and indent <= current_path[-1][1]:
                current_path.pop()
            if ":" in line:
                key = line.split(":", 1)[0].strip()
                current_path.append((key, indent))
                structure["hierarchy"].append(
                    {
                        "line": i,
                        "key": key,
                        "indent": indent,
                        "path": [p[0] for p in current_path],
                    }
                )
            if "{%" in line:
                structure["template_blocks"].append(i)
            if "{{" in line:
                structure["inline_templates"].append(i)
        return structure

    def _simple_template_extraction(self, content: str) -> dict[str, Any]:
        rendered = content
        try:
            parsed = yaml.safe_load(rendered)
            if not isinstance(parsed, dict):
                raise ValueError(
                    f"Expected YAML to load as dict, got {type(parsed).__name__}"
                )
            return cast(dict[str, Any], parsed)
        except yaml.YAMLError as exc:
            logger.error("Failed to parse YAML with templates: %s", exc)
            logger.debug("YAML content with templates:\n%s", rendered)
            raise

    @staticmethod
    def _yaml_filter(value: Any) -> str:
        return yaml.dump(value, default_flow_style=False).strip()

    @staticmethod
    def _indent_filter(value: Any, spaces: int = 2) -> str:
        return "\n".join(" " * spaces + line for line in str(value).split("\n"))

    @staticmethod
    def _sum_filter(value: Any) -> Any:
        try:
            return sum(value)
        except Exception as exc:  # pragma: no cover - defensive
            raise ValueError(f"Cannot sum value: {value}") from exc

    @staticmethod
    def _selectattr_filter(
        sequence: list[Any], attr: str, default: Any = None
    ) -> list[Any]:
        results: list[Any] = []
        for item in sequence:
            if hasattr(item, attr):
                results.append(getattr(item, attr))
            else:
                results.append(default)
        return results

    @staticmethod
    def _create_render_context(context: dict[str, Any]) -> dict[str, Any]:
        nested_context = context.get("context")
        nested_mapping = (
            cast(dict[str, Any], nested_context)
            if isinstance(nested_context, dict)
            else {}
        )
        base_context: dict[str, Any] = {"context": nested_mapping.copy()}
        return base_context | context
