"""Main client for interacting with the Vicoa Agent Dashboard API."""

import logging
import time
import uuid
from typing import Optional, Dict, Any, Union, List, Callable
from urllib.parse import urljoin

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

from vicoa.constants import DEFAULT_API_URL

from .exceptions import AuthenticationError, TimeoutError, APIError
from .models import (
    EndSessionResponse,
    CreateMessageResponse,
    PendingMessagesResponse,
    Message,
    RegisterAgentInstanceResponse,
)
from .utils import (
    validate_agent_instance_id,
    build_message_request_data,
)


class LoggingRetry(Retry):
    """Custom Retry class that logs retry attempts."""

    def __init__(self, *args, log_func=None, **kwargs):
        self._log_func = log_func
        super().__init__(*args, **kwargs)

    def new(self, **kw):
        """Ensure log_func is passed to new instances."""
        kw["log_func"] = self._log_func
        return super().new(**kw)

    def increment(
        self,
        method=None,
        url=None,
        response=None,
        error=None,
        _pool=None,
        _stacktrace=None,
    ):
        """Log retry attempts."""
        if self._log_func and error and self.total:
            remaining = self.total - 1 if self.total else 0
            if remaining >= 0:
                error_msg = str(error)
                if len(error_msg) > 100:
                    error_msg = error_msg[:100] + "..."
                self._log_func(
                    f"[WARNING] Retry attempt for {method} {url} (remaining: {remaining}) - {error_msg}"
                )
        return super().increment(method, url, response, error, _pool, _stacktrace)


class VicoaClient:
    """Client for interacting with the Vicoa Agent Dashboard API.

    Args:
        api_key: JWT API key for authentication
        base_url: Base URL of the API server (default: https://api.vicoa.ai:8443)
        timeout: Default timeout for requests in seconds (default: 30)
    """

    def __init__(
        self,
        api_key: str,
        base_url: str = DEFAULT_API_URL,
        timeout: int = 30,
        max_retries: int = 5,
        backoff_factor: float = 1.0,
        backoff_max: float = 60.0,
        log_func: Optional[Callable[[str], None]] = None,
    ):
        self.api_key = api_key
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout
        self.max_retries = max_retries
        self.backoff_factor = backoff_factor
        self.backoff_max = backoff_max
        self.log_func = log_func

        if not log_func:
            logging.getLogger("urllib3.connectionpool").setLevel(logging.WARNING)

        self.session = requests.Session()

        retry_class = LoggingRetry if self.log_func else Retry
        retry_kwargs = {
            "total": max_retries,
            "backoff_factor": backoff_factor,
            "backoff_max": backoff_max,
            "status_forcelist": [429, 500, 502, 503, 504],
            "allowed_methods": ["GET", "POST", "PUT", "DELETE", "PATCH"],
            "raise_on_status": False,
            "connect": max_retries,
            "read": max_retries,
            "other": max_retries,
        }
        if self.log_func:
            retry_kwargs["log_func"] = self.log_func
        retry_strategy = retry_class(**retry_kwargs)
        adapter = HTTPAdapter(max_retries=retry_strategy)
        self.session.mount("http://", adapter)
        self.session.mount("https://", adapter)

        # Set default headers
        self.session.headers.update(
            {
                "Authorization": f"Bearer {api_key}",
                "Content-Type": "application/json",
                "User-Agent": "vicoa-python-sdk",
            }
        )

    def __enter__(self):
        """Context manager entry."""
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """Context manager exit."""
        self.close()

    def _make_request(
        self,
        method: str,
        endpoint: str,
        json: Optional[Dict[str, Any]] = None,
        params: Optional[Dict[str, Any]] = None,
        timeout: Optional[int] = None,
    ) -> Dict[str, Any]:
        """Make an HTTP request to the API.

        Args:
            method: HTTP method (GET, POST, etc.)
            endpoint: API endpoint path
            json: JSON body for the request
            params: Query parameters for the request
            timeout: Request timeout in seconds

        Returns:
            Response JSON data

        Raises:
            AuthenticationError: If authentication fails
            APIError: If the API returns an error
            TimeoutError: If the request times out
        """
        url = urljoin(self.base_url, endpoint)
        timeout = timeout or self.timeout

        try:
            response = self.session.request(
                method=method, url=url, json=json, params=params, timeout=timeout
            )

            if response.status_code == 401:
                raise AuthenticationError("Invalid API key or authentication failed")

            if not response.ok:
                try:
                    error_detail = response.json().get("detail", response.text)
                except Exception:
                    error_detail = response.text
                raise APIError(response.status_code, error_detail)

            return response.json()

        except requests.exceptions.Timeout:
            raise TimeoutError(f"Request timed out after {timeout} seconds")
        except requests.exceptions.RequestException as e:
            raise APIError(0, f"Request failed: {str(e)}")

    def send_message(
        self,
        content: str,
        agent_type: Optional[str] = None,
        agent_instance_id: Optional[Union[str, uuid.UUID]] = None,
        requires_user_input: bool = False,
        timeout_minutes: int = 1440,
        poll_interval: float = 10.0,
        send_push: Optional[bool] = None,
        send_email: Optional[bool] = None,
        send_sms: Optional[bool] = None,
        git_diff: Optional[str] = None,
        message_metadata: Optional[Dict[str, Any]] = None,
    ) -> CreateMessageResponse:
        """Send a message to the dashboard.

        Args:
            content: The message content (step description or question text)
            agent_type: Type of agent (required if agent_instance_id not provided)
            agent_instance_id: Existing agent instance ID (optional)
            requires_user_input: Whether this message requires user input (default: False)
            timeout_minutes: If requires_user_input, max time to wait in minutes (default: 1440)
            poll_interval: If requires_user_input, time between polls in seconds (default: 10.0)
            send_push: Send push notification (default: False for steps, user pref for questions)
            send_email: Send email notification (default: False for steps, user pref for questions)
            send_sms: Send SMS notification (default: False for steps, user pref for questions)
            git_diff: Git diff content to include (optional). This SDK encodes
                the diff in base64 for transmission; the server auto-detects and decodes.

        Returns:
            CreateMessageResponse with any queued user messages

        Raises:
            ValueError: If neither agent_type nor agent_instance_id is provided
            TimeoutError: If requires_user_input and no answer is received within timeout
        """
        # If no agent_instance_id provided, generate one client-side
        if not agent_instance_id:
            if not agent_type:
                raise ValueError("agent_type is required when creating a new instance")
            agent_instance_id = uuid.uuid4()

        # Validate and convert agent_instance_id to string
        agent_instance_id_str = validate_agent_instance_id(agent_instance_id)

        # Build request data using shared utility
        data = build_message_request_data(
            content=content,
            agent_instance_id=agent_instance_id_str,
            requires_user_input=requires_user_input,
            agent_type=agent_type,
            send_push=send_push,
            send_email=send_email,
            send_sms=send_sms,
            git_diff=git_diff,
            message_metadata=message_metadata,
        )

        # Send the message
        response = self._make_request("POST", "/api/v1/messages/agent", json=data)
        response_agent_instance_id = response["agent_instance_id"]
        message_id = response["message_id"]

        queued_contents = [
            msg["content"] if isinstance(msg, dict) else msg
            for msg in response.get("queued_user_messages", [])
        ]

        create_response = CreateMessageResponse(
            success=response["success"],
            agent_instance_id=response_agent_instance_id,
            message_id=message_id,
            queued_user_messages=queued_contents,
        )

        # If it doesn't require user input, return immediately with any queued messages
        if not requires_user_input:
            return create_response

        # Otherwise, we need to poll for user response
        # Use the message ID we just created as our starting point
        last_read_message_id = message_id

        timeout_seconds = timeout_minutes * 60
        start_time = time.time()
        all_messages = []

        while time.time() - start_time < timeout_seconds:
            # Poll for pending messages
            pending_response = self.get_pending_messages(
                agent_instance_id_str, last_read_message_id
            )

            # If status is "stale", another process has read the messages
            if pending_response.status == "stale":
                raise TimeoutError("Another process has read the messages")

            # Check if we got any messages
            if pending_response.messages:
                # Collect all messages
                all_messages.extend(pending_response.messages)

                # Return the response with all collected messages
                create_response.queued_user_messages = [
                    msg.content for msg in all_messages
                ]
                return create_response

            time.sleep(poll_interval)

        raise TimeoutError(f"Question timed out after {timeout_minutes} minutes")

    def register_agent_instance(
        self,
        *,
        agent_type: str,
        transport: str = "ws",
        agent_instance_id: Optional[Union[str, uuid.UUID]] = None,
        name: Optional[str] = None,
        project: Optional[str] = None,
        home_dir: Optional[str] = None,
    ) -> RegisterAgentInstanceResponse:
        """Register or update an agent instance for terminal relay sessions.

        Args:
            agent_type: Agent type identifier (e.g., "claude" or "codex")
            transport: Transport mechanism used by the instance
            agent_instance_id: Optional fixed UUID to reuse for reconnect scenarios
            name: Optional display name for the instance
            project: Optional project identifier (directory path or repository URL)
            home_dir: Optional home directory path for expanding tilde (~) in project paths
        """

        payload: Dict[str, Any] = {
            "agent_type": agent_type,
            "transport": transport,
        }

        target_instance_id: Optional[str] = None
        if agent_instance_id:
            target_instance_id = validate_agent_instance_id(agent_instance_id)
            payload["agent_instance_id"] = target_instance_id

        if name is not None:
            payload["name"] = name
        if project is not None:
            payload["project"] = project
        if home_dir is not None:
            payload["home_dir"] = home_dir

        try:
            response = self._make_request(
                "POST", "/api/v1/agent-instances", json=payload
            )
        except APIError as err:
            if err.status_code == 409 and target_instance_id:
                detail = self._make_request(
                    "GET",
                    f"/api/v1/agent-instances/{target_instance_id}",
                    params={"message_limit": 0},
                )
                return RegisterAgentInstanceResponse(
                    agent_instance_id=detail["id"],
                    agent_type_id=detail.get("agent_type_id"),
                    agent_type_name=detail.get("agent_type_name"),
                    status=detail.get("status", ""),
                    name=None,
                    instance_metadata=detail.get("instance_metadata"),
                    project=detail.get("project"),
                )
            raise

        instance_id = response.get("agent_instance_id") or response.get("id")
        if instance_id is None:
            raise KeyError("agent_instance_id")

        return RegisterAgentInstanceResponse(
            agent_instance_id=instance_id,
            agent_type_id=response.get("agent_type_id"),
            agent_type_name=response.get("agent_type_name"),
            status=response.get("status", ""),
            name=response.get("name"),
            instance_metadata=response.get("instance_metadata"),
            project=response.get("project"),
        )

    def get_pending_messages(
        self,
        agent_instance_id: Union[str, uuid.UUID],
        last_read_message_id: Optional[str] = None,
    ) -> PendingMessagesResponse:
        """Get pending user messages for an agent instance.

        Args:
            agent_instance_id: Agent instance ID
            last_read_message_id: The last message ID that was read (optional)

        Returns:
            PendingMessagesResponse with messages and status
        """
        # Validate and convert agent_instance_id to string
        agent_instance_id_str = validate_agent_instance_id(agent_instance_id)

        params = {"agent_instance_id": agent_instance_id_str}
        # Only include last_read_message_id if it's not None
        # Empty string is treated as None by the API
        if last_read_message_id is not None:
            params["last_read_message_id"] = last_read_message_id
        else:
            params["last_read_message_id"] = ""

        response = self._make_request("GET", "/api/v1/messages/pending", params=params)

        return PendingMessagesResponse(
            agent_instance_id=response["agent_instance_id"],
            messages=[Message(**msg) for msg in response["messages"]],
            status=response["status"],
        )

    def send_user_message(
        self,
        agent_instance_id: Union[str, uuid.UUID],
        content: str,
        mark_as_read: bool = True,
    ) -> Dict[str, Any]:
        """Send a user message to an agent instance.

        Args:
            agent_instance_id: The agent instance ID to send the message to
            content: Message content
            mark_as_read: Whether to mark as read (update last_read_message_id) (default: True)

        Returns:
            Dict containing:
                - success: Whether the message was created
                - message_id: ID of the created message
                - marked_as_read: Whether the message was marked as read

        Raises:
            ValueError: If agent instance not found or access denied
            APIError: If the API request fails
        """
        # Validate and convert agent_instance_id
        agent_instance_id = validate_agent_instance_id(agent_instance_id)

        data = {
            "agent_instance_id": str(agent_instance_id),
            "content": content,
            "mark_as_read": mark_as_read,
        }

        return self._make_request("POST", "/api/v1/messages/user", json=data)

    def request_user_input(
        self,
        message_id: Union[str, uuid.UUID],
        timeout_minutes: int = 1440,
        poll_interval: float = 10.0,
    ) -> List[str]:
        """Request user input for a previously sent agent message.

        This method updates an agent message to require user input and polls for responses.
        It's useful when you initially send a message without requiring input, but later
        decide you need user feedback.

        Args:
            message_id: The message ID to update (must be an agent message)
            timeout_minutes: Max time to wait for user response in minutes (default: 1440)
            poll_interval: Time between polls in seconds (default: 10.0)

        Returns:
            List of user message contents received as responses

        Raises:
            ValueError: If message not found, already requires input, or not an agent message
            TimeoutError: If no user response is received within timeout
            APIError: If the API request fails
        """
        # Convert message_id to string if it's a UUID
        message_id_str = str(message_id)

        # Call the endpoint to update the message
        response = self._make_request(
            "PATCH", f"/api/v1/messages/{message_id_str}/request-input"
        )

        agent_instance_id = response["agent_instance_id"]
        messages = response.get("messages", [])

        if messages:
            return [msg["content"] for msg in messages]

        # Otherwise, poll for user response
        timeout_seconds = timeout_minutes * 60
        start_time = time.time()
        all_messages = []

        while time.time() - start_time < timeout_seconds:
            # Poll for pending messages using the message_id as last_read
            pending_response = self.get_pending_messages(
                agent_instance_id, message_id_str
            )

            # If status is "stale", another process has read the messages
            if pending_response.status == "stale":
                raise TimeoutError("Another process has read the messages")

            # Check if we got any messages
            if pending_response.messages:
                # Collect all message contents
                all_messages.extend([msg.content for msg in pending_response.messages])
                return all_messages

            time.sleep(poll_interval)

        raise TimeoutError(f"No user response received after {timeout_minutes} minutes")

    def mark_message_requires_input(
        self, message_id: Union[str, uuid.UUID]
    ) -> List[str]:
        """Mark an agent message as requiring user input without polling.

        Args:
            message_id: The agent message ID to mark.

        Returns:
            Any already-queued user message contents returned by the API.
        """
        message_id_str = str(message_id)
        response = self._make_request(
            "PATCH", f"/api/v1/messages/{message_id_str}/request-input"
        )
        messages = response.get("messages", [])
        return [msg["content"] for msg in messages if isinstance(msg, dict)]

    def update_agent_instance_status(
        self, agent_instance_id: Union[str, uuid.UUID], status: str
    ) -> Dict[str, Any]:
        """Update the status of an existing agent instance."""

        agent_instance_id_str = validate_agent_instance_id(agent_instance_id)
        endpoint = f"/api/v1/agent-instances/{agent_instance_id_str}/status"
        return self._make_request("PUT", endpoint, json={"status": status})

    def end_session(
        self, agent_instance_id: Union[str, uuid.UUID]
    ) -> EndSessionResponse:
        """End an agent session and mark it as completed.

        Args:
            agent_instance_id: Agent instance ID to end

        Returns:
            EndSessionResponse with success status and final details
        """
        # Validate and convert agent_instance_id to string
        agent_instance_id_str = validate_agent_instance_id(agent_instance_id)

        data: Dict[str, Any] = {"agent_instance_id": agent_instance_id_str}
        response = self._make_request("POST", "/api/v1/sessions/end", json=data)

        return EndSessionResponse(
            success=response["success"],
            agent_instance_id=response["agent_instance_id"],
            final_status=response["final_status"],
        )

    def sync_commands(
        self, agent_type: str, commands: Dict[str, Dict[str, str]]
    ) -> Dict[str, Any]:
        """Sync slash commands from CLI to the backend.

        Args:
            agent_type: Agent type ('claude', 'codex', or 'opencode')
            commands: Dict of commands {command_name: {description: ...}}

        Returns:
            Dict with sync response data
        """
        data: Dict[str, Any] = {"agent_type": agent_type, "commands": commands}
        response = self._make_request("POST", "/api/v1/commands/sync", json=data)
        return response

    def sync_files(self, project_path: str, files: List[str]) -> Dict[str, Any]:
        """Sync project files from CLI to the backend for @ mentions.

        Args:
            project_path: Absolute path to the project directory
            files: List of relative file paths from project_path

        Returns:
            Dict with sync response data
        """
        data: Dict[str, Any] = {"project_path": project_path, "files": files}
        response = self._make_request("POST", "/api/v1/files/sync", json=data)
        return response

    def close(self):
        """Close the session and clean up resources."""
        self.session.close()
