from __future__ import annotations

import logging
import os
import warnings
from typing import Any, Dict, List, Optional

from ._utils import mask_api_key

import httpx
import requests

from .api import (
    DEFAULT_TIMEOUT,
    ahandle_response,
    apost_with_attempts,
    handle_response,
    post_with_attempts, DEFAULT_HOST,
)
from .models import (
    AnalyzeBatchItemResult,
    AnalyzeTextResult,
    BatchScanRequest,
    SingleScanRequest,
    ApiError as ApiErrorPayload,
)
from .errors import ApiError, ItsAIError, ValidationError


logger = logging.getLogger("its_ai")


def _normalize_base_url(base_url: str) -> str:
    return base_url.rstrip("/")


def _extract_error_fields(source: Any, *, default_message: str) -> ApiErrorPayload:
    code: Optional[str] = None
    message: str = default_message
    details: Optional[Dict[str, Any]] = None

    if isinstance(source, dict):
        nested_error = source.get("error")
        nested_fields: Optional[ApiErrorPayload] = None
        if isinstance(nested_error, dict):
            nested_fields = _extract_error_fields(nested_error, default_message=default_message)

        raw_code = source.get("code") or source.get("error_code")
        if raw_code is None and nested_fields:
            raw_code = nested_fields.get("code")
        if raw_code is None:
            status_value = source.get("status")
            if isinstance(status_value, str) and status_value not in {"success", "ok"}:
                raw_code = status_value
        if raw_code is not None:
            code = str(raw_code)

        raw_message = source.get("message")
        if isinstance(raw_message, str):
            message = raw_message
        else:
            fallback_message = source.get("error")
            if isinstance(fallback_message, str):
                message = fallback_message
            elif nested_fields and nested_fields.get("message"):
                message = nested_fields["message"]  # type: ignore[index]

        details_raw = source.get("details")
        if isinstance(details_raw, dict):
            details = details_raw
        elif nested_fields:
            nested_details = nested_fields.get("details")
            if isinstance(nested_details, dict):
                details = nested_details

        if details is None:
            extra = {
                key: value
                for key, value in source.items()
                if key not in {"code", "status", "error_code", "message", "error", "details"}
            }
            if extra:
                details = extra
    else:
        details = {"payload": source}

    return {"code": code, "message": message, "details": details}


def _parse_segmentation_tokens(value: Any, *, field: str, status_code: Optional[int]) -> Optional[List[float]]:
    if value is None:
        return None
    if not isinstance(value, list):
        raise ItsAIError(status_code=status_code, type="invalid_response", message=f"{field} must be a list", payload={"value": value})
    try:
        return [float(x) for x in value]
    except (TypeError, ValueError) as exc:  # pragma: no cover - defensive
        raise ItsAIError(
            status_code=status_code,
            type="invalid_response",
            message=f"{field} must contain numeric values",
            payload={"value": value},
        ) from exc


def _parse_answer(value: Any, *, field: str, status_code: Optional[int]) -> float:
    try:
        return float(value)
    except (TypeError, ValueError) as exc:
        raise ItsAIError(
            status_code=status_code,
            type="invalid_response",
            message=f"{field} missing or not a number",
            payload={"value": value},
        ) from exc


def _parse_single_scan_response(
    data: Any,
    *,
    status_code: Optional[int],
    requested_deep_scan: bool,
) -> AnalyzeTextResult:
    if not isinstance(data, dict):
        raise ItsAIError(
            status_code=status_code,
            type="invalid_response",
            message="Unexpected response shape",
            payload={"body": data},
        )

    answer = _parse_answer(data.get("answer"), field="answer", status_code=status_code)
    raw_status = data.get("status")
    status = raw_status if isinstance(raw_status, str) and raw_status else "success"

    if status != "success":
        error_fields = _extract_error_fields(data, default_message=f"analysis status is {status}")
        raise ApiError(status_code=status_code, **error_fields)

    segmentation = _parse_segmentation_tokens(
        data.get("segmentation_tokens"), field="segmentation_tokens", status_code=status_code
    )

    if requested_deep_scan and segmentation is None:
        raise ApiError(
            code="missing_segmentation",
            message="segmentation_tokens missing for deep_scan response",
            details={"body": data},
            status_code=status_code,
        )

    return AnalyzeTextResult(answer=answer, segmentation_tokens=segmentation, status=status)


def _parse_batch_scan_response(
    data: Any,
    *,
    status_code: Optional[int],
) -> List[AnalyzeBatchItemResult]:
    if not isinstance(data, dict):
        raise ItsAIError(
            status_code=status_code,
            type="invalid_response",
            message="Unexpected response shape",
            payload={"body": data},
        )

    error_payload = data.get("error")
    if error_payload is not None:
        error_fields = _extract_error_fields(error_payload, default_message="Batch request failed")
        raise ApiError(status_code=status_code, **error_fields)

    answer_payload = data.get("answer")
    if not isinstance(answer_payload, list):
        raise ItsAIError(
            status_code=status_code,
            type="invalid_response",
            message="answer must be a list",
            payload={"answer": answer_payload},
        )

    results: List[AnalyzeBatchItemResult] = []
    for index, item in enumerate(answer_payload):
        if not isinstance(item, dict):
            raise ItsAIError(
                status_code=status_code,
                type="invalid_response",
                message="Batch item must be an object",
                payload={"item": item, "index": index},
            )

        text_value = item.get("text")
        if not isinstance(text_value, str) or not text_value:
            raise ItsAIError(
                status_code=status_code,
                type="invalid_response",
                message="Batch item text missing",
                payload={"item": item, "index": index},
            )

        answer_value = _parse_answer(item.get("answer"), field=f"answer[{index}]", status_code=status_code)
        deep_flag = bool(item.get("deep_scan", False))
        segmentation = _parse_segmentation_tokens(
            item.get("segmentation_tokens"), field=f"segmentation_tokens[{index}]", status_code=status_code
        )

        if deep_flag and segmentation is None:
            raise ApiError(
                code="missing_segmentation",
                message="segmentation_tokens missing for deep_scan batch item",
                details={"item": item, "index": index},
                status_code=status_code,
            )

        results.append(
            AnalyzeBatchItemResult(
                text=text_value,
                answer=answer_value,
                segmentation_tokens=segmentation,
                deep_scan=deep_flag,
            )
        )

    return results


class ItsAIClient:
    """Synchronous ITS-AI client.

    Parameters:
        api_key: API key string. If omitted, reads from env `ITS_AI_API_KEY`.
        base_url: Base URL of the API (`https://api.its-ai.org` by default).
        timeout: Default request timeout in seconds (overridable per-call).
        max_attempts: Number of attempts on 429/5xx (default: 3).

    Usage:
        >>> with ItsAIClient(api_key="sk_live_...") as client:
        ...     res = client.analyze_text("Hello world")
    """

    def __init__(
        self,
        api_key: Optional[str] = None,
        *,
        base_url: str = DEFAULT_HOST,
        timeout: float = DEFAULT_TIMEOUT,
        max_attempts: int = 3,
        session: Optional[requests.Session] = None,
    ) -> None:
        if api_key is None:
            self.api_key = os.getenv("ITS_AI_API_KEY", "")
        else:
            self.api_key = api_key
        if not isinstance(self.api_key, str) or not self.api_key:
            raise ValidationError(status_code=None, type="validation", message="api_key is required")

        self.base_url: str = _normalize_base_url(base_url)
        self.timeout: float = timeout
        self.max_attempts: int = max(1, int(max_attempts))
        self._session: requests.Session = session or requests.Session()

    # Context manager support
    def __enter__(self) -> "ItsAIClient":
        return self

    def __exit__(self, exc_type, exc, tb) -> None:  # type: ignore[override]
        self.close()

    def close(self) -> None:
        self._session.close()

    def __repr__(self) -> str:
        return (
            f"{self.__class__.__name__}("
            f"api_key={mask_api_key(self.api_key)!r}, "
            f"base_url={self.base_url!r}, "
            f"timeout={self.timeout}, "
            f"max_attempts={self.max_attempts})"
        )

    # Public API
    def analyze_text(
        self,
        text: str,
        deep_scan: bool = False,
        *,
        timeout: Optional[float] = None,
    ) -> AnalyzeTextResult:
        """Analyze a single text.

        Args:
            text: Input text (UTF-8 English).
            deep_scan: If True, the response may include `segmentation_tokens`.
            timeout: Optional per-call timeout.

        Returns:
            AnalyzeTextResult containing the score, optional segmentation tokens, and response status.

        Raises:
            ValidationError: If the input text is empty.
            ApiError: When the API reports a non-success status or omits deep scan data.
        """
        if not isinstance(deep_scan, bool):
            raise ValidationError(status_code=None, type="validation", message="deep_scan argument must be bool")
        if not isinstance(text, str):
            raise ValidationError(status_code=None, type="validation", message="text must be type string")
        if not text.strip():
            raise ValidationError(status_code=None, type="validation", message="text must be a non-empty string")

        url = f"{self.base_url}/api/text"
        payload: SingleScanRequest = {
            "text": text,
            "deep_scan": deep_scan,
        }

        response = post_with_attempts(
            self._session,
            url,
            payload,
            timeout or self.timeout,
            self.api_key,
            self.max_attempts,
        )
        data = handle_response(response)
        return _parse_single_scan_response(
            data,
            status_code=response.status_code,
            requested_deep_scan=bool(deep_scan),
        )

    def analyze_batch(
        self,
        texts: List[str],
        deep_scan: bool = False,
        *,
        timeout: Optional[float] = None,
        max_batch_size: Optional[int] = None,
    ) -> List[AnalyzeBatchItemResult]:
        """Analyze multiple texts in one call.

        Args:
            texts: List of strings. Must be non-empty.
            deep_scan: If True, per-item segmentation tokens may appear.
            timeout: Optional per-call timeout.
            max_batch_size: Optional chunking; if provided, requests are chunked and results concatenated.

        Returns:
            List of AnalyzeBatchItemResult preserving input order.

        Raises:
            ValidationError: If `texts` is empty or contains non-string elements.
            ApiError: When the API response embeds an error payload or omits expected deep scan data.
        """
        if not isinstance(texts, list) or not texts:
            raise ValidationError(status_code=None, type="validation", message="texts must be a non-empty list of strings")
        for t in texts:
            if not isinstance(t, str) or not t:
                raise ValidationError(status_code=None, type="validation", message="each item in texts must be a non-empty string")

        if max_batch_size and max_batch_size > 0 and len(texts) > max_batch_size:
            results: List[AnalyzeBatchItemResult] = []
            for i in range(0, len(texts), max_batch_size):
                chunk = texts[i : i + max_batch_size]
                results.extend(self.analyze_batch(chunk, deep_scan=deep_scan, timeout=timeout))
            return results

        url = f"{self.base_url}/api/batch"
        payload: BatchScanRequest = {
            "texts": texts,
            "deep_scan": bool(deep_scan),
        }
        response = post_with_attempts(
            self._session,
            url,
            payload,
            timeout or self.timeout,
            self.api_key,
            self.max_attempts,
        )
        data = handle_response(response)
        return _parse_batch_scan_response(data, status_code=response.status_code)


class AsyncItsAIClient:
    """Asynchronous ITS-AI client using httpx.AsyncClient."""

    def __init__(
        self,
        api_key: Optional[str] = None,
        *,
        base_url: str = "https://api.its-ai.org",
        timeout: float = DEFAULT_TIMEOUT,
        max_attempts: int = 3,
        client: Optional[httpx.AsyncClient] = None,
    ) -> None:
        if api_key is None:
            self.api_key = os.getenv("ITS_AI_API_KEY", "")
        else:
            self.api_key = api_key
        if not isinstance(self.api_key, str) or not self.api_key:
            raise ValidationError(status_code=None, type="validation", message="api_key is required")

        self.base_url: str = _normalize_base_url(base_url)
        self.timeout: float = timeout
        self.max_attempts: int = max(1, int(max_attempts))
        self._client: httpx.AsyncClient = client or httpx.AsyncClient()

    async def __aenter__(self) -> "AsyncItsAIClient":  # type: ignore[override]
        return self

    async def __aexit__(self, exc_type, exc, tb) -> None:  # type: ignore[override]
        await self.aclose()

    async def aclose(self) -> None:
        await self._client.aclose()

    def __repr__(self) -> str:
        return (
            f"{self.__class__.__name__}("
            f"api_key={mask_api_key(self.api_key)!r}, "
            f"base_url={self.base_url!r}, "
            f"timeout={self.timeout}, "
            f"max_attempts={self.max_attempts})"
        )

    async def analyze_text(
        self,
        text: str,
        deep_scan: bool = False,
        *,
        timeout: Optional[float] = None,
    ) -> AnalyzeTextResult:
        if not isinstance(text, str) or not text.strip():
            raise ValidationError(status_code=None, type="validation", message="text must be a non-empty string")

        url = f"{self.base_url}/api/text"
        payload: SingleScanRequest = {
            "text": text,
            "deep_scan": bool(deep_scan),
        }

        response = await apost_with_attempts(
            self._client,
            url,
            payload,
            timeout or self.timeout,
            self.api_key,
            self.max_attempts,
        )
        data = await ahandle_response(response)
        return _parse_single_scan_response(
            data,
            status_code=response.status_code,
            requested_deep_scan=bool(deep_scan),
        )

    async def analyze_batch(
        self,
        texts: List[str],
        deep_scan: bool = False,
        *,
        timeout: Optional[float] = None,
        max_batch_size: Optional[int] = None,
    ) -> List[AnalyzeBatchItemResult]:
        if not isinstance(texts, list) or not texts:
            raise ValidationError(status_code=None, type="validation", message="texts must be a non-empty list of strings")
        for t in texts:
            if not isinstance(t, str) or not t:
                raise ValidationError(status_code=None, type="validation", message="each item in texts must be a non-empty string")

        if max_batch_size and max_batch_size > 0 and len(texts) > max_batch_size:
            results: List[AnalyzeBatchItemResult] = []
            for i in range(0, len(texts), max_batch_size):
                chunk = texts[i : i + max_batch_size]
                results.extend(
                    await self.analyze_batch(chunk, deep_scan=deep_scan, timeout=timeout)
                )
            return results

        url = f"{self.base_url}/api/batch"
        payload: BatchScanRequest = {
            "texts": texts,
            "deep_scan": bool(deep_scan),
        }
        response = await apost_with_attempts(
            self._client,
            url,
            payload,
            timeout or self.timeout,
            self.api_key,
            self.max_attempts,
        )
        data = await ahandle_response(response)
        return _parse_batch_scan_response(data, status_code=response.status_code)


class ItsAIAsyncClient(AsyncItsAIClient):
    """Deprecated alias for AsyncItsAIClient.

    This alias exists for backwards compatibility and will be removed in a future major release.
    Please use AsyncItsAIClient instead.
    """

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        warnings.warn(
            "ItsAIAsyncClient is deprecated, use AsyncItsAIClient instead",
            DeprecationWarning,
            stacklevel=2,
        )
        super().__init__(*args, **kwargs)
