#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""把 client_files/news.php 推到「API 可调」的宝塔站点运行目录。

前置：面板 API 白名单已放行本机出口 IP（当前 82.41.66.212），
且 nodes_ssh.json 里 api_key 有效。

用法：
  python3 deploy_news_via_baota_api.py
  python3 deploy_news_via_baota_api.py --dry-run
"""
from __future__ import annotations

import argparse
import hashlib
import json
import ssl
import time
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path

NODES = Path("/www/wwwroot/tiaozhuan/data/nodes_ssh.json")
NEWS = Path(__file__).resolve().parent / "news.php"
CTX = ssl._create_unverified_context()


def md5(s: str) -> str:
    return hashlib.md5(s.encode()).hexdigest()


def panel_bases(panel: str) -> list[str]:
    p = panel.rstrip("/")
    out = [p]
    if p.startswith("https://"):
        out.append("http://" + p[8:])
    elif p.startswith("http://"):
        out.append("https://" + p[7:])
    return out


def api_post(base: str, path: str, api_key: str, extra: dict | None = None) -> tuple[int, str]:
    t = str(int(time.time()))
    token = md5(t + md5(api_key))
    data = {"request_time": t, "request_token": token}
    if extra:
        data.update(extra)
    body = urllib.parse.urlencode(data).encode()
    url = base.rstrip("/") + path
    req = urllib.request.Request(url, data=body, method="POST")
    try:
        with urllib.request.urlopen(req, timeout=20, context=CTX) as r:
            return r.status, r.read().decode("utf-8", "replace")
    except urllib.error.HTTPError as e:
        return e.code, e.read().decode("utf-8", "replace")
    except Exception as e:
        return 0, str(e)


def try_system(n: dict) -> str | None:
    key = n.get("api_key") or ""
    if not key:
        return None
    for base in panel_bases(n.get("panel") or ""):
        code, text = api_post(base, "/system?action=GetSystemTotal", key)
        if code == 200 and "IP" in text and "校验" in text:
            return None  # IP blocked
        if code == 200 and ("memTotal" in text or '"status":true' in text or "cpuNum" in text):
            return base
        if code == 200 and "status" in text and "false" not in text[:80]:
            return base
    return None


def save_file(base: str, api_key: str, path: str, content: str) -> tuple[bool, str]:
    # 宝塔常见：files?action=SaveFileBody
    code, text = api_post(
        base,
        "/files?action=SaveFileBody",
        api_key,
        {"path": path, "data": content, "encoding": "utf-8"},
    )
    ok = code == 200 and ("true" in text.lower() or "成功" in text)
    return ok, text[:300]


def list_sites(base: str, api_key: str) -> list[dict]:
    code, text = api_post(
        base,
        "/data?action=getData",
        api_key,
        {"table": "sites", "limit": "500", "p": "1", "search": "", "order": "id desc"},
    )
    try:
        d = json.loads(text)
    except Exception:
        return []
    data = d.get("data") if isinstance(d, dict) else None
    if isinstance(data, list):
        return data
    return []


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--dry-run", action="store_true")
    args = ap.parse_args()
    content = NEWS.read_text(encoding="utf-8")
    nodes = json.loads(NODES.read_text(encoding="utf-8")).get("nodes") or []
    print("news.php bytes", len(content), "nodes", len(nodes))

    for n in nodes:
        ip = n.get("ip")
        print("===", ip, "===")
        base = try_system(n)
        if not base:
            print("  SKIP: API 不可用（IP白名单/面板挂）")
            continue
        print("  API base OK", base)
        sites = list_sites(base, n["api_key"])
        print("  sites", len(sites))
        if args.dry_run:
            continue
        for s in sites:
            path = (s.get("path") or "").rstrip("/")
            if not path:
                continue
            target = path + "/news.php"
            ok, msg = save_file(base, n["api_key"], target, content)
            print(" ", "OK" if ok else "FAIL", target, msg[:120])
    return 0


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