import subprocess
import tempfile
from pathlib import Path
from typing import Any
from uuid import UUID

from pydantic import BaseModel

from cli.infrastructure.editor import EditorError, get_editor
from cli.infrastructure.formatting.fields import format_optional, format_tokens
from cli.infrastructure.macro_parser import extract_jinja_syntax
from cli.infrastructure.output import OutputFormat, OutputService
from cli.infrastructure.renderers.entity_renderer import FieldConfig
from alloy_runtime_types.dtos.templates import RenderTemplateResponse

pyperclip: Any | None = None
try:
    import pyperclip as _pyperclip
except ImportError:
    pass
else:
    pyperclip = _pyperclip

PYPERCLIP_AVAILABLE = pyperclip is not None


class _RenderConsoleOutput(BaseModel):
    template_name: str
    character_count: int
    estimated_token_count: int | None = None
    template_version_id: UUID
    content_hash: str
    render_log_id: UUID
    rendered_text: str


class _RenderFallbackOutput(BaseModel):
    template_name: str
    character_count: int
    estimated_token_count: int | None = None
    rendered_text: str


class _EscapedJinjaOutput(BaseModel):
    match_count: int
    escaped_syntax: str | None = None


def present_render_result(
    response: RenderTemplateResponse,
    template_name: str,
    output_mode: str = "console",
    output_file: str | None = None,
    print_escaped: bool = False,
) -> bool:
    output = OutputService.get()

    if output_mode == "clipboard":
        result = _copy_to_clipboard(
            response.rendered_text,
            template_name,
            output,
            estimated_token_count=response.estimated_token_count,
        )
    elif output_mode == "file":
        result = _save_to_file(response, template_name, output_file, output)
    elif output_mode == "editor":
        result = _open_in_editor(
            response.rendered_text,
            template_name,
            output,
            estimated_token_count=response.estimated_token_count,
        )
    else:
        result = _display_to_console(response, template_name, output)

    if print_escaped:
        _print_escaped_jinja(response.rendered_text, output)

    return result


def _print_escaped_jinja(rendered_text: str, output: OutputService) -> None:
    matches = extract_jinja_syntax(rendered_text)

    if not matches:
        output.result(
            action="inspect",
            resource="escaped_jinja",
            status="success",
            identifier=None,
            name=None,
            extra_fields={"Matches": 0},
        )
        return

    escaped_output = _EscapedJinjaOutput(
        match_count=len(matches),
        escaped_syntax="\n".join(match.content for match in matches),
    )
    fields = [
        FieldConfig("match_count", "Match Count", str),
        FieldConfig("escaped_syntax", "Escaped Syntax"),
    ]
    output.entity(
        escaped_output, "Escaped Jinja Syntax", fields, raw_payload=escaped_output
    )


def _display_to_console(
    response: RenderTemplateResponse,
    template_name: str,
    output: OutputService,
) -> bool:
    if getattr(output, "format", OutputFormat.COMPACT) == OutputFormat.JSON:
        output.raw(response)
        return True

    render_output = _RenderConsoleOutput(
        template_name=template_name,
        character_count=response.character_count,
        estimated_token_count=response.estimated_token_count,
        template_version_id=response.template_version_id,
        content_hash=response.content_hash,
        render_log_id=response.render_log_id,
        rendered_text=response.rendered_text,
    )
    metadata_fields = [
        FieldConfig("template_name", "Template"),
        FieldConfig("character_count", "Character Count", lambda value: f"{value:,}"),
        FieldConfig(
            "estimated_token_count",
            "Estimated Tokens",
            lambda value: format_optional(value, format_tokens, "-"),
        ),
        FieldConfig("template_version_id", "Version ID", str),
        FieldConfig("content_hash", "Content Hash"),
        FieldConfig("render_log_id", "Render Log ID", str),
    ]
    body_fields = [
        FieldConfig("rendered_text", "Rendered Text"),
    ]

    output.entity(render_output, f"Rendered Template: {template_name}", metadata_fields)
    output.entity(render_output, f"Rendered Output: {template_name}", body_fields)
    return True


def _save_to_file(
    response: RenderTemplateResponse,
    template_name: str,
    output_file: str | None,
    output: OutputService,
) -> bool:
    if not output_file:
        _emit_destination_result(
            output,
            template_name,
            destination="file",
            status="failed",
            reason="No output file specified",
        )
        return False

    try:
        file_path = Path(output_file).expanduser().resolve()
        file_path.parent.mkdir(parents=True, exist_ok=True)
        file_path.write_text(response.rendered_text, encoding="utf-8")
        _emit_destination_result(
            output,
            template_name,
            destination=str(file_path),
            status="success",
            characters=response.character_count,
            estimated_token_count=response.estimated_token_count,
        )
        return True
    except PermissionError:
        _emit_destination_result(
            output,
            template_name,
            destination="file",
            status="failed",
            reason=f"Permission denied: Cannot write to '{output_file}'",
        )
        return False
    except OSError as e:
        _emit_destination_result(
            output,
            template_name,
            destination="file",
            status="failed",
            reason=f"Failed to save file: {e}",
        )
        return False


def _copy_to_clipboard(
    rendered_text: str,
    template_name: str,
    output: OutputService,
    *,
    estimated_token_count: int | None = None,
) -> bool:
    if not PYPERCLIP_AVAILABLE or pyperclip is None:
        _emit_destination_result(
            output,
            template_name,
            destination="clipboard",
            status="failed",
            reason="pyperclip not installed. Install with: pip install pyperclip",
        )
        _present_destination_fallback(
            output,
            template_name,
            rendered_text,
            estimated_token_count=estimated_token_count,
        )
        return False

    try:
        pyperclip.copy(rendered_text)
        _emit_destination_result(
            output,
            template_name,
            destination="clipboard",
            status="success",
            characters=len(rendered_text),
            estimated_token_count=estimated_token_count,
        )
        return True
    except Exception as e:
        _emit_destination_result(
            output,
            template_name,
            destination="clipboard",
            status="failed",
            reason=str(e),
        )
        _present_destination_fallback(
            output,
            template_name,
            rendered_text,
            estimated_token_count=estimated_token_count,
        )
        return False


def _open_in_editor(
    rendered_text: str,
    template_name: str,
    output: OutputService,
    *,
    estimated_token_count: int | None = None,
) -> bool:
    try:
        editor = get_editor()
    except EditorError:
        _emit_destination_result(
            output,
            template_name,
            destination="editor",
            status="failed",
            reason="No editor found. Set $EDITOR environment variable or install nano/vim.",
        )
        _present_destination_fallback(
            output,
            template_name,
            rendered_text,
            estimated_token_count=estimated_token_count,
        )
        return False

    try:
        with tempfile.NamedTemporaryFile(
            mode="w",
            suffix=".txt",
            delete=False,
            encoding="utf-8",
        ) as f:
            f.write(rendered_text)
            temp_path = Path(f.name)
    except OSError as e:
        _emit_destination_result(
            output,
            template_name,
            destination="editor",
            status="failed",
            reason=f"Failed to create temporary file: {e}",
        )
        _present_destination_fallback(
            output,
            template_name,
            rendered_text,
            estimated_token_count=estimated_token_count,
        )
        return False

    try:
        editor_parts = editor.split()
        cmd = editor_parts + [str(temp_path)]
        result = subprocess.call(cmd)

        if result != 0:
            _emit_destination_result(
                output,
                template_name,
                destination="editor",
                status="failed",
                reason=f"Editor exited with non-zero status: {result}",
            )
            return False

        _emit_destination_result(
            output,
            template_name,
            destination="editor",
            status="success",
            characters=len(rendered_text),
            estimated_token_count=estimated_token_count,
        )
        return True
    except FileNotFoundError:
        _emit_destination_result(
            output,
            template_name,
            destination="editor",
            status="failed",
            reason=f"Editor '{editor}' not found",
        )
        _present_destination_fallback(
            output,
            template_name,
            rendered_text,
            estimated_token_count=estimated_token_count,
        )
        return False
    except OSError as e:
        _emit_destination_result(
            output,
            template_name,
            destination="editor",
            status="failed",
            reason=f"Failed to open editor: {e}",
        )
        _present_destination_fallback(
            output,
            template_name,
            rendered_text,
            estimated_token_count=estimated_token_count,
        )
        return False
    finally:
        try:
            temp_path.unlink()
        except OSError:
            pass


def _emit_destination_result(
    output: OutputService,
    template_name: str,
    *,
    destination: str,
    status: str,
    characters: int | None = None,
    estimated_token_count: int | None = None,
    reason: str | None = None,
) -> None:
    extra_fields: dict[str, Any] = {"Destination": destination}
    if characters is not None:
        extra_fields["Characters"] = characters
    if estimated_token_count is not None:
        extra_fields["Estimated Tokens"] = format_tokens(estimated_token_count)
    if reason is not None:
        extra_fields["Reason"] = reason

    output.result(
        action="render",
        resource="template",
        status=status,
        identifier=None,
        name=template_name,
        extra_fields=extra_fields,
    )


def _present_destination_fallback(
    output: OutputService,
    template_name: str,
    rendered_text: str,
    *,
    estimated_token_count: int | None = None,
) -> None:
    fallback_output = _RenderFallbackOutput(
        template_name=template_name,
        character_count=len(rendered_text),
        estimated_token_count=estimated_token_count,
        rendered_text=rendered_text,
    )
    fields = [
        FieldConfig("template_name", "Template"),
        FieldConfig("character_count", "Characters", lambda value: f"{value:,}"),
        FieldConfig(
            "estimated_token_count",
            "Estimated Tokens",
            lambda value: format_optional(value, format_tokens, "-"),
        ),
        FieldConfig("rendered_text", "Rendered Text"),
    ]
    output.entity(fallback_output, f"Rendered Output: {template_name}", fields)
