import logging
import time
from typing import Optional, TypeVar, Dict, Any, Union, Mapping, Sequence, Tuple
from collections import deque

import httpx
from httpx import HTTPStatusError, Response
from pydantic import BaseModel

from ._meta import __version__
from .exceptions import PhrappyError
from .tags import (
    AdditionalWorkflowStepOperations,
    AnalysisOperations,
    AsyncRequestOperations,
    AuthenticationOperations,
    BilingualFileOperations,
    BusinessUnitOperations,
    BuyerOperations,
    ClientOperations,
    ConnectorOperations,
    CostCenterOperations,
    CustomFieldsOperations,
    CustomFileTypeOperations,
    DomainOperations,
    DueDateSchemeOperations,
    EmailTemplateOperations,
    FileOperations,
    GlossaryOperations,
    ImportsettingsOperations,
    ConversationsOperations,
    JobOperations,
    SupportedLanguagesOperations,
    LanguageQualityAssessmentOperations,
    QualityAssuranceOperations,
    MachineTranslationSettingsOperations,
    MachineTranslationOperations,
    MappingOperations,
    LanguageAIOperations,
    NetRateSchemeOperations,
    NotificationsOperations,
    PriceListOperations,
    ProjectTemplateOperations,
    TermBaseOperations,
    TranslationMemoryOperations,
    ProjectOperations,
    TranslationOperations,
    SegmentOperations,
    ProviderOperations,
    ProjectReferenceFileOperations,
    QuoteOperations,
    SCIMOperations,
    SegmentationRulesOperations,
    ServiceOperations,
    SpellCheckOperations,
    SubDomainOperations,
    UserOperations,
    WorkflowStepOperations,
    VendorOperations,
    WebhookOperations,
    XMLAssistantOperations,
    WorkflowchangesOperations,
)


MEMSOURCE_BASE_URL = "https://cloud.memsource.com/web"

logger = logging.getLogger(__name__)

T = TypeVar("T", bound=BaseModel)
FileType = Tuple[str, Union[bytes, Any]]
RequestFiles = Union[Mapping[str, FileType], Sequence[Tuple[str, FileType]]]

ua = f"phrappy/{__version__} (+https://github.com/kuhnemann/phrappy)"


class Phrappy:
    """Client for interacting with the Phrase TMS API."""

    def __init__(
        self,
        token: Optional[str] = None,
        base_url: str = MEMSOURCE_BASE_URL,
        timeout=180,
    ):
        """Initialize the Phrase TMS client.

        Args:
            token: Optional API token. If provided without "ApiToken " prefix, it will be added.
            base_url: Base URL for API requests. Defaults to cloud.memsource.com.
        """
        self._client = httpx.Client(timeout=timeout, headers={"User-Agent": ua})
        self.base_url = base_url
        self.token = self._validate_token(token)
        self.last_responses = deque(maxlen=5)
        self._init_operations()

    def close(self):
        self._client.close()

    def __enter__(self):
        return self

    def __exit__(self, *exc):
        self.close()

    @staticmethod
    def _validate_token(token: Optional[str]) -> Optional[str]:
        """Validate and format the API token.

        Args:
            token: Raw API token string

        Returns:
            Properly formatted token or None
        """
        if not token:
            return None
        if token.startswith("ApiToken "):
            return token
        return f"ApiToken {token}"

    def _init_operations(self) -> None:
        """Initialize all API operation objects."""
        self.additional_workflow_step = AdditionalWorkflowStepOperations(self)
        self.analysis = AnalysisOperations(self)
        self.async_request = AsyncRequestOperations(self)
        self.authentication = AuthenticationOperations(self)
        self.bilingual_file = BilingualFileOperations(self)
        self.business_unit = BusinessUnitOperations(self)
        self.buyer = BuyerOperations(self)
        self.client = ClientOperations(self)
        self.connector = ConnectorOperations(self)
        self.cost_center = CostCenterOperations(self)
        self.custom_fields = CustomFieldsOperations(self)
        self.custom_file_type = CustomFileTypeOperations(self)
        self.domain = DomainOperations(self)
        self.due_date_scheme = DueDateSchemeOperations(self)
        self.email_template = EmailTemplateOperations(self)
        self.file = FileOperations(self)
        self.glossary = GlossaryOperations(self)
        self.importsettings = ImportsettingsOperations(self)
        self.conversations = ConversationsOperations(self)
        self.job = JobOperations(self)
        self.supported_languages = SupportedLanguagesOperations(self)
        self.language_quality_assessment = LanguageQualityAssessmentOperations(self)
        self.quality_assurance = QualityAssuranceOperations(self)
        self.machine_translation_settings = MachineTranslationSettingsOperations(self)
        self.machine_translation = MachineTranslationOperations(self)
        self.mapping = MappingOperations(self)
        self.language_ai = LanguageAIOperations(self)
        self.net_rate_scheme = NetRateSchemeOperations(self)
        self.notifications = NotificationsOperations(self)
        self.price_list = PriceListOperations(self)
        self.project_template = ProjectTemplateOperations(self)
        self.term_base = TermBaseOperations(self)
        self.translation_memory = TranslationMemoryOperations(self)
        self.project = ProjectOperations(self)
        self.translation = TranslationOperations(self)
        self.segment = SegmentOperations(self)
        self.provider = ProviderOperations(self)
        self.project_reference_file = ProjectReferenceFileOperations(self)
        self.quote = QuoteOperations(self)
        self.scim = SCIMOperations(self)
        self.segmentation_rules = SegmentationRulesOperations(self)
        self.service = ServiceOperations(self)
        self.spell_check = SpellCheckOperations(self)
        self.sub_domain = SubDomainOperations(self)
        self.user = UserOperations(self)
        self.workflow_step = WorkflowStepOperations(self)
        self.vendor = VendorOperations(self)
        self.webhook = WebhookOperations(self)
        self.xml_assistant = XMLAssistantOperations(self)
        self.workflowchanges = WorkflowchangesOperations(self)

    def make_request(
        self,
        method: str,
        path: str,
        phrase_token: Optional[str] = None,
        params: Optional[Dict[str, Any]] = None,
        payload: Optional[Union[T, Dict[str, Any]]] = None,
        files: Optional[RequestFiles] = None,
        headers: Optional[Dict[str, str]] = None,
        content: Optional[bytes] = None,
        timeout: float = 180.0,
    ) -> Response:
        """Make an HTTP request to the Phrase TMS API.

        Args:
            method: HTTP method (GET, POST, etc)
            path: API endpoint path
            phrase_token: Optional token override
            params: Optional URL parameters
            payload: Optional request body
            files: Optional files to upload
            headers: Optional HTTP headers
            content: Optional raw content body
            timeout: Request timeout in seconds

        Returns:
            httpx.Response object

        Raises:
            PhrappyError: If the API request fails
        """
        # Build request URL and headers
        if not path.startswith("/"):
            path = "/" + path
        url = self.base_url.rstrip("/") + path
        request_headers = {}
        request_headers.setdefault("User-Agent", ua)
        if token := (phrase_token or self.token):
            request_headers["Authorization"] = token
        if headers:
            request_headers.update(headers)

        # Convert Pydantic models to dicts
        if payload is not None and not isinstance(payload, dict):
            try:
                payload = payload.model_dump(exclude_none=True)
            except Exception as e:
                logger.exception("Failed to serialize payload")
                raise PhrappyError("Failed to serialize request payload") from e

        # Clean up None values from params
        if params:
            params = {k: v for k, v in params.items() if v is not None}
        response = None
        for attempt in range(3):
            try:
                response = self._client.request(
                    method=method,
                    url=url,
                    headers=request_headers,
                    params=params,
                    json=payload,
                    files=files,
                    content=content,
                    timeout=timeout,
                )
                if len(response.content) < 10240:
                    self.last_responses.append(response)
                response.raise_for_status()
                return response

            except HTTPStatusError as exc:
                if response:
                    if response.status_code in (429, 503):
                        time.sleep(min(2**attempt, 8))
                        continue
                    try:
                        error_data = response.json()
                        error_code = error_data.get("errorCode")
                        error_desc = error_data.get("errorDescription")
                        msg = (
                            f"API request failed: {method} {url}\n"
                            f"Status: {response.status_code}\n"
                            f"Error code: {error_code}\n"
                            f"Description: {error_desc}"
                        )
                        raise PhrappyError(msg) from exc
                    except ValueError:
                        raise PhrappyError(
                            f"API request failed with invalid JSON response: {response.text}"
                        ) from exc
                else:
                    raise PhrappyError(
                        f"Request failed with no response: {str(exc)}"
                    ) from exc

            except Exception as e:
                logger.exception(f"Unexpected error during request: {url}")
                raise PhrappyError(f"An unexpected error occurred: {str(e)}") from e

    @classmethod
    def from_creds(
        cls, username: str, password: str, base_url=MEMSOURCE_BASE_URL, timeout=180
    ) -> "Phrappy":
        pp = cls(base_url=base_url, timeout=timeout)
        login_dto = {"userName": username, "password": password}
        resp = pp.authentication.login(login_dto)
        pp.token = pp._validate_token(getattr(resp, "token", None))
        return pp
