"""
ScriptTemplateEngine — renders parameterized PowerShell templates.

Template syntax:
  {{ param_name }}           — simple substitution (string-escaped)
  {{ param | filter }}       — substitution with filter applied
  {% if param %} ... {% endif %}  — conditional block

Security: all string values are PowerShell-escaped before insertion.
Sensitive parameter values are never stored in generation records — use redact_sensitive().
"""
import re
from typing import Any


class ScriptRenderError(Exception):
    """Raised when a required parameter is missing or rendering fails."""


class ScriptTemplateEngine:

    # ── Public API ────────────────────────────────────────────────────────

    def render(self, body: str, parameters: dict[str, Any]) -> str:
        """Render a template body with the given parameters. Raises ScriptRenderError on missing required params."""
        result = self._process_conditionals(body, parameters)
        result = self._substitute_params(result, parameters)
        return result

    def redact_sensitive(self, parameters: dict[str, Any], sensitive_keys: set[str]) -> dict[str, Any]:
        """Return a copy of parameters with sensitive values replaced by [REDACTED]."""
        return {
            k: "[REDACTED]" if k in sensitive_keys else v
            for k, v in parameters.items()
        }

    # ── Conditional processing ────────────────────────────────────────────

    def _process_conditionals(self, body: str, parameters: dict[str, Any]) -> str:
        """Process {% if param %} ... {% endif %} blocks."""
        pattern = re.compile(
            r'\{%\s*if\s+(\w+)\s*%\}(.*?)\{%\s*endif\s*%\}',
            re.DOTALL
        )

        def replace_block(match: re.Match) -> str:
            key = match.group(1)
            content = match.group(2)
            value = parameters.get(key)
            # Truthy: non-empty string, non-empty list, True, non-zero number
            if value is None or value == "" or value == [] or value is False:
                return ""
            return content

        return pattern.sub(replace_block, body)

    # ── Parameter substitution ────────────────────────────────────────────

    # Matches {{ param }} or {{ param | filter }}
    _PLACEHOLDER = re.compile(r'\{\{\s*(\w+)(?:\s*\|\s*(\w+))?\s*\}\}')

    def _substitute_params(self, body: str, parameters: dict[str, Any]) -> str:
        missing: list[str] = []

        def replace(match: re.Match) -> str:
            key = match.group(1)
            filter_name = match.group(2)

            if key not in parameters:
                missing.append(key)
                return match.group(0)  # leave as-is, report at end

            value = parameters[key]

            if filter_name:
                return self._apply_filter(filter_name, value)
            return self._escape_string(value)

        result = self._PLACEHOLDER.sub(replace, body)

        if missing:
            raise ScriptRenderError(
                f"Missing required template parameter(s): {', '.join(missing)}"
            )

        return result

    # ── Filters ──────────────────────────────────────────────────────────

    def _apply_filter(self, filter_name: str, value: Any) -> str:
        filters = {
            "as_secure_string": self._filter_as_secure_string,
            "as_array": self._filter_as_array,
            "as_bool": self._filter_as_bool,
            "escape_single": self._filter_escape_single,
        }
        if filter_name not in filters:
            raise ScriptRenderError(f"Unknown template filter: {filter_name}")
        return filters[filter_name](value)

    def _filter_as_secure_string(self, value: Any) -> str:
        escaped = str(value).replace("'", "''")
        return f"(ConvertTo-SecureString '{escaped}' -AsPlainText -Force)"

    def _filter_as_array(self, value: Any) -> str:
        if not isinstance(value, list):
            value = [value]
        items = ",".join(f"'{self._escape_single_quote(str(v))}'" for v in value)
        return items

    def _filter_as_bool(self, value: Any) -> str:
        if isinstance(value, bool):
            return "$true" if value else "$false"
        return "$true" if str(value).lower() in ("true", "1", "yes") else "$false"

    def _filter_escape_single(self, value: Any) -> str:
        return self._escape_single_quote(str(value))

    # ── Escaping helpers ──────────────────────────────────────────────────

    def _escape_string(self, value: Any) -> str:
        """Escape a value for safe insertion into a PowerShell single-quoted context."""
        if isinstance(value, bool):
            return "$true" if value else "$false"
        if isinstance(value, (int, float)):
            return str(value)
        if isinstance(value, list):
            # Lists rendered without filter — join with space (caller should use | as_array for proper PS arrays)
            return " ".join(str(v) for v in value)
        s = str(value)
        # Escape backtick (PS escape char) first, then dollar (variable interpolation)
        s = s.replace("`", "``")
        s = s.replace("$", "`$")
        # Escape single quotes (doubling for PS single-quoted strings)
        s = self._escape_single_quote(s)
        return s

    def _escape_single_quote(self, s: str) -> str:
        return s.replace("'", "''")
