"""Templates screen for browsing and managing templates.

This screen provides:
- Search and browse templates
- View template content in preview panel
- Edit templates and create versions
- Copy template names and content
"""

import os
import subprocess
import tempfile
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any
from uuid import UUID

from textual import on, work
from textual.app import ComposeResult
from textual.binding import Binding
from textual.containers import Horizontal, Vertical, VerticalScroll
from textual.widgets import Button, DataTable, Input, Static

from alloy_runtime_types.dtos.templates import (
    CreateTemplateVersionRequest,
    RenderTemplateRequest,
    TemplateListItemResponse,
    UpdateTemplateRequest,
)

from cli.infrastructure.forms.resolution_modal import ResolutionModal
from cli.infrastructure.macro_parser import (
    MacroMatch,
    build_jinja_replacement,
    compile_content,
    deduplicate_macros,
    parse_macros,
)
from cli.infrastructure.tui.clipboard import copy_to_clipboard
from cli.infrastructure.tui.formatters import format_datetime, format_tags_list
from cli.tui.screens.base import BrowserWidget
from cli.tui.screens.nav_screen import NavScreen
from cli.tui.widgets.template_create_modal import (
    TemplateCreateModal,
    TemplateCreateResult,
)
from cli.tui.widgets.template_update_modal import (
    TemplateUpdateModal,
    TemplateUpdateResult,
)

if TYPE_CHECKING:
    from cli.tui.app import AlloyRuntimeApp


def _macro_list_factory() -> list[MacroMatch]:
    return []


def _replacements_factory() -> dict[str, str]:
    return {}


@dataclass
class PendingEdit:
    """State for a template edit waiting on macro resolution."""

    template_id: str
    content: str
    is_new_version: bool
    all_macros: list[MacroMatch] = field(default_factory=_macro_list_factory)
    pending_macros: list[MacroMatch] = field(default_factory=_macro_list_factory)
    replacements: dict[str, str] = field(default_factory=_replacements_factory)
    current_macro_index: int = 0


class TemplatesWidget(BrowserWidget[TemplateListItemResponse]):
    """Widget for browsing and managing templates."""

    app: "AlloyRuntimeApp"

    TITLE = "Templates"
    TABLE_COLUMNS = ["Type", "Name", "Visibility", "Version", "Tags", "Updated"]

    def __init__(self, **kwargs: Any) -> None:
        super().__init__(**kwargs)
        self._pending_edit: PendingEdit | None = None

    def compose(self) -> ComposeResult:
        """Compose the browser layout with a New Template button."""
        with Vertical(id="browser-root"):
            with Vertical(id="search-container"):
                with Horizontal(id="search-row"):
                    yield Input(
                        placeholder=f"Search {self.TITLE.lower()}...",
                        id="search-input",
                    )
                    yield Button("+ New", id="new-template-btn", variant="primary")
                yield Static("Loading...", id="status-bar")

            with Horizontal(id="main-container"):
                with Vertical(id="list-container"):
                    yield DataTable(id="data-table", cursor_type="row")

                with Vertical(id="detail-container"):
                    yield Static("Preview", id="preview-title")
                    with VerticalScroll(id="preview-scroll"):
                        yield Static(
                            "Select an item to view details",
                            id="preview-content",
                        )

    @on(Button.Pressed, "#new-template-btn")
    def _on_new_template_btn_pressed(self, event: Button.Pressed) -> None:
        """Handle New Template button press."""
        self.open_new_template_modal()

    async def _fetch_items(
        self, query: str
    ) -> tuple[list[TemplateListItemResponse], int]:
        """Fetch templates from the API."""
        client = await self._ensure_client()
        response = await client.list_templates(
            search=query if query else None,
            limit=50,
            include_content=True,
        )
        return response.templates, response.total_count

    def _format_row(self, item: TemplateListItemResponse) -> tuple[str, ...]:
        """Format a template as a table row."""
        tags_dicts = [{"tag_path": t.tag_path} for t in item.tags]
        return (
            item.content_type_id,
            item.name,
            item.visibility_id,
            f"v{item.latest_version_number}",
            format_tags_list(tags_dicts, max_display=2),
            format_datetime(item.updated_at),
        )

    def _update_preview(self, item: TemplateListItemResponse) -> None:
        """Update the preview pane with template details."""
        preview = self.query_one("#preview-content", Static)

        tags_display = ", ".join(t.tag_path for t in item.tags) if item.tags else "-"
        vars_display = (
            ", ".join(item.required_variables) if item.required_variables else "-"
        )

        created = format_datetime(item.created_at)
        updated = format_datetime(item.updated_at)
        version_created = format_datetime(item.latest_version_created_at)

        content_display = item.latest_version_content or "(content not available)"

        preview_text = f"""[bold]Template ID:[/] {item.id}
[bold]Version ID:[/] {item.latest_version_id}
[bold]Name:[/] {item.name}
[bold]Type:[/] {item.content_type_id}
[bold]Visibility:[/] {item.visibility_id}
[bold]Version:[/] v{item.latest_version_number}

[bold]Description:[/]
{item.description or "(no description)"}

[bold]Required Variables:[/] {vars_display}
[bold]Tags:[/] {tags_display}

[bold]Created:[/] {created}
[bold]Updated:[/] {updated}
[bold]Version Created:[/] {version_created}

[bold]Content:[/]
{content_display}"""

        preview.update(preview_text)

    # =========================================================================
    # Public methods for parent Screen to call
    # =========================================================================

    def copy_template_name(self) -> None:
        """Copy the template name to clipboard."""
        template = self.get_selected_item()
        if not template:
            self.app.notify("No template selected", severity="warning")
            return

        copy_to_clipboard(self.app, template.name, "Template name")

    def copy_template_content(self) -> None:
        """Copy the template content to clipboard."""
        template = self.get_selected_item()
        if not template:
            self.app.notify("No template to copy", severity="warning")
            return

        content = template.latest_version_content
        if content:
            copy_to_clipboard(self.app, content, "Template content")
        else:
            self.app.notify("No content available", severity="warning")

    def edit_template_content(self) -> None:
        """Open template content in $EDITOR for editing.

        For single-version templates, updates the existing version.
        For multi-version templates, automatically creates a new version.
        If content changes and contains macros, opens macro resolution flow before saving.
        """
        template = self.get_selected_item()
        if not template:
            self.app.notify("No template to edit", severity="warning")
            return

        # Check if template has multiple versions
        is_multi_version = template.latest_version_number > 1

        if is_multi_version:
            # For multi-version templates, edit will create a new version
            self.app.notify(
                f"Template has {template.latest_version_number} versions - edit will create v{template.latest_version_number + 1}",
                severity="information",
            )

        new_content = self._open_editor_for_template(
            template.name,
            template.latest_version_content or "",
        )
        if new_content is None:
            return

        if new_content == (template.latest_version_content or ""):
            self.app.notify("No changes made", severity="information")
            return

        self._handle_content_after_edit(
            template_id=str(template.id),
            content=new_content,
            is_new_version=is_multi_version,  # Use versioning flow for multi-version templates
        )

    def create_template_version(self) -> None:
        """Create a new version of the template in $EDITOR.

        Opens the current content in the editor. If content changes and
        contains macros, opens macro resolution flow before creating version.
        """
        template = self.get_selected_item()
        if not template:
            self.app.notify("No template selected", severity="warning")
            return

        version_hint = f"_v{template.latest_version_number + 1}"
        new_content = self._open_editor_for_template(
            f"{template.name}{version_hint}",
            template.latest_version_content or "",
        )
        if new_content is None:
            return

        if new_content == (template.latest_version_content or ""):
            self.app.notify(
                "No changes made - version not created", severity="information"
            )
            return

        self._handle_content_after_edit(
            template_id=str(template.id),
            content=new_content,
            is_new_version=True,
        )

    def _open_editor_for_template(
        self, name_hint: str, initial_content: str
    ) -> str | None:
        """Open external editor with template content.

        Args:
            name_hint: Name to include in temp file name
            initial_content: Initial content for the editor

        Returns:
            Edited content, or None if editor failed
        """
        editor = os.environ.get("EDITOR") or os.environ.get("VISUAL") or "vi"

        try:
            with tempfile.NamedTemporaryFile(
                mode="w",
                suffix=".jinja2",
                prefix=f"template_{name_hint}_",
                delete=False,
                encoding="utf-8",
            ) as f:
                f.write(initial_content)
                temp_path = f.name

            with self.app.suspend():
                subprocess.run([editor, temp_path], check=False)

            with open(temp_path, encoding="utf-8") as f:
                new_content = f.read()

            try:
                os.unlink(temp_path)
            except OSError:
                pass

            return new_content

        except Exception as e:
            self.app.notify(f"Failed to open editor: {e}", severity="error")
            return None

    def _handle_content_after_edit(
        self, template_id: str, content: str, is_new_version: bool
    ) -> None:
        """Handle content after editor closes - check for macros and save.

        If the content contains macros, starts the macro resolution flow.
        Otherwise, saves/creates version immediately.
        """
        all_macros = parse_macros(content)

        if not all_macros:
            # No macros - save immediately
            if is_new_version:
                self._create_template_version(template_id, content)
            else:
                self._save_template_content(template_id, content)
            return

        # Has macros - set up resolution flow
        unique_macros = deduplicate_macros(all_macros)
        self._pending_edit = PendingEdit(
            template_id=template_id,
            content=content,
            is_new_version=is_new_version,
            all_macros=all_macros,
            pending_macros=unique_macros,
            replacements={},
            current_macro_index=0,
        )

        self.app.notify(
            f"Found {len(unique_macros)} macro(s) to resolve",
            severity="information",
        )
        self._start_macro_resolution()

    def _start_macro_resolution(self) -> None:
        """Start or continue the macro resolution flow."""
        if self._pending_edit is None:
            return

        self._process_next_macro()

    def _process_next_macro(self) -> None:
        """Process the next macro in the resolution queue."""
        if self._pending_edit is None:
            return

        if self._pending_edit.current_macro_index >= len(
            self._pending_edit.pending_macros
        ):
            # All macros processed - finalize
            self._finalize_edit()
            return

        macro = self._pending_edit.pending_macros[
            self._pending_edit.current_macro_index
        ]
        self._open_resolution_modal_for_macro(macro)

    @work(exclusive=True)
    async def _open_resolution_modal_for_macro(self, macro: MacroMatch) -> None:
        """Open the resolution modal for a specific macro."""
        try:
            client = await self._ensure_client()
            self.app.push_screen(
                ResolutionModal(
                    client=client,
                    macro_type=macro.macro_type,
                    query=macro.query,
                ),
                self._on_macro_resolved,
            )
        except Exception as e:
            self.app.notify(f"Failed to open resolution modal: {e}", severity="error")
            self._pending_edit = None

    def _on_macro_resolved(self, result: dict[str, Any] | None) -> None:
        """Callback when a macro resolution modal is dismissed."""
        if self._pending_edit is None:
            return

        macro = self._pending_edit.pending_macros[
            self._pending_edit.current_macro_index
        ]

        if result is not None:
            # User selected a value - build jinja replacement
            resolved_value = result["value"]
            jinja_replacement = build_jinja_replacement(
                macro.macro_type, resolved_value
            )

            # Apply to all matching macros (same type and query)
            matching_count = 0
            for m in self._pending_edit.all_macros:
                if m.macro_type == macro.macro_type and m.query == macro.query:
                    self._pending_edit.replacements[m.original_text] = jinja_replacement
                    matching_count += 1

            display = result.get("display", resolved_value)
            if matching_count > 1:
                self.app.notify(
                    f"Resolved {matching_count}x @{macro.macro_type}({macro.query}) -> {display}",
                    severity="information",
                )
            else:
                self.app.notify(
                    f"Resolved @{macro.macro_type}({macro.query}) -> {display}",
                    severity="information",
                )
        else:
            # User skipped - macro will remain unchanged
            self.app.notify(
                f"Skipped @{macro.macro_type}({macro.query})",
                severity="warning",
            )

        # Move to next macro
        self._pending_edit.current_macro_index += 1
        self._process_next_macro()

    def _finalize_edit(self) -> None:
        """Compile content with replacements and save."""
        if self._pending_edit is None:
            return

        # Apply all replacements to get final content
        final_content = compile_content(
            self._pending_edit.content,
            self._pending_edit.replacements,
        )

        resolved_count = len(
            {
                (m.macro_type, m.query)
                for m in self._pending_edit.all_macros
                if m.original_text in self._pending_edit.replacements
            }
        )
        total_unique = len(self._pending_edit.pending_macros)

        if resolved_count < total_unique:
            self.app.notify(
                f"Resolved {resolved_count}/{total_unique} macros (skipped macros unchanged)",
                severity="information",
            )

        # Save based on edit type
        if self._pending_edit.is_new_version:
            self._create_template_version(self._pending_edit.template_id, final_content)
        else:
            self._save_template_content(self._pending_edit.template_id, final_content)

        # Clear pending state
        self._pending_edit = None

    def open_new_template_modal(self) -> None:
        """Open the template creation modal."""
        self._open_template_create_modal()

    def update_template_metadata(self) -> None:
        """Open the template update modal for editing metadata."""
        template = self.get_selected_item()
        if not template:
            self.app.notify("No template selected", severity="warning")
            return
        self._open_template_update_modal(template)

    def render_and_open(self) -> None:
        """Render the template and open the result in $EDITOR.

        Renders the selected template with empty variables and opens
        the rendered output in the user's editor. No action is taken
        on editor exit.
        """
        template = self.get_selected_item()
        if not template:
            self.app.notify("No template selected", severity="warning")
            return

        # Warn if template has required variables
        if template.required_variables:
            vars_list = ", ".join(template.required_variables)
            self.app.notify(
                f"Note: template has variables ({vars_list}) - rendering with empty values",
                severity="warning",
            )

        # Delegate to async worker
        self._render_and_open_template(str(template.id), template.name)

    # =========================================================================
    # Internal async operations
    # =========================================================================

    @work(exclusive=True)
    async def _save_template_content(self, template_id: str, content: str) -> None:
        """Save updated template content via API."""
        try:
            client = await self._ensure_client()
            request = UpdateTemplateRequest(content=content)
            await client.update_template(template_id, request)

            self.app.notify("Template updated successfully", severity="information")
            self._do_search(self._last_search)

        except Exception as e:
            error_msg = str(e)
            if "multiple versions" in error_msg.lower() or "409" in error_msg:
                self.app.notify(
                    "Cannot edit: template has multiple versions. Use Ctrl+V to create new version.",
                    severity="error",
                )
            else:
                self.app.notify(f"Failed to update template: {e}", severity="error")

    @work(exclusive=True)
    async def _create_template_version(self, template_id: str, content: str) -> None:
        """Create a new template version via API."""
        try:
            client = await self._ensure_client()
            request = CreateTemplateVersionRequest(content=content)
            await client.create_template_version(UUID(template_id), request)

            self.app.notify("New version created successfully", severity="information")
            self._do_search(self._last_search)

        except Exception as e:
            self.app.notify(f"Failed to create version: {e}", severity="error")

    @work(exclusive=True)
    async def _open_template_create_modal(self) -> None:
        """Open the template creation modal."""
        try:
            client = await self._ensure_client()
            self.app.push_screen(
                TemplateCreateModal(client=client),
                self._on_template_created,
            )
        except Exception as e:
            self.app.notify(f"Failed to open template creator: {e}", severity="error")

    def _on_template_created(self, result: TemplateCreateResult | None) -> None:
        """Callback when template creation modal is dismissed."""
        if result is not None:
            self.app.notify(
                f"Created template: {result.template_name} (v{result.version_number})",
                severity="information",
            )
            # Refresh the template list
            self._do_search(self._last_search)

    @work(exclusive=True)
    async def _open_template_update_modal(
        self, template: TemplateListItemResponse
    ) -> None:
        """Open the template update modal."""
        try:
            client = await self._ensure_client()
            self.app.push_screen(
                TemplateUpdateModal(client=client, template=template),
                self._on_template_updated,
            )
        except Exception as e:
            self.app.notify(f"Failed to open template editor: {e}", severity="error")

    def _on_template_updated(self, result: TemplateUpdateResult | None) -> None:
        """Callback when template update modal is dismissed."""
        if result is not None:
            self.app.notify(
                f"Updated template: {result.template_name}",
                severity="information",
            )
            # Refresh the template list
            self._do_search(self._last_search)

    @work(exclusive=True)
    async def _render_and_open_template(
        self, template_id: str, template_name: str
    ) -> None:
        """Render template and open in $EDITOR."""
        try:
            client = await self._ensure_client()
            request = RenderTemplateRequest(variables={})
            response = await client.render_template(UUID(template_id), request)

            rendered_text = response.rendered_text
            editor = os.environ.get("EDITOR") or os.environ.get("VISUAL") or "vi"

            # Create temp file with rendered content
            with tempfile.NamedTemporaryFile(
                mode="w",
                suffix=".txt",
                prefix=f"rendered_{template_name}_",
                delete=False,
                encoding="utf-8",
            ) as f:
                f.write(rendered_text)
                temp_path = f.name

            # Suspend TUI and open editor
            with self.app.suspend():
                subprocess.run([editor, temp_path], check=False)

            # Clean up temp file (no action on exit)
            try:
                os.unlink(temp_path)
            except OSError:
                pass

        except Exception as e:
            self.app.notify(f"Failed to render template: {e}", severity="error")


class TemplatesScreen(NavScreen):
    """Screen for browsing and managing templates.

    This is a proper Textual Screen that wraps the TemplatesWidget
    and provides keybindings that work regardless of focus.
    """

    app: "AlloyRuntimeApp"

    SCREEN_ID = "templates"

    DEFAULT_CSS = """
    TemplatesScreen #screen-root {
        height: 1fr;
    }
    
    TemplatesScreen .browser-widget {
        height: 100%;
        width: 100%;
    }
    """

    # Screen-specific bindings (Ctrl+ prefixed)
    BINDINGS = NavScreen.BINDINGS + [
        Binding("ctrl+f", "focus_search", "Search", show=True),
        Binding("ctrl+r", "refresh", "Refresh", show=True),
        Binding("ctrl+y", "copy_name", "Copy Name", show=True),
        Binding("ctrl+shift+y", "copy_content", "Copy Content", show=True),
        Binding("ctrl+e", "edit_content", "Edit", show=True),
        Binding("ctrl+u", "update_metadata", "Edit Metadata", show=True),
        Binding("ctrl+v", "create_version", "New Version", show=True),
        Binding("ctrl+n", "new_template", "New", show=True),
        Binding("ctrl+o", "render_and_open", "Render & Open", show=True),
        Binding("escape", "focus_table", "Focus List", show=False),
        Binding("enter", "copy_name", "Copy Name", show=False),
    ]

    def compose_content(self) -> ComposeResult:
        """Compose the screen content."""
        yield TemplatesWidget(id="templates-widget")

    def on_screen_resume(self) -> None:
        """Focus the data table when screen becomes active."""
        self.call_after_refresh(self._focus_table)

    def _focus_table(self) -> None:
        """Focus the data table."""
        try:
            widget = self.query_one("#templates-widget", TemplatesWidget)
            widget.focus_table()
        except Exception:
            pass

    def _get_widget(self) -> TemplatesWidget:
        """Get the templates widget."""
        return self.query_one("#templates-widget", TemplatesWidget)

    # =========================================================================
    # Actions (keybinding handlers)
    # =========================================================================

    def action_focus_search(self) -> None:
        """Focus the search input."""
        self._get_widget().focus_search()

    def action_focus_table(self) -> None:
        """Focus the data table."""
        self._get_widget().focus_table()

    def action_refresh(self) -> None:
        """Refresh the current search results."""
        self._get_widget().refresh_data()

    def action_copy_name(self) -> None:
        """Copy the template name to clipboard."""
        self._get_widget().copy_template_name()

    def action_copy_content(self) -> None:
        """Copy the template content to clipboard."""
        self._get_widget().copy_template_content()

    def action_edit_content(self) -> None:
        """Edit the template content in $EDITOR."""
        self._get_widget().edit_template_content()

    def action_update_metadata(self) -> None:
        """Open the template update modal for editing metadata."""
        self._get_widget().update_template_metadata()

    def action_create_version(self) -> None:
        """Create a new version of the template."""
        self._get_widget().create_template_version()

    def action_new_template(self) -> None:
        """Open the template creation modal."""
        self._get_widget().open_new_template_modal()

    def action_render_and_open(self) -> None:
        """Render the template and open in $EDITOR."""
        self._get_widget().render_and_open()
