"""Qwen (Aliyun) adapter using Dashscope SDK.

Note:
    Dashscope SDK uses module-level configuration (dashscope.api_key, etc.).
    This adapter passes api_key per-call (supported by all dashscope API methods),
    eliminating the need for global state manipulation for credentials.
    A lock is only acquired when base_url or timeout overrides are needed
    (typically proxy users), making concurrent usage with different api_keys
    fully lock-free in the common case.

File Upload Strategy:
    - VL models: Prefer Bailian temp storage (oss:// URL) for better model understanding
    - Long context models (Qwen-Long): Use OpenAI-compatible Files API with fileid:// reference
    - Fallback to Base64 encoding for images when temp storage fails
    - Video files require temp storage (not supported in Base64 mode)
"""

import base64
import mimetypes
import threading
from contextlib import contextmanager
from pathlib import Path
from typing import List, Optional

import requests
from openai import OpenAI

import dashscope
from dashscope import Generation
from dashscope.aigc import MultiModalConversation

from .base import VendorAdapter
from ..models import Response, Usage


# Thread lock for protecting dashscope config changes
_config_lock = threading.Lock()


class QwenAdapter(VendorAdapter):
    """
    Aliyun Qwen adapter using official Dashscope SDK.

    Supports:
    - Text chat via Generation API
    - Vision/multimodal chat via MultiModalConversation API
    - Video understanding via Bailian temp storage
    - Custom base_url for proxies
    - Thread-safe concurrent usage with different credentials

    File Handling:
    - Images: Bailian temp storage (preferred) or Base64 (fallback)
    - Videos: Bailian temp storage only (not supported in Base64 mode)

    Routing principle:
    - Prefer explicitly verified chat-capable models
    - Use conservative naming rules for obvious model families
    - Reject file inputs unless the model is clearly multimodal for chat
    """

    DEFAULT_MODEL = "qwen-plus"

    # Bailian temp storage API endpoint
    TEMP_STORAGE_API = "https://dashscope.aliyuncs.com/api/v1/uploads"

    TEXT_CHAT_MODELS = {
        "qwen-plus",
        "qwen-turbo",
        "qwen-max",
        "qwen-flash",
    }

    MULTIMODAL_CHAT_MODELS = {
        "qwen3.5-plus",
        "qwen3-vl-plus",
        "qwen3-vl-max",
        "qwen3-vl-flash",
        "qwen-vl-plus",
        "qwen-vl-max",
        "qwen-vl-ocr",
        "qwen-ocr",
    }

    # Long context models that support file upload via Files API
    LONG_CONTEXT_MODELS = {
        "qwen-long",
    }

    UNSUPPORTED_CHAT_PATTERNS = (
        "-tts",
        "-asr",
        "-realtime",
        "-livetranslate",
    )

    UNSUPPORTED_CHAT_MODELS = {
        "qwen-image",
        "qwen-image-edit",
        "qwen-flash-character",
        "gui-plus",
    }

    def __init__(
        self,
        api_key: str,
        model: Optional[str] = None,
        base_url: Optional[str] = None,
        timeout: Optional[float] = None,
        use_temp_storage: bool = True,
        fallback_to_base64: bool = True,
        **kwargs,
    ):
        """
        Initialize Qwen adapter.

        Args:
            api_key: Dashscope API key.
            model: Model name to use. Defaults to qwen-plus.
            base_url: Custom API endpoint (for proxies).
            timeout: Request timeout in seconds.
            use_temp_storage: Whether to use Bailian temp storage for file upload.
                              Default True. Temp storage provides better model understanding
                              and supports video files. URLs are valid for 48 hours.
            fallback_to_base64: Whether to fallback to Base64 encoding when temp storage
                                fails (images only). Default True. Video files cannot use
                                Base64 and will raise error if temp storage fails.
            **kwargs: Additional options.
        """
        super().__init__(
            api_key=api_key,
            model=model or self.DEFAULT_MODEL,
            base_url=base_url,
            timeout=timeout,
            **kwargs,
        )
        self.use_temp_storage = use_temp_storage
        self.fallback_to_base64 = fallback_to_base64
        # Note: Don't set dashscope module-level config here.
        # Instead, use _with_config() context manager for each API call.

    @contextmanager
    def _with_config(self):
        """
        Context manager to temporarily override dashscope global settings.

        api_key is passed per-call (not via global state), so no lock is needed
        for credentials. The lock is only acquired when base_url or timeout needs
        to override dashscope's global settings — typically for proxy users.

        For the common case (no custom base_url/timeout), this is a zero-cost no-op
        and fully concurrent with other QwenAdapter instances.
        """
        if not self.base_url and not self.timeout:
            yield
            return

        with _config_lock:
            # Store original values (use sentinel for "not set" state)
            _NOT_SET = object()
            old_base_url = getattr(dashscope, "base_http_api_url", _NOT_SET)
            old_timeout = getattr(dashscope, "api_timeout", _NOT_SET)

            # Apply new values
            if self.base_url:
                dashscope.base_http_api_url = self.base_url
            if self.timeout:
                dashscope.api_timeout = self.timeout

            try:
                yield
            finally:
                # Restore original values (use None instead of delattr to avoid race conditions)
                if old_base_url is not _NOT_SET:
                    dashscope.base_http_api_url = old_base_url
                else:
                    dashscope.base_http_api_url = None  # Safe alternative to delattr
                if old_timeout is not _NOT_SET:
                    dashscope.api_timeout = old_timeout
                else:
                    dashscope.api_timeout = None  # Safe alternative to delattr

    def _get_chat_capability(self) -> str:
        """Return chat routing capability for the configured model."""
        model_lower = self.model.lower()

        if model_lower in self.TEXT_CHAT_MODELS:
            return "text_chat"

        if model_lower in self.MULTIMODAL_CHAT_MODELS:
            return "multimodal_chat"

        if model_lower in self.LONG_CONTEXT_MODELS:
            return "long_context_chat"

        if model_lower in self.UNSUPPORTED_CHAT_MODELS:
            return "unsupported_for_chat"

        if any(pattern in model_lower for pattern in self.UNSUPPORTED_CHAT_PATTERNS):
            return "unsupported_for_chat"

        if "-vl-" in model_lower or model_lower.startswith("qwen-vl-") or model_lower == "qwen-vl-ocr":
            return "multimodal_chat"

        if "long" in model_lower:
            return "long_context_chat"

        return "unknown"

    def _extract_text_content(self, content) -> str:
        if content is None:
            return ""
        if isinstance(content, str):
            return content
        if isinstance(content, list):
            parts = []
            for item in content:
                if isinstance(item, str):
                    parts.append(item)
                elif isinstance(item, dict):
                    text = item.get("text")
                    if text:
                        parts.append(text)
                else:
                    text = getattr(item, "text", None)
                    if text:
                        parts.append(text)
            return "\n".join(parts)
        return str(content)

    def _extract_response_content(self, response) -> str:
        output = getattr(response, "output", None)
        if output is None and isinstance(response, dict):
            output = response.get("output")
        choices = getattr(output, "choices", None) if output else None
        if choices is None and isinstance(output, dict):
            choices = output.get("choices")
        if not choices:
            return ""

        first_choice = choices[0]
        message = getattr(first_choice, "message", None)
        if message is None and isinstance(first_choice, dict):
            message = first_choice.get("message")
        if message is None:
            return self._extract_text_content(first_choice)

        content = getattr(message, "content", None)
        if content is None and isinstance(message, dict):
            content = message.get("content")

        extracted = self._extract_text_content(content)
        if extracted:
            return extracted

        for attr in ("text", "output_text"):
            value = getattr(message, attr, None)
            if value:
                return str(value)
            if isinstance(message, dict) and message.get(attr):
                return str(message[attr])

        return ""

    def _encode_image_as_data_url(self, path: Path) -> str:
        """Encode image file as base64 data URL (fallback method)."""
        self._check_file_size(path)  # Prevent OOM on large files
        mime_type, _ = mimetypes.guess_type(str(path))
        if not mime_type or not mime_type.startswith("image/"):
            raise ValueError(f"Qwen vision models only accept image files: {path.name}")

        with open(path, "rb") as f:
            image_data = base64.b64encode(f.read()).decode("utf-8")
        return f"data:{mime_type};base64,{image_data}"

    def _get_upload_policy(self) -> dict:
        """
        Get Bailian temp storage upload policy.

        Returns:
            dict with upload credentials:
            - policy: Upload policy
            - signature: Policy signature
            - upload_dir: Target directory
            - upload_host: OSS upload endpoint
            - oss_access_key_id: OSS access key
            - x_oss_object_acl: Object ACL (private)
            - x_oss_forbid_overwrite: Whether to forbid overwrite

        Raises:
            RuntimeError: If failed to get policy
        """
        url = f"{self.TEMP_STORAGE_API}?action=getPolicy&model={self.model}"
        headers = {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json"
        }

        response = requests.get(url, headers=headers, timeout=30)
        if response.status_code != 200:
            raise RuntimeError(
                f"Failed to get Bailian upload policy: {response.status_code} - {response.text}"
            )

        data = response.json().get("data")
        if not data:
            raise RuntimeError(
                f"Failed to get Bailian upload policy: unexpected response format - {response.text}"
            )
        return data

    def _upload_to_temp_storage(self, file_path: Path) -> str:
        """
        Upload file to Bailian temp storage and return oss:// URL.

        The temp storage is free but has limitations:
        - URL valid for 48 hours
        - Rate limit: 100 QPS per account+model
        - File must be used with the same model and account

        Args:
            file_path: Local file path to upload

        Returns:
            oss:// URL string (e.g., "oss://dashscope-instant/xxx/xxx/file.ext")

        Raises:
            RuntimeError: If upload fails
        """
        path = self._validate_file(file_path)

        # 1. Get upload policy
        policy_data = self._get_upload_policy()

        # 2. Build OSS upload form
        file_name = path.name
        key = f"{policy_data['upload_dir'].rstrip('/')}/{file_name}"

        with open(path, "rb") as f:
            files = {
                'OSSAccessKeyId': (None, policy_data['oss_access_key_id']),
                'Signature': (None, policy_data['signature']),
                'policy': (None, policy_data['policy']),
                'x-oss-object-acl': (None, policy_data['x_oss_object_acl']),
                'x-oss-forbid-overwrite': (None, policy_data['x_oss_forbid_overwrite']),
                'key': (None, key),
                'success_action_status': (None, '200'),
                'file': (file_name, f)
            }

            # 3. Upload to temp OSS
            response = requests.post(
                policy_data['upload_host'],
                files=files,
                timeout=120  # Larger timeout for big files
            )

        if response.status_code != 200:
            raise RuntimeError(
                f"Failed to upload file to Bailian temp storage: {response.status_code} - {response.text}"
            )

        return f"oss://{key}"

    def upload_file(self, file_path: Path) -> str:
        """
        Upload file to Qwen Files API (for Qwen-Long document processing).

        Note: This is for Qwen-Long's file-extract purpose, not for VL models.
        For VL models, use _upload_to_temp_storage() instead.

        Args:
            file_path: Path to the file to upload

        Returns:
            File ID for referencing in Qwen-Long API calls
        """
        path = self._validate_file(file_path)

        with self._with_config():
            response = dashscope.Files.upload(
                file_path=str(path),
                purpose="file-extract",
                api_key=self.api_key,
            )

        # Safely extract file_id from response
        output = getattr(response, "output", None)
        if output is None:
            raise RuntimeError(f"Qwen file upload failed: no output in response")

        if isinstance(output, dict):
            uploaded_files = output.get("uploaded_files")
        else:
            uploaded_files = getattr(output, "uploaded_files", None)

        if not uploaded_files or not isinstance(uploaded_files, (list, tuple)):
            raise RuntimeError(f"Qwen file upload failed: no uploaded_files in response")

        file_id = uploaded_files[0].get("file_id") if isinstance(uploaded_files[0], dict) else getattr(uploaded_files[0], "file_id", None)
        if not file_id:
            raise RuntimeError(f"Qwen file upload failed: no file_id in response")

        return file_id

    def chat(
        self,
        prompt: str,
        files: Optional[List[Path]] = None,
        **kwargs,
    ) -> Response:
        """
        Send chat request to Qwen model.

        Args:
            prompt: User prompt text
            files: Optional list of file paths (images or videos for VL models)
            **kwargs: Additional parameters (temperature, max_tokens, etc.)

        Returns:
            Response with content, usage, and raw response

        Raises:
            ValueError: If model doesn't support files or unsupported file type
        """
        validated_files = [self._validate_file(Path(file_path)) for file_path in files] if files else []
        capability = self._get_chat_capability()

        if validated_files:
            if capability == "multimodal_chat":
                return self._chat_multimodal(prompt, validated_files, **kwargs)
            elif capability == "long_context_chat":
                return self._chat_long_context(prompt, validated_files, **kwargs)
            else:
                names = ", ".join(path.name for path in validated_files)
                if capability == "unsupported_for_chat":
                    raise ValueError(
                        f"Qwen model '{self.model}' is not supported for chat(); "
                        f"cannot use files: {names}"
                    )
                raise ValueError(
                    f"Qwen model '{self.model}' does not support file inputs in chat(); "
                    f"unsupported files: {names}"
                )

        if capability == "multimodal_chat":
            return self._chat_multimodal(prompt, validated_files, **kwargs)

        if capability == "long_context_chat":
            return self._chat_long_context(prompt, validated_files, **kwargs)

        if capability == "unsupported_for_chat":
            raise ValueError(f"Qwen model '{self.model}' is not supported for chat()")

        call_kwargs = {"result_format": "message"}
        if "temperature" in kwargs:
            call_kwargs["temperature"] = kwargs.pop("temperature")
        if "max_tokens" in kwargs:
            call_kwargs["max_tokens"] = kwargs.pop("max_tokens")
        call_kwargs.update(kwargs)

        with self._with_config():
            response = Generation.call(
                model=self.model,
                api_key=self.api_key,
                messages=[{"role": "user", "content": prompt}],
                **call_kwargs,
            )
        return Response(
            content=self._extract_response_content(response),
            usage=Usage.from_dashscope(getattr(response, "usage", None)),
            raw=response,
        )

    def _chat_multimodal(
        self,
        prompt: str,
        validated_files: List[Path],
        **kwargs,
    ) -> Response:
        """
        Handle chat for multimodal models using MultiModalConversation API.

        File handling strategy:
        1. Use Bailian temp storage (oss:// URL) by default
        2. Fallback to Base64 for images if temp storage fails
        3. Video files require temp storage (no Base64 fallback)

        Args:
            prompt: User prompt text
            validated_files: List of validated file paths
            **kwargs: Additional parameters

        Returns:
            Response with content, usage, and raw response
        """
        content_list = []

        for path in validated_files:
            mime_type, _ = mimetypes.guess_type(str(path))
            mime_type = mime_type or "application/octet-stream"

            # Determine file type
            is_image = mime_type.startswith("image/")
            is_video = mime_type.startswith("video/")

            if not (is_image or is_video):
                raise ValueError(
                    f"Qwen multimodal model '{self.model}' only supports image/video files; "
                    f"unsupported file: {path.name} ({mime_type})"
                )

            # Try temp storage first (if enabled)
            if self.use_temp_storage:
                try:
                    oss_url = self._upload_to_temp_storage(path)
                    if is_image:
                        content_list.append({"image": oss_url})
                    else:
                        content_list.append({"video": oss_url})
                    continue
                except Exception as e:
                    # Video cannot fallback to Base64
                    if is_video:
                        raise RuntimeError(
                            f"Video files require Bailian temp storage. "
                            f"Upload failed: {e}"
                        )
                    # Image can fallback to Base64
                    if not self.fallback_to_base64:
                        raise RuntimeError(
                            f"Failed to upload image to temp storage: {e}"
                        )
                    # Fall through to Base64 encoding

            # Base64 encoding (fallback or disabled temp storage)
            if is_image:
                content_list.append({"image": self._encode_image_as_data_url(path)})
            else:
                # Video without temp storage
                raise ValueError(
                    f"Video files are not supported in Base64 mode. "
                    f"Enable use_temp_storage=True (default) to process video files."
                )

        content_list.append({"text": prompt})

        conv = MultiModalConversation()
        with self._with_config():
            response = conv.call(
                model=self.model,
                api_key=self.api_key,
                messages=[{"role": "user", "content": content_list}],
                **kwargs,
            )

        return Response(
            content=self._extract_response_content(response),
            usage=Usage.from_dashscope(getattr(response, "usage", None)),
            raw=response,
        )

    def _chat_long_context(
        self,
        prompt: str,
        validated_files: List[Path],
        **kwargs,
    ) -> Response:
        """
        Handle chat for long context models (Qwen-Long) with file support.

        Uses OpenAI-compatible Files API to upload documents, then references
        them in system messages using fileid:// format.

        Official documentation:
        https://help.aliyun.com/zh/model-studio/long-context-qwen-long

        Args:
            prompt: User prompt text
            validated_files: List of validated file paths (PDF, DOC, etc.)
            **kwargs: Additional parameters

        Returns:
            Response with content, usage, and raw response
        """
        # Create OpenAI client for Qwen-Long (must use compatible-mode endpoint)
        client = OpenAI(
            api_key=self.api_key,
            base_url=self.base_url or "https://dashscope.aliyuncs.com/compatible-mode/v1",
            timeout=self.timeout,
        )

        messages = []

        # System message 1: Role definition
        messages.append({"role": "system", "content": "You are a helpful assistant."})

        # Upload files and add file references in system message
        if validated_files:
            file_ids = []
            for path in validated_files:
                # Upload via OpenAI-compatible Files API
                with open(path, "rb") as f:
                    file_obj = client.files.create(file=f, purpose="file-extract")
                file_ids.append(file_obj.id)

            # System message 2: File references (fileid:// format)
            file_refs = ",".join(f"fileid://{fid}" for fid in file_ids)
            messages.append({"role": "system", "content": file_refs})

        # User message
        messages.append({"role": "user", "content": prompt})

        # Call API
        call_kwargs = {"model": self.model, "messages": messages}
        if "temperature" in kwargs:
            call_kwargs["temperature"] = kwargs.pop("temperature")
        if "max_tokens" in kwargs:
            call_kwargs["max_tokens"] = kwargs.pop("max_tokens")
        call_kwargs.update(kwargs)

        completion = client.chat.completions.create(**call_kwargs)

        # Extract content
        content = ""
        if completion.choices:
            content = completion.choices[0].message.content or ""

        # Extract usage
        usage = Usage.from_openai(completion.usage)

        return Response(
            content=content,
            usage=usage,
            raw=completion,
        )
