#!/usr/bin/env python3
import json
import os
import re
import sys
import time
import traceback
import urllib.request
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

BASE_API = "https://server.cloud.yt.net/api/v1"
DEFAULT_CONFIG = {
    "telegram_token": "",
    "allowed_chat_ids": [165067365],
    "yt_email": "",
    "yt_password": "",
    "monitor_enabled": True,
    "monitor_interval": 5,
    "monitored_categories": ["sz-bgp"],
    "auto_order_plan_ids": [53, 54, 55],
    "auto_order": True,
    "stop_on_success": True,
    "last_update_id": 0,
    "last_seen_stock": {},
    "last_order_attempt": {},
}


class Bot:
    def __init__(self, config_path: str):
        self.config_path = Path(config_path)
        self.config_path.parent.mkdir(parents=True, exist_ok=True)
        self.config = self._load_config()

    def _load_config(self) -> Dict[str, Any]:
        if self.config_path.exists():
            with self.config_path.open("r", encoding="utf-8") as f:
                data = json.load(f)
        else:
            data = {}
        merged = dict(DEFAULT_CONFIG)
        merged.update(data)
        self._save_config(merged)
        return merged

    def _save_config(self, data: Optional[Dict[str, Any]] = None):
        if data is not None:
            self.config = data
        tmp = self.config_path.with_suffix(".tmp")
        with tmp.open("w", encoding="utf-8") as f:
            json.dump(self.config, f, ensure_ascii=False, indent=2)
        os.replace(tmp, self.config_path)
        try:
            os.chmod(self.config_path, 0o600)
        except Exception:
            pass

    def api_json(self, url: str, method: str = "GET", data: Optional[dict] = None, headers: Optional[dict] = None, timeout: int = 20) -> dict:
        req_headers = {"Content-Type": "application/json"}
        if headers:
            req_headers.update(headers)
        body = None
        if data is not None:
            body = json.dumps(data).encode()
        req = urllib.request.Request(url, data=body, headers=req_headers, method=method)
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            return json.loads(resp.read().decode("utf-8"))

    def tg_api(self, method: str, data: Optional[dict] = None, timeout: int = 20) -> dict:
        token = self.config["telegram_token"]
        if not token:
            raise RuntimeError("telegram_token 未配置")
        url = f"https://api.telegram.org/bot{token}/{method}"
        return self.api_json(url, method="POST", data=data or {}, timeout=timeout)

    def answer_callback(self, callback_id: str, text: str = "已执行"):
        try:
            self.tg_api("answerCallbackQuery", {"callback_query_id": callback_id, "text": text})
        except Exception as e:
            print(f"answer_callback failed: {e}", file=sys.stderr)

    def command_keyboard(self) -> dict:
        return {
            "inline_keyboard": [
                [
                    {"text": "状态", "callback_data": "/status"},
                    {"text": "立即检查", "callback_data": "/check"},
                ],
                [
                    {"text": "分类列表", "callback_data": "/categories"},
                    {"text": "当前产品", "callback_data": "/plans"},
                ],
                [
                    {"text": "开始监控", "callback_data": "/startmon"},
                    {"text": "停止监控", "callback_data": "/stopmon"},
                ],
                [
                    {"text": "自动下单开", "callback_data": "/autoon"},
                    {"text": "自动下单关", "callback_data": "/autooff"},
                ],
            ]
        }

    def send_message(self, chat_id: int, text: str, reply_markup: Optional[dict] = None):
        try:
            payload = {"chat_id": chat_id, "text": text}
            if reply_markup is not None:
                payload["reply_markup"] = reply_markup
            self.tg_api("sendMessage", payload)
        except Exception as e:
            print(f"send_message failed: {e}", file=sys.stderr)

    def edit_message(self, chat_id: int, message_id: int, text: str, reply_markup: Optional[dict] = None):
        try:
            payload = {"chat_id": chat_id, "message_id": message_id, "text": text}
            if reply_markup is not None:
                payload["reply_markup"] = reply_markup
            self.tg_api("editMessageText", payload)
        except Exception as e:
            print(f"edit_message failed: {e}", file=sys.stderr)

    def send_panel(self, chat_id: int, text: str):
        self.send_message(chat_id, text, reply_markup=self.command_keyboard())

    def send_plans_panel(self, chat_id: int, slug: Optional[str] = None):
        self.send_message(chat_id, self.plans_text(slug), reply_markup=self.plans_keyboard(slug))

    def broadcast(self, text: str):
        for chat_id in self.config.get("allowed_chat_ids", []):
            self.send_panel(chat_id, text)

    def login(self) -> str:
        email = self.config.get("yt_email", "")
        password = self.config.get("yt_password", "")
        if not email or not password:
            raise RuntimeError("YT 账号未配置")
        resp = self.api_json(
            f"{BASE_API}/auth/login",
            method="POST",
            data={"email": email, "password": password},
        )
        if not resp.get("success"):
            raise RuntimeError(resp.get("message", "登录失败"))
        return resp["data"]["token"]

    def get_all_plans(self) -> Tuple[List[dict], Dict[str, dict]]:
        resp = self.api_json(f"{BASE_API}/auth/plans")
        if not resp.get("success"):
            raise RuntimeError(resp.get("message", "获取套餐失败"))
        data = resp.get("data", {})
        cages = {c["slug"]: c for c in data.get("cages", [])}
        cage_by_id = {c["id"]: c for c in data.get("cages", [])}
        plans = []
        for p in data.get("plans", []):
            p = dict(p)
            cage = cage_by_id.get(p.get("cage"), {})
            p["cage_info"] = cage
            p["slug"] = cage.get("slug", "")
            plans.append(p)
        return plans, cages

    def get_monitored_plans(self) -> List[dict]:
        plans, _ = self.get_all_plans()
        monitored = set(self.config.get("monitored_categories", []))
        return [p for p in plans if p.get("slug") in monitored]

    def parse_yt_link(self, text: str) -> Optional[str]:
        m = re.search(r"cloud\.yt\.net/server/([a-z0-9-]+)", text, re.I)
        if not m:
            return None
        return m.group(1).lower()

    def add_link(self, text: str) -> Tuple[str, List[dict]]:
        slug = self.parse_yt_link(text)
        if not slug:
            raise RuntimeError("链接格式不对，发 cloud.yt.net/server/xxx 这种")
        plans, cages = self.get_all_plans()
        cage = cages.get(slug)
        matched = [p for p in plans if p.get("slug") == slug]
        if not cage or not matched:
            raise RuntimeError(f"没识别到分类：{slug}")
        arr = set(self.config.get("monitored_categories", []))
        arr.add(slug)
        self.config["monitored_categories"] = sorted(arr)
        self._save_config()
        matched.sort(key=lambda x: x.get("id", 0))
        return slug, matched

    def get_templates(self, token: str, plan_id: int) -> List[dict]:
        resp = self.api_json(
            f"{BASE_API}/vps/templates/plan/{plan_id}",
            headers={"Authorization": f"Bearer {token}"},
        )
        if not resp.get("success"):
            raise RuntimeError(resp.get("message", "获取模板失败"))
        return resp.get("data", [])

    def order_plan(self, token: str, plan_id: int, template_id: int) -> dict:
        return self.api_json(
            f"{BASE_API}/vps/order/{plan_id}",
            method="POST",
            headers={"Authorization": f"Bearer {token}"},
            data={"Template": template_id},
        )

    def categories_text(self) -> str:
        plans, cages = self.get_all_plans()
        counts: Dict[str, int] = {}
        monitored = set(self.config.get("monitored_categories", []))
        for p in plans:
            slug = p.get("slug") or "-"
            counts[slug] = counts.get(slug, 0) + 1
        lines = ["YT 分类列表："]
        for slug, cage in sorted(cages.items()):
            mark = "✅" if slug in monitored else "▫️"
            lines.append(f"{mark} {slug} | {cage.get('label', cage.get('name', slug))} | {counts.get(slug, 0)} 个产品")
        lines += ["", "命令：/watch 分类slug", "命令：/unwatch 分类slug"]
        return "\n".join(lines)

    def get_display_plans(self, slug: Optional[str] = None) -> List[dict]:
        plans, _ = self.get_all_plans()
        monitored = set(self.config.get("monitored_categories", []))
        if slug:
            plans = [p for p in plans if p.get("slug") == slug]
        else:
            plans = [p for p in plans if p.get("slug") in monitored]
        plans.sort(key=lambda x: (x.get("slug", ""), x.get("id", 0)))
        return plans

    def plans_text(self, slug: Optional[str] = None) -> str:
        plans = self.get_display_plans(slug)
        auto_ids = set(int(x) for x in self.config.get("auto_order_plan_ids", []))
        lines = [f"产品列表{'（' + slug + '）' if slug else '（当前监控分类）'}："]
        for p in plans:
            auto = "✅" if int(p.get("id", 0)) in auto_ids else "▫️"
            lines.append(
                f"{auto} ID={p['id']} | {p['name']} | {p.get('slug','-')} | stock={p.get('stock',0)} | ¥{p.get('price','?')}"
            )
        lines += ["", "点下面按钮可直接开关自动下单"]
        return "\n".join(lines)

    def plans_keyboard(self, slug: Optional[str] = None) -> dict:
        plans = self.get_display_plans(slug)
        auto_ids = set(int(x) for x in self.config.get("auto_order_plan_ids", []))
        rows = []
        for p in plans:
            pid = int(p["id"])
            mark = "✅" if pid in auto_ids else "▫️"
            rows.append([
                {
                    "text": f"{mark} {p['name']}",
                    "callback_data": f"toggle_auto:{pid}:{slug or ''}",
                }
            ])
        rows.append([
            {"text": "刷新产品", "callback_data": f"show_plans:{slug or ''}"},
            {"text": "状态", "callback_data": "/status"},
        ])
        return {"inline_keyboard": rows}

    def status_text(self) -> str:
        plans = self.get_monitored_plans()
        auto_ids = set(int(x) for x in self.config.get("auto_order_plan_ids", []))
        lines = [
            "YT 补货 Bot 状态：",
            f"监控开关：{'开' if self.config.get('monitor_enabled') else '关'}",
            f"监控间隔：{self.config.get('monitor_interval')} 秒",
            f"监控分类：{', '.join(self.config.get('monitored_categories', [])) or '-'}",
            f"自动下单总开关：{'开' if self.config.get('auto_order') else '关'}",
            f"自动下单产品数：{len(auto_ids)}",
            "",
            "当前监控产品：",
        ]
        for p in plans:
            auto = "[自动]" if int(p.get("id", 0)) in auto_ids else "[仅监控]"
            lines.append(f"- {p['name']} {auto} stock={p.get('stock', 0)} price=¥{p.get('price', '?')}")
        return "\n".join(lines)

    def toggle_auto_plan(self, plan_id: int) -> bool:
        arr = set(int(x) for x in self.config.get("auto_order_plan_ids", []))
        enabled = plan_id not in arr
        if enabled:
            arr.add(plan_id)
        else:
            arr.discard(plan_id)
        self.config["auto_order_plan_ids"] = sorted(arr)
        self._save_config()
        return enabled

    def handle_command(self, chat_id: int, text: str):
        parts = text.strip().split(maxsplit=1)
        cmd = parts[0].lower()
        arg = parts[1].strip() if len(parts) > 1 else ""

        if cmd == "/start":
            self.send_panel(chat_id, "YT 补货 Bot 在线。")
            return
        if cmd == "/status":
            self.send_panel(chat_id, self.status_text())
            return
        if cmd == "/categories":
            self.send_panel(chat_id, self.categories_text())
            return
        if cmd == "/plans":
            self.send_plans_panel(chat_id, arg or None)
            return
        if cmd == "/addlink":
            if not arg:
                self.send_panel(chat_id, "用法：/addlink https://cloud.yt.net/server/sz-bgp")
                return
            slug, matched = self.add_link(arg)
            self.send_message(chat_id, f"已加入监控链接：{slug}\n识别到 {len(matched)} 个产品，点下面按钮选择自动下单。", reply_markup=self.plans_keyboard(slug))
            return
        if cmd == "/listlinks":
            arr = self.config.get("monitored_categories", [])
            self.send_panel(chat_id, "当前监控链接：\n" + "\n".join(f"- https://cloud.yt.net/server/{x}" for x in arr))
            return
        if cmd == "/check":
            msg = self.check_once(manual=True)
            self.send_panel(chat_id, msg or "当前没有新变化")
            return
        if cmd == "/startmon":
            self.config["monitor_enabled"] = True
            self._save_config()
            self.send_panel(chat_id, "监控已开启")
            return
        if cmd == "/stopmon":
            self.config["monitor_enabled"] = False
            self._save_config()
            self.send_panel(chat_id, "监控已停止")
            return
        if cmd == "/autoon":
            self.config["auto_order"] = True
            self._save_config()
            self.send_panel(chat_id, "自动下单总开关已开启")
            return
        if cmd == "/autooff":
            self.config["auto_order"] = False
            self._save_config()
            self.send_panel(chat_id, "自动下单总开关已关闭")
            return
        if cmd == "/setinterval":
            if not arg.isdigit():
                self.send_panel(chat_id, "用法：/setinterval 5")
                return
            val = max(3, int(arg))
            self.config["monitor_interval"] = val
            self._save_config()
            self.send_panel(chat_id, f"监控间隔已设为 {val} 秒")
            return
        if cmd == "/watch":
            if not arg:
                self.send_panel(chat_id, "用法：/watch 分类slug")
                return
            arr = set(self.config.get("monitored_categories", []))
            arr.add(arg)
            self.config["monitored_categories"] = sorted(arr)
            self._save_config()
            self.send_panel(chat_id, f"已加入监控分类：{arg}")
            return
        if cmd == "/unwatch":
            if not arg:
                self.send_panel(chat_id, "用法：/unwatch 分类slug")
                return
            arr = set(self.config.get("monitored_categories", []))
            arr.discard(arg)
            self.config["monitored_categories"] = sorted(arr)
            self._save_config()
            self.send_panel(chat_id, f"已移除监控分类：{arg}")
            return
        if cmd == "/autoadd":
            if not arg.isdigit():
                self.send_panel(chat_id, "用法：/autoadd 产品ID")
                return
            arr = set(int(x) for x in self.config.get("auto_order_plan_ids", []))
            arr.add(int(arg))
            self.config["auto_order_plan_ids"] = sorted(arr)
            self._save_config()
            self.send_panel(chat_id, f"已加入自动下单：ID={arg}")
            return
        if cmd == "/autodel":
            if not arg.isdigit():
                self.send_panel(chat_id, "用法：/autodel 产品ID")
                return
            arr = set(int(x) for x in self.config.get("auto_order_plan_ids", []))
            arr.discard(int(arg))
            self.config["auto_order_plan_ids"] = sorted(arr)
            self._save_config()
            self.send_panel(chat_id, f"已移除自动下单：ID={arg}")
            return
        self.send_panel(chat_id, "未知命令")

    def poll_updates(self):
        offset = int(self.config.get("last_update_id", 0)) + 1
        resp = self.tg_api("getUpdates", {"offset": offset, "timeout": 5, "allowed_updates": ["message", "callback_query"]}, timeout=15)
        for item in resp.get("result", []):
            self.config["last_update_id"] = item["update_id"]
            self._save_config()

            callback = item.get("callback_query") or {}
            if callback:
                msg = callback.get("message") or {}
                chat_id = ((msg.get("chat") or {}).get("id"))
                message_id = msg.get("message_id")
                if chat_id in self.config.get("allowed_chat_ids", []):
                    data = (callback.get("data") or "").strip()
                    if data.startswith("toggle_auto:"):
                        _, pid, slug = data.split(":", 2)
                        enabled = self.toggle_auto_plan(int(pid))
                        self.edit_message(chat_id, message_id, self.plans_text(slug or None), self.plans_keyboard(slug or None))
                        self.answer_callback(callback.get("id"), "已加入自动下单" if enabled else "已取消自动下单")
                    elif data.startswith("show_plans:"):
                        _, slug = data.split(":", 1)
                        self.edit_message(chat_id, message_id, self.plans_text(slug or None), self.plans_keyboard(slug or None))
                        self.answer_callback(callback.get("id"), "已刷新")
                    else:
                        self.answer_callback(callback.get("id"), "已执行")
                        if data.startswith("/"):
                            self.handle_command(chat_id, data)
                continue

            msg = item.get("message") or {}
            chat = msg.get("chat") or {}
            chat_id = chat.get("id")
            if chat_id not in self.config.get("allowed_chat_ids", []):
                continue
            text = (msg.get("text") or "").strip()
            if text.startswith("/"):
                self.handle_command(chat_id, text)

    def check_once(self, manual: bool = False) -> Optional[str]:
        plans = self.get_monitored_plans()
        plans.sort(key=lambda x: (x.get("slug", ""), x.get("id", 0)))
        last_seen = self.config.setdefault("last_seen_stock", {})
        newly_available = []
        currently_available = []

        for p in plans:
            pid = str(p["id"])
            stock = int(p.get("stock", 0) or 0)
            prev = int(last_seen.get(pid, 0) or 0)
            if stock > 0:
                currently_available.append(p)
            if stock > 0 and prev <= 0:
                newly_available.append(p)
            last_seen[pid] = stock

        self._save_config()

        if manual:
            if currently_available:
                lines = ["当前有货："]
                for p in currently_available:
                    auto = "[自动]" if int(p["id"]) in set(int(x) for x in self.config.get("auto_order_plan_ids", [])) else "[仅监控]"
                    lines.append(f"- ID={p['id']} {p['name']} {auto} stock={p['stock']} price=¥{p.get('price','?')}")
                return "\n".join(lines)
            return "当前无货"

        if not newly_available:
            return None

        auto_ids = set(int(x) for x in self.config.get("auto_order_plan_ids", []))
        lines = ["发现新补货："]
        for p in newly_available:
            auto = "[自动下单]" if int(p["id"]) in auto_ids and self.config.get("auto_order") else "[仅通知]"
            lines.append(f"- ID={p['id']} {p['name']} {auto} stock={p['stock']} price=¥{p.get('price','?')}")

        order_results = []
        success_count = 0
        if self.config.get("auto_order"):
            token = self.login()
            now = int(time.time())
            attempts = self.config.setdefault("last_order_attempt", {})
            for p in newly_available:
                plan_id = int(p["id"])
                if plan_id not in auto_ids:
                    continue
                key = str(plan_id)
                if now - int(attempts.get(key, 0)) < 20:
                    order_results.append(f"- {p['name']}: 跳过（20秒内已尝试过）")
                    continue
                attempts[key] = now
                template_id = 1
                try:
                    templates = self.get_templates(token, plan_id)
                    if templates:
                        template_id = int(templates[0]["id"])
                    resp = self.order_plan(token, plan_id, template_id)
                    if resp.get("success"):
                        success_count += 1
                        order_results.append(f"- {p['name']}: 下单成功")
                    else:
                        order_results.append(f"- {p['name']}: 下单失败 -> {resp.get('message', '未知错误')}")
                except Exception as e:
                    order_results.append(f"- {p['name']}: 下单异常 -> {e}")

        if success_count > 0 and self.config.get("stop_on_success", True):
            self.config["monitor_enabled"] = False
            self._save_config()
            order_results.append("已自动关闭监控（下单成功后停机）")

        if order_results:
            lines += [""] + order_results
        return "\n".join(lines)

    def run(self):
        next_check = 0.0
        while True:
            try:
                self.poll_updates()
            except Exception as e:
                print(f"poll_updates error: {e}", file=sys.stderr)
                time.sleep(3)

            try:
                interval = max(3, int(self.config.get("monitor_interval", 5)))
                if self.config.get("monitor_enabled") and time.time() >= next_check:
                    msg = self.check_once(manual=False)
                    if msg:
                        self.broadcast(msg)
                    next_check = time.time() + interval
            except Exception as e:
                print(f"监控异常: {e}", file=sys.stderr)
                traceback.print_exc()
                time.sleep(5)


if __name__ == "__main__":
    config_path = sys.argv[1] if len(sys.argv) > 1 else "/opt/yt-stock-bot/config.json"
    Bot(config_path).run()
