"""Evaluation engine for texer specs and LaTeX elements."""

from __future__ import annotations

import subprocess
from datetime import datetime
from typing import Any, Protocol, runtime_checkable

from texer.specs import Spec, resolve_value, Raw
from texer.utils import escape_latex


def _get_git_sha() -> str | None:
    """Get the full SHA of the current git commit.

    Returns:
        Full git SHA string, or None if not in a git repo or git unavailable.
    """
    try:
        result = subprocess.run(
            ["git", "rev-parse", "HEAD"],
            capture_output=True,
            text=True,
            check=True,
        )
        return result.stdout.strip()
    except (subprocess.CalledProcessError, FileNotFoundError):
        return None


def _get_version() -> str:
    """Get the texer version.

    Returns:
        Version string.
    """
    from texer import __version__

    return __version__


def _generate_header() -> str:
    """Generate a LaTeX comment header with version, creation date, and git SHA.

    Returns:
        LaTeX comment string with metadata.
    """
    version = _get_version()
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    git_sha = _get_git_sha()

    lines = [f"% Generated by texer v{version} on {timestamp}"]
    if git_sha:
        lines.append(f"% Git commit: {git_sha}")

    return "\n".join(lines) + "\n"


@runtime_checkable
class Renderable(Protocol):
    """Protocol for objects that can render to LaTeX."""

    def render(self, data: Any, scope: dict[str, Any] | None = None) -> str:
        """Render this object to LaTeX string."""
        ...


def evaluate(
    element: Any,
    data: Any | None = None,
    scope: dict[str, Any] | None = None,
    escape: bool = True,
    header: bool = True,
    output_file: str | None = None,
    with_preamble: bool = False,
    compile: bool = False,
    output_dir: str | None = None,
) -> str:
    """Evaluate an element and return LaTeX string.

    This is the main entry point for converting texer elements to LaTeX.

    Args:
        element: The element to evaluate (Spec, Renderable, or plain value).
        data: The data context for resolving specs.
        scope: Additional scope variables.
        escape: Whether to escape LaTeX special characters in strings.
        header: Whether to include a header comment with creation date and git SHA.
        output_file: Optional path to save the LaTeX output to a file.
        with_preamble: Whether to include document preamble for standalone compilation.
        compile: Whether to compile the output to PDF using pdflatex (requires output_file).
        output_dir: Optional output directory for PDF compilation (default: same as output_file).

    Returns:
        LaTeX string representation (or path to PDF if compile=True).

    Raises:
        RuntimeError: If compile=True and pdflatex is not available or compilation fails.
        ValueError: If compile=True but output_file is not specified.

    Examples:
        >>> from texer import Ref, Table, Tabular, Row
        >>> data = {"name": "Alice", "value": 42}
        >>> evaluate(Ref("name"), data)
        'Alice'

        # Save to file with preamble
        >>> evaluate(table, output_file="my_table.tex", with_preamble=True)

        # Save and compile to PDF
        >>> pdf_path = evaluate(table, output_file="my_table.tex", with_preamble=True, compile=True)
    """
    import shutil
    from pathlib import Path

    if compile and output_file is None:
        raise ValueError("output_file is required when compile=True")

    if compile and not with_preamble:
        # Automatically enable preamble when compiling
        with_preamble = True

    if data is None:
        data = {}

    if scope is None:
        scope = {}

    if with_preamble:
        # For elements with with_preamble method (like PGFPlot), use it
        if hasattr(element, "with_preamble"):
            result: str = element.with_preamble(data)
        else:
            # Render content and wrap with preamble
            content = _evaluate_impl(element, data, scope, escape)
            result = _wrap_with_preamble(element, content, data)
    else:
        result = _evaluate_impl(element, data, scope, escape)

    if header:
        result = _generate_header() + result

    if output_file is not None:
        with open(output_file, "w", encoding="utf-8") as f:
            f.write(result)

    if compile:
        # Check if pdflatex is available
        if shutil.which("pdflatex") is None:
            raise RuntimeError(
                "pdflatex not found. Please install a LaTeX distribution (e.g., TeX Live, MiKTeX)."
            )

        # Determine paths
        tex_path = Path(output_file).resolve()  # type: ignore[arg-type]
        compile_output_path: Path
        if output_dir is None:
            compile_output_path = tex_path.parent
        else:
            compile_output_path = Path(output_dir).resolve()

        # Run pdflatex
        try:
            subprocess.run(
                [
                    "pdflatex",
                    "-interaction=nonstopmode",
                    f"-output-directory={compile_output_path}",
                    str(tex_path),
                ],
                capture_output=True,
                text=True,
                check=True,
            )
        except subprocess.CalledProcessError as e:
            raise RuntimeError(
                f"pdflatex compilation failed:\n{e.stderr}\n\nOutput:\n{e.stdout}"
            ) from e

        # Return path to PDF
        pdf_path = compile_output_path / tex_path.with_suffix(".pdf").name
        return str(pdf_path)

    return result


def _evaluate_impl(
    element: Any,
    data: Any,
    scope: dict[str, Any],
    escape: bool,
) -> str:
    """Internal implementation of evaluate."""
    # Handle None
    if element is None:
        return ""

    # Handle Raw specs (don't escape)
    if isinstance(element, Raw):
        return element.resolve(data, scope)

    # Handle Specs
    if isinstance(element, Spec):
        resolved = element.resolve(data, scope)
        return _evaluate_impl(resolved, data, scope, escape)

    # Handle Renderables (Table, Tabular, Row, etc.)
    if isinstance(element, Renderable):
        return element.render(data, scope)

    # Handle lists/tuples
    if isinstance(element, (list, tuple)):
        return "".join(_evaluate_impl(item, data, scope, escape) for item in element)

    # Handle plain values
    result = str(element)
    if escape:
        return escape_latex(result)
    return result


def evaluate_value(
    value: Any,
    data: Any,
    scope: dict[str, Any] | None = None,
) -> Any:
    """Evaluate a value without converting to string.

    This resolves Specs but doesn't convert to LaTeX string.
    Useful for getting raw values (lists, numbers, etc.)

    Args:
        value: The value to evaluate.
        data: The data context.
        scope: Additional scope variables.

    Returns:
        The resolved value (may be any type).
    """
    if scope is None:
        scope = {}

    return resolve_value(value, data, scope)


def _get_preamble(element: Any) -> list[str]:
    """Get the appropriate preamble for an element type.

    Args:
        element: The element to get preamble for.

    Returns:
        List of preamble lines.
    """
    # Check if element has a with_preamble method (like PGFPlot)
    if hasattr(element, "with_preamble"):
        # PGFPlot handles its own preamble
        return []

    # Check for table-related imports
    from texer.tables import Table, Tabular

    if isinstance(element, (Table, Tabular)):
        return [
            "\\documentclass{standalone}",
            "\\usepackage{booktabs}",
            "",
            "\\begin{document}",
        ]

    # Default preamble for unknown elements
    return [
        "\\documentclass{standalone}",
        "",
        "\\begin{document}",
    ]


def _wrap_with_preamble(element: Any, content: str, data: Any) -> str:
    """Wrap content with appropriate preamble.

    Args:
        element: The original element (for type detection).
        content: The rendered LaTeX content.
        data: Data dict for rendering.

    Returns:
        Complete LaTeX document string.
    """
    # If element has with_preamble, use it directly
    if hasattr(element, "with_preamble"):
        result: str = element.with_preamble(data)
        return result

    preamble = _get_preamble(element)
    closing = ["\\end{document}"]

    return "\n".join(preamble + [content] + closing)


