#!/usr/bin/env python3
"""Sophclaw Skill 共享认证模块：JWT 自动刷新。

用法：
    from sophclaw_auth import SophclawAuth

    auth = SophclawAuth(api_base_url="https://yagent.sophnet.com/api")
    token = auth.get_valid_token()  # 自动检测过期并刷新

refresh token 读取优先级（从高到低）：
    1. jwt.json 中的 web_refresh_token 字段（Web 端同步，v1.1.0+）
    2. 环境变量 SOPHCLAW_REFRESH_TOKEN
    3. 文件 ~/.openclaw/refresh_token
    4. 若都没有且 access token 过期，抛出 AuthError 提示用户 setup
"""

from __future__ import annotations

import base64
import json
from pathlib import Path
from typing import Any, Optional
import sys
import time

# DNS fix: container ndots:5 breaks Python thread pool DNS
import os as _os
import time
_os.environ.setdefault('RES_OPTIONS', 'ndots:1')
_os.environ.setdefault('SOCKET_THREAD_POOL_SIZE', '0')
del _os

import requests

DEFAULT_JWT_PATH = Path("/home/node/.openclaw/jwt.json")
DEFAULT_REFRESH_TOKEN_FILE = Path("/home/node/.openclaw/refresh_token")
# 提前 5 分钟刷新，避免边界情况
REFRESH_MARGIN_SECONDS = 300
MAX_RETRIES = 3
RETRY_DELAY = 2
REQUEST_TIMEOUT = 30

SETUP_INSTRUCTIONS = """
========================================
未找到 refresh token，无法自动续期 JWT。

获取 refresh token 的方法：
  1. 浏览器打开 Sophnet，按 F12 → Application → Local Storage → user-info
     复制其中 refreshToken 字段的值
  2. 执行以下命令完成设置（二选一）：

     方式 A — 写入环境变量（推荐，重启后失效需重设）：
       export SOPHCLAW_REFRESH_TOKEN="<你的refreshToken>"

     方式 B — 写入文件（持久化）：
       echo "<你的refreshToken>" > ~/.openclaw/refresh_token

     方式 C — 用脚本 setup 子命令（写入 jwt.json，持久化）：
       uv run {baseDir}/scripts/auto_accept.py setup --refresh-token "<你的refreshToken>"

  3. 设置完成后重新执行原有命令即可。
========================================
"""


class AuthError(RuntimeError):
    """认证相关错误。"""
    pass


def _resolve_api_base(api_base_url: Optional[str] = None) -> str:
    if api_base_url and api_base_url.strip():
        return api_base_url.strip().rstrip("/")
    env_base = os.environ.get("SOPHCLAW_API_BASE_URL", "").strip()
    if env_base:
        return env_base.rstrip("/")
    return "https://yagent.sophnet.com/api"


def _load_json_file(path: Path) -> dict[str, Any]:
    try:
        with path.open("r", encoding="utf-8") as fp:
            data = json.load(fp)
    except OSError as exc:
        raise AuthError("无法读取文件 {}: {}".format(path, exc)) from exc
    except json.JSONDecodeError as exc:
        raise AuthError("文件不是有效 JSON: {}: {}".format(path, exc)) from exc
    if not isinstance(data, dict):
        raise AuthError("JSON 根对象必须是对象: {}".format(path))
    return data


def _save_json_file(path: Path, data: dict[str, Any]) -> None:
    try:
        parent = path.parent
        if not parent.exists():
            parent.mkdir(parents=True, exist_ok=True)
        tmp_path = path.with_suffix(path.suffix + ".tmp")
        with tmp_path.open("w", encoding="utf-8") as fp:
            json.dump(data, fp, ensure_ascii=False, indent=2)
        tmp_path.replace(path)
    except OSError as exc:
        raise AuthError("无法写入文件 {}: {}".format(path, exc)) from exc


def _read_refresh_token_file(path: Path) -> Optional[str]:
    """读取纯文本 refresh token 文件。"""
    try:
        with path.open("r", encoding="utf-8") as fp:
            token = fp.read().strip()
            return token if token else None
    except OSError:
        return None


def _save_refresh_token_file(token: str, path: Optional[Path] = None) -> None:
    """将 refresh token 写入独立纯文本文件（不被 Web 同步覆盖）。"""
    if path is None:
        path = DEFAULT_REFRESH_TOKEN_FILE
    try:
        parent = path.parent
        if not parent.exists():
            parent.mkdir(parents=True, exist_ok=True)
        with path.open("w", encoding="utf-8") as fp:
            fp.write(token.strip() + "\n")
    except OSError as exc:
        raise AuthError("无法写入 refresh token 文件 {}: {}".format(path, exc)) from exc


def _resolve_refresh_token(
    jwt_data: dict[str, Any],
    jwt_path: Path,
) -> str:
    """查找 refresh token，优先级：独立文件 → jwt.json → 环境变量。

    - 独立文件 ~/.openclaw/refresh_token 不会被 Web 前端覆盖
    - jwt.json 的 web_refresh_token 会在界面刷新时丢失
    - 环境变量重启后失效

    返回 refresh token 字符串，找不到则抛出 AuthError。
    """
    # 1. 独立文件 ~/.openclaw/refresh_token（最稳定，不被 Web 覆盖）
    rt = _read_refresh_token_file(DEFAULT_REFRESH_TOKEN_FILE)
    if rt:
        _sophlog("从 {} 读取 refresh token".format(DEFAULT_REFRESH_TOKEN_FILE))
        return rt

    # 2. jwt.json 中的 web_refresh_token（会被 Web 刷新重置）
    rt = jwt_data.get("web_refresh_token")
    if isinstance(rt, str) and rt.strip():
        # 顺便备份到独立文件，防止 Web 刷新后丢失
        try:
            _save_refresh_token_file(rt)
        except AuthError:
            pass
        return rt.strip()

    # 3. 环境变量
    rt = os.environ.get("SOPHCLAW_REFRESH_TOKEN", "").strip()
    if rt:
        _sophlog("从环境变量 SOPHCLAW_REFRESH_TOKEN 读取 refresh token")
        try:
            _save_refresh_token_file(rt)
            jwt_data["web_refresh_token"] = rt
            _save_json_file(jwt_path, jwt_data)
        except AuthError:
            pass
        return rt

    raise AuthError(SETUP_INSTRUCTIONS)


def _http_request(
    method: str,
    url: str,
    token: str,
    timeout: int = REQUEST_TIMEOUT,
    payload: Optional[dict[str, Any]] = None,
    expect_json: bool = True,
) -> Any:
    headers = {
        "Authorization": "Bearer {}".format(token),
        "Accept": "application/json",
    }
    if payload is not None:
        headers["Content-Type"] = "application/json"

    last_error = None
    for attempt in range(1, MAX_RETRIES + 1):
        try:
            resp = requests.request(
                method=method,
                url=url,
                headers=headers,
                json=payload,
                timeout=timeout,
            )
            if not expect_json:
                return resp
            if not resp.ok:
                raise AuthError(
                    "HTTP {} {}: {}".format(resp.status_code, url, resp.text[:500])
                )
            data = resp.json()
            if isinstance(data, dict) and data.get("status") not in (None, 0):
                raise AuthError(
                    "接口错误: status={} message={!r}".format(
                        data.get("status"), data.get("message")
                    )
                )
            return data if isinstance(data, dict) else {"result": data}
        except requests.exceptions.ConnectionError as exc:
            last_error = exc
            if attempt < MAX_RETRIES:
                time.sleep(RETRY_DELAY * attempt)
        except requests.exceptions.Timeout as exc:
            last_error = exc
            if attempt < MAX_RETRIES:
                time.sleep(RETRY_DELAY * attempt)
        except AuthError:
            raise
        except Exception as exc:
            last_error = exc
            if attempt < MAX_RETRIES:
                time.sleep(RETRY_DELAY * attempt)

    raise AuthError("请求失败 {} (重试 {} 次后仍失败): {}".format(url, MAX_RETRIES, last_error))


def _sophlog(msg: str) -> None:
    """模块级日志函数，供 _resolve_refresh_token 等模块函数调用。"""
    print("[sophclaw-auth] {}".format(msg), file=sys.stderr)


class SophclawAuth:
    """Sophclaw 认证管理器：自动检测 JWT 过期并刷新。"""

    def __init__(
        self,
        jwt_path: Optional[Path] = None,
        api_base_url: Optional[str] = None,
    ):
        self._jwt_path = Path(jwt_path or DEFAULT_JWT_PATH).expanduser().resolve()
        self._api_base = _resolve_api_base(api_base_url)

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def get_valid_token(self) -> str:
        """获取有效的 JWT access token，过期时自动刷新。"""
        data = _load_json_file(self._jwt_path)

        # 提取 access token
        raw = data.get("web_jwt")
        if not isinstance(raw, str) or not raw.strip():
            raise AuthError("jwt.json 缺少有效 web_jwt: {}".format(self._jwt_path))
        raw = raw.strip()
        if raw.lower().startswith("bearer "):
            raw = raw[7:].strip()

        if not self._is_expired(raw):
            return raw

        # 尝试用 refresh token 续期（多来源查找）
        refresh = _resolve_refresh_token(data, self._jwt_path)

        self._log("JWT 已过期，尝试用 refreshToken 续期...")
        new_token, new_refresh = self._do_refresh(refresh)

        # 写回 jwt.json + 独立文件（双重保险）
        data["web_jwt"] = "Bearer {}".format(new_token)
        data["web_refresh_token"] = new_refresh
        data["savedAt"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
        _save_json_file(self._jwt_path, data)
        _save_refresh_token_file(new_refresh)

        self._log("JWT 续期成功。")
        return new_token

    def get_api_base(self) -> str:
        return self._api_base

    @staticmethod
    def setup_refresh_token(
        refresh_token: str,
        jwt_path: Optional[Path] = None,
    ) -> Path:
        """将 refresh token 写入 jwt.json。

        如果 jwt.json 不存在则创建，如果已存在则保留原有字段并更新 web_refresh_token。
        返回 jwt.json 的路径。
        """
        path = Path(jwt_path or DEFAULT_JWT_PATH).expanduser().resolve()

        try:
            data = _load_json_file(path)
        except AuthError:
            # 文件不存在或无效，创建新的
            data = {}

        data["web_refresh_token"] = refresh_token.strip()
        if "savedAt" not in data:
            data["savedAt"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())

        _save_json_file(path, data)
        _save_refresh_token_file(refresh_token.strip())
        _log("refresh token 已写入 {} + {}".format(path, DEFAULT_REFRESH_TOKEN_FILE))
        return path

    # ------------------------------------------------------------------
    # Internal
    # ------------------------------------------------------------------

    def _is_expired(self, token: str) -> bool:
        """检查 token 是否已过期（含 5 分钟提前量）。"""
        exp = self._jwt_exp_unix(token)
        if exp is None:
            return False  # 无法解析则不拦截
        return time.time() >= exp - REFRESH_MARGIN_SECONDS

    @staticmethod
    def _jwt_exp_unix(token: str) -> Optional[int]:
        try:
            parts = token.split(".")
            if len(parts) != 3:
                return None
            payload = parts[1] + "=" * (-len(parts[1]) % 4)
            raw = base64.urlsafe_b64decode(payload.encode("ascii"))
            data = json.loads(raw)
            exp = data.get("exp")
            return int(exp) if isinstance(exp, (int, float)) else None
        except Exception:
            return None

    def _do_refresh(self, refresh_token: str) -> tuple[str, str]:
        """调用 refresh 接口换取新 token。

        返回: (new_access_token, new_refresh_token)
        """
        # refresh 接口只认 sophnet.com，不认 yagent.sophnet.com
        url = "https://sophnet.com/api/sys/login/refresh"
        try:
            # refresh 接口不需要 Authorization header，
            # 请求体传 refreshToken 即可
            resp = requests.post(
                url,
                json={"refreshToken": refresh_token},
                headers={
                    "Content-Type": "application/json",
                    "Accept": "application/json",
                },
                timeout=REQUEST_TIMEOUT,
            )
            if resp.status_code == 401:
                raise AuthError("Refresh token 已失效，请重新登录获取新的 refresh token。")
            if not resp.ok:
                raise AuthError(
                    "Refresh 接口返回 HTTP {}: {}".format(
                        resp.status_code, resp.text[:500]
                    )
                )
            data = resp.json()

            # 兼容两种响应格式：
            #   { status: 0, result: { token, refreshToken, ... } }
            #   { token, refreshToken, ... }
            if isinstance(data, dict) and data.get("status") not in (None, 0):
                raise AuthError(
                    "Refresh 接口错误: status={} message={!r}".format(
                        data.get("status"), data.get("message")
                    )
                )
            result = data.get("result") if isinstance(data, dict) else data
            if isinstance(result, dict):
                token_data = result
            else:
                token_data = data

            new_token = token_data.get("token") or token_data.get("accessToken")
            new_refresh = token_data.get("refreshToken")

            if not new_token:
                raise AuthError("Refresh 接口未返回 token: {}".format(data))
            if not new_refresh:
                # 某些实现可能不返回新 refreshToken，沿用旧的
                new_refresh = refresh_token

            return str(new_token), str(new_refresh)
        except requests.exceptions.RequestException as exc:
            raise AuthError("Refresh 请求失败: {}".format(exc)) from exc

    @staticmethod
    def _log(msg: str) -> None:
        _sophlog(msg)