"""
核心基类 - 与 npm BaseClient 行为对齐。
网关模式：base_url = gateway_url，path = /api/{service_path}{path}
后端模式：base_url = 对应服务 URL，path 使用原样（不加 /api/ 前缀）
"""
import json
from typing import Any, Dict, NoReturn, Optional, Tuple

import requests

from .types import (
    ClientConfig,
    UserContext,
    RequestOptions,
    PaginatedResponse,
    TokenStorage,
    create_memory_token_storage,
)

_SERVICE_URL_KEYS = {
    "auth": "auth_service_url",
    "msg": "msg_service_url",
    "users": "user_service_url",
    "files": "file_service_url",
    "file": "file_service_url",
    "token": "token_service_url",
    "ai": "ai_service_url",
    "aigc": "aigc_service_url",
    "providers": "provider_service_url",
    "logs": "audit_log_service_url",
    "gateway-h5": "gateway_h5_service_url",
    "org": "org_service_url",
    "delivery-stream": "delivery_stream_service_url",
    "ez-ability": "ez_ability_service_url",
    "im": "im_service_url",
    "hub": "hub_service_url",
    "task": "task_service_url",
    "notes": "notes_service_url",
    "v1/notes": "notes_service_url",
    "memory": "memory_service_url",
    "chat": "chat_service_url",
}


class BaseClient:
    """所有服务继承此类，与 npm BaseClient 对齐。"""

    service_path: str = ""

    def __init__(
        self,
        config: ClientConfig,
        service_path: Optional[str] = None,
        token_storage: Optional[TokenStorage] = None,
    ) -> None:
        self.config = config
        if service_path is not None:
            self.service_path = service_path
        self.is_frontend_mode = bool(config.gateway_url)
        self.service_base_path = f"/api/{self.service_path}"
        self.token_storage = token_storage or create_memory_token_storage()

        self.project_id: Optional[str] = config.project_id if self.is_frontend_mode else None
        self.org_id: Optional[str] = config.org_id if self.is_frontend_mode else None
        self.app_id: Optional[str] = config.app_id if self.is_frontend_mode else None
        self.user_context: Optional[UserContext] = None if self.is_frontend_mode else config.user_context

        self.session = requests.Session()
        self.session.headers.update({
            "Content-Type": "application/json",
            "Accept": "application/json",
        })
        self._timeout = (config.timeout or 30000) / 1000.0

    @property
    def service_name(self) -> str:
        return self.service_path

    def _get_base_url(self) -> str:
        if self.is_frontend_mode:
            return self.config.gateway_url or ""
        key = _SERVICE_URL_KEYS.get(self.service_path)
        if not key:
            raise ValueError(
                f"服务路径 '{self.service_path}' 未在 _SERVICE_URL_KEYS 中注册，"
                f"无法确定对应的 ClientConfig 属性"
            )
        url = getattr(self.config, key, None)
        if not url:
            raise ValueError(
                f"服务 {self.service_path} 的 URL 未配置，请在 ClientConfig 中设置 {key}"
            )
        return url

    def _get_full_path(self, path: str) -> str:
        if self.is_frontend_mode:
            return f"{self.service_base_path}{path}"
        return path

    def _prepare_request(
        self, path: str, options: RequestOptions
    ) -> Tuple[str, Dict[str, str], Dict[str, Any]]:
        """构建 url / headers / params，注入 context 与 token。"""
        base_url = self._get_base_url().rstrip("/")
        full_path = self._get_full_path(path)
        url = f"{base_url}{full_path}"

        headers = dict(options.headers or {})
        params = dict(options.params or {})

        if self.is_frontend_mode:
            pid = options.project_id or self.project_id
            oid = options.org_id or self.org_id
            aid = options.app_id or self.app_id
            if pid:
                headers["x-project-id"] = str(pid)
            if oid:
                headers["x-org-id"] = str(oid)
            if aid:
                headers["x-app-id"] = str(aid)
        else:
            ctx = options.user_context or self.user_context
            if ctx:
                headers.update(self.init_user_context_headers(ctx))

        token = self.token_storage.get_token()
        if token:
            headers["Authorization"] = f"Bearer {token}"

        return url, headers, params

    def init_user_context_headers(self, context: UserContext) -> Dict[str, str]:
        headers = {
            "v-user-id": context.user_id,
            "v-user-role": context.role,
            "v-project-id": context.project_id,
        }
        if context.org_id:
            headers["v-org-id"] = context.org_id
        if context.app_id:
            headers["v-app-id"] = context.app_id
        return headers

    def set_user_context(self, context: UserContext) -> "BaseClient":
        if not self.is_frontend_mode:
            self.user_context = context
        return self

    def set_project_id(self, project_id: str) -> "BaseClient":
        if self.is_frontend_mode:
            self.project_id = project_id
        return self

    def set_app_id(self, app_id: str) -> "BaseClient":
        if self.is_frontend_mode:
            self.app_id = app_id
        return self

    def set_org_id(self, org_id: str) -> "BaseClient":
        if self.is_frontend_mode:
            self.org_id = org_id
        return self

    def set_project_and_app(self, project_id: str, app_id: str) -> "BaseClient":
        if self.is_frontend_mode:
            self.project_id = project_id
            self.app_id = app_id
        return self

    def set_project_org_app(
        self, project_id: str, org_id: str, app_id: str
    ) -> "BaseClient":
        if self.is_frontend_mode:
            self.project_id = project_id
            self.org_id = org_id
            self.app_id = app_id
        return self

    def set_token_storage(self, storage: TokenStorage) -> "BaseClient":
        self.token_storage = storage
        return self

    def set_token(self, token: str) -> "BaseClient":
        self.token_storage.set_token(token)
        return self

    def clear_token(self) -> "BaseClient":
        self.token_storage.clear_token()
        return self

    def _handle_api_error(self, error: Exception, path: str) -> NoReturn:
        msg = f"[{self.service_name}服务] "
        if hasattr(error, "response") and error.response is not None:
            resp = error.response
            try:
                data = resp.json()
                msg += data.get("message") or data.get("error") or str(resp.status_code)
            except (json.JSONDecodeError, AttributeError):
                msg += f"请求失败，状态码: {resp.status_code}"
            if getattr(resp, "headers", None) and "x-request-id" in resp.headers:
                msg += f" | 请求ID: {resp.headers['x-request-id']}"
        else:
            msg += str(error)
        msg += f" (路径: {path})"
        raise Exception(msg) from error

    def request(
        self,
        path: str,
        options: Optional[RequestOptions] = None,
    ) -> Any:
        options = options or RequestOptions(method="GET")
        url, headers, params = self._prepare_request(path, options)

        try:
            resp = self.session.request(
                method=options.method,
                url=url,
                headers=headers,
                params=params,
                json=options.body,
                timeout=self._timeout,
            )
        except requests.RequestException as e:
            self._handle_api_error(e, path)

        try:
            resp.raise_for_status()
        except requests.HTTPError as e:
            self._handle_api_error(e, path)

        try:
            data = resp.json()
        except (ValueError, json.JSONDecodeError) as e:
            self._handle_api_error(e, path)

        if not data.get("success", False):
            raise Exception(
                f"[{self.service_name}服务] {data.get('message', '业务请求失败')} (路径: {path})"
            )
        return data.get("data")

    def request_with_pagination(
        self,
        path: str,
        options: Optional[RequestOptions] = None,
    ) -> Dict[str, Any]:
        """游标分页请求，返回 data 与 pagination（用于 memory 等）。"""
        options = options or RequestOptions(method="GET")
        url, headers, params = self._prepare_request(path, options)

        try:
            resp = self.session.request(
                method=options.method,
                url=url,
                headers=headers,
                params=params,
                json=options.body,
                timeout=self._timeout,
            )
        except requests.RequestException as e:
            self._handle_api_error(e, path)

        try:
            resp.raise_for_status()
        except requests.HTTPError as e:
            self._handle_api_error(e, path)

        try:
            data = resp.json()
        except (ValueError, json.JSONDecodeError) as e:
            self._handle_api_error(e, path)

        if not data.get("success", False):
            raise Exception(
                f"[{self.service_name}服务] {data.get('message', '业务请求失败')} (路径: {path})"
            )
        return {
            "data": data.get("data"),
            "pagination": data.get("pagination") or {},
        }

    def stream_request(
        self,
        path: str,
        options: Optional[RequestOptions] = None,
    ):
        """SSE 流式请求，yield 解析后的事件 dict。
        使用 requests stream=True 逐行读取 text/event-stream。

        Example::

            for event in client.chat.stream_completion({"roomId": "r1", "message": "hi"}):
                if event["type"] == "delta":
                    print(event["delta"], end="", flush=True)
                elif event["type"] == "done":
                    print()
        """
        import json as _json
        options = options or RequestOptions(method="POST")
        url, headers, params = self._prepare_request(path, options)
        headers["Accept"] = "text/event-stream"

        try:
            resp = self.session.request(
                method=options.method,
                url=url,
                headers=headers,
                params=params,
                json=options.body,
                stream=True,
                timeout=self._timeout,
            )
            resp.raise_for_status()
        except requests.RequestException as e:
            self._handle_api_error(e, path)

        for raw_line in resp.iter_lines(decode_unicode=True):
            if not raw_line or raw_line.strip() == "data: [DONE]":
                continue
            if raw_line.startswith("data: "):
                try:
                    yield _json.loads(raw_line[6:])
                except _json.JSONDecodeError:
                    pass

    def paginated_request(
        self,
        path: str,
        options: Optional[RequestOptions] = None,
    ) -> PaginatedResponse:
        options = options or RequestOptions(method="GET")
        url, headers, params = self._prepare_request(path, options)

        try:
            resp = self.session.request(
                method=options.method,
                url=url,
                headers=headers,
                params=params,
                json=options.body,
                timeout=self._timeout,
            )
        except requests.RequestException as e:
            self._handle_api_error(e, path)

        try:
            resp.raise_for_status()
        except requests.HTTPError as e:
            self._handle_api_error(e, path)

        try:
            data = resp.json()
        except (ValueError, json.JSONDecodeError) as e:
            self._handle_api_error(e, path)

        if not data.get("success", False):
            raise Exception(
                f"[{self.service_name}服务] {data.get('message', '业务请求失败')} (路径: {path})"
            )
        return PaginatedResponse(
            data=data.get("data", []),
            success=data.get("success", False),
            message=data.get("message", ""),
            pagination=data.get("pagination"),
        )
