from __future__ import annotations

import json
import time
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from zoneinfo import ZoneInfo

from aiohttp import web


SHANGHAI_TZ = ZoneInfo("Asia/Shanghai")


SIGNAL_PAGE = """<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
  <title>LoL Signal</title>
  <style>
    :root {
      color-scheme: dark;
      --bg: #0f1419;
      --panel: #171d24;
      --line: #28313b;
      --text: #f2f5f8;
      --muted: #98a6b3;
      --green: #2fb66d;
      --red: #e24b50;
      --blue: #4d83ff;
      --amber: #d99b2b;
    }
    * { box-sizing: border-box; }
    body {
      margin: 0;
      background: var(--bg);
      color: var(--text);
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      font-size: 14px;
    }
    main {
      max-width: 760px;
      margin: 0 auto;
      padding: 12px;
      padding-bottom: calc(18px + env(safe-area-inset-bottom));
    }
    .top {
      display: grid;
      grid-template-columns: minmax(0, 1fr) 108px;
      gap: 8px;
      align-items: end;
      margin-bottom: 10px;
    }
    label {
      display: block;
      color: var(--muted);
      font-size: 12px;
      margin-bottom: 4px;
    }
    input, select {
      width: 100%;
      height: 38px;
      border: 1px solid var(--line);
      border-radius: 6px;
      background: #10161d;
      color: var(--text);
      padding: 0 10px;
      font-size: 14px;
      outline: none;
    }
    .panel {
      border: 1px solid var(--line);
      border-radius: 8px;
      background: var(--panel);
      padding: 10px;
      margin-bottom: 10px;
    }
    .teams {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 8px;
    }
    .team-title {
      font-weight: 700;
      margin: 0 0 8px;
      font-size: 15px;
    }
    .team-token {
      margin: -4px 0 8px;
      color: var(--muted);
      font-size: 10px;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    }
    .grid {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 8px;
    }
    button {
      width: 100%;
      min-height: 58px;
      border: 0;
      border-radius: 8px;
      color: white;
      font-weight: 800;
      font-size: 15px;
      touch-action: manipulation;
      user-select: none;
    }
    button:active { transform: translateY(1px); filter: brightness(1.08); }
    .a button { background: var(--blue); }
    .b button { background: var(--red); }
    .neutral { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; }
    .neutral button { min-height: 48px; background: #3c4652; }
    .neutral button.warn { background: var(--amber); }
    .status {
      min-height: 72px;
      white-space: pre-wrap;
      line-height: 1.45;
      color: var(--muted);
      font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
      font-size: 12px;
    }
    .ok { color: #68d391; }
    .bad { color: #ff7a7a; }
    @media (max-width: 520px) {
      .top { grid-template-columns: 1fr; }
      main { padding: 8px; }
      button { min-height: 64px; font-size: 14px; }
      .grid { grid-template-columns: 1fr; }
    }
  </style>
</head>
<body>
  <main>
    <div class="top">
      <div>
        <label>Event</label>
        <input id="eventSlug" placeholder="lol-wb-we-2026-05-07" readonly />
      </div>
      <div>
        <label>Market</label>
        <select id="market"></select>
      </div>
    </div>
    <input id="token" type="hidden" />

    <div class="teams">
      <section class="panel a">
        <p class="team-title" id="titleA">A</p>
        <p class="team-token" id="tokenA"></p>
        <div class="grid">
          <button data-team="A" data-signal="first_blood">一血</button>
          <button data-team="A" data-signal="dragon">小龙</button>
          <button data-team="A" data-signal="baron">大龙</button>
          <button data-team="A" data-signal="teamfight_win">团战胜利</button>
          <button data-team="A" data-signal="tower">推塔</button>
          <button data-team="A" data-signal="manual">手动利好</button>
        </div>
      </section>
      <section class="panel b">
        <p class="team-title" id="titleB">B</p>
        <p class="team-token" id="tokenB"></p>
        <div class="grid">
          <button data-team="B" data-signal="first_blood">一血</button>
          <button data-team="B" data-signal="dragon">小龙</button>
          <button data-team="B" data-signal="baron">大龙</button>
          <button data-team="B" data-signal="teamfight_win">团战胜利</button>
          <button data-team="B" data-signal="tower">推塔</button>
          <button data-team="B" data-signal="manual">手动利好</button>
        </div>
      </section>
    </div>

    <section class="panel neutral">
      <button data-team="" data-signal="pause">暂停</button>
      <button data-team="" data-signal="resume">恢复</button>
      <button class="warn" data-team="" data-signal="cancel">取消/撤单</button>
    </section>

    <section class="panel">
      <div id="status" class="status">ready</div>
    </section>
  </main>

  <script>
    const CONFIG = __CONFIG__;
    const els = {
      eventSlug: document.getElementById("eventSlug"),
      market: document.getElementById("market"),
      teamA: document.getElementById("teamA"),
      teamB: document.getElementById("teamB"),
      token: document.getElementById("token"),
      titleA: document.getElementById("titleA"),
      titleB: document.getElementById("titleB"),
      tokenA: document.getElementById("tokenA"),
      tokenB: document.getElementById("tokenB"),
      status: document.getElementById("status"),
    };

    function load() {
      const marketKeys = Object.keys(CONFIG.markets || {});
      els.market.innerHTML = marketKeys.map((key) => `<option value="${key}">${key}</option>`).join("");
      if (CONFIG.event_slug) els.eventSlug.value = CONFIG.event_slug;
      if (CONFIG.default_market && marketKeys.includes(CONFIG.default_market)) els.market.value = CONFIG.default_market;
      else if (marketKeys.length) els.market.value = marketKeys[0];
      if (CONFIG.token) els.token.value = CONFIG.token;
      for (const key of ["market"]) {
        const value = localStorage.getItem("lol_signal_" + key);
        if (value) els[key].value = value;
      }
      updateTitles();
    }

    function save() {
      for (const key of ["market"]) {
        localStorage.setItem("lol_signal_" + key, els[key].value);
      }
    }

    function selectedMarketConfig() {
      return (CONFIG.markets && CONFIG.markets[els.market.value]) || CONFIG.markets.moneyline || {};
    }

    function shortToken(token) {
      if (!token) return "-";
      return token.length > 14 ? token.slice(0, 6) + "..." + token.slice(-6) : token;
    }

    function updateTitles() {
      const cfg = selectedMarketConfig();
      const a = (cfg.teams && cfg.teams[0]) || {};
      const b = (cfg.teams && cfg.teams[1]) || {};
      els.titleA.textContent = a.name || "A";
      els.titleB.textContent = b.name || "B";
      els.tokenA.textContent = shortToken(a.token_id || "");
      els.tokenB.textContent = shortToken(b.token_id || "");
    }

    function nowIso() {
      return new Date().toISOString();
    }

    async function sendSignal(teamKey, signal) {
      save();
      const clickAt = performance.now();
      const cfg = selectedMarketConfig();
      const a = (cfg.teams && cfg.teams[0]) || {};
      const b = (cfg.teams && cfg.teams[1]) || {};
      const teamName = teamKey === "A" ? a.name : teamKey === "B" ? b.name : "";
      const teamToken = teamKey === "A" ? a.token_id : teamKey === "B" ? b.token_id : "";
      const body = {
        event_slug: els.eventSlug.value.trim(),
        market: els.market.value,
        team_key: teamKey,
        team: teamName,
        team_token_id: teamToken || "",
        signal,
        client_ts: nowIso(),
        nonce: crypto.randomUUID ? crypto.randomUUID() : String(Date.now()) + Math.random(),
      };
      if (!body.event_slug) {
        els.status.innerHTML = '<span class="bad">missing event slug</span>';
        return;
      }
      els.status.textContent = "sending " + signal + " " + (teamName || teamKey);
      try {
        const resp = await fetch("/api/signal", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "X-Signal-Token": els.token.value.trim(),
          },
          body: JSON.stringify(body),
        });
        const data = await resp.json();
        const rtt = performance.now() - clickAt;
        if (!resp.ok || !data.ok) {
          throw new Error(data.error || resp.statusText);
        }
        els.status.innerHTML = '<span class="ok">sent</span>\\n'
          + "signal: " + signal + "\\n"
          + "team: " + (teamName || "-") + "\\n"
          + "server: " + data.received_at_wall + "\\n"
          + "rtt_ms: " + Math.round(rtt) + "\\n"
          + "seq: " + data.sequence;
      } catch (err) {
        els.status.innerHTML = '<span class="bad">failed</span>\\n' + String(err.message || err);
      }
    }

    document.querySelectorAll("button[data-signal]").forEach((button) => {
      button.addEventListener("click", () => sendSignal(button.dataset.team, button.dataset.signal));
    });
    ["market"].forEach((key) => {
      els[key].addEventListener("change", save);
      els[key].addEventListener("input", () => { save(); updateTitles(); });
    });
    load();
  </script>
</body>
</html>
"""


class SignalStore:
    def __init__(self, output_root: Path) -> None:
        self.output_root = output_root
        self.sequence = 0

    def write(self, event_slug: str, row: dict[str, Any]) -> None:
        safe_slug = "".join(ch for ch in event_slug if ch.isalnum() or ch in {"-", "_"})
        if not safe_slug:
            safe_slug = "unknown"
        path = self.output_root / safe_slug / "signals.jsonl"
        path.parent.mkdir(parents=True, exist_ok=True)
        with path.open("a", encoding="utf-8") as f:
            f.write(json.dumps(row, ensure_ascii=False, sort_keys=True))
            f.write("\n")
            f.flush()


def _utc_now() -> datetime:
    return datetime.now(timezone.utc)


def _json_error(message: str, status: int = 400) -> web.Response:
    return web.json_response({"ok": False, "error": message}, status=status)


async def index(_request: web.Request) -> web.Response:
    config = _request.app["page_config"]
    page = SIGNAL_PAGE.replace(
        "__CONFIG__",
        json.dumps(config, ensure_ascii=False, separators=(",", ":")),
    )
    return web.Response(text=page, content_type="text/html")


async def health(request: web.Request) -> web.Response:
    return web.json_response(
        {
            "ok": True,
            "server_time": _utc_now().isoformat(),
            "output_root": str(request.app["store"].output_root),
            "page_config": {
                key: value
                for key, value in request.app["page_config"].items()
                if key != "token"
            },
        }
    )


async def signal(request: web.Request) -> web.Response:
    token = request.app["token"]
    if token:
        provided = request.headers.get("X-Signal-Token", "")
        if provided != token:
            return _json_error("unauthorized", status=401)
    try:
        payload = await request.json()
    except json.JSONDecodeError:
        return _json_error("invalid json")
    if not isinstance(payload, dict):
        return _json_error("payload must be object")

    event_slug = str(payload.get("event_slug") or "").strip()
    signal_name = str(payload.get("signal") or "").strip()
    if not event_slug:
        return _json_error("event_slug is required")
    if not signal_name:
        return _json_error("signal is required")

    store: SignalStore = request.app["store"]
    store.sequence += 1
    received_at = _utc_now()
    source = request.headers.get("X-Forwarded-For") or request.remote or ""
    row = {
        "sequence": store.sequence,
        "event_slug": event_slug,
        "market": str(payload.get("market") or ""),
        "team_key": str(payload.get("team_key") or ""),
        "team": str(payload.get("team") or ""),
        "team_token_id": str(payload.get("team_token_id") or ""),
        "signal": signal_name,
        "client_ts": str(payload.get("client_ts") or ""),
        "received_at_wall": received_at.isoformat(),
        "received_at_wall_cn": received_at.astimezone(SHANGHAI_TZ).isoformat(),
        "received_at_monotonic_ns": time.monotonic_ns(),
        "nonce": str(payload.get("nonce") or uuid.uuid4()),
        "source": source,
        "user_agent": request.headers.get("User-Agent", ""),
        "raw_json": payload,
    }
    store.write(event_slug, row)
    return web.json_response(
        {
            "ok": True,
            "sequence": store.sequence,
            "received_at_wall": row["received_at_wall"],
            "received_at_wall_cn": row["received_at_wall_cn"],
        }
    )


def make_app(
    *,
    output_root: Path,
    token: str = "",
    event_slug: str = "",
    team_a: str = "A",
    team_b: str = "B",
    team_a_token_id: str = "",
    team_b_token_id: str = "",
    markets: dict[str, Any] | None = None,
    default_market: str = "game1",
    lock_config: bool = True,
    expose_token: bool = True,
) -> web.Application:
    app = web.Application(client_max_size=1024 * 1024)
    app["store"] = SignalStore(output_root)
    app["token"] = token
    app["page_config"] = {
        "event_slug": event_slug,
        "team_a": team_a,
        "team_b": team_b,
        "team_a_token_id": team_a_token_id,
        "team_b_token_id": team_b_token_id,
        "markets": markets
        or {
            "moneyline": {
                "slug": event_slug,
                "teams": [
                    {"name": team_a, "token_id": team_a_token_id},
                    {"name": team_b, "token_id": team_b_token_id},
                ],
            }
        },
        "default_market": default_market,
        "token": token if expose_token else "",
        "lock_config": lock_config,
        "hide_token": bool(token and expose_token),
    }
    app.router.add_get("/", index)
    app.router.add_get("/health", health)
    app.router.add_post("/api/signal", signal)
    return app
