"""Schema update command implementation."""

import asyncio
import json
import os
import shutil
import subprocess
import tempfile
from pathlib import Path
from typing import Optional

import typer

from cli.commands.shared_flags import CLEAR_DESCRIPTION, DESCRIPTION, FILE_INPUT
from cli.commands.schemas.update.presenter import present_schema_update_success
from cli.infrastructure.command import async_command, authenticated_client
from cli.infrastructure.error_display import display_error
from cli.infrastructure.file_content import FileContentError, resolve_content
from cli.infrastructure.formatting.fields import format_optional
from cli.infrastructure.output import OutputService
from cli.infrastructure.renderers.entity_renderer import FieldConfig
from alloy_runtime_types.dtos.schemas import UpdateSchemaRequest
from alloy_runtime_sdk.exceptions.errors import NotFoundError


class EditorError(Exception):
    """Raised when editor operations fail."""


def _get_editor() -> str:
    """Get the user's preferred editor.

    Checks in order:
    1. $EDITOR environment variable
    2. $VISUAL environment variable
    3. Common editors on PATH (nano, vim, vi)

    Returns:
        Path/name of editor to use

    Raises:
        EditorError: If no suitable editor is found
    """
    # Check environment variables first
    editor = os.environ.get("EDITOR") or os.environ.get("VISUAL")
    if editor:
        return editor

    # Fall back to common editors
    for fallback in ("nano", "vim", "vi"):
        if shutil.which(fallback):
            return fallback

    raise EditorError(
        "No editor found. Set $EDITOR environment variable or install nano/vim."
    )


def _open_editor_for_schema(initial_content: str, schema_format: str) -> str | None:
    """Open editor with schema-specific file extension for syntax highlighting.

    Args:
        initial_content: Initial schema definition content
        schema_format: Schema format ID (json_schema or regex)

    Returns:
        Edited content or None if editor failed

    Raises:
        EditorError: If no editor is available
    """
    editor = _get_editor()

    # Choose file extension based on schema format
    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):
            # If parsing fails, use as-is
            pass
    else:
        # Regex pattern
        suffix = ".txt"

    # Create temp file
    with tempfile.NamedTemporaryFile(
        mode="w",
        suffix=suffix,
        delete=False,
    ) as f:
        f.write(initial_content)
        temp_path = Path(f.name)

    try:
        # Get file modification time before editing
        mtime_before = temp_path.stat().st_mtime

        # Open editor and wait for it to close
        editor_parts = editor.split()
        cmd = editor_parts + [str(temp_path)]

        result = subprocess.call(cmd)

        if result != 0:
            return None

        # Check if file was modified
        mtime_after = temp_path.stat().st_mtime
        if mtime_after == mtime_before:
            # File wasn't modified - still return content in case no changes needed
            pass

        # Read the edited content
        content = temp_path.read_text()
        return content

    finally:
        # Clean up temp file
        try:
            temp_path.unlink()
        except OSError:
            pass  # Best effort cleanup


def schemas_update_command(
    schema: str = typer.Argument(..., help="Schema name or UUID"),
    # Metadata fields
    description: Optional[str] = DESCRIPTION,
    # Definition fields
    definition: Optional[str] = typer.Option(
        None,
        "-D",
        "--definition",
        help="Schema definition (or @file)",
    ),
    definition_file: Optional[Path] = FILE_INPUT,
    # Clear flags
    clear_description: bool = CLEAR_DESCRIPTION,
) -> None:
    """Update an existing schema.

    By default, opens interactive editor. Use flags for non-interactive mode.
    Name and format cannot be changed.

    Examples:

        # Interactive (default)
        alloy schemas update my-schema

        # Update description
        alloy schemas update my-schema -d "New description"

        # Update definition from file
        alloy schemas update my-schema -f schema.json

        # Update definition inline
        alloy schemas update my-schema -D '{"type": "object", ...}'
    """
    # Validate definition source - can't have both file and inline
    if definition_file is not None and definition is not None:
        raise typer.BadParameter("Cannot specify both --file and --definition")

    # Validate clear flags
    if clear_description and description is not None:
        raise typer.BadParameter(
            "Cannot specify both --description and --clear-description"
        )

    # Check if any flags were provided (non-interactive mode)
    has_flags = any(
        [
            description is not None,
            definition is not None,
            definition_file is not None,
            clear_description,
        ]
    )

    if has_flags:
        # Non-interactive mode - execute with provided flags
        _execute_update_non_interactive(
            schema_identifier=schema,
            description=description,
            definition=definition,
            definition_file=definition_file,
            clear_description=clear_description,
        )
    else:
        # Interactive mode - fetch current data and launch editor
        _execute_update_interactive(schema_identifier=schema)


def _execute_update_interactive(schema_identifier: str) -> None:
    """Launch interactive editor for updating schema.

    This function is synchronous because editor operations are blocking.
    We use asyncio.run() just for the initial schema fetch, then
    run the editor synchronously.

    Args:
        schema_identifier: Schema name or UUID

    Raises:
        ConfigurationError: If environment variables are missing
        AuthenticationError: If API key is invalid
        NotFoundError: If schema doesn't exist
        ServerError: If server returns 5xx error
    """
    try:
        # Fetch current schema data (async operation, run in its own event loop)
        async def fetch_schema():
            async with authenticated_client() as (_, client):
                return await client.get_schema(schema_identifier)

        try:
            current_data = asyncio.run(fetch_schema())
        except NotFoundError:
            raise typer.BadParameter(
                f"Schema '{schema_identifier}' not found. "
                "Use 'alloy schemas list' to see available schemas."
            )

        output = OutputService.get()

        output.entity(
            current_data,
            f"Current Schema: {current_data.schema_name}",
            [
                FieldConfig("schema_name", "Name"),
                FieldConfig("version", "Version", str),
                FieldConfig("schema_format_id", "Format"),
                FieldConfig("description", "Description", format_optional),
            ],
            raw_payload=current_data,
        )

        try:
            edited_content = _open_editor_for_schema(
                initial_content=current_data.schema_definition,
                schema_format=current_data.schema_format_id,
            )
        except EditorError as e:
            raise typer.BadParameter(str(e))

        if edited_content is None:
            output.result(
                action="update",
                resource="schema",
                status="cancelled",
                identifier=str(current_data.id),
                name=current_data.schema_name,
                extra_fields={"Reason": "Editor exited with error"},
            )
            raise typer.Exit(code=0)

        # Check if definition was modified
        definition_changed = (
            edited_content.strip() != current_data.schema_definition.strip()
        )

        if not definition_changed:
            output.result(
                action="update",
                resource="schema",
                status="cancelled",
                identifier=str(current_data.id),
                name=current_data.schema_name,
                extra_fields={"Reason": "No changes to schema definition"},
            )
            raise typer.Exit(code=0)

        update_description = typer.confirm(
            "Do you want to update the description as well?",
            default=False,
        )

        new_description: Optional[str] = None
        if update_description:
            new_description = typer.prompt(
                "Enter new description (leave empty to clear)",
                default=current_data.description or "",
            )
            # Empty string means clear (strip to handle whitespace-only input)
            if new_description and not new_description.strip():
                new_description = ""

        # Build request
        request = UpdateSchemaRequest(
            schema_definition=edited_content.strip(),
            description=new_description,
        )

        # Execute update via API
        async def update_schema():
            async with authenticated_client() as (_, client):
                # Resolve to UUID (server only accepts UUID for update)
                return await client.update_schema(current_data.id, request)

        response = asyncio.run(update_schema())

        # Present success output
        present_schema_update_success(response)

    except typer.Exit:
        # Re-raise Exit exceptions (don't treat as errors)
        raise
    except Exception as e:
        display_error(e)
        raise typer.Exit(code=1)


@async_command
async def _execute_update_non_interactive(
    schema_identifier: str,
    description: Optional[str],
    definition: Optional[str],
    definition_file: Optional[Path],
    clear_description: bool,
) -> None:
    """Execute the schema update operation in non-interactive mode.

    Args:
        schema_identifier: Schema name or UUID
        description: Optional new description
        definition: Optional new definition (inline or @file)
        definition_file: Optional definition file path
        clear_description: If True, clear the description

    Raises:
        ConfigurationError: If environment variables are missing
        AuthenticationError: If API key is invalid
        NotFoundError: If schema doesn't exist
        ValidationError: If update data is invalid
        ServerError: If server returns 5xx error
    """
    try:
        definition = resolve_content(definition)
    except FileContentError as e:
        raise typer.BadParameter(str(e))

    schema_definition: Optional[str] = None
    if definition_file is not None:
        schema_definition = definition_file.read_text()
    elif definition is not None:
        schema_definition = definition

    final_description: Optional[str] = description
    if clear_description:
        final_description = ""

    request = UpdateSchemaRequest(
        description=final_description
        if (description is not None or clear_description)
        else None,
        schema_definition=schema_definition,
    )

    async with authenticated_client() as (_config, client):
        try:
            schema_data = await client.get_schema(schema_identifier)
        except NotFoundError:
            raise typer.BadParameter(
                f"Schema '{schema_identifier}' not found. "
                "Use 'alloy schemas list' to see available schemas."
            )

        response = await client.update_schema(schema_data.id, request)

    present_schema_update_success(response)
