#!/usr/bin/env python3
"""抓取虾友 IM 消息：好友列表、消息拉取、附件下载。"""

from __future__ import annotations

import argparse
import hashlib
import json
import os
import sys
import time
import urllib.parse
from pathlib import Path
from typing import Any, Optional

import requests

from sophclaw_auth import SophclawAuth

# 修复容器 DNS ndots:5 问题：避免 Python urllib3 在 search domain 上耗尽重试
os.environ.setdefault("RES_OPTIONS", "ndots:1")
os.environ.setdefault("SOCKET_THREAD_POOL_SIZE", "0")

DEFAULT_TIMEOUT = 30
DEFAULT_API_BASE_URL = "https://yagent.sophnet.com/api"
DEFAULT_JWT_PATH = Path("/home/node/.openclaw/jwt.json")
MAX_RETRIES = 3
RETRY_DELAY = 2
DEFAULT_PAGE_SIZE = 50


class AppError(RuntimeError):
    pass


def configure_stdio() -> None:
    for name in ("stdout", "stderr"):
        stream = getattr(sys, name, None)
        if stream is not None and hasattr(stream, "reconfigure"):
            stream.reconfigure(encoding="utf-8", errors="replace")


def print_json(payload: Any) -> None:
    print(json.dumps(payload, ensure_ascii=False, indent=2))


def request_json(
    method: str,
    url: str,
    token: str,
    timeout: int,
    payload: Optional[dict[str, Any]] = None,
    raw_response: bool = False,
) -> 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 raw_response:
                return resp
            if not resp.ok:
                raise AppError(
                    "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 AppError(
                    "接口错误: 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 AppError:
            raise
        except Exception as exc:
            last_error = exc
            if attempt < MAX_RETRIES:
                time.sleep(RETRY_DELAY * attempt)

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


def unwrap_result(data: dict[str, Any]) -> Any:
    return data.get("result") if "result" in data else data


def resolve_api_base(args: argparse.Namespace) -> str:
    if args.api_base_url and args.api_base_url.strip():
        return args.api_base_url.strip().rstrip("/")
    env_base = os.environ.get("SOPHCLAW_API_BASE_URL", "").strip()
    if env_base:
        return env_base.rstrip("/")
    return DEFAULT_API_BASE_URL


def resolve_origin(args: argparse.Namespace) -> str:
    if args.origin and args.origin.strip():
        return args.origin.strip().rstrip("/")
    env_origin = os.environ.get("SOPHCLAW_ORIGIN", "").strip()
    if env_origin:
        return env_origin.rstrip("/")
    return "https://www.sophnet.com"


def expand_path(path_str: str) -> Path:
    return Path(path_str).expanduser().resolve()


def looks_like_image(url: str, name: Optional[str] = None) -> bool:
    lower = (name or "") + (url or "")
    return bool(
        __import__("re").search(r"\.(jpg|jpeg|png|gif|webp|bmp|heic)(\?|$)", lower, __import__("re").IGNORECASE)
    )


def cmd_setup(args: argparse.Namespace) -> int:
    """将 refresh token 写入 jwt.json。"""
    try:
        path = SophclawAuth.setup_refresh_token(
            refresh_token=args.refresh_token,
            jwt_path=expand_path(args.jwt_path),
        )
        print("refresh token 已写入 {}".format(path))
        print("现在可以正常使用技能了，JWT 过期时将会自动续期。")
        return 0
    except Exception as exc:
        print("设置失败: {}".format(exc), file=sys.stderr)
        return 1


def normalize_message(m: dict[str, Any], origin: str) -> dict[str, Any]:
    """标准化消息对象，分离图片和文件附件。"""
    msg = {
        "id": str(m.get("_id", m.get("id", ""))),
        "rid": str(m.get("rid", "")),
        "msg": str(m.get("msg", "")),
        "ts": m.get("ts"),
        "updatedAt": m.get("_updatedAt", m.get("updatedAt")),
        "u": None,
        "imageAttachments": [],
        "fileAttachments": [],
    }

    if isinstance(m.get("u"), dict):
        u = m["u"]
        msg["u"] = {
            "id": str(u.get("_id", u.get("id", ""))),
            "username": str(u.get("username", "")),
            "name": u.get("name"),
        }

    all_attachments = []
    if isinstance(m.get("attachments"), list):
        all_attachments.extend(m["attachments"])
    if isinstance(m.get("file"), dict):
        all_attachments.append(m["file"])
    if isinstance(m.get("files"), list):
        all_attachments.extend(m["files"])

    for att in all_attachments:
        if not isinstance(att, dict):
            continue
        url = att.get("image_url") or att.get("imageUrl") or att.get("url", "")
        name = att.get("name") or att.get("title")
        if url and (str(att.get("type", "")).startswith("image/") or looks_like_image(url, name)):
            full_url = _resolve_url(url, origin)
            msg["imageAttachments"].append({"url": full_url, "title": name})
        elif url:
            full_url = _resolve_url(url, origin)
            msg["fileAttachments"].append({
                "url": full_url,
                "name": name,
                "type": att.get("type"),
                "size": att.get("size"),
            })

    return msg


def _resolve_url(raw: str, origin: str) -> str:
    if not raw:
        return raw
    if raw.startswith("http://") or raw.startswith("https://"):
        return raw
    if raw.startswith("/"):
        return origin.rstrip("/") + raw
    return raw


def download_file(url: str, dest: Path, token: str, timeout: int) -> Path:
    """下载文件到 dest，dest 为父目录，自动处理文件名。"""
    dest = dest.expanduser().resolve()
    dest.mkdir(parents=True, exist_ok=True)

    resp = request_json("GET", url, token, timeout, raw_response=True)
    content_type = resp.headers.get("Content-Type", "")
    content_disp = resp.headers.get("Content-Disposition", "")

    filename = None
    if "filename=" in content_disp:
        import re
        m = re.search(r'filename[^=]*="?([^";\s]+)"?', content_disp)
        if m:
            filename = m.group(1)

    if not filename:
        parsed = urllib.parse.urlparse(url)
        path_part = parsed.path
        if path_part and "/" in path_part:
            filename = path_part.rsplit("/", 1)[-1]
        if not filename or "." not in filename:
            ext = ".jpg" if "image" in content_type else ".bin"
            h = hashlib.md5(url.encode()).hexdigest()[:8]
            filename = "{}{}".format(h, ext)

    filepath = dest / filename
    with filepath.open("wb") as fp:
        for chunk in resp.iter_content(chunk_size=8192):
            fp.write(chunk)

    return filepath


# ---------------------------------------------------------------------------
# 子命令
# ---------------------------------------------------------------------------


def cmd_friends(args: argparse.Namespace) -> int:
    """列出所有虾友及其 DM 频道。"""
    api_base = resolve_api_base(args)
    auth = SophclawAuth(jwt_path=expand_path(args.jwt_path), api_base_url=api_base)
    token = auth.get_valid_token()

    # 获取好友列表
    data = request_json("GET", "{}/sys/openclaw/friend/list".format(api_base), token, args.timeout)
    result = unwrap_result(data)
    friends = result.get("friends", []) if isinstance(result, dict) else []

    output = []
    for f in friends:
        friend_id = f.get("friendId")
        nickname = f.get("nickname", "未知")
        dm_channel = None

        # 获取/创建 DM 频道
        if friend_id:
            try:
                dm_data = request_json(
                    "POST",
                    "{}/sys/openclaw/friend/dm".format(api_base),
                    token,
                    args.timeout,
                    payload={"friendId": friend_id},
                )
                dm_result = unwrap_result(dm_data)
                dm_channel = (
                    dm_result.get("channel") if isinstance(dm_result, dict) else dm_result
                )
            except AppError:
                pass

        output.append({
            "friendId": friend_id,
            "nickname": nickname,
            "avatarUrl": f.get("avatarUrl"),
            "dmChannelId": str(dm_channel.get("_id", "")) if dm_channel else None,
            "dmChannelName": dm_channel.get("fname") or dm_channel.get("name") if dm_channel else None,
        })

    if args.json:
        print_json({"total": len(output), "friends": output})
    else:
        for item in output:
            ch_id = item["dmChannelId"] or "无"
            print("  {} (friendId={}, dm={})".format(item["nickname"], item["friendId"], ch_id))

    return 0


def cmd_list(args: argparse.Namespace) -> int:
    """拉取指定好友/频道的历史消息。"""
    api_base = resolve_api_base(args)
    origin = resolve_origin(args)
    auth = SophclawAuth(jwt_path=expand_path(args.jwt_path), api_base_url=api_base)
    token = auth.get_valid_token()

    rid = args.rid
    if not rid and args.friend_id is not None:
        # 通过 friendId 查找 DM 频道
        dm_data = request_json(
            "POST",
            "{}/sys/openclaw/friend/dm".format(api_base),
            token,
            args.timeout,
            payload={"friendId": args.friend_id},
        )
        dm_result = unwrap_result(dm_data)
        dm_channel = dm_result.get("channel") if isinstance(dm_result, dict) else dm_result
        if dm_channel:
            rid = str(dm_channel.get("_id", ""))

    if not rid:
        raise AppError("请指定 --rid 或 --friend-id")

    count = args.count if args.count and args.count > 0 else DEFAULT_PAGE_SIZE
    params = {"rid": rid, "count": min(count, 100)}
    if args.latest is not None:
        params["latest"] = args.latest
    if args.oldest is not None:
        params["oldest"] = args.oldest
    if args.offset is not None:
        params["offset"] = args.offset

    query = "&".join("{}={}".format(k, v) for k, v in params.items())
    url = "{}/sys/openclaw/im/channels.history?{}".format(api_base, query)
    data = request_json("GET", url, token, args.timeout)
    result = unwrap_result(data)

    raw_messages = []
    if isinstance(result, dict):
        raw_messages = result.get("messages", [])
    elif isinstance(result, list):
        raw_messages = result

    messages = [normalize_message(m, origin) for m in raw_messages if isinstance(m, dict)]

    output = {
        "rid": rid,
        "count": len(messages),
        "messages": messages,
    }
    print_json(output)
    return 0


def cmd_unread(args: argparse.Namespace) -> int:
    """拉取所有 DM 频道的最近消息（模拟未读消息）。"""
    api_base = resolve_api_base(args)
    origin = resolve_origin(args)
    auth = SophclawAuth(jwt_path=expand_path(args.jwt_path), api_base_url=api_base)
    token = auth.get_valid_token()

    # 拉取频道列表（含未读数）
    data = request_json("GET", "{}/sys/openclaw/im/channels.list".format(api_base), token, args.timeout)
    result = unwrap_result(data)
    channels = result.get("channels", []) if isinstance(result, dict) else []
    if isinstance(channels, list):
        pass
    else:
        channels = []

    # 过滤出 DM 频道 (t == "d")
    dm_channels = [ch for ch in channels if isinstance(ch, dict) and ch.get("t") == "d"]

    min_unread = args.min_unread if args.min_unread is not None else 0
    if min_unread > 0:
        dm_channels = [ch for ch in dm_channels if ch.get("unread", 0) >= min_unread]

    count = args.count if args.count and args.count > 0 else 20
    output_channels = []

    for ch in dm_channels:
        rid = str(ch.get("_id", ch.get("rid", "")))
        # 拉取最近消息
        try:
            h_url = "{}/sys/openclaw/im/channels.history?rid={}&count={}".format(
                api_base, rid, min(count, 100)
            )
            h_data = request_json("GET", h_url, token, args.timeout)
            h_result = unwrap_result(h_data)
            raw_msgs = (
                h_result.get("messages", [])
                if isinstance(h_result, dict)
                else (h_result if isinstance(h_result, list) else [])
            )
            messages = [
                normalize_message(m, origin) for m in raw_msgs if isinstance(m, dict)
            ]
        except AppError:
            messages = []

        output_channels.append({
            "rid": rid,
            "name": ch.get("fname") or ch.get("name", ""),
            "unread": ch.get("unread", 0),
            "lm": ch.get("lm"),
            "lastMessage": ch.get("lastMessage"),
            "messages": messages,
            "messageCount": len(messages),
        })

    output = {
        "channels": output_channels,
        "total": len(output_channels),
    }
    print_json(output)
    return 0


def cmd_download(args: argparse.Namespace) -> int:
    """下载指定消息的附件。"""
    api_base = resolve_api_base(args)
    origin = resolve_origin(args)
    auth = SophclawAuth(jwt_path=expand_path(args.jwt_path), api_base_url=api_base)
    token = auth.get_valid_token()
    out_dir = expand_path(args.out_dir or "./shrimp_attachments")

    rid = args.rid
    msg_id = args.msg_id

    if not rid or not msg_id:
        raise AppError("请同时指定 --rid 和 --msg-id")

    # 查历史消息找到目标消息
    url = "{}/sys/openclaw/im/channels.history?rid={}&count=100".format(api_base, rid)
    data = request_json("GET", url, token, args.timeout)
    result = unwrap_result(data)
    raw_msgs = (
        result.get("messages", []) if isinstance(result, dict) else (result if isinstance(result, list) else [])
    )

    target_msg = None
    for m in raw_msgs:
        m_id = str(m.get("_id", m.get("id", "")))
        if m_id == msg_id or msg_id in m_id:
            target_msg = m
            break

    if target_msg is None:
        raise AppError("未找到消息: msgId={}".format(msg_id))

    normalized = normalize_message(target_msg, origin)
    image_dir = out_dir / "images"
    file_dir = out_dir / "files"
    downloaded = []

    # 下载图片
    for idx, img in enumerate(normalized.get("imageAttachments", [])):
        img_url = img.get("url")
        if img_url:
            try:
                dest = download_file(img_url, image_dir, token, args.timeout)
                downloaded.append({"type": "image", "url": img_url, "path": str(dest)})
            except AppError as exc:
                downloaded.append({"type": "image", "url": img_url, "error": str(exc)})

    # 下载文件
    for idx, file_att in enumerate(normalized.get("fileAttachments", [])):
        file_url = file_att.get("url")
        if file_url:
            try:
                dest = download_file(file_url, file_dir, token, args.timeout)
                downloaded.append({"type": "file", "url": file_url, "path": str(dest)})
            except AppError as exc:
                downloaded.append({"type": "file", "url": file_url, "error": str(exc)})

    output = {
        "msgId": normalized["id"],
        "downloaded": downloaded,
        "total": len(downloaded),
    }
    print_json(output)
    return 0


# ---------------------------------------------------------------------------
# 命令行入口
# ---------------------------------------------------------------------------


def add_http_args(parser: argparse.ArgumentParser) -> None:
    parser.add_argument(
        "--api-base-url",
        default="",
        help="虾友 API 根地址（默认 {}）".format(DEFAULT_API_BASE_URL),
    )
    parser.add_argument("--jwt-path", default=str(DEFAULT_JWT_PATH), help="JWT 文件路径")
    parser.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT, help="HTTP 超时秒数")


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="抓取虾友 IM 消息")
    sub = parser.add_subparsers(dest="command", required=True)

    # friends
    p_friends = sub.add_parser("friends", help="列出所有虾友及 DM 频道")
    add_http_args(p_friends)
    p_friends.add_argument("--json", action="store_true", help="JSON 输出")

    # list
    p_list = sub.add_parser("list", help="拉取指定好友/频道的历史消息")
    add_http_args(p_list)
    p_list.add_argument("--origin", default="", help="附件 URL 拼接前缀（默认 https://www.sophnet.com）")
    p_list.add_argument("--rid", default="", help="DM 频道 ID")
    p_list.add_argument("--friend-id", type=int, default=None, help="好友 ID（自动查找 DM 频道）")
    p_list.add_argument("--count", type=int, default=DEFAULT_PAGE_SIZE, help="拉取条数（默认 {}，上限 100）".format(DEFAULT_PAGE_SIZE))
    p_list.add_argument("--latest", type=int, default=None, help="拉取此时间戳之前的消息（毫秒）")
    p_list.add_argument("--oldest", type=int, default=None, help="拉取此时间戳之后的消息（毫秒）")
    p_list.add_argument("--offset", type=int, default=None, help="偏移量")

    # unread
    p_unread = sub.add_parser("unread", help="拉取所有 DM 频道的最近/未读消息")
    add_http_args(p_unread)
    p_unread.add_argument("--origin", default="", help="附件 URL 拼接前缀（默认 https://www.sophnet.com）")
    p_unread.add_argument("--count", type=int, default=20, help="每个频道拉取条数（默认 20）")
    p_unread.add_argument("--min-unread", type=int, default=0, help="最小未读数过滤（默认 0 表示全部）")

    # download
    p_download = sub.add_parser("download", help="下载指定消息的附件")
    add_http_args(p_download)
    p_download.add_argument("--origin", default="", help="附件 URL 拼接前缀")
    p_download.add_argument("--rid", required=True, help="频道 ID")
    p_download.add_argument("--msg-id", required=True, help="消息 ID")
    p_download.add_argument("--out-dir", default="./shrimp_attachments", help="输出目录")

    # setup — 设置 refresh token
    p_setup = sub.add_parser("setup", help="设置 refresh token 到 jwt.json")
    p_setup.add_argument(
        "--refresh-token", required=True, help="从浏览器 localStorage user-info 中复制的 refreshToken"
    )
    p_setup.add_argument("--jwt-path", default=str(DEFAULT_JWT_PATH), help="JWT 文件路径")

    return parser


def main(argv: Optional[list[str]] = None) -> int:
    configure_stdio()
    args = build_parser().parse_args(argv)
    try:
        if args.timeout <= 0:
            raise AppError("timeout 必须为正整数")

        if args.command == "friends":
            return cmd_friends(args)
        if args.command == "list":
            return cmd_list(args)
        if args.command == "unread":
            return cmd_unread(args)
        if args.command == "download":
            return cmd_download(args)
        if args.command == "setup":
            return cmd_setup(args)

        raise AppError("未知命令: {}".format(args.command))
    except AppError as exc:
        print("ERROR: {}".format(exc), file=sys.stderr)
        return 1


if __name__ == "__main__":
    raise SystemExit(main())