#!/usr/bin/env python3
"""notiMessage 本地调试转发服务 — 在 PC 浏览器查看手机抓取的消息。"""

import json
import threading
from collections import deque
from datetime import datetime
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlparse

HOST = "0.0.0.0"
PORT = 8765
MAX_MESSAGES = 500

_messages = deque(maxlen=MAX_MESSAGES)
_lock = threading.Lock()


def _now_iso():
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")


def _resolve_group(payload):
    group = (payload.get("group") or payload.get("title") or "").strip()
    if group and "TLRPC$" not in group and "org.telegram.tgnet." not in group:
        return group
    app = payload.get("appName") or payload.get("packageName") or ""
    return app + " / 未分类" if app else "未分类"


def _add_message(payload):
    item = dict(payload)
    item["group"] = _resolve_group(payload)
    item["receivedAt"] = _now_iso()
    with _lock:
        _messages.appendleft(item)
        item["id"] = len(_messages)
    print("[{0}] [{1}] [{2}] {3} | {4}".format(
        _now_iso(),
        item["group"],
        payload.get("source", "?"),
        payload.get("appName", payload.get("packageName", "")),
        (payload.get("content", "") or "")[:80],
    ))
    return item


def _json_response(handler, status, data):
    body = json.dumps(data, ensure_ascii=False).encode("utf-8")
    handler.send_response(status)
    handler.send_header("Content-Type", "application/json; charset=utf-8")
    handler.send_header("Access-Control-Allow-Origin", "*")
    handler.send_header("Content-Length", str(len(body)))
    handler.end_headers()
    handler.wfile.write(body)


def _group_messages(messages):
    groups = {}
    for msg in messages:
        key = msg.get("group") or _resolve_group(msg)
        if key not in groups:
            groups[key] = {
                "key": key,
                "appName": msg.get("appName") or msg.get("packageName") or "",
                "count": 0,
                "latestAt": msg.get("receivedAt", ""),
                "messages": [],
            }
        g = groups[key]
        g["count"] += 1
        g["messages"].append(msg)
        if (msg.get("receivedAt") or "") > (g.get("latestAt") or ""):
            g["latestAt"] = msg.get("receivedAt", "")
    result = sorted(groups.values(), key=lambda x: x.get("latestAt", ""), reverse=True)
    for g in result:
        g["messages"].sort(key=lambda m: m.get("receivedAt", ""), reverse=True)
    return result


def _html_page():
    html = """<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>notiMessage 调试台</title>
  <style>
    * { box-sizing: border-box; }
    body { font-family: ui-monospace, Consolas, monospace; margin: 0; background: #0f1115; color: #e6edf3; height: 100vh; display: flex; flex-direction: column; }
    header { padding: 14px 20px; background: #161b22; border-bottom: 1px solid #30363d; display: flex; gap: 14px; align-items: center; flex-wrap: wrap; flex-shrink: 0; }
    h1 { margin: 0; font-size: 18px; }
    .stat { color: #8b949e; font-size: 13px; }
    button { background: #238636; color: #fff; border: 0; padding: 8px 14px; border-radius: 6px; cursor: pointer; font-family: inherit; font-size: 13px; }
    button.secondary { background: #21262d; border: 1px solid #30363d; }
    .layout { display: flex; flex: 1; min-height: 0; }
    .sidebar { width: 280px; border-right: 1px solid #30363d; background: #161b22; overflow-y: auto; flex-shrink: 0; }
    .sidebar h2 { margin: 0; padding: 14px 16px 8px; font-size: 13px; color: #8b949e; font-weight: 600; }
    .group-item { padding: 12px 16px; border-bottom: 1px solid #21262d; cursor: pointer; }
    .group-item:hover { background: #1c2128; }
    .group-item.active { background: #1f2937; border-left: 3px solid #58a6ff; padding-left: 13px; }
    .group-name { color: #e6edf3; font-size: 13px; word-break: break-word; }
    .group-meta { color: #8b949e; font-size: 11px; margin-top: 4px; }
    .content-panel { flex: 1; overflow-y: auto; padding: 16px 20px; }
    .panel-title { font-size: 16px; color: #ffa657; margin: 0 0 12px; word-break: break-word; }
    .msg { padding: 12px 14px; margin-bottom: 10px; background: #161b22; border: 1px solid #21262d; border-radius: 8px; }
    .msg-head { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 6px; font-size: 11px; color: #8b949e; }
    .msg-source { color: #58a6ff; }
    .msg-sender { color: #d2a8ff; }
    .msg-body { color: #e6edf3; font-size: 13px; line-height: 1.5; word-break: break-word; white-space: pre-wrap; }
    .empty { padding: 40px; text-align: center; color: #8b949e; }
  </style>
</head>
<body>
  <header>
    <h1>notiMessage 调试台</h1>
    <span class="stat" id="count">0 条</span>
    <span class="stat" id="groupCount">0 群</span>
    <span class="stat" id="updated">-</span>
    <button onclick="loadMessages()">刷新</button>
    <button class="secondary" onclick="clearMessages()">清空</button>
  </header>
  <div class="layout">
    <aside class="sidebar">
      <h2>群 / 会话</h2>
      <div id="groups"></div>
    </aside>
    <main class="content-panel">
      <h2 class="panel-title" id="panelTitle">请选择左侧群聊</h2>
      <div id="messages"></div>
      <div class="empty" id="empty" style="display:none">暂无消息，请在手机上开启监听并收一条 Telegram 消息</div>
    </main>
  </div>
  <script>
    let groups = [];
    let activeGroup = null;

    function groupKey(g) { return g.key; }

    async function loadMessages() {
      const res = await fetch('/api/groups');
      groups = await res.json();
      document.getElementById('count').textContent =
        groups.reduce((n, g) => n + g.count, 0) + ' 条';
      document.getElementById('groupCount').textContent = groups.length + ' 群';
      document.getElementById('updated').textContent = '更新: ' + new Date().toLocaleTimeString();
      const empty = document.getElementById('empty');
      const groupsEl = document.getElementById('groups');
      groupsEl.innerHTML = '';

      if (!groups.length) {
        empty.style.display = 'block';
        document.getElementById('messages').innerHTML = '';
        document.getElementById('panelTitle').textContent = '请选择左侧群聊';
        activeGroup = null;
        return;
      }
      empty.style.display = 'none';

      if (!activeGroup || !groups.find(g => groupKey(g) === activeGroup)) {
        activeGroup = groupKey(groups[0]);
      }

      for (const g of groups) {
        const div = document.createElement('div');
        div.className = 'group-item' + (groupKey(g) === activeGroup ? ' active' : '');
        div.onclick = () => { activeGroup = groupKey(g); loadMessages(); };
        div.innerHTML = `
          <div class="group-name">${esc(g.key)}</div>
          <div class="group-meta">${esc(g.appName || '')} · ${g.count} 条 · ${esc(g.latestAt || '')}</div>`;
        groupsEl.appendChild(div);
      }
      renderActiveGroup();
    }

    function renderActiveGroup() {
      const g = groups.find(x => groupKey(x) === activeGroup);
      const panel = document.getElementById('messages');
      if (!g) {
        panel.innerHTML = '';
        return;
      }
      document.getElementById('panelTitle').textContent = g.key + '（' + g.count + ' 条）';
      panel.innerHTML = '';
      for (const m of g.messages) {
        const div = document.createElement('div');
        div.className = 'msg';
        const sender = (m.title && m.title !== g.key) ? m.title : '';
        div.innerHTML = `
          <div class="msg-head">
            <span>${esc(m.receivedAt || '')}</span>
            <span class="msg-source">${esc(m.source || '')}</span>
            ${sender ? `<span class="msg-sender">${esc(sender)}</span>` : ''}
          </div>
          <div class="msg-body">${esc(m.content || '')}</div>`;
        panel.appendChild(div);
      }
    }

    async function clearMessages() {
      await fetch('/api/messages', { method: 'DELETE' });
      activeGroup = null;
      loadMessages();
    }

    function esc(s) {
      return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
    }
    loadMessages();
    setInterval(loadMessages, 2000);
  </script>
</body>
</html>"""
    return html.encode("utf-8")


class Handler(BaseHTTPRequestHandler):
    def log_message(self, fmt, *args):
        if self.path.startswith("/api/messages") and self.command == "GET":
            return
        BaseHTTPRequestHandler.log_message(self, fmt, *args)

    def do_OPTIONS(self):
        self.send_response(204)
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS")
        self.send_header("Access-Control-Allow-Headers", "Content-Type")
        self.end_headers()

    def do_GET(self):
        path = urlparse(self.path).path
        if path == "/":
            body = _html_page()
            self.send_response(200)
            self.send_header("Content-Type", "text/html; charset=utf-8")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
            return
        if path == "/api/messages":
            with _lock:
                data = list(_messages)
            _json_response(self, 200, data)
            return
        if path == "/api/groups":
            with _lock:
                data = _group_messages(list(_messages))
            _json_response(self, 200, data)
            return
        if path == "/health":
            _json_response(self, 200, {"ok": True})
            return
        _json_response(self, 404, {"error": "not found"})

    def do_POST(self):
        path = urlparse(self.path).path
        if path not in ("/api/messages", "/api/debug/push", "/api/bills/app-upload"):
            _json_response(self, 404, {"error": "not found"})
            return
        length = int(self.headers.get("Content-Length", "0"))
        raw = self.rfile.read(length) if length else b"{}"
        try:
            payload = json.loads(raw.decode("utf-8") or "{}")
        except ValueError:
            _json_response(self, 400, {"error": "invalid json"})
            return

        if path == "/api/bills/app-upload":
            data = payload.get("data") or {}
            normalized = {
                "source": "app-upload",
                "packageName": payload.get("packageName", ""),
                "appName": payload.get("appName", ""),
                "title": data.get("title", ""),
                "content": data.get("context", data.get("content", "")),
                "timestamp": data.get("timestamp"),
                "raw": payload,
            }
        else:
            normalized = payload

        if not normalized.get("group"):
            normalized["group"] = _resolve_group(normalized)

        item = _add_message(normalized)
        _json_response(self, 200, {"ok": True, "id": item.get("id")})

    def do_DELETE(self):
        path = urlparse(self.path).path
        if path != "/api/messages":
            _json_response(self, 404, {"error": "not found"})
            return
        with _lock:
            _messages.clear()
        _json_response(self, 200, {"ok": True})


def main():
    server = ThreadingHTTPServer((HOST, PORT), Handler)
    print("notiMessage debug server: http://127.0.0.1:{0}".format(PORT))
    print("浏览器打开上述地址即可查看消息")
    print("手机经 USB 调试时先执行: adb reverse tcp:8765 tcp:8765")
    print("Wi-Fi 调试时将 AppConfig.DEBUG_SERVER_URL 改为 http://<PC局域网IP>:8765")
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        print("\nstopped")


if __name__ == "__main__":
    main()
