#!/usr/bin/env python3
# 常驻服务：HTTP 文件服务 + 定时抓取（默认每 60s，可用 STOCK_INTERVAL 环境变量覆盖）。
# 手机访问 http://<IP>:8011/latest.html 即为最新盘面；盘中每间隔更新，16:30后转完整版。
# 同时提供 /api/* 自选股接口：
#   GET  /api/watchlist            -> {"codes":[...]}
#   POST /api/watchlist {"code":}  -> 新增（自动识别 sh/sz/bj 前缀）
#   DELETE /api/watchlist?code=    -> 删除
#   GET  /api/quote?codes=a,b      -> 腾讯批量行情
# 跨平台：用运行本脚本的 python 自身（sys.executable），无需写死解释器路径。
import os
import sys
import json
import time
import datetime
import threading
import subprocess
import http.server
import socketserver
import urllib.parse
import urllib.request
import re
import sqlite3
from pathlib import Path

BASE = Path(__file__).parent
PY = os.environ.get("STOCK_PY", sys.executable)
PORT = 8011
INTERVAL = int(os.environ.get("STOCK_INTERVAL", "60"))
WL_PATH = BASE / "watchlist.json"   # 仅用于首次迁移
DB_PATH = BASE / "stock.db"

UA = {"User-Agent": "Mozilla/5.0 (compatible; stocklive/1.0)"}


def decide_mode():
    now = datetime.datetime.now()
    return "close" if (now.hour > 16 or (now.hour == 16 and now.minute >= 30)) else "intraday"


_fetch_lock = threading.Lock()


def run_once():
    if not _fetch_lock.acquire(blocking=False):
        print(f"[{datetime.datetime.now():%H:%M:%S}] 上次抓取仍在进行，跳过本次", flush=True)
        return
    try:
        mode = decide_mode()
        r = subprocess.run(
            [PY, str(BASE / "run_live.py"), mode],
            capture_output=True, text=True, timeout=150,
        )
        print(f"[{datetime.datetime.now():%Y-%m-%d %H:%M:%S}] 抓取完成 mode={mode} rc={r.returncode}", flush=True)
    except Exception as e:
        print(f"[{datetime.datetime.now():%H:%M:%S}] 抓取异常: {e}", flush=True)
    finally:
        _fetch_lock.release()


def scheduler():
    run_once()
    while True:
        time.sleep(INTERVAL)
        run_once()


# ---------------- 自选股持久化（SQLite，按设备令牌隔离）----------------
# 数据模型：
#   devices(uid PK, created_at, last_seen)            —— 用户标识（设备令牌即身份，无密码）
#   watchlists(id, uid FK, code, sort_order, created_at, UNIQUE(uid, code))
#   => 满足「用户标识字段 + 与用户表关联 + 联合唯一索引(user_id,code)」

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


def init_db():
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("""CREATE TABLE IF NOT EXISTS devices(
        uid TEXT PRIMARY KEY, created_at TEXT, last_seen TEXT)""")
    c.execute("""CREATE TABLE IF NOT EXISTS watchlists(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        uid TEXT NOT NULL,
        code TEXT NOT NULL,
        sort_order INTEGER NOT NULL DEFAULT 0,
        created_at TEXT,
        UNIQUE(uid, code),
        FOREIGN KEY(uid) REFERENCES devices(uid))""")
    conn.commit()
    # 兼容迁移：旧的全局 watchlist.json 导入为 __legacy__ 设备（仅留存，不自动分配给新用户）
    legacy = BASE / "watchlist.json"
    if legacy.exists():
        try:
            old = json.loads(legacy.read_text(encoding="utf-8")).get("codes", [])
            if old:
                c.execute("INSERT OR IGNORE INTO devices(uid,created_at,last_seen) VALUES(?,?,?)",
                          ("__legacy__", _now(), _now()))
                for i, code in enumerate(old):
                    c.execute("INSERT OR IGNORE INTO watchlists(uid,code,sort_order,created_at) VALUES(?,?,?,?)",
                              ("__legacy__", code, i, _now()))
                conn.commit()
                print(f"[{_now()}] 已迁移旧 watchlist.json 的 {len(old)} 只到 __legacy__", flush=True)
        except Exception as e:
            print(f"[{_now()}] 迁移 watchlist.json 失败: {e}", flush=True)
        try:
            legacy.rename(legacy.with_suffix(".json.bak"))
        except Exception:
            pass
    conn.close()


_TOKEN_RE = re.compile(r"^[0-9a-f]{32,64}$")


def get_uid(handler):
    """从 Authorization: Bearer <token> 或 X-User-Token 头取出令牌；格式非法/缺失返回 None。"""
    auth = handler.headers.get("Authorization") or handler.headers.get("X-User-Token") or ""
    auth = (auth or "").strip()
    if auth.lower().startswith("bearer "):
        auth = auth[7:].strip()
    return auth if _TOKEN_RE.match(auth) else None


def audit(uid, action, ok, detail=""):
    mask = (uid[:8] + "...") if uid else "-"
    msg = f"[{_now()}] WATCH uid={mask} action={action} ok={ok} {detail}"
    print(msg, flush=True)
    try:
        with open(BASE / "watch_audit.log", "a", encoding="utf-8") as f:
            f.write(msg + "\n")
    except Exception:
        pass


def touch_device(uid):
    try:
        conn = sqlite3.connect(DB_PATH)
        conn.execute("INSERT OR IGNORE INTO devices(uid,created_at,last_seen) VALUES(?,?,?)", (uid, _now(), _now()))
        conn.execute("UPDATE devices SET last_seen=? WHERE uid=?", (_now(), uid))
        conn.commit(); conn.close()
    except Exception:
        pass


def list_watch(uid):
    conn = sqlite3.connect(DB_PATH)
    rows = conn.execute("SELECT code FROM watchlists WHERE uid=? ORDER BY sort_order, id", (uid,)).fetchall()
    conn.close()
    return {"codes": [r[0] for r in rows]}


def add_watch(uid, code):
    conn = sqlite3.connect(DB_PATH)
    n = conn.execute("SELECT COUNT(*) FROM watchlists WHERE uid=?", (uid,)).fetchone()[0]
    try:
        conn.execute("INSERT INTO watchlists(uid,code,sort_order,created_at) VALUES(?,?,?,?)",
                     (uid, code, n, _now()))
        conn.commit(); added = True
    except sqlite3.IntegrityError:
        added = False  # 联合唯一索引命中 => 重复添加，静默去重
    conn.close()
    return added


def del_watch(uid, code):
    """仅删除属于该 uid 的记录（WHERE 已限定归属）。返回是否真的删到。"""
    conn = sqlite3.connect(DB_PATH)
    cur = conn.execute("DELETE FROM watchlists WHERE uid=? AND code=?", (uid, code))
    n = cur.rowcount
    conn.commit(); conn.close()
    return n > 0


def order_watch(uid, codes):
    conn = sqlite3.connect(DB_PATH)
    for i, code in enumerate(codes):
        conn.execute("UPDATE watchlists SET sort_order=? WHERE uid=? AND code=?", (i, uid, code))
    conn.commit(); conn.close()


def normalize_code(s):
    s = (s or "").strip().lower()
    if not s:
        return ""
    if s[:2] in ("sh", "sz", "bj") and s[2:].isdigit() and len(s[2:]) == 6:
        return s
    if s.isdigit() and len(s) == 6:
        a = s[0]
        if a in ("0", "3"):
            return "sz" + s
        if a in ("8", "4"):
            return "bj" + s
        return "sh" + s  # 6/9 开头默认上海
    return ""


def http_get(url, encoding=None, timeout=12):
    req = urllib.request.Request(url, headers=UA)
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        raw = resp.read()
    return raw.decode(encoding) if encoding else raw.decode("utf-8", "replace")


def fetch_quotes(codes_str):
    codes = [normalize_code(c) for c in (codes_str or "").split(",") if normalize_code(c)]
    if not codes:
        return []
    url = "https://qt.gtimg.cn/q=" + ",".join(codes)
    try:
        txt = http_get(url, encoding="gbk")
    except Exception as e:
        return [{"code": c, "name": "", "error": str(e)} for c in codes]
    import re
    out = []
    for m in re.finditer(r'v_(\w+)="([^"]*)"', txt):
        code = m.group(1)
        f = m.group(2).split("~")
        def num(i):
            try:
                return float(f[i])
            except Exception:
                return 0.0
        out.append({
            "code": code,
            "name": f[1] if len(f) > 1 else "",
            "price": num(3),
            "prev_close": num(4),
            "open": num(5),
            "change": num(31),
            "change_pct": num(32),
            "time": f[30] if len(f) > 30 else "",
        })
    # 补齐请求了但没返回的代码
    got = {x["code"] for x in out}
    for c in codes:
        if c not in got:
            out.append({"code": c, "name": "", "price": 0, "change_pct": 0})
    return out


# ---------------- HTTP 处理 ----------------
class NoCacheHandler(http.server.SimpleHTTPRequestHandler):
    def end_headers(self):
        self.send_header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
        self.send_header("Pragma", "no-cache")
        self.send_header("Expires", "0")
        super().end_headers()

    def _send_json(self, obj, status=200):
        body = json.dumps(obj, ensure_ascii=False).encode("utf-8")
        self.send_response(status)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Cache-Control", "no-store")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def _qs(self):
        return urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)

    def do_GET(self):
        p = urllib.parse.urlparse(self.path).path
        if p == "/api/watchlist":
            uid = get_uid(self)
            if not uid:
                self._send_json({"error": "未登录或身份无效", "code": "AUTH_REQUIRED"}, 401)
                audit(None, "list", False, "no_uid"); return
            touch_device(uid)
            self._send_json(list_watch(uid)); return
        if p == "/api/quote":
            self._send_json(fetch_quotes(self._qs().get("codes", [""])[0])); return
        super().do_GET()

    def do_POST(self):
        if urllib.parse.urlparse(self.path).path == "/api/watchlist":
            uid = get_uid(self)
            if not uid:
                self._send_json({"error": "未登录或身份无效", "code": "AUTH_REQUIRED"}, 401)
                audit(None, "add", False, "no_uid"); return
            try:
                length = int(self.headers.get("Content-Length", 0))
                data = json.loads(self.rfile.read(length) or b"{}")
            except Exception:
                data = {}
            code = normalize_code(data.get("code", ""))
            if not code:
                self._send_json({"error": "无效代码，示例 600000 / sh600000", "code": "BAD_CODE"}, 400); return
            touch_device(uid)
            added = add_watch(uid, code)
            audit(uid, "add", added, code)
            self._send_json({"codes": list_watch(uid)["codes"], "added": added}); return
        self._send_json({"error": "not found"}, 404)

    def do_DELETE(self):
        if urllib.parse.urlparse(self.path).path == "/api/watchlist":
            uid = get_uid(self)
            if not uid:
                self._send_json({"error": "未登录或身份无效", "code": "AUTH_REQUIRED"}, 401)
                audit(None, "del", False, "no_uid"); return
            code = normalize_code(self._qs().get("code", [""])[0])
            if not code:  # 兼容前端以 body 传 code 的写法
                try:
                    length = int(self.headers.get("Content-Length", 0))
                    bd = json.loads(self.rfile.read(length) or b"{}")
                    code = normalize_code(bd.get("code", ""))
                except Exception:
                    code = ""
            if not code:
                self._send_json({"error": "无效代码", "code": "BAD_CODE"}, 400); return
            touch_device(uid)
            if not del_watch(uid, code):  # 归属不匹配或未拥有 => 越权/不存在，统一 403 + 日志
                self._send_json({"error": "无权限或记录不存在", "code": "FORBIDDEN"}, 403)
                audit(uid, "del", False, "forbidden:" + code); return
            audit(uid, "del", True, code)
            self._send_json(list_watch(uid)); return
        self._send_json({"error": "not found"}, 404)

    def do_PUT(self):
        if urllib.parse.urlparse(self.path).path == "/api/watchlist/order":
            uid = get_uid(self)
            if not uid:
                self._send_json({"error": "未登录或身份无效", "code": "AUTH_REQUIRED"}, 401); return
            try:
                length = int(self.headers.get("Content-Length", 0))
                data = json.loads(self.rfile.read(length) or b"{}")
            except Exception:
                data = {}
            codes = [normalize_code(c) for c in data.get("codes", []) if normalize_code(c)]
            touch_device(uid)
            order_watch(uid, codes)
            audit(uid, "order", True, str(len(codes)) + " items")
            self._send_json(list_watch(uid)); return
        self._send_json({"error": "not found"}, 404)


if __name__ == "__main__":
    init_db()
    threading.Thread(target=scheduler, daemon=True).start()
    os.chdir(BASE)
    handler = NoCacheHandler
    socketserver.TCPServer.allow_reuse_address = True
    with socketserver.ThreadingTCPServer(("0.0.0.0", PORT), handler) as httpd:
        print(f"Live server on http://0.0.0.0:{PORT}  (Ctrl+C to stop)", flush=True)
        httpd.serve_forever()
