#!/usr/bin/env python3
"""Publish RobotDaily digests to Discord via OpenClaw and optional Discord REST."""

from __future__ import annotations

import argparse
import json
import re
from typing import Any, Dict, List, Optional
from urllib.error import HTTPError
from urllib.request import Request, urlopen

from fetch_arxiv import DOMAIN_CONFIGS
from utils import (
    format_authors,
    log,
    normalize_space,
    now_local,
    read_json,
    run_command_json,
    truncate,
)

DOMAIN_ORDER = ["embodied", "representation", "reinforcement"]
DISCORD_API = "https://discord.com/api/v10"
DOMAIN_STYLE = {
    "embodied": {"color": 0x4F8CFF, "emoji": "🤖"},
    "representation": {"color": 0x9B59B6, "emoji": "🧠"},
    "reinforcement": {"color": 0x2ECC71, "emoji": "🎯"},
}


class PublishError(RuntimeError):
    pass


def normalize_channel_name(name: str) -> str:
    text = normalize_space(name).lstrip("#").lower()
    text = re.sub(r"\s+", "-", text)
    return text


class DiscordPublisher:
    def __init__(
        self,
        *,
        openclaw_bin: str,
        account_id: str,
        mode: str,
        guild_id: str,
        parent_channel_id: str,
        target_channel_id: str,
        target_channel_name: str,
        category_id: str,
        bot_token: str,
        thread_auto_archive_min: int,
        dry_run: bool,
    ) -> None:
        self.openclaw_bin = openclaw_bin
        self.account_id = account_id
        self.mode = mode
        self.guild_id = guild_id
        self.parent_channel_id = parent_channel_id
        self.target_channel_id = target_channel_id
        self.target_channel_name = target_channel_name
        self.category_id = category_id
        self.bot_token = bot_token
        self.thread_auto_archive_min = thread_auto_archive_min
        self.dry_run = dry_run

    def openclaw(self, *args: str) -> Dict[str, Any]:
        command = [self.openclaw_bin, "message", *args, "--channel", "discord", "--account", self.account_id, "--json"]
        if self.dry_run:
            command.append("--dry-run")
        return run_command_json(command)

    def list_channels(self) -> List[Dict[str, Any]]:
        if not self.guild_id:
            return []
        payload = self.openclaw("channel", "list", "--guild-id", self.guild_id)
        return payload.get("payload", {}).get("channels", [])

    def list_threads(self) -> List[Dict[str, Any]]:
        if not self.guild_id or not self.parent_channel_id:
            return []
        payload = self.openclaw(
            "thread",
            "list",
            "--guild-id",
            self.guild_id,
            "--channel-id",
            self.parent_channel_id,
            "--include-archived",
            "--limit",
            "100",
        )
        return payload.get("payload", {}).get("threads", {}).get("threads", [])

    def find_existing_channel(self, name: str) -> Optional[str]:
        wanted_exact = normalize_space(name)
        wanted_normalized = normalize_channel_name(name)
        for channel in self.list_channels():
            current_name = str(channel.get("name", ""))
            if current_name == wanted_exact or normalize_channel_name(current_name) == wanted_normalized:
                return str(channel.get("id", ""))
        return None

    def find_existing_thread(self, name: str) -> Optional[str]:
        for thread in self.list_threads():
            if thread.get("name") == name:
                return str(thread.get("id", ""))
        return None

    def create_channel_via_rest(self, name: str, topic: str = "") -> str:
        if not self.guild_id:
            raise PublishError("channel 模式需要 DISCORD_GUILD_ID")
        if self.dry_run:
            return f"dry-run-channel-{normalize_channel_name(name) or 'robotdaily'}"
        if not self.bot_token:
            raise PublishError("创建 Discord 频道需要 DISCORD_BOT_TOKEN；当前 OpenClaw CLI 版本没有公开 channel create 子命令")

        body: Dict[str, Any] = {"name": normalize_channel_name(name) or "robotdaily", "type": 0}
        if self.category_id:
            body["parent_id"] = self.category_id
        if topic:
            body["topic"] = topic

        request = Request(
            url=f"{DISCORD_API}/guilds/{self.guild_id}/channels",
            method="POST",
            data=json.dumps(body).encode("utf-8"),
            headers={
                "Authorization": f"Bot {self.bot_token}",
                "Content-Type": "application/json",
                "User-Agent": "DiscordBot (https://github.com/openclaw/openclaw, 1.0)",
            },
        )
        try:
            with urlopen(request, timeout=30) as response:
                payload = json.loads(response.read().decode("utf-8", errors="ignore"))
        except HTTPError as exc:
            detail = exc.read().decode("utf-8", errors="ignore")
            raise PublishError(f"Discord channel create failed: {exc.code} {detail}") from exc
        return str(payload.get("id", ""))

    def create_thread(self, thread_name: str, opening_message: str) -> str:
        if not self.parent_channel_id:
            raise PublishError("thread 模式需要 DISCORD_PARENT_CHANNEL_ID")
        existing = self.find_existing_thread(thread_name)
        if existing:
            return existing
        payload = self.openclaw(
            "thread",
            "create",
            "--target",
            f"channel:{self.parent_channel_id}",
            "--thread-name",
            thread_name,
            "--message",
            opening_message,
            "--auto-archive-min",
            str(self.thread_auto_archive_min),
        )
        thread = payload.get("payload", {}).get("thread", {})
        thread_id = str(thread.get("id", ""))
        if not thread_id and self.dry_run:
            return f"dry-run-thread-{thread_name}"
        if not thread_id:
            raise PublishError("OpenClaw thread create 没有返回 thread id")
        return thread_id

    def create_fixed_channel(self, title: str) -> str:
        channel_name = normalize_channel_name(self.target_channel_name or "robotdaily") or "robotdaily"
        existing = self.find_existing_channel(channel_name)
        if existing:
            return existing
        topic = truncate(title, 180)
        channel_id = self.create_channel_via_rest(channel_name, topic=topic)
        if not channel_id:
            raise PublishError("Discord fixed channel create 返回空 id")
        return channel_id

    def create_or_resolve_target(self, title: str, opening_message: str) -> str:
        date_slug = now_local().strftime("%Y-%m-%d")
        if self.mode == "existing-channel":
            if not self.target_channel_id:
                raise PublishError("existing-channel 模式需要 DISCORD_TARGET_CHANNEL_ID")
            return self.target_channel_id

        if self.mode == "fixed-channel":
            return self.create_fixed_channel(title)

        if self.mode == "thread":
            thread_name = f"RobotDaily {date_slug}"
            return self.create_thread(thread_name, opening_message)

        if self.mode == "channel":
            prefix = normalize_channel_name(self.target_channel_name or "robotdaily") or "robotdaily"
            channel_name = f"{prefix}-{date_slug}"
            existing = self.find_existing_channel(channel_name)
            if existing:
                return existing
            topic = truncate(title, 180)
            channel_id = self.create_channel_via_rest(channel_name, topic=topic)
            if not channel_id:
                raise PublishError("Discord channel create 返回空 id")
            return channel_id

        raise PublishError(f"未知的投递模式: {self.mode}")

    def send_embeds_via_rest(
        self,
        target_channel_id: str,
        content: str = "",
        embeds: Optional[List[Dict[str, Any]]] = None,
        components: Optional[List[Dict[str, Any]]] = None,
    ) -> Dict[str, Any]:
        if self.dry_run:
            log(
                f"[Dry Run] send_embeds_via_rest: channel {target_channel_id}, embeds count {len(embeds or [])}, components count {len(components or [])}"
            )
            return {"id": "dry-run-msg-id"}
        if not self.bot_token:
            raise PublishError("发送富文本卡片需要 DISCORD_BOT_TOKEN")

        body: Dict[str, Any] = {}
        if content:
            body["content"] = content
        if embeds:
            body["embeds"] = embeds
        if components:
            body["components"] = components

        request = Request(
            url=f"{DISCORD_API}/channels/{target_channel_id}/messages",
            method="POST",
            data=json.dumps(body).encode("utf-8"),
            headers={
                "Authorization": f"Bot {self.bot_token}",
                "Content-Type": "application/json",
                "User-Agent": "DiscordBot (https://github.com/openclaw/openclaw, 1.0)",
            },
        )
        try:
            with urlopen(request, timeout=30) as response:
                result = json.loads(response.read().decode("utf-8", errors="ignore"))
                return result
        except HTTPError as exc:
            detail = exc.read().decode("utf-8", errors="ignore")
            log(f"REST API 错误: {exc.code} {exc.reason} - {detail}")
            raise PublishError(f"创建 Discord 消息失败: {exc.code} {detail}") from exc

    def send_message(self, target_channel_id: str, message: str, media: str = "") -> Dict[str, Any]:
        args = ["send", "--target", f"channel:{target_channel_id}", "--message", message]
        if media:
            args.extend(["--media", media])
        return self.openclaw(*args)


def build_opening_message(payload: Dict[str, Any]) -> str:
    total = len(payload.get("papers", []))
    counts = payload.get("counts", {})
    breakdown = []
    for domain in DOMAIN_ORDER:
        count = counts.get(domain, 0)
        if count:
            breakdown.append(f"{DOMAIN_CONFIGS[domain]['label_zh']} {count}")
    suffix = " / ".join(breakdown)
    if suffix:
        return f"**RobotDaily | {now_local().strftime('%Y-%m-%d')}**\n共 {total} 篇偏应用论文，按方向整理成短卡片：{suffix}。"
    return f"**RobotDaily | {now_local().strftime('%Y-%m-%d')}**\n共 {total} 篇偏应用论文。"


def build_domain_header(domain: str, count: int) -> str:
    style = DOMAIN_STYLE.get(domain, {})
    emoji = style.get("emoji", "📌")
    return f"## {emoji} {DOMAIN_CONFIGS[domain]['label_zh']}（{count} 篇）"


def build_paper_embed(paper: Dict[str, Any]) -> Dict[str, Any]:
    domain = paper.get("domain", "")
    label_zh = DOMAIN_CONFIGS.get(domain, {}).get("label_zh", domain or "未分类")
    style = DOMAIN_STYLE.get(domain, {"color": 0x3498DB, "emoji": "📄"})

    title_prefix = f"{paper.get('domain_rank', '?')}. "
    title = truncate(title_prefix + str(paper.get("title", "")), 256)
    brief = truncate(
        paper.get("brief_explanation_zh", "")
        or paper.get("selection_reason", "")
        or paper.get("translated_abstract_zh", ""),
        200,
    )
    authors = truncate(format_authors(paper.get("authors", []), limit=4), 120) or "—"
    tags = " ".join(f"`{tag}`" for tag in paper.get("tags", [])[:4]) or "—"
    published = truncate(str(paper.get("published_local", ""))[:10], 32) or "—"
    arxiv_id = truncate(paper.get("arxiv_id", ""), 64) or "—"

    fields: List[Dict[str, Any]] = [
        {"name": "💡 简析", "value": brief or "—", "inline": False},
        {"name": "👤 作者", "value": authors, "inline": True},
        {"name": "🏷️ 标签", "value": tags, "inline": True},
    ]

    embed: Dict[str, Any] = {
        "title": title,
        "url": paper.get("abs_url", ""),
        "description": f"{style['emoji']} **{label_zh}** · `{published}` · `arXiv:{arxiv_id}`",
        "color": style["color"],
        "fields": fields,
        "footer": {"text": "RobotDaily 卡片视图"},
    }
    return embed


def build_link_buttons(paper: Dict[str, Any]) -> List[Dict[str, Any]]:
    buttons: List[Dict[str, Any]] = []
    for label, url in [
        ("DOI", paper.get("doi_url", "")),
        ("arXiv", paper.get("abs_url", "")),
        ("PDF", paper.get("pdf_url", "")),
    ]:
        if not url:
            continue
        buttons.append({"type": 2, "style": 5, "label": label, "url": url})
    if not buttons:
        return []
    return [{"type": 1, "components": buttons[:3]}]


def build_fallback_paper_markdown(paper: Dict[str, Any]) -> str:
    links = []
    if paper.get("doi_url"):
        links.append(f"[DOI]({paper.get('doi_url')})")
    if paper.get("abs_url"):
        links.append(f"[arXiv]({paper.get('abs_url')})")
    if paper.get("pdf_url"):
        links.append(f"[PDF]({paper.get('pdf_url')})")
    tags = " ".join(f"`{tag}`" for tag in paper.get("tags", [])[:4]) or "—"
    brief = truncate(
        paper.get("brief_explanation_zh", "") or paper.get("selection_reason", "") or paper.get("translated_abstract_zh", ""),
        180,
    )
    abstract = truncate(paper.get("translated_abstract_zh", "") or paper.get("summary", ""), 220)
    lines = [
        f"### {paper.get('domain_rank', '?')}. {paper.get('title', '')}",
        f"> {brief or '—'}",
        f"- 作者：{truncate(format_authors(paper.get('authors', []), limit=4), 120) or '—'}",
        f"- 标签：{tags}",
        f"- 中文摘要：{abstract or '—'}",
    ]
    if links:
        lines.append(f"- 链接：{' | '.join(links)}")
    return "\n".join(lines)


def publish_digest(
    payload: Dict[str, Any],
    *,
    html_path: str = "",
    markdown_path: str = "",
    publisher: DiscordPublisher,
) -> str:
    opening_message = build_opening_message(payload)
    target_channel_id = publisher.create_or_resolve_target(opening_message, opening_message)

    publisher.send_message(target_channel_id, opening_message)

    grouped: Dict[str, List[Dict[str, Any]]] = {domain: [] for domain in DOMAIN_ORDER}
    for paper in payload.get("papers", []):
        grouped.setdefault(paper["domain"], []).append(paper)

    for domain in DOMAIN_ORDER:
        papers = grouped.get(domain, [])
        if not papers:
            continue

        publisher.send_message(target_channel_id, build_domain_header(domain, len(papers)))
        for paper in papers:
            if publisher.bot_token:
                try:
                    publisher.send_embeds_via_rest(
                        target_channel_id,
                        embeds=[build_paper_embed(paper)],
                        components=build_link_buttons(paper),
                    )
                    continue
                except PublishError as exc:
                    log(f"富文本卡片发送失败，回退到纯 Markdown：{exc}")
            publisher.send_message(target_channel_id, build_fallback_paper_markdown(paper))

    if markdown_path:
        publisher.send_message(target_channel_id, "附一份 Markdown 归档版，方便桌面端检索。", media=markdown_path)

    return target_channel_id


def main() -> None:
    parser = argparse.ArgumentParser(description="Publish RobotDaily digest to Discord")
    parser.add_argument("--input", required=True)
    parser.add_argument("--html", default="")
    parser.add_argument("--markdown", default="")
    parser.add_argument("--mode", default="thread")
    parser.add_argument("--openclaw-bin", default="openclaw")
    parser.add_argument("--account-id", default="codex")
    parser.add_argument("--guild-id", default="")
    parser.add_argument("--parent-channel-id", default="")
    parser.add_argument("--target-channel-id", default="")
    parser.add_argument("--target-channel-name", default="")
    parser.add_argument("--category-id", default="")
    parser.add_argument("--bot-token", default="")
    parser.add_argument("--thread-auto-archive-min", type=int, default=10080)
    parser.add_argument("--dry-run", action="store_true")
    args = parser.parse_args()

    payload = read_json(args.input, default={}) or {}
    publisher = DiscordPublisher(
        openclaw_bin=args.openclaw_bin,
        account_id=args.account_id,
        mode=args.mode,
        guild_id=args.guild_id,
        parent_channel_id=args.parent_channel_id,
        target_channel_id=args.target_channel_id,
        target_channel_name=args.target_channel_name,
        category_id=args.category_id,
        bot_token=args.bot_token,
        thread_auto_archive_min=args.thread_auto_archive_min,
        dry_run=args.dry_run,
    )

    target = publish_digest(
        payload,
        html_path=args.html,
        markdown_path=args.markdown,
        publisher=publisher,
    )
    log(f"Digest published to Discord target: {target}")


if __name__ == "__main__":
    main()
