"""Claude SDK backend implementation.

Wraps the Claude Agent SDK to provide the Backend interface.
This is the default backend when using Claude Code.
"""

import asyncio
from collections.abc import AsyncIterator
from pathlib import Path
from typing import Any

from cleave.core.retry import retry_on_network_errors
from cleave.tui.backends.base import (
    Backend,
    BackendAuthError,
    BackendConfig,
    BackendConnectionError,
    BackendQueryError,
    BackendState,
    Message,
    MessageType,
)

# Defer SDK import - it's optional
_SDK_AVAILABLE: bool | None = None


def _check_sdk() -> bool:
    """Check if Claude SDK is available."""
    global _SDK_AVAILABLE
    if _SDK_AVAILABLE is None:
        try:
            import claude_agent_sdk  # noqa: F401

            _SDK_AVAILABLE = True
        except ImportError:
            _SDK_AVAILABLE = False
    return _SDK_AVAILABLE


class ClaudeBackend(Backend):
    """Backend using Claude Agent SDK.

    Requires:
        - claude-agent-sdk package installed
        - Authentication via Claude CLI (~/.claude/.credentials.json)

    This backend supports the full Claude Code feature set including
    tool use, multi-turn conversations, and skill invocation.
    """

    # Default models
    MODELS = [
        "claude-sonnet-4-20250514",
        "claude-opus-4-20250514",
        "claude-haiku-4-20250514",
    ]

    # Model name aliases
    MODEL_ALIASES = {
        "sonnet": "claude-sonnet-4-20250514",
        "opus": "claude-opus-4-20250514",
        "haiku": "claude-haiku-4-20250514",
    }

    def __init__(self) -> None:
        """Initialize Claude backend."""
        self._state = BackendState.DISCONNECTED
        self._error: str | None = None
        self._client: Any = None
        self._config: BackendConfig | None = None
        self._streaming_active: bool = False

    @property
    def name(self) -> str:
        """Human-readable backend name."""
        return "Claude"

    @property
    def state(self) -> BackendState:
        """Current connection state."""
        return self._state

    @property
    def error(self) -> str | None:
        """Last error message."""
        return self._error

    async def connect(self, config: BackendConfig) -> None:
        """Connect to Claude SDK.

        Args:
            config: Backend configuration.

        Raises:
            BackendConnectionError: If SDK unavailable or connection fails.
            BackendAuthError: If not authenticated.
        """
        if self._state == BackendState.CONNECTED:
            return

        if not _check_sdk():
            self._state = BackendState.ERROR
            self._error = "Claude SDK not installed"
            raise BackendConnectionError(
                "Claude SDK not installed.\n\n"
                "Install with: pip install styrene-cleave[tui]\n"
                "Or: pip install claude-agent-sdk"
            )

        # Check authentication
        is_auth, msg = self.check_auth()
        if not is_auth:
            self._state = BackendState.ERROR
            self._error = "Not authenticated"
            raise BackendAuthError(
                f"{msg}\n\n"
                "Authenticate with: claude\n"
                "(Opens browser for login)"
            )

        self._state = BackendState.CONNECTING
        self._error = None
        self._config = config

        try:
            from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient

            # Resolve model alias
            model = config.model or "claude-sonnet-4-20250514"
            model = self.MODEL_ALIASES.get(model, model)

            options = ClaudeAgentOptions(
                max_turns=config.max_turns,
                include_partial_messages=True,  # Enable streaming deltas
            )

            if config.system_prompt:
                options.system_prompt = config.system_prompt

            if config.allowed_tools:
                options.allowed_tools = config.allowed_tools

            if config.disallowed_tools:
                options.disallowed_tools = config.disallowed_tools

            self._client = ClaudeSDKClient(options=options)
            await self._client.__aenter__()
            self._state = BackendState.CONNECTED
            self._streaming_active = False  # Track streaming state

        except BackendAuthError:
            # Re-raise auth errors as-is
            raise
        except Exception as e:
            self._state = BackendState.ERROR
            error_msg = str(e)
            self._error = error_msg

            # Provide actionable error messages
            if "credentials" in error_msg.lower() or "auth" in error_msg.lower():
                raise BackendAuthError(
                    f"Authentication failed: {error_msg}\n\n"
                    "Check credentials at: ~/.claude/.credentials.json\n"
                    "Re-authenticate with: claude"
                ) from e
            elif "network" in error_msg.lower() or "connection" in error_msg.lower():
                raise BackendConnectionError(
                    f"Network error: {error_msg}\n\n"
                    "Check your internet connection and try again."
                ) from e
            else:
                raise BackendConnectionError(
                    f"Failed to connect: {error_msg}\n\n"
                    "If this persists, check ~/.claude/logs/ for details."
                ) from e

    async def disconnect(self) -> None:
        """Disconnect from Claude SDK."""
        if self._client is not None:
            try:
                await self._client.__aexit__(None, None, None)
            except Exception:
                pass  # Best effort cleanup
            finally:
                self._client = None
                self._state = BackendState.DISCONNECTED
        else:
            # Even if no client, mark as disconnected
            self._state = BackendState.DISCONNECTED

    async def query(self, prompt: str) -> AsyncIterator[Message]:
        """Send a prompt and stream responses.

        Args:
            prompt: User message to send.

        Yields:
            Message objects as they arrive.

        Raises:
            RuntimeError: If not connected.
            BackendQueryError: If query fails.
        """
        if self._state != BackendState.CONNECTED or self._client is None:
            self._state = BackendState.ERROR
            self._error = "Not connected"
            raise RuntimeError(
                "Backend not connected.\n\n"
                "Call connect() first or check connection status."
            )

        from cleave.tui.backends.base import BackendQueryError

        try:
            await self._client.query(prompt)

            async for event in self._client.receive_response():
                message = self._parse_event(event)
                if message:
                    yield message

        except asyncio.TimeoutError as e:
            self._state = BackendState.ERROR
            self._error = "Request timeout"
            raise BackendQueryError(
                f"Request timed out after {self._config.timeout if self._config else 120}s.\n\n"
                "The model is taking too long to respond. Try again or use a smaller prompt."
            ) from e
        except ConnectionError as e:
            self._state = BackendState.ERROR
            self._error = "Connection lost"
            raise BackendQueryError(
                f"Connection lost during query: {e}\n\n"
                "Check your internet connection and try reconnecting."
            ) from e
        except Exception as e:
            self._state = BackendState.ERROR
            error_msg = str(e)
            self._error = error_msg

            # Provide context-specific error messages
            if "rate limit" in error_msg.lower():
                raise BackendQueryError(
                    f"Rate limit exceeded: {error_msg}\n\n"
                    "Wait a moment before trying again."
                ) from e
            elif "context length" in error_msg.lower() or "token" in error_msg.lower():
                raise BackendQueryError(
                    f"Context limit exceeded: {error_msg}\n\n"
                    "Try a shorter conversation or start a new session."
                ) from e
            else:
                raise BackendQueryError(
                    f"Query failed: {error_msg}\n\n"
                    "Check error details and try again."
                ) from e

    def _parse_event(self, event: Any) -> Message | None:
        """Parse SDK event into a Message.

        Args:
            event: Raw SDK event.

        Returns:
            Message if displayable content, None otherwise.
        """
        from claude_agent_sdk.types import AssistantMessage, ResultMessage, SystemMessage

        # Handle StreamEvent for token-level streaming
        if hasattr(event, "event") and hasattr(event.event, "type"):
            return self._parse_stream_event(event.event)

        if isinstance(event, AssistantMessage):
            # Complete message - end any active streaming
            text_parts = []
            for block in event.content:
                if hasattr(block, "text"):
                    text_parts.append(block.text)
            if text_parts:
                content = "".join(text_parts)
                if self._streaming_active:
                    self._streaming_active = False
                    return Message(
                        role="assistant",
                        content=content,
                        message_type=MessageType.STREAM_END,
                    )
                return Message(role="assistant", content=content)
            return None

        if isinstance(event, SystemMessage):
            if event.subtype == "init":
                return None
            return Message(
                role="system",
                content=f"[{event.subtype}]",
                metadata=event.data if hasattr(event, "data") else {},
            )

        if isinstance(event, ResultMessage):
            if event.is_error:
                return Message(
                    role="system",
                    content=f"[Error: {event.result}]",
                    metadata={"error": True},
                )
            return None

        # Handle legacy/generic event types
        if hasattr(event, "type"):
            if event.type == "text":
                return Message(role="assistant", content=event.text)
            elif event.type == "tool_use":
                return Message(
                    role="tool",
                    content=f"[Tool: {event.name}]",
                    metadata={"tool": event.name, "input": getattr(event, "input", {})},
                )

        # Fallback for string events
        if isinstance(event, str):
            return Message(role="assistant", content=event)

        return None

    def _parse_stream_event(self, raw_event: Any) -> Message | None:
        """Parse raw Anthropic stream event into a Message.

        Handles content_block_start, content_block_delta, content_block_stop.

        Args:
            raw_event: Raw Anthropic API stream event.

        Returns:
            Message with appropriate streaming type, or None.
        """
        event_type = getattr(raw_event, "type", None)

        if event_type == "content_block_start":
            # Start of a content block
            content_block = getattr(raw_event, "content_block", None)
            if content_block and getattr(content_block, "type", None) == "text":
                self._streaming_active = True
                return Message(
                    role="assistant",
                    content="",
                    message_type=MessageType.STREAM_START,
                )
            return None

        elif event_type == "content_block_delta":
            # Token/chunk during streaming
            delta = getattr(raw_event, "delta", None)
            if delta and getattr(delta, "type", None) == "text_delta":
                text = getattr(delta, "text", "")
                if text:
                    return Message(
                        role="assistant",
                        content=text,
                        message_type=MessageType.STREAM_DELTA,
                    )
            return None

        elif event_type == "content_block_stop":
            # End of content block (but message may continue)
            return None  # Wait for AssistantMessage for final content

        elif event_type == "message_start":
            # Message starting - could emit a signal
            return None

        elif event_type == "message_delta":
            # Message metadata update (stop_reason, etc.)
            return None

        elif event_type == "message_stop":
            # Message complete
            if self._streaming_active:
                self._streaming_active = False
                return Message(
                    role="assistant",
                    content="",
                    message_type=MessageType.STREAM_END,
                )
            return None

        return None

    @classmethod
    def check_auth(cls) -> tuple[bool, str]:
        """Check if Claude SDK authentication is configured.

        Validates both credentials file existence and API key validity.

        Returns:
            Tuple of (is_authenticated, status_message).
        """
        if not _check_sdk():
            return False, "Claude SDK not installed"

        auth_file = Path.home() / ".claude" / ".credentials.json"
        if not auth_file.exists():
            return False, "Not authenticated. Run 'claude' CLI to login via browser."

        # Validate credentials file is well-formed
        try:
            import json

            with open(auth_file) as f:
                creds = json.load(f)

            # Check for required fields
            if not isinstance(creds, dict):
                return False, "Credentials file is malformed"

            # Check for API key
            api_key = creds.get("apiKey")
            if not api_key or not isinstance(api_key, str):
                return False, "API key missing or invalid in credentials file"

            # Basic format validation
            if not api_key.startswith("sk-ant-"):
                return False, "API key has invalid format"

            # Note: We don't test actual connectivity here to keep it fast
            # Actual validation happens during connect()
            return True, "Authenticated"

        except json.JSONDecodeError:
            return False, "Credentials file is not valid JSON"
        except Exception as e:
            return False, f"Error reading credentials: {e}"

    @classmethod
    def available_models(cls, config: BackendConfig | None = None) -> list[str]:
        """List available Claude models.

        Returns:
            List of model identifiers.
        """
        return cls.MODELS.copy()

    @classmethod
    def default_config(cls) -> BackendConfig:
        """Get default configuration for Claude backend.

        Returns:
            BackendConfig with Claude defaults.
        """
        return BackendConfig(
            model="claude-sonnet-4-20250514",
            max_turns=50,
        )
