"""SQL template rendering for the test-runner pipeline.

Resolves ``{0}``, ``{1}`` positional placeholders from the test case
parameter array, and renders dialect-specific SQL via Jinja2 templates
(``capture_outputs_*.sql.j2``, ``fetch_table_*.sql.j2``, etc.).

The literal formatter can be overridden per-dialect via
:class:`~test_runner.common.database_executor.DatabaseExecutorFactory`.
"""

from __future__ import annotations

import logging
import re
from pathlib import Path
from typing import Any, Callable, Optional

from jinja2 import Environment, FileSystemLoader

from .models import ValidationSteps

LOGGER = logging.getLogger(__name__)
_INDEXED_PLACEHOLDER_RE = re.compile(r"\{(\d+)\}")

_TEMPLATES_DIR = Path(__file__).resolve().parent.parent / "templates"
_JINJA_ENV = Environment(
    loader=FileSystemLoader(str(_TEMPLATES_DIR)),
    keep_trailing_newline=False,
    trim_blocks=True,
    lstrip_blocks=True,
)


def build_parameters(
    parameter_names: list[str],
    test_case: list[Any],
) -> dict[str, Any]:
    """Zip *parameter_names* with *test_case*; falls back to ``param_0``, ``param_1``, etc."""
    if parameter_names and len(parameter_names) == len(test_case):
        return dict(zip(parameter_names, test_case))
    return {f"param_{i}": v for i, v in enumerate(test_case)}


def format_sql_literal(value: Any) -> str:
    """Default SQL literal formatter (T-SQL compatible).

    Used as the fallback when no dialect-specific formatter is provided.

    >>> format_sql_literal(None)
    'NULL'
    >>> format_sql_literal(42)
    '42'
    >>> format_sql_literal(3.14)
    '3.14'
    >>> format_sql_literal("hello")
    "'hello'"
    >>> format_sql_literal("it's")
    "'it''s'"
    >>> format_sql_literal(True)
    '1'
    """
    if value is None:
        return "NULL"
    if isinstance(value, bool):
        return "1" if value else "0"
    if isinstance(value, (int, float)):
        return str(value)
    if isinstance(value, str):
        escaped = value.replace("'", "''")
        return f"'{escaped}'"
    # Fallback -- convert to quoted string
    escaped = str(value).replace("'", "''")
    return f"'{escaped}'"


def render_template(
    run_template: str,
    test_case: list[Any],
    formatter: Callable[[Any], str] = format_sql_literal,
) -> str:
    """Replace ``{0}``, ``{1}`` … in *run_template*; ``{OUT:...}`` tokens pass through.

    Args:
        run_template: SQL string with ``{0}``, ``{1}`` … placeholders.
        test_case: Ordered list of values to substitute.
        formatter: Converts a Python value to a SQL literal string.

    Returns:
        The fully rendered SQL string.
    """
    literals = [formatter(v) for v in test_case]
    result = run_template
    for idx, lit in enumerate(literals):
        result = result.replace(f"{{{idx}}}", lit)
    return result


def render_full_sql(
    steps: ValidationSteps,
    test_case: list[Any],
    formatter: Callable[[Any], str] = format_sql_literal,
) -> str:
    """Combine ``pre_execute`` (if any) with the rendered ``run`` template.

    Args:
        steps: The validation steps from the test YAML.
        test_case: Ordered list of values to substitute.
        formatter: Converts a Python value to a SQL literal string.

    Returns:
        Complete SQL string (may contain multiple statements separated by newlines).
    """
    parts: list[str] = []

    if steps.pre_execute:
        rendered_pre = render_template(steps.pre_execute, test_case, formatter)
        parts.append(rendered_pre.strip())

    rendered_run = render_template(steps.run, test_case, formatter)
    parts.append(rendered_run.strip())

    return "\n".join(parts)


def render_outputs_sql(
    steps: ValidationSteps,
    test_case: list[Any],
    dialect: str,
    formatter: Callable[[Any], str] = format_sql_literal,
) -> str:
    """Render output-parameter capture SQL via ``capture_outputs_{dialect}.sql.j2``."""
    rendered_run = render_template(steps.run, test_case, formatter)
    rendered_pre = (
        render_template(steps.pre_execute, test_case, formatter).strip()
        if steps.pre_execute else ""
    )

    template_name = f"capture_outputs_{dialect.lower()}.sql.j2"
    template = _JINJA_ENV.get_template(template_name)
    return template.render(
        pre_execute=rendered_pre,
        run=rendered_run.strip(),
        outputs=steps.output_parameters or [],
    )


def render_capture_sql(
    steps: ValidationSteps,
    test_case: list[Any],
    dialect: str = "sqlserver",
    formatter: Callable[[Any], str] = format_sql_literal,
) -> str:
    """Render capture SQL; delegates to Jinja template when output parameters are present."""
    if not steps.output_parameters:
        return render_full_sql(steps, test_case, formatter)

    return render_outputs_sql(steps, test_case, dialect, formatter)


def render_validate_resultset_sql(
    steps: ValidationSteps,
    test_case: list[Any],
    formatter: Callable[[Any], str] = format_sql_literal,
) -> str:
    """Render Snowflake result-set capture SQL.

    Wraps statements in a ``BEGIN … END`` Snowflake scripting block when
    ``pre_execute`` contains scripting declarations (for example ``LET``).
    Uses ``validate_resultset_snowflake.sql.j2`` when output parameters
    must be returned as a table; otherwise uses an execute-only block.
    Falls back to :func:`render_full_sql` when no scripting wrapper is needed.
    """
    rendered_run = render_template(steps.run, test_case, formatter)
    rendered_pre = (
        render_template(steps.pre_execute, test_case, formatter).strip()
        if steps.pre_execute else ""
    )

    if steps.output_parameters:
        template = _JINJA_ENV.get_template("validate_resultset_snowflake.sql.j2")
        return template.render(
            pre_execute=rendered_pre,
            run=rendered_run.strip(),
        )

    if rendered_pre:
        template = _JINJA_ENV.get_template("validate_execute_snowflake.sql.j2")
        return template.render(
            pre_execute=rendered_pre,
            run=rendered_run.strip(),
        )

    return rendered_run.strip()


def render_validate_outputs_sql(
    steps: ValidationSteps,
    test_case: list[Any],
    formatter: Callable[[Any], str] = format_sql_literal,
) -> str:
    """Shorthand: ``render_outputs_sql(…, "snowflake", …)``."""
    return render_outputs_sql(steps, test_case, "snowflake", formatter)


def _resolve_raw_placeholders(template: str, test_case: list[Any]) -> str:
    """Resolve ``{0}``, ``{1}`` placeholders using raw (unquoted) string values."""
    referenced_indexes = {int(match.group(1)) for match in _INDEXED_PLACEHOLDER_RE.finditer(template)}
    missing_indexes = sorted(idx for idx in referenced_indexes if idx >= len(test_case))
    if missing_indexes:
        raise ValueError(
            f"Template placeholder index(es) {missing_indexes} out of range for "
            f"test_case length {len(test_case)}: {template!r}",
        )

    result = template
    for idx, val in enumerate(test_case):
        result = result.replace(f"{{{idx}}}", str(val) if val is not None else "NULL")
    return result


def render_output_fetch_statements(
    steps: ValidationSteps,
    test_case: list[Any],
    dialect: str,
) -> list[str]:
    """Render SQL statements for output tables and cursors via Jinja templates.

    Resolves ``{0}``, ``{1}`` placeholders in table/cursor names using raw
    (unquoted) values from *test_case*, then renders each through the
    appropriate dialect-specific template.

    Returns a list of SQL strings to execute in sequence after the CALL.
    """
    stmts: list[str] = []

    if steps.output_tables:
        tpl = _JINJA_ENV.get_template(f"fetch_table_{dialect.lower()}.sql.j2")
        for name_template in steps.output_tables:
            resolved = _resolve_raw_placeholders(name_template, test_case)
            stmts.append(tpl.render(table=resolved).strip())

    if steps.output_cursors:
        tpl = _JINJA_ENV.get_template(f"fetch_cursor_{dialect.lower()}.sql.j2")
        for name_template in steps.output_cursors:
            resolved = _resolve_raw_placeholders(name_template, test_case)
            stmts.append(tpl.render(cursor=resolved).strip())

    return stmts
