"""
ChatLiteLLM - LiteLLM chat model wrapper.

Requires the `litellm` package to be installed separately:
    pip install litellm

Note: litellm is NOT included as a dependency of browser-use.
"""

import logging
from dataclasses import dataclass, field
from typing import Any, TypeVar, overload

from pydantic import BaseModel

from browser_use.llm.base import BaseChatModel
from browser_use.llm.exceptions import ModelProviderError, ModelRateLimitError
from browser_use.llm.messages import BaseMessage
from browser_use.llm.schema import SchemaOptimizer
from browser_use.llm.views import ChatInvokeCompletion, ChatInvokeUsage

from .serializer import LiteLLMMessageSerializer

logger = logging.getLogger(__name__)

T = TypeVar('T', bound=BaseModel)


@dataclass
class ChatLiteLLM(BaseChatModel):
	model: str
	api_key: str | None = None
	api_base: str | None = None
	temperature: float | None = 0.0
	max_tokens: int | None = 4096
	max_retries: int = 3
	metadata: dict[str, Any] | None = None

	_provider_name: str = field(default='', init=False, repr=False)
	_clean_model: str = field(default='', init=False, repr=False)

	def __post_init__(self) -> None:
		"""Resolve provider info from the model string via litellm."""
		try:
			from litellm import get_llm_provider  # type: ignore[reportMissingImports]

			self._clean_model, self._provider_name, _, _ = get_llm_provider(self.model)
		except Exception:
			if '/' in self.model:
				self._provider_name, self._clean_model = self.model.split('/', 1)
			else:
				self._provider_name = 'openai'
				self._clean_model = self.model

		logger.debug(
			'ChatLiteLLM initialized: model=%s, provider=%s, clean=%s, api_base=%s',
			self.model,
			self._provider_name,
			self._clean_model,
			self.api_base or '(default)',
		)

	@property
	def provider(self) -> str:
		return self._provider_name or 'litellm'

	@property
	def name(self) -> str:
		return self._clean_model or self.model

	@staticmethod
	def _parse_usage(response: Any) -> ChatInvokeUsage | None:
		"""Extract token usage from a litellm response."""
		usage = getattr(response, 'usage', None)
		if usage is None:
			return None

		prompt_tokens = getattr(usage, 'prompt_tokens', 0) or 0
		completion_tokens = getattr(usage, 'completion_tokens', 0) or 0

		prompt_cached = getattr(usage, 'cache_read_input_tokens', None)
		cache_creation = getattr(usage, 'cache_creation_input_tokens', None)

		if prompt_cached is None:
			details = getattr(usage, 'prompt_tokens_details', None)
			if details:
				prompt_cached = getattr(details, 'cached_tokens', None)

		return ChatInvokeUsage(
			prompt_tokens=prompt_tokens,
			prompt_cached_tokens=int(prompt_cached) if prompt_cached is not None else None,
			prompt_cache_creation_tokens=int(cache_creation) if cache_creation is not None else None,
			prompt_image_tokens=None,
			completion_tokens=completion_tokens,
			total_tokens=prompt_tokens + completion_tokens,
		)

	@overload
	async def ainvoke(
		self,
		messages: list[BaseMessage],
		output_format: None = None,
		**kwargs: Any,
	) -> ChatInvokeCompletion[str]: ...

	@overload
	async def ainvoke(
		self,
		messages: list[BaseMessage],
		output_format: type[T],
		**kwargs: Any,
	) -> ChatInvokeCompletion[T]: ...

	async def ainvoke(
		self,
		messages: list[BaseMessage],
		output_format: type[T] | None = None,
		**kwargs: Any,
	) -> ChatInvokeCompletion[T] | ChatInvokeCompletion[str]:
		from litellm import acompletion  # type: ignore[reportMissingImports]
		from litellm.exceptions import APIConnectionError, APIError, RateLimitError, Timeout  # type: ignore[reportMissingImports]
		from litellm.types.utils import ModelResponse  # type: ignore[reportMissingImports]

		litellm_messages = LiteLLMMessageSerializer.serialize(messages)

		params: dict[str, Any] = {
			'model': self.model,
			'messages': litellm_messages,
			'num_retries': self.max_retries,
		}

		if self.temperature is not None:
			params['temperature'] = self.temperature
		if self.max_tokens is not None:
			params['max_tokens'] = self.max_tokens
		if self.api_key:
			params['api_key'] = self.api_key
		if self.api_base:
			params['api_base'] = self.api_base
		if self.metadata:
			params['metadata'] = self.metadata

		if output_format is not None:
			schema = SchemaOptimizer.create_optimized_json_schema(output_format)
			params['response_format'] = {
				'type': 'json_schema',
				'json_schema': {
					'name': 'agent_output',
					'strict': True,
					'schema': schema,
				},
			}

		try:
			raw_response = await acompletion(**params)
		except RateLimitError as e:
			raise ModelRateLimitError(
				message=str(e),
				model=self.name,
			) from e
		except Timeout as e:
			raise ModelProviderError(
				message=f'Request timed out: {e}',
				model=self.name,
			) from e
		except APIConnectionError as e:
			raise ModelProviderError(
				message=str(e),
				model=self.name,
			) from e
		except APIError as e:
			status = getattr(e, 'status_code', 502) or 502
			raise ModelProviderError(
				message=str(e),
				status_code=status,
				model=self.name,
			) from e
		except ModelProviderError:
			raise
		except Exception as e:
			raise ModelProviderError(
				message=str(e),
				model=self.name,
			) from e

		assert isinstance(raw_response, ModelResponse), f'Expected ModelResponse, got {type(raw_response)}'
		response: ModelResponse = raw_response

		choice = response.choices[0] if response.choices else None
		if choice is None:
			raise ModelProviderError(
				message='Empty response: no choices returned by the model',
				status_code=502,
				model=self.name,
			)

		content = choice.message.content or ''
		usage = self._parse_usage(response)
		stop_reason = choice.finish_reason

		thinking: str | None = None
		msg_obj = choice.message
		reasoning = getattr(msg_obj, 'reasoning_content', None)
		if reasoning:
			thinking = str(reasoning)

		if output_format is not None:
			if not content:
				raise ModelProviderError(
					message='Model returned empty content for structured output request',
					status_code=500,
					model=self.name,
				)
			parsed = output_format.model_validate_json(content)
			return ChatInvokeCompletion(
				completion=parsed,
				thinking=thinking,
				usage=usage,
				stop_reason=stop_reason,
			)

		return ChatInvokeCompletion(
			completion=content,
			thinking=thinking,
			usage=usage,
			stop_reason=stop_reason,
		)
