"""魔方云（DcimCloud）HTTP 客户端。

按 account_type 分两种模式（参考 mfcw `app\\common\\logic\\DcimCloud.php`）：

- admin（自营/管理员）
    - 登录: POST {url}/v1/login?a=a   form: {username, password}    → token = response body 原文（去首尾引号）
    - 调用: {url}/v1{path}            header: access-token: <token>

- agent（资源池/代理商）
    - 登录: POST {url}/index.php?path=token   form: {username, password}   → token = json.data.token
    - 调用: {url}/index.php?path={path}        header: access-token: <token>

请求体一律用 application/x-www-form-urlencoded（与上游约定一致，不是 JSON）。

token 缓存：登录成功后写到 Redis `qycw:dcim_cloud:token:{server_id}`，TTL 21600s（与 mfcw 一致）。
"""
from __future__ import annotations

import asyncio
import logging
import time
from dataclasses import dataclass
from typing import Any

import aiohttp

from app.redis_client import k, redis_client

log = logging.getLogger("qycw.dcim_cloud")

TOKEN_TTL_SECONDS = 21600


def token_cache_key(server_id: int) -> str:
    return k("dcim_cloud", "token", str(server_id))


def status_cache_key(server_id: int) -> str:
    return k("dcim_cloud", "status", str(server_id))


class DcimCloudError(Exception):
    """统一异常：上游连接/鉴权/业务错误。"""

    def __init__(self, message: str, *, http_code: int | None = None):
        super().__init__(message)
        self.message = message
        self.http_code = http_code


@dataclass
class DcimCloudCredentials:
    base_url: str
    username: str
    password: str
    account_type: str = "admin"


class DcimCloudClient:
    """单次会话级客户端。login() 默认走 Redis 缓存（TTL 21600s）。

    传入 server_id 即启用缓存；不传则每次都重新登录（用于"未保存试连"）。
    """

    def __init__(
        self,
        creds: DcimCloudCredentials,
        *,
        server_id: int | None = None,
        timeout: float = 10.0,
    ):
        self.creds = creds
        self.server_id = server_id
        self.timeout = aiohttp.ClientTimeout(total=timeout)

    @property
    def is_agent(self) -> bool:
        return self.creds.account_type == "agent"

    def _build_url(self, path: str) -> str:
        path = path if path.startswith("/") else "/" + path
        if self.is_agent:
            agent_path = path.lstrip("/")
            if "?" in agent_path:
                endpoint, query = agent_path.split("?", 1)
                return f"{self.creds.base_url}/index.php?path={endpoint}&{query}"
            return f"{self.creds.base_url}/index.php?path={agent_path}"
        return f"{self.creds.base_url}/v1{path}"

    async def _basecurl(
        self,
        session: aiohttp.ClientSession,
        path: str,
        data: dict[str, Any] | None = None,
        method: str = "POST",
        headers: dict[str, str] | None = None,
    ) -> tuple[int, str, dict[str, Any] | None]:
        url = self._build_url(path)
        h = {"User-Agent": "QingYun-Cloud/1.0"}
        if headers:
            h.update(headers)

        try:
            if method.upper() == "GET":
                async with session.get(
                    url, params=data, headers=h, ssl=False, allow_redirects=True
                ) as resp:
                    text = await resp.text()
                    code = resp.status
            else:
                async with session.request(
                    method.upper(),
                    url,
                    data=data,
                    headers=h,
                    ssl=False,
                    allow_redirects=True,
                ) as resp:
                    text = await resp.text()
                    code = resp.status
        except aiohttp.ClientConnectorError as e:
            raise DcimCloudError(f"无法连接到 {self.creds.base_url}：{e}") from e
        except aiohttp.ServerTimeoutError as e:
            raise DcimCloudError(f"连接 {self.creds.base_url} 超时") from e
        except asyncio.TimeoutError as e:
            raise DcimCloudError(f"连接 {self.creds.base_url} 超时") from e
        except aiohttp.ClientError as e:
            raise DcimCloudError(f"HTTP 错误：{e}") from e

        json_body: dict[str, Any] | None = None
        stripped = text.strip()
        if stripped.startswith("{") or stripped.startswith("["):
            try:
                import json as _json

                parsed = _json.loads(stripped)
                if isinstance(parsed, dict):
                    json_body = parsed
            except ValueError:
                json_body = None

        return code, text, json_body

    async def login(self, *, force: bool = False) -> str:
        """走 admin 或 agent 模式获取 access-token。失败抛 DcimCloudError。

        - 默认命中 Redis 缓存（`qycw:dcim_cloud:token:{server_id}`，TTL 21600s）。
        - `force=True` 或缓存未命中时调远端，成功后回写缓存。
        - 没传 server_id 的实例（未保存试连）不走缓存。
        """
        if self.server_id is not None and not force:
            cached = await redis_client.get(token_cache_key(self.server_id))
            if cached:
                return cached

        async with aiohttp.ClientSession(timeout=self.timeout) as session:
            data = {"username": self.creds.username, "password": self.creds.password}
            login_path = "/token" if self.is_agent else "/login?a=a"

            code, text, body = await self._basecurl(
                session, login_path, data=data, method="POST"
            )

            if code == 404:
                raise DcimCloudError(
                    "无法连接魔方云管理系统：登录端点 404。请确认主机/端口/协议是否正确。",
                    http_code=404,
                )
            if code >= 500:
                raise DcimCloudError(f"上游 5xx 错误（HTTP {code}）", http_code=code)

            ok = 200 <= code < 300
            if self.is_agent:
                if not ok or not body or "token" not in (body.get("data") or {}):
                    msg = (body or {}).get("msg") or text[:200] or f"HTTP {code}"
                    raise DcimCloudError(f"登录失败：{msg}", http_code=code)
                token = str(body["data"]["token"])
            else:
                if not ok:
                    msg = (body or {}).get("msg") or text[:200] or f"HTTP {code}"
                    raise DcimCloudError(f"登录失败：{msg}", http_code=code)
                token = text.strip().strip('"')
                if not token:
                    raise DcimCloudError("登录失败：响应体为空", http_code=code)

            if self.server_id is not None:
                await redis_client.set(
                    token_cache_key(self.server_id), token, ex=TOKEN_TTL_SECONDS
                )
            return token

    async def invalidate_token(self) -> None:
        if self.server_id is not None:
            await redis_client.delete(token_cache_key(self.server_id))

    async def request(
        self,
        path: str,
        data: dict[str, Any] | None = None,
        *,
        method: str = "POST",
        timeout: float | None = None,
    ) -> tuple[int, str, dict[str, Any] | None]:
        """带 access-token 的业务调用。401 时自动清缓存 → 强制重登 → 重试一次。

        对齐 mfcw `DcimCloud::curl()` 的 `$relogin` 行为。
        返回 (http_code, text, json_body)。鉴权/连通性失败抛 DcimCloudError。
        """
        cs_timeout = aiohttp.ClientTimeout(total=timeout) if timeout else self.timeout

        async def _do(token: str) -> tuple[int, str, dict[str, Any] | None]:
            async with aiohttp.ClientSession(timeout=cs_timeout) as session:
                return await self._basecurl(
                    session,
                    path,
                    data=data,
                    method=method,
                    headers={"access-token": token},
                )

        token = await self.login()
        code, text, body = await _do(token)

        if code == 401 and self.server_id is not None:
            log.info(
                "dcim_cloud %s got 401 on %s, refreshing token", self.server_id, path
            )
            await self.invalidate_token()
            token = await self.login(force=True)
            code, text, body = await _do(token)

        return code, text, body


async def test_connection(
    creds: DcimCloudCredentials, *, server_id: int | None = None, force: bool = True
) -> tuple[bool, str, int]:
    """测试一组凭据是否可用。返回 (ok, message, latency_ms)。

    - server_id 不为 None 时，成功登录会顺便刷新 Redis 缓存。
    - 默认 force=True，避免拿到上一次缓存的 token 误认为还在线。
    """
    started = time.perf_counter()
    client = DcimCloudClient(creds, server_id=server_id, timeout=8.0)
    try:
        await client.login(force=force)
    except DcimCloudError as e:
        latency = int((time.perf_counter() - started) * 1000)
        if server_id is not None:
            await client.invalidate_token()
        return False, e.message, latency
    except Exception as e:  # noqa: BLE001
        latency = int((time.perf_counter() - started) * 1000)
        log.exception("dcim_cloud test_connection unexpected error")
        if server_id is not None:
            await client.invalidate_token()
        return False, f"未知错误：{e}", latency
    latency = int((time.perf_counter() - started) * 1000)
    mode = "代理商" if creds.account_type == "agent" else "管理员"
    return True, f"连接成功（{mode}模式，耗时 {latency} ms）", latency
