"""Kimi Code 订阅 plan 适配器（K1.2 / PROGRESS K1）。

Kimi 的 coding 端点是 **OpenAI 兼容** 的，因此本适配器复用 `OpenAICompatAdapter`——
区别仅在：① base_url 指向 coding 端点；② 必须携带伪造的官方客户端头集；③ access_token
作为 bearer（OpenAI SDK 的 `api_key` 自动发 `Authorization: Bearer <token>`）。

**伪造头集的真源 = `github.com/ooojustin/opencode-kimi`（`src/headers.ts` + `src/constants.ts`，
1:1 镜像 kimi-cli v1.37.0）。** coding API 会校验这 7 个头；任何偏差都会让 Moonshot 后端
返回 `access_terminated_error: only available for Coding Agents`（403）。本实现据此发送完整
7 头：UA `KimiCLI/1.37.0` + 6 个 `X-Msh-*`。

注意：早先曾参照 `picassio/pi-kimi-coder`（`extensions/index.ts`，**仅**发 UA、不带 X-Msh-*）
把头集裁成 UA-only——那是分歧/错误的参考实现，导致了误修。现已回退到 opencode 规范的完整
7 头（见 `memory/gotchas.md`）。

`access_token` 由 @backend（K1.3 OAuth device 服务）按需刷新后传入；本适配器只接收当前
access token 作为 `api_key`，不负责刷新/获取 token。
"""

from __future__ import annotations

import os
import platform
import socket
import uuid
from pathlib import Path

import instructor
from openai import AsyncOpenAI

from .openai_compat import OpenAICompatAdapter, StructuredClient

#: Kimi 订阅 plan 的 coding 推理端点（OpenAI 兼容）。
KIMI_CODE_BASE_URL = "https://api.kimi.com/coding/v1"

#: CLI 版本（opencode `constants.ts` KIMI_CLI_VERSION，镜像 kimi-cli v1.37.0）。
KIMI_CLI_VERSION = "1.37.0"

#: 伪造的官方客户端 UA（opencode `USER_AGENT = f"KimiCLI/{KIMI_CLI_VERSION}"`）。
#: UA 前缀必须精确为 `KimiCLI/<version>`，且 version 必须与 X-Msh-Version 一致。
KIMI_CODE_USER_AGENT = f"KimiCLI/{KIMI_CLI_VERSION}"

#: `X-Msh-Platform` 是字面常量字符串（**不是** OS 名）。
KIMI_CODE_PLATFORM = "kimi_cli"

#: provider 名（与既有 API-key provider `kimi` 分离，便于档位切换）。
KIMI_CODE_PROVIDER = "kimi-code"

#: device-id 覆盖用环境变量（优先级高于持久化文件，便于测试/CI 注入）。
KIMI_DEVICE_ID_ENV = "KIMI_DEVICE_ID"

#: device-id 持久化路径（与 kimi-cli / opencode 共享 `~/.kimi/device_id`，保证单一稳定指纹）。
KIMI_DEVICE_ID_DIR = Path.home() / ".kimi"
KIMI_DEVICE_ID_PATH = KIMI_DEVICE_ID_DIR / "device_id"

#: HTTP 头值若含非 ASCII 会被底层 fetch/httpx 拒绝；opencode `asciiHeaderValue` 同样裁剪。
_ASCII_FALLBACK = "unknown"


def _ascii_header_value(value: str, fallback: str = _ASCII_FALLBACK) -> str:
    """裁掉非可打印 ASCII（`\\x20-\\x7e` 之外）+ 去空白；空则回退。

    镜像 opencode `asciiHeaderValue`（含非 ASCII 的头值会被底层 fetch/httpx 拒绝）。
    """
    sanitized = "".join(ch for ch in value if "\x20" <= ch <= "\x7e").strip()
    return sanitized or fallback


def kimi_device_id() -> str:
    """返回稳定的 device-id（32 位无连字符 UUID4 hex）。

    优先级：① 环境变量 `KIMI_DEVICE_ID`；② 持久化文件 `~/.kimi/device_id`（不存在则生成一次
    `uuid.uuid4().hex` 写入、之后复用）。跨调用/进程重启保持稳定，**绝不**每次随机。
    """
    env_value = os.environ.get(KIMI_DEVICE_ID_ENV, "").strip()
    if env_value:
        return env_value

    if KIMI_DEVICE_ID_PATH.exists():
        existing = KIMI_DEVICE_ID_PATH.read_text(encoding="utf-8").strip()
        if existing:
            return existing

    KIMI_DEVICE_ID_DIR.mkdir(mode=0o700, parents=True, exist_ok=True)
    device_id = uuid.uuid4().hex
    KIMI_DEVICE_ID_PATH.write_text(device_id, encoding="utf-8")
    KIMI_DEVICE_ID_PATH.chmod(0o600)
    return device_id


def kimi_device_model() -> str:
    """`X-Msh-Device-Model`：镜像 opencode `kimiDeviceModel()` 的 OS 分支格式。

    - macOS：`f"macOS {mac_ver} {machine}"`（如 `"macOS 14.5 arm64"`）；
    - Windows：`f"Windows {release} {machine}"`；
    - 其它：`f"{system} {release} {machine}"`。
    `platform.machine()` 返回 `arm64`/`x86_64`（**不**做归一化）。
    """
    system = platform.system()
    release = platform.release()
    machine = platform.machine()

    if system == "Darwin":
        version = platform.mac_ver()[0] or release
        if version and machine:
            return f"macOS {version} {machine}"
        if version:
            return f"macOS {version}"
        return f"macOS {machine}".strip()

    if system == "Windows":
        if release and machine:
            return f"Windows {release} {machine}"
        if release:
            return f"Windows {release}"
        return f"Windows {machine}".strip()

    if system:
        if release and machine:
            return f"{system} {release} {machine}"
        if release:
            return f"{system} {release}"
        return f"{system} {machine}".strip()

    return "Unknown"


def _device_name() -> str:
    """`X-Msh-Device-Name`：主机名（ASCII 化）。`socket.gethostname()` ≈ Node `os.hostname()`。"""
    return _ascii_header_value(socket.gethostname() or platform.node())


def _os_version() -> str:
    """`X-Msh-Os-Version`：OS 内核版本串。`platform.version()` ≈ Node `os.version()`。"""
    fallback = f"{platform.system()} {platform.release()}"
    return _ascii_header_value(platform.version() or fallback)


def kimi_code_headers() -> dict[str, str]:
    """组装 Kimi coding API 必需的伪造客户端头集（opencode 规范的完整 7 头）。

    真源：`ooojustin/opencode-kimi` `src/headers.ts` `kimiHeaders()`。
    """
    return {
        "User-Agent": KIMI_CODE_USER_AGENT,
        "X-Msh-Platform": KIMI_CODE_PLATFORM,
        "X-Msh-Version": KIMI_CLI_VERSION,
        "X-Msh-Device-Name": _device_name(),
        "X-Msh-Device-Model": _ascii_header_value(kimi_device_model()),
        "X-Msh-Device-Id": kimi_device_id(),
        "X-Msh-Os-Version": _os_version(),
    }


def build_kimi_code_client(access_token: str, *, base_url: str | None = None) -> AsyncOpenAI:
    """构造带伪造头的 `AsyncOpenAI` 客户端（access_token → bearer，coding base）。"""
    return AsyncOpenAI(
        api_key=access_token,
        base_url=base_url or KIMI_CODE_BASE_URL,
        default_headers=kimi_code_headers(),
    )


def build_kimi_code_structured_client(client: AsyncOpenAI) -> StructuredClient:
    """Kimi 结构化输出走 instructor **JSON 模式**（`response_format`），**不**用 TOOLS 模式。

    缘由：`kimi-for-coding` 默认开启 thinking，而 Moonshot 后端对「thinking 开启 + 强制
    `tool_choice`」组合返回 `400 tool_choice 'specified' is incompatible with thinking enabled`。
    instructor 默认 `Mode.TOOLS` 会发强制 `tool_choice` → 触发该 400。改用 `Mode.JSON`
    （`response_format={"type":"json_object"}` + schema 注入 prompt）即避开 tool_choice，
    与 thinking 兼容。仅对 `kimi-code` 生效，其它 provider 维持默认模式。
    """
    return instructor.from_openai(client, mode=instructor.Mode.JSON)


class KimiCodeAdapter(OpenAICompatAdapter):
    """Kimi Code 适配器：复用 OpenAI 兼容行为，固定 provider 名 `kimi-code`。

    model（如 `kimi-for-coding`）经 `.complete(req, model)` 传入（路由/档位关注点），
    **不**在此硬编码。结构化输出强制走 JSON 模式（见 `build_kimi_code_structured_client`）。
    """

    def __init__(
        self, client: AsyncOpenAI, *, structured_client: StructuredClient | None = None
    ) -> None:
        super().__init__(
            KIMI_CODE_PROVIDER,
            client,
            structured_client=structured_client or build_kimi_code_structured_client(client),
        )
