"""Schemas screen for browsing and managing validation schemas.

This screen provides:
- Search and browse input/output schemas
- View schema definitions in preview panel
- Edit schema definitions in $EDITOR
- Create new schema versions
- Copy schema names and definitions
"""

import json
import os
import subprocess
import tempfile
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.widgets import DataTable, Static

from alloy_runtime_types.dtos.schemas import (
    GetSchemaResponse,
    SchemaSummary,
    UpdateSchemaRequest,
)

from cli.infrastructure.scope_utils import format_scope_label
from cli.infrastructure.tui.clipboard import copy_to_clipboard
from cli.infrastructure.tui.formatters import format_datetime
from cli.tui.screens.base import BrowserWidget
from cli.tui.screens.nav_screen import NavScreen
from cli.tui.widgets.schema_create_modal import SchemaCreateModal, SchemaCreateResult
from cli.tui.widgets.schema_update_modal import SchemaUpdateModal, SchemaUpdateResult

if TYPE_CHECKING:
    from cli.tui.app import AlloyRuntimeApp


class SchemasWidget(BrowserWidget[SchemaSummary]):
    """Widget for browsing and managing schemas."""

    app: "AlloyRuntimeApp"

    TITLE = "Schemas"
    TABLE_COLUMNS = ["Name", "Format", "Version", "Updated"]

    def __init__(self, **kwargs: Any) -> None:
        super().__init__(**kwargs)
        # Cache for full schema details (keyed by schema ID)
        self._schema_cache: dict[str, GetSchemaResponse] = {}

    async def _fetch_items(self, query: str) -> tuple[list[SchemaSummary], int]:
        """Fetch schemas from the API."""
        client = await self._ensure_client()
        response = await client.list_schemas(
            search=query if query else None,
            limit=50,
        )
        return response.schemas, response.total

    def _format_row(self, item: SchemaSummary) -> tuple[str, ...]:
        """Format a schema as a table row."""
        return (
            item.schema_name,
            item.schema_format_id,
            f"v{item.version}" if item.is_latest else f"v{item.version} (old)",
            format_datetime(item.updated_at),
        )

    @on(DataTable.RowHighlighted)
    def _on_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
        """Fetch full schema details when row is highlighted."""
        row_index = event.cursor_row
        if 0 <= row_index < len(self._items):
            schema_summary = self._items[row_index]
            schema_id = str(schema_summary.id)

            # Check cache first
            if schema_id in self._schema_cache:
                self._update_preview_with_full_schema(
                    schema_summary, self._schema_cache[schema_id]
                )
            else:
                # Fetch full details and update preview
                self._fetch_full_schema(schema_summary)

    @work(exclusive=True, group="schema_fetch")
    async def _fetch_full_schema(self, schema_summary: SchemaSummary) -> None:
        """Fetch full schema details from API and update preview."""
        try:
            client = await self._ensure_client()
            full_schema = await client.get_schema(str(schema_summary.id))

            # Cache the result
            self._schema_cache[str(schema_summary.id)] = full_schema

            # Update preview with full details
            self._update_preview_with_full_schema(schema_summary, full_schema)

        except Exception as e:
            # Fall back to summary-only preview
            self._update_preview(schema_summary)
            self.app.notify(f"Failed to fetch schema details: {e}", severity="warning")

    def _update_preview(self, item: SchemaSummary) -> None:
        """Update the preview pane with schema summary (fallback)."""
        preview = self.query_one("#preview-content", Static)

        created = format_datetime(item.created_at)
        scope = format_scope_label(
            str(item.organization_id) if item.organization_id else None,
            str(item.user_id) if item.user_id else None,
        )

        preview_text = f"""[bold]Schema ID:[/] {item.id}
[bold]Name:[/] {item.schema_name}
[bold]Format:[/] {item.schema_format_id}
[bold]Version:[/] v{item.version} {"(latest)" if item.is_latest else "(superseded)"}
[bold]Scope:[/] {scope}

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

[bold]Created:[/] {created}

[italic]Loading schema definition...[/]"""

        preview.update(preview_text)

    def _update_preview_with_full_schema(
        self, summary: SchemaSummary, full_schema: GetSchemaResponse
    ) -> None:
        """Update the preview pane with full schema details including definition."""
        preview = self.query_one("#preview-content", Static)

        created = format_datetime(summary.created_at)
        scope = format_scope_label(
            str(summary.organization_id) if summary.organization_id else None,
            str(summary.user_id) if summary.user_id else None,
        )

        # Format the schema definition for display
        definition_display = self._format_definition_for_display(
            full_schema.schema_definition, full_schema.schema_format_id
        )

        preview_text = f"""[bold]Schema ID:[/] {summary.id}
[bold]Name:[/] {summary.schema_name}
[bold]Format:[/] {summary.schema_format_id}
[bold]Version:[/] v{summary.version} {"(latest)" if summary.is_latest else "(superseded)"}
[bold]Scope:[/] {scope}

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

[bold]Created:[/] {created}

[bold]Definition:[/]
{definition_display}"""

        preview.update(preview_text)

    def _format_definition_for_display(
        self, definition: str, schema_format: str
    ) -> str:
        """Format schema definition for preview display."""
        if schema_format == "json_schema":
            try:
                parsed = json.loads(definition)
                formatted = json.dumps(parsed, indent=2, ensure_ascii=False)
                return formatted
            except (json.JSONDecodeError, TypeError):
                return definition
        else:
            # Regex or other formats - display as-is
            return definition

    def _get_cached_schema(self, schema_id: str) -> GetSchemaResponse | None:
        """Get cached full schema details, or None if not cached."""
        return self._schema_cache.get(schema_id)

    def _open_editor_for_schema(
        self, name_hint: str, initial_content: str, schema_format: str
    ) -> str | None:
        """Open external editor with schema definition.

        Args:
            name_hint: Name to include in temp file name
            initial_content: Initial schema definition content
            schema_format: Schema format (json_schema or regex)

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

        # Choose file extension based on schema format for syntax highlighting
        if schema_format == "json_schema":
            suffix = ".json"
            # Pretty-print JSON for better editing experience
            try:
                parsed = json.loads(initial_content)
                initial_content = json.dumps(parsed, indent=2, ensure_ascii=False)
            except (json.JSONDecodeError, TypeError):
                pass
        else:
            suffix = ".txt"

        try:
            with tempfile.NamedTemporaryFile(
                mode="w",
                suffix=suffix,
                prefix=f"schema_{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

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

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

        copy_to_clipboard(self.app, schema.schema_name, "Schema name")

    async def open_new_schema_modal(self) -> None:
        """Open the schema creation form."""
        client = await self._ensure_client()

        def on_schema_created(result: SchemaCreateResult | None) -> None:
            if result is not None:
                self.app.notify(
                    f"Created schema: {result.schema_name} v{result.version}",
                    severity="information",
                )
                self.refresh_data()

        self.app.push_screen(
            SchemaCreateModal(client=client),
            on_schema_created,
        )

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

        # Get full schema from cache
        full_schema = self._get_cached_schema(str(schema.id))
        if not full_schema:
            self.app.notify(
                "Schema definition not loaded yet. Please wait.", severity="warning"
            )
            return

        # Format JSON nicely for clipboard
        definition = full_schema.schema_definition
        if full_schema.schema_format_id == "json_schema":
            try:
                parsed = json.loads(definition)
                definition = json.dumps(parsed, indent=2, ensure_ascii=False)
            except (json.JSONDecodeError, TypeError):
                pass

        copy_to_clipboard(self.app, definition, "Schema definition")

    def edit_schema_definition(self) -> None:
        """Open schema definition in $EDITOR for editing.

        If the definition changes, creates a new version of the schema.
        """
        schema = self.get_selected_item()
        if not schema:
            self.app.notify("No schema selected", severity="warning")
            return

        # Get full schema from cache
        full_schema = self._get_cached_schema(str(schema.id))
        if not full_schema:
            self.app.notify(
                "Schema definition not loaded yet. Please wait.", severity="warning"
            )
            return

        # Open in editor
        new_content = self._open_editor_for_schema(
            name_hint=schema.schema_name,
            initial_content=full_schema.schema_definition,
            schema_format=full_schema.schema_format_id,
        )

        if new_content is None:
            return

        # Normalize for comparison (strip whitespace, compact JSON)
        original = full_schema.schema_definition.strip()
        edited = new_content.strip()

        # For JSON schemas, compare parsed content to ignore formatting differences
        if full_schema.schema_format_id == "json_schema":
            try:
                original_parsed = json.loads(original)
                edited_parsed = json.loads(edited)
                if original_parsed == edited_parsed:
                    self.app.notify("No changes made", severity="information")
                    return
            except json.JSONDecodeError:
                # Fall back to string comparison
                if original == edited:
                    self.app.notify("No changes made", severity="information")
                    return
        else:
            if original == edited:
                self.app.notify("No changes made", severity="information")
                return

        # Save the updated schema (creates new version)
        self._save_schema_update(str(schema.id), edited)

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

        Opens the current definition in editor. If content changes,
        creates a new version.
        """
        # Same as edit - both create new versions on change
        self.edit_schema_definition()

    @work(exclusive=True)
    async def _save_schema_update(self, schema_id: str, new_definition: str) -> None:
        """Save updated schema definition via API (creates new version)."""
        try:
            client = await self._ensure_client()
            request = UpdateSchemaRequest(schema_definition=new_definition)
            response = await client.update_schema(UUID(schema_id), request)

            # Clear cache for this schema (new version has new ID)
            if schema_id in self._schema_cache:
                del self._schema_cache[schema_id]

            self.app.notify(
                f"Schema updated: {response.schema_name} v{response.version}",
                severity="information",
            )

            # Refresh the list to show new version
            self._do_search(self._last_search)

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

    def open_edit_schema_modal(self) -> None:
        """Open the schema update modal to edit name/description."""
        schema = self.get_selected_item()
        if not schema:
            self.app.notify("No schema selected", severity="warning")
            return

        # Get full schema from cache
        full_schema = self._get_cached_schema(str(schema.id))
        if not full_schema:
            self.app.notify(
                "Schema details not loaded yet. Please wait.", severity="warning"
            )
            return

        self._open_schema_update_modal(full_schema)

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

    def _on_schema_updated(self, result: SchemaUpdateResult | None) -> None:
        """Callback when schema update modal is dismissed."""
        if result is not None:
            # Clear cache for this schema (name may have changed)
            if result.schema_id in self._schema_cache:
                del self._schema_cache[result.schema_id]

            self.app.notify(
                f"Updated schema: {result.schema_name}",
                severity="information",
            )
            # Refresh the schema list
            self._do_search(self._last_search)


class SchemasScreen(NavScreen):
    """Screen for browsing and managing schemas.

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

    app: "AlloyRuntimeApp"

    SCREEN_ID = "schemas"

    DEFAULT_CSS = """
    SchemasScreen #screen-root {
        height: 1fr;
    }
    
    SchemasScreen .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_definition", "Copy Definition", show=True),
        Binding("ctrl+e", "edit_definition", "Edit Def", show=True),
        Binding("ctrl+u", "edit_schema", "Edit Name", show=True),
        Binding("ctrl+v", "create_version", "New Version", show=True),
        Binding("ctrl+n", "new_schema", "New", 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 SchemasWidget(id="schemas-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("#schemas-widget", SchemasWidget)
            widget.focus_table()
        except Exception:
            pass

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

    # =========================================================================
    # 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 schema name to clipboard."""
        self._get_widget().copy_schema_name()

    def action_copy_definition(self) -> None:
        """Copy the schema definition to clipboard."""
        self._get_widget().copy_schema_definition()

    def action_edit_definition(self) -> None:
        """Edit the schema definition in $EDITOR."""
        self._get_widget().edit_schema_definition()

    def action_edit_schema(self) -> None:
        """Edit the schema name and description in a modal."""
        self._get_widget().open_edit_schema_modal()

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

    async def action_new_schema(self) -> None:
        """Open the schema creation modal."""
        await self._get_widget().open_new_schema_modal()
