"""Base modal class for agent create/update forms.

Provides a comprehensive modal screen with all agent fields that can be
subclassed for create and update operations.
"""

import json
from abc import abstractmethod
from dataclasses import dataclass
from typing import Any, cast
from uuid import UUID

from textual import on, work
from textual.app import ComposeResult
from textual.containers import Horizontal, VerticalScroll
from textual.widgets import (
    Button,
    Input,
    Label,
    RadioButton,
    RadioSet,
    Static,
    TextArea,
)

from cli.infrastructure.scope_utils import get_scope_from_radioset
from cli.tui.widgets.base_form_modal import BaseFormModal

from alloy_runtime_sdk.api_client.client import ApiClient
from alloy_runtime_types.dtos.agents import (
    AgentModelConfig,
    AgentToolConfig,
    AgentToolResponse,
)
from alloy_runtime_types.dtos.generation import (
    GenerationParameters,
    REASONING_EFFORT_VALUES,
    ReasoningEffort,
)
from alloy_runtime_types.dtos.models import ModelListItemResponse
from alloy_runtime_types.enums.ownership_scope import OwnershipScope
from alloy_runtime_types.enums.provider import Provider
from alloy_runtime_types.enums.template_enums import TemplateContentType
from cli.infrastructure.forms.agent_picker import AgentPicker
from cli.infrastructure.forms.agent_tools_manager import AgentToolsManager
from cli.infrastructure.forms.json_schema_builder import JsonSchemaBuilder
from cli.infrastructure.forms.model_picker import ModelPicker
from cli.infrastructure.forms.schema_picker import SchemaPicker
from cli.infrastructure.forms.tag_picker import TagPicker
from cli.infrastructure.forms.template_picker import TemplatePicker


@dataclass
class AgentFormResult:
    """Result of successful agent form submission."""

    agent_id: str
    agent_name: str
    is_update: bool = False


def reasoning_effort_label(value: ReasoningEffort) -> str:
    return "X-High" if value == "xhigh" else value.capitalize()


def reasoning_effort_radio_id(value: ReasoningEffort) -> str:
    return f"reasoning-{value}"


def reasoning_effort_from_radio_id(radio_id: str) -> ReasoningEffort | None:
    if not radio_id.startswith("reasoning-"):
        return None

    raw_value = radio_id.removeprefix("reasoning-")
    if raw_value in REASONING_EFFORT_VALUES:
        return raw_value
    return None


class AgentFormModal(BaseFormModal[AgentFormResult]):
    """Base modal for agent create/update forms.

    Provides a comprehensive form with:
    - Required fields: name, models, tags
    - Basic info: description, scope, base agent
    - System instruction: template picker
    - Schemas: input/output schema pickers or inline JSON schema builder
    - Generation parameters: temperature, max_tokens, etc.
    - Advanced: default headers with quick-add buttons

    Subclasses must implement:
    - _get_modal_title(): Return the modal title
    - _get_submit_button_text(): Return the submit button text
    - _finalize_submission(): Perform the create/update API call
    - _is_scope_editable(): Whether the scope can be changed

    Optionally override:
    - on_mount(): To pre-populate fields with existing data
    """

    def __init__(
        self,
        client: ApiClient,
        **kwargs: Any,
    ) -> None:
        """Initialize the agent form modal.

        Args:
            client: ApiClient instance for API calls
        """
        super().__init__(**kwargs)
        self._client = client

    @abstractmethod
    def _get_modal_title(self) -> str:
        """Return the modal title."""
        ...

    @abstractmethod
    def _get_submit_button_text(self) -> str:
        """Return the submit button text."""
        ...

    @abstractmethod
    def _is_scope_editable(self) -> bool:
        """Return whether the scope can be changed."""
        ...

    @abstractmethod
    async def _submit_form(self) -> AgentFormResult:
        """Submit the form and return the result.

        This should call the appropriate API (create or update) and return
        the result. Errors should be raised as exceptions.
        """
        ...

    def compose(self) -> ComposeResult:
        with VerticalScroll(id="agent-create-modal"):
            yield Label(self._get_modal_title(), classes="modal-title")

            # ===== REQUIRED FIELDS =====
            yield Static("[bold]Required[/]", classes="form-section")

            yield Static("Name *", classes="field-label")
            yield Input(
                placeholder="Unique agent name (e.g., my-writer-agent)",
                id="name-input",
            )

            yield Static(
                "Models * (first = primary, rest = fallbacks)", classes="field-label"
            )
            yield ModelPicker(
                client=self._client,
                id="model-picker",
            )

            yield Static(
                "Tags * (required for user/org scoped agents)", classes="field-label"
            )
            yield TagPicker(
                client=self._client,
                placeholder="Search tags...",
                empty_message="No tags selected (required)",
                id="tag-picker",
            )

            # ===== BASIC INFO =====
            yield Static("[bold]Basic Info[/]", classes="form-section")

            yield Static("Description", classes="field-label")
            yield Input(
                placeholder="Brief description of what this agent does",
                id="description-input",
            )

            yield Static("Scope", classes="field-label")
            with RadioSet(id="scope-radios", disabled=not self._is_scope_editable()):
                yield RadioButton("User (personal)", id="scope-user")
                yield RadioButton("Organization", id="scope-org", value=True)
                yield RadioButton("Global", id="scope-global")

            yield Static("Base Agent (inherit configuration)", classes="field-label")
            yield AgentPicker(
                client=self._client,
                placeholder="Search agents to inherit from...",
                empty_message="No base agent selected (optional)",
                id="base-agent-picker",
            )

            # ===== SYSTEM INSTRUCTION =====
            yield Static("[bold]System Instruction[/]", classes="form-section")

            yield Static("System Prompt Template", classes="field-label")
            yield TemplatePicker(
                client=self._client,
                content_type=TemplateContentType.SYSTEM_INSTRUCTION,
                placeholder="Search system instruction templates...",
                empty_message="No template selected (optional)",
                id="system-template-picker",
            )

            # ===== SCHEMAS =====
            yield Static("[bold]Output Schema[/]", classes="form-section")

            yield Static("Output Schema (search or leave empty)", classes="field-label")
            yield SchemaPicker(
                client=self._client,
                schema_format="json_schema",
                placeholder="Search output schemas...",
                empty_message="No output schema selected (optional)",
                id="output-schema-picker",
            )

            yield Static(
                "Or: Output Schema from JSON example (generates schema)",
                classes="field-label",
            )
            yield JsonSchemaBuilder(id="output-schema-builder")

            # ===== TOOLS =====
            yield Static("[bold]Tools[/]", classes="form-section")

            yield Static(
                "Configure agent tools (search, RAG, etc.)",
                classes="field-label",
            )
            yield AgentToolsManager(
                client=self._client,
                id="tools-manager",
            )

            # ===== GENERATION PARAMETERS =====
            yield Static("[bold]Generation Parameters[/]", classes="form-section")

            with Horizontal(classes="param-row"):
                with VerticalScroll(classes="param-field"):
                    yield Static("Temperature", classes="field-label")
                    yield Input(
                        placeholder="0.0-2.0 (higher = more creative)",
                        id="temperature-input",
                    )
                with VerticalScroll(classes="param-field"):
                    yield Static("Max Tokens", classes="field-label")
                    yield Input(
                        placeholder="Max output length (e.g., 4096)",
                        id="max-tokens-input",
                    )

            with Horizontal(classes="param-row"):
                with VerticalScroll(classes="param-field"):
                    yield Static("Top P", classes="field-label")
                    yield Input(
                        placeholder="0.0-1.0 (nucleus sampling)",
                        id="top-p-input",
                    )
                with VerticalScroll(classes="param-field"):
                    yield Static("Top K", classes="field-label")
                    yield Input(
                        placeholder="Limit token choices (e.g., 40)",
                        id="top-k-input",
                    )

            with Horizontal(classes="param-row"):
                with VerticalScroll(classes="param-field"):
                    yield Static("Frequency Penalty", classes="field-label")
                    yield Input(
                        placeholder="-2.0 to 2.0",
                        id="frequency-penalty-input",
                    )
                with VerticalScroll(classes="param-field"):
                    yield Static("Presence Penalty", classes="field-label")
                    yield Input(
                        placeholder="-2.0 to 2.0",
                        id="presence-penalty-input",
                    )

            yield Static(
                "Reasoning Effort (Claude extended thinking)", classes="field-label"
            )
            with RadioSet(id="reasoning-radios"):
                yield RadioButton("Unset", id="reasoning-unset", value=True)
                for value in REASONING_EFFORT_VALUES:
                    yield RadioButton(
                        reasoning_effort_label(value),
                        id=reasoning_effort_radio_id(value),
                    )

            yield Static("Seed", classes="field-label")
            yield Input(
                placeholder="Integer for reproducible outputs",
                id="seed-input",
            )

            # ===== ADVANCED =====
            yield Static("[bold]Advanced[/]", classes="form-section")

            yield Static("Default Headers (JSON)", classes="field-label")
            yield TextArea(
                id="default-headers-textarea",
            )
            with Horizontal(classes="quick-add-buttons"):
                yield Button(
                    "+ Anthropic 1M Context",
                    id="add-anthropic-1m-btn",
                    variant="default",
                )

            # Error display
            yield Static("", id="error-display", classes="error-display")

            with Horizontal(classes="button-row"):
                yield Button(
                    self._get_submit_button_text(), id="submit-btn", variant="success"
                )
                yield Button("Cancel", id="cancel-btn", variant="default")

    def on_mount(self) -> None:
        """Focus the name input on mount."""
        self.query_one("#name-input", Input).focus()

    @on(Button.Pressed, "#submit-btn")
    def on_submit_pressed(self, event: Button.Pressed) -> None:
        """Handle submit button press."""
        self.action_submit()

    @on(Button.Pressed, "#cancel-btn")
    def on_cancel_pressed(self, event: Button.Pressed) -> None:
        """Handle cancel button press."""
        self.dismiss(None)

    @on(Button.Pressed, "#add-anthropic-1m-btn")
    def on_add_anthropic_1m_pressed(self, event: Button.Pressed) -> None:
        """Add Anthropic 1M context header."""
        self._add_header("anthropic-beta", "context-1m-2025-08-07")

    def action_submit(self) -> None:
        """Handle form submission."""
        try:
            self._validate_fields()
            self._hide_error()
            self._finalize_submission()
        except ValueError as e:
            self._show_error(str(e))

    def _validate_fields(self) -> None:
        """Validate all required fields."""
        name = self.query_one("#name-input", Input).value.strip()
        if not name:
            raise ValueError("Name is required")

        model_picker = self.query_one("#model-picker", ModelPicker)
        if not model_picker.selected_models:
            raise ValueError("At least one model is required")

        # Tags are required for user and org scope
        scope = self._get_scope()
        tag_picker = self.query_one("#tag-picker", TagPicker)
        if scope != OwnershipScope.GLOBAL and not tag_picker.selected_tags:
            raise ValueError("At least one tag is required for user/org scoped agents")

        # Validate output schema exclusivity
        output_schema_picker = self.query_one("#output-schema-picker", SchemaPicker)
        output_schema_builder = self.query_one(
            "#output-schema-builder", JsonSchemaBuilder
        )
        if output_schema_picker.selected_id and output_schema_builder.generated_schema:
            raise ValueError(
                "Specify either an existing output schema OR paste a JSON example, not both"
            )

        # Validate JSON builder state
        if (
            output_schema_builder.json_input
            and not output_schema_builder.generated_schema
        ):
            raise ValueError(
                "Output Schema JSON has errors. Fix them or clear the field."
            )

        # Validate tools
        tools_manager = self.query_one("#tools-manager", AgentToolsManager)
        tools_error = tools_manager.validate_all()
        if tools_error:
            raise ValueError(tools_error)

    def _get_scope(self) -> OwnershipScope:
        """Get scope from RadioSet."""
        return get_scope_from_radioset(self.query_one("#scope-radios", RadioSet))

    def _get_reasoning_effort(self) -> ReasoningEffort | None:
        """Get reasoning effort from RadioSet."""
        radios = self.query_one("#reasoning-radios", RadioSet)
        pressed = radios.pressed_button
        if pressed is None or pressed.id is None:
            return None
        if pressed.id == "reasoning-unset":
            return None
        return reasoning_effort_from_radio_id(pressed.id)

    def _parse_float(
        self, value: str, field_name: str, min_val: float, max_val: float
    ) -> float | None:
        """Parse a float value with validation."""
        if not value.strip():
            return None
        try:
            result = float(value)
            if result < min_val or result > max_val:
                raise ValueError(
                    f"{field_name} must be between {min_val} and {max_val}"
                )
            return result
        except ValueError as e:
            if "must be between" in str(e):
                raise
            raise ValueError(f"{field_name} must be a valid number") from e

    def _parse_int(
        self, value: str, field_name: str, min_val: int | None = None
    ) -> int | None:
        """Parse an integer value with validation."""
        if not value.strip():
            return None
        try:
            result = int(value)
            if min_val is not None and result < min_val:
                raise ValueError(f"{field_name} must be at least {min_val}")
            return result
        except ValueError as e:
            if "must be" in str(e):
                raise
            raise ValueError(f"{field_name} must be a valid integer") from e

    def _parse_uuid(self, value: str | None, field_name: str) -> UUID | None:
        """Parse a UUID value."""
        if not value:
            return None
        try:
            return UUID(value)
        except ValueError:
            raise ValueError(f"{field_name} has an invalid ID format")

    def _parse_json(self, value: str, field_name: str) -> dict[str, Any] | None:
        """Parse JSON value for headers."""
        if not value.strip():
            return None
        try:
            result = json.loads(value)
            if not isinstance(result, dict):
                raise ValueError(f"{field_name} must be a JSON object")
            return cast(dict[str, Any], result)
        except json.JSONDecodeError as e:
            raise ValueError(f"{field_name} has invalid JSON: {e.msg}") from e

    def _add_header(self, key: str, value: str) -> None:
        """Add a header to the default headers JSON field."""
        headers_textarea = self.query_one("#default-headers-textarea", TextArea)
        current_text = headers_textarea.text.strip()

        if current_text:
            try:
                headers = json.loads(current_text)
                if not isinstance(headers, dict):
                    headers = {}
            except json.JSONDecodeError:
                self._show_error(
                    "Cannot add header: Default Headers contains invalid JSON"
                )
                return
        else:
            headers = {}

        headers[key] = value
        headers_textarea.text = json.dumps(headers, indent=2)

    def _build_model_configs(self) -> list[AgentModelConfig]:
        """Build model configs from the model picker."""
        model_picker = self.query_one("#model-picker", ModelPicker)
        return [
            AgentModelConfig(
                provider_key=Provider(m.provider_key),
                provider_model_name=m.provider_model_name,
                preference_order=i,
            )
            for i, m in enumerate(model_picker.selected_models)
        ]

    def _build_generation_params(self) -> GenerationParameters | None:
        """Build GenerationParameters from form fields."""
        temperature = self._parse_float(
            self.query_one("#temperature-input", Input).value,
            "Temperature",
            0.0,
            2.0,
        )
        max_tokens = self._parse_int(
            self.query_one("#max-tokens-input", Input).value,
            "Max Tokens",
            1,
        )
        top_p = self._parse_float(
            self.query_one("#top-p-input", Input).value,
            "Top P",
            0.0,
            1.0,
        )
        top_k = self._parse_int(
            self.query_one("#top-k-input", Input).value,
            "Top K",
            1,
        )
        frequency_penalty = self._parse_float(
            self.query_one("#frequency-penalty-input", Input).value,
            "Frequency Penalty",
            -2.0,
            2.0,
        )
        presence_penalty = self._parse_float(
            self.query_one("#presence-penalty-input", Input).value,
            "Presence Penalty",
            -2.0,
            2.0,
        )
        reasoning_effort = self._get_reasoning_effort()
        seed = self._parse_int(
            self.query_one("#seed-input", Input).value,
            "Seed",
        )

        # Only return if at least one param is set
        if any(
            [
                temperature,
                max_tokens,
                top_p,
                top_k,
                frequency_penalty,
                presence_penalty,
                reasoning_effort,
                seed,
            ]
        ):
            return GenerationParameters(
                temperature=temperature,
                max_tokens=max_tokens,
                top_p=top_p,
                top_k=top_k,
                frequency_penalty=frequency_penalty,
                presence_penalty=presence_penalty,
                reasoning_effort=reasoning_effort,
                seed=seed,
            )

        return None

    def _get_output_schema(self) -> dict[str, Any] | None:
        """Get inline output schema from JSON builder if present."""
        output_schema_builder = self.query_one(
            "#output-schema-builder", JsonSchemaBuilder
        )
        if output_schema_builder.generated_schema:
            return {
                "type": "json_schema",
                "schema_def": output_schema_builder.generated_schema,
            }
        return None

    @work(exclusive=True)
    async def _finalize_submission(self) -> None:
        """Build request and submit to API."""
        try:
            result = await self._submit_form()
            self.dismiss(result)
        except Exception as e:
            self._show_error(f"Failed to save agent: {e}")

    # ========== Helper methods for pre-populating fields ==========

    def _set_name(self, name: str) -> None:
        """Set the name input value."""
        self.query_one("#name-input", Input).value = name

    def _set_description(self, description: str | None) -> None:
        """Set the description input value."""
        self.query_one("#description-input", Input).value = description or ""

    def _set_scope(self, scope: OwnershipScope) -> None:
        """Set the scope radio selection."""
        scope_map = {
            OwnershipScope.USER: "scope-user",
            OwnershipScope.ORGANIZATION: "scope-org",
            OwnershipScope.GLOBAL: "scope-global",
        }
        radio_id = scope_map.get(scope, "scope-user")
        radio = self.query_one(f"#{radio_id}", RadioButton)
        radio.value = True

    def _set_models(self, models: list[ModelListItemResponse]) -> None:
        """Set the model picker selections."""
        model_picker = self.query_one("#model-picker", ModelPicker)
        model_picker.set_initial_models(models)

    def _set_tags(self, tags: list[dict[str, Any]]) -> None:
        """Set the tag picker selections."""
        tag_picker = self.query_one("#tag-picker", TagPicker)
        tag_picker.set_initial_selection(tags)

    def _set_base_agent(self, agent_id: str, name: str) -> None:
        """Set the base agent picker selection."""
        agent_picker = self.query_one("#base-agent-picker", AgentPicker)
        agent_picker.set_initial_agent_by_id(agent_id, name)

    def _set_system_template(
        self, version_id: str, name: str = "System Template"
    ) -> None:
        """Set the system template picker selection."""
        template_picker = self.query_one("#system-template-picker", TemplatePicker)
        template_picker.set_initial_template_by_version_id(version_id, name)

    def _set_output_schema(self, schema_id: str, name: str = "Output Schema") -> None:
        """Set the output schema picker selection."""
        schema_picker = self.query_one("#output-schema-picker", SchemaPicker)
        schema_picker.set_initial_schema_by_id(schema_id, name)

    def _set_generation_params(self, params: GenerationParameters | None) -> None:
        """Set the generation parameter input values."""
        if params is None:
            return

        if params.temperature is not None:
            self.query_one("#temperature-input", Input).value = str(params.temperature)
        if params.max_tokens is not None:
            self.query_one("#max-tokens-input", Input).value = str(params.max_tokens)
        if params.top_p is not None:
            self.query_one("#top-p-input", Input).value = str(params.top_p)
        if params.top_k is not None:
            self.query_one("#top-k-input", Input).value = str(params.top_k)
        if params.frequency_penalty is not None:
            self.query_one("#frequency-penalty-input", Input).value = str(
                params.frequency_penalty
            )
        if params.presence_penalty is not None:
            self.query_one("#presence-penalty-input", Input).value = str(
                params.presence_penalty
            )
        if params.seed is not None:
            self.query_one("#seed-input", Input).value = str(params.seed)

        # Set reasoning effort radio
        if params.reasoning_effort:
            radio = self.query_one(
                f"#{reasoning_effort_radio_id(params.reasoning_effort)}", RadioButton
            )
            radio.value = True

    def _set_default_headers(self, headers: dict[str, str] | None) -> None:
        """Set the default headers textarea value."""
        if headers:
            self.query_one("#default-headers-textarea", TextArea).text = json.dumps(
                headers, indent=2
            )

    def _build_tools(self) -> list[AgentToolConfig] | None:
        """Build tool configs from the tools manager."""
        tools_manager = self.query_one("#tools-manager", AgentToolsManager)
        tools = tools_manager.configured_tools
        return tools if tools else None

    def _set_tools(self, tools: list[AgentToolResponse]) -> None:
        """Pre-populate tools manager with existing tools."""
        tools_manager = self.query_one("#tools-manager", AgentToolsManager)
        tools_manager.set_initial_tools(tools)
