"""
B站直播延迟播放 - 统一服务 (API + HLS + 前端)

启动: python server.py
然后访问: http://localhost:8090

架构: Python Flask → FFmpeg HLS → 浏览器播放
"""

import json
import os
import re
import io
import subprocess
import sys
import time
import threading
import signal
import shutil
from pathlib import Path

# ================================================================
#  路径配置
# ================================================================
BASE_DIR = Path(__file__).parent.parent   # scripts/ → project root
HLS_DIR  = BASE_DIR / "hls"
WEB_DIR  = BASE_DIR / "web"

# ================================================================
#  Flask with static serving built-in
# ================================================================
from flask import Flask, request, jsonify, send_file, Response

app = Flask(__name__)
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 0

# 活跃流管理
active_streams = {}   # { room_id: { process, ... } }
lock = threading.RLock()

# ================================================================
#  获取 B站直播流 URL
# ================================================================
def bilibili_stream_url(room_id):
    """获取 B站直播流 URL - 使用原生 urllib 避免 subprocess 线程问题"""
    import urllib.request
    import json as _json

    def bili_get(url, referer):
        req = urllib.request.Request(url)
        req.add_header("User-Agent",
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
            "AppleWebKit/537.36 (KHTML, like Gecko) "
            "Chrome/125.0.0.0 Safari/537.36")
        req.add_header("Referer", referer)
        with urllib.request.urlopen(req, timeout=10) as resp:
            return _json.loads(resp.read().decode())

    # Step 1: 获取直播间真实 ID
    data = bili_get(
        f"https://api.live.bilibili.com/room/v1/Room/get_info?id={room_id}",
        f"https://live.bilibili.com/{room_id}"
    )
    if data["code"] != 0:
        raise Exception(f"直播间信息获取失败: {data.get('message','未知')}")
    real_rid = data["data"]["room_id"]

    # Step 2: 获取直播流 URLs (多个 CDN 节点)
    qualities = [4, 3, 2]  # 按质量降级尝试
    all_urls = []
    for q in qualities:
        try:
            d2 = bili_get(
                f"https://api.live.bilibili.com/room/v1/Room/playUrl"
                f"?cid={real_rid}&quality={q}&platform=web&qn=10000",
                f"https://live.bilibili.com/{room_id}"
            )
            if d2["code"] == 0:
                for du in d2["data"].get("durl", []):
                    all_urls.append(du["url"])
                if all_urls:
                    break
        except Exception:
            continue

    if not all_urls:
        raise Exception("该直播间未开播或CDN不可达")
    return all_urls, real_rid


# ================================================================
#  获取 咪咕视频直播 m3u8 流地址
# ================================================================
def migu_stream_url(cont_id):
    """获取咪咕视频直播 m3u8 流地址（通过 Node.js WASM 解密）"""
    node_path = "C:/Users/47757/.workbuddy/binaries/node/versions/22.12.0/node.exe"
    script = BASE_DIR / "scripts" / "migu_stream.js"
    result = subprocess.run(
        [node_path, str(script), str(cont_id), "3"],
        capture_output=True, text=True, timeout=30,
        env={**os.environ, "NODE_OPTIONS": ""}  # 清除冲突的 NODE_OPTIONS
    )
    if result.returncode != 0:
        err = result.stderr.strip()
        raise Exception(f"咪咕流获取失败: {err or '未知错误'}")
    m3u8_url = result.stdout.strip()
    if not m3u8_url.startswith("http"):
        raise Exception(f"咪咕返回异常: {m3u8_url[:100]}")
    return m3u8_url, cont_id


def fszb_stream_url(room_id):
    """获取 飞速直播 (fszb30.com) FLV 流地址（通过 Playwright 浏览器提取）"""
    node_path = "C:/Users/47757/.workbuddy/binaries/node/versions/22.12.0/node.exe"
    script = BASE_DIR / "scripts" / "fszb_stream.js"
    result = subprocess.run(
        [node_path, str(script), str(room_id), "1"],
        capture_output=True, text=True, timeout=30,
        env={**os.environ, "NODE_OPTIONS": ""}
    )
    if result.returncode != 0:
        err = result.stderr.strip().split('\n')[-1] if result.stderr else '未知错误'
        raise Exception(f"飞速直播流获取失败: {err}")
    flv_url = result.stdout.strip()
    if not flv_url.startswith("http"):
        raise Exception(f"飞速直播返回异常: {flv_url[:100]}")
    return flv_url, room_id


def detect_source(input_text):
    """
    自动检测输入类型，支持：
      - B站直播链接  → bilibili
      - B站房间号     → bilibili
      - 咪咕直播链接  → migu
      - 咪咕点播链接  → migu
      - 飞速直播链接  → fszb
      - 直接 m3u8/flv → raw
    """
    rid = input_text.strip()

    # 飞速直播 URL: fszb30.com, fszb*.com
    fszb = re.match(r"https?://(?:www\.)?fszb\d*\.\w+/broadcast/details\?room_id=(\d+)", rid)
    if fszb:
        return "fszb", fszb.group(1)

    # 咪咕 URL
    migu = re.match(r"https?://(?:www\.)?miguvideo\.com/p/(?:live|detail)/(\d+)", rid)
    if migu:
        return "migu", migu.group(1)

    # 咪咕纯 contId
    if re.match(r"^\d{10,20}$", rid) and (rid.startswith("120") or rid.startswith("1008") or rid.startswith("700")):
        return "migu", rid

    # B站直播 URL: https://live.bilibili.com/22747736 或 /22747736?...
    bili = re.match(r"https?://(?:www\.)?live\.bilibili\.com/(\d+)", rid)
    if bili:
        return "bilibili", bili.group(1)

    # B站短链: https://b23.tv/xxxxx (暂不解析，提示)
    if re.match(r"https?://b23\.tv/", rid):
        raise Exception("B站短链暂不支持，请使用完整链接 live.bilibili.com/房间号")

    # 直接 m3u8 / flv 链接
    if rid.startswith("http") and (".m3u8" in rid or ".flv" in rid):
        return "raw", rid

    # 默认 B站房间号 (纯数字)
    if rid.isdigit():
        return "bilibili", rid

    # 无法识别
    raise Exception(f"无法识别的直播源: {rid[:60]}，支持 B站链接/房间号、咪咕链接/contId")

# ================================================================
#  启动 FFmpeg HLS 转换
# ================================================================
def start_ffmpeg(room_id, delay_seconds):
    # 用检测后的纯净 source_id 做目录名，避免 URL 含特殊字符
    source, source_id = detect_source(room_id)
    if source == "raw":
        # raw 源用 URL hash 做目录名
        safe_id = str(abs(hash(source_id)))
    else:
        safe_id = source_id  # B站: 房间号, 咪咕: contId

    stream_dir = HLS_DIR / safe_id
    stream_dir.mkdir(parents=True, exist_ok=True)
    for f in stream_dir.iterdir():
        f.unlink()

    print(f"[{safe_id}] 获取直播流 ({source})...")
    stream_urls = []  # 可能多个备用 URL
    if source == "migu":
        stream_urls = [migu_stream_url(source_id)[0]]
    elif source == "fszb":
        stream_urls = [fszb_stream_url(source_id)[0]]
    elif source == "raw":
        stream_urls = [source_id]
    else:
        stream_urls, _ = bilibili_stream_url(source_id)
        print(f"[{safe_id}] 获取到 {len(stream_urls)} 个备用流")

    print(f"[{safe_id}] 流URL: {stream_urls[0][:120]}...")

    seg_time     = 2
    # 固定大缓冲区：900 分片 = 30 分钟，支持任意延迟回退
    # 实际延迟由前端 hls.js startPosition 控制
    list_size    = 900
    m3u8_path    = stream_dir / "live.m3u8"
    seg_pattern  = stream_dir / "seg_%08d.ts"

    proc = None
    used_url = stream_urls[0] if stream_urls else ""
    for idx, stream_url in enumerate(stream_urls):
        if idx > 0:
            print(f"[{safe_id}] 尝试备用URL {idx+1}/{len(stream_urls)}...")

        if source in ("migu", "raw"):
            # m3u8 源，无需额外 headers
            args = [
                "ffmpeg",
                "-loglevel", "warning",
                "-reconnect", "1",
                "-reconnect_streamed", "1",
                "-reconnect_delay_max", "5",
                "-user_agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
                "-i", stream_url,
                "-c", "copy",
                "-f", "hls",
                "-hls_time", str(seg_time),
                "-hls_list_size", str(list_size),
                "-hls_flags", "delete_segments+append_list+omit_endlist",
                "-hls_segment_filename", str(seg_pattern),
                str(m3u8_path)
            ]
        elif source == "fszb":
            args = [
                "ffmpeg",
                "-loglevel", "warning",
                "-reconnect", "1",
                "-reconnect_streamed", "1",
                "-reconnect_delay_max", "5",
                "-user_agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
                "-headers", "Referer: https://www.fszb30.com/\r\n",
                "-i", stream_url,
                "-c", "copy",
                "-f", "hls",
                "-hls_time", str(seg_time),
                "-hls_list_size", str(list_size),
                "-hls_flags", "delete_segments+append_list+omit_endlist",
                "-hls_segment_filename", str(seg_pattern),
                str(m3u8_path)
            ]
        else:
            # B站：FLV 源，需要 Referer
            args = [
                "ffmpeg",
                "-loglevel", "warning",
                "-reconnect", "1",
                "-reconnect_streamed", "1",
                "-reconnect_delay_max", "5",
                "-headers", f"Referer: https://live.bilibili.com/\r\nUser-Agent: Mozilla/5.0",
                "-i", stream_url,
                "-c", "copy",
                "-f", "hls",
                "-hls_time", str(seg_time),
                "-hls_list_size", str(list_size),
                "-hls_flags", "delete_segments+append_list+omit_endlist",
                "-hls_segment_filename", str(seg_pattern),
                str(m3u8_path)
            ]

        proc = subprocess.Popen(
            args,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            creationflags=subprocess.CREATE_NO_WINDOW
        )
        # 等 3 秒判断是否正常启动
        time.sleep(3)
        if proc.poll() is None:
            # 进程还在运行，URL 可用
            used_url = stream_url
            print(f"[{safe_id}] FFmpeg 启动成功 (URL {idx+1})")
            break
        else:
            print(f"[{safe_id}] 该 URL 启动失败，尝试下一个...")

    if proc is None or proc.poll() is not None:
        raise Exception("所有流 URL 均无法连接，请稍后重试")

    with lock:
        active_streams[safe_id] = {
            "process": proc,
            "delay_seconds": delay_seconds,
            "source": source,
            "source_id": source_id,
            "started_at": time.time(),
            "stream_url": used_url
        }
    return {
        "room_id": room_id,
        "safe_id": safe_id,
        "source": source,
        "source_id": source_id,
        "delay_seconds": delay_seconds,
        "hls_url": f"/hls/{safe_id}/delayed_{delay_seconds}.m3u8",
        "started_at": time.time()
    }

def stop_ffmpeg(room_id):
    with lock:
        info = active_streams.pop(room_id, None)
    if info is None:
        return {"error": "not_found"}
    proc = info["process"]
    try:
        proc.terminate()
        proc.wait(timeout=5)
    except Exception:
        proc.kill()
    # 不删 HLS 文件，下次启动可复用（ffmpeg delete_segments 自清理）
    print(f"[{room_id}] 已停止")
    return {"status": "stopped", "room_id": room_id}

# ================================================================
#  Flask 路由
# ================================================================
@app.route("/")
def index():
    return send_file(WEB_DIR / "index.html")

@app.route("/api/health")
def health():
    return jsonify({"status": "ok"})

@app.route("/api/start", methods=["POST"])
def api_start():
    data = request.get_json(force=True)
    rid = str(data.get("room_id", "")).strip()
    delay = int(data.get("delay_seconds", 60))
    if not rid:
        return jsonify({"error": "缺少 room_id"}), 400
    if delay < 6:
        return jsonify({"error": "延迟最短 6 秒"}), 400
    if delay > 3600:
        return jsonify({"error": "延迟最长 3600 秒"}), 400

    with lock:
        # 清理同 source_id 的旧流
        _, sid = detect_source(rid)
        if sid in active_streams:
            stop_ffmpeg(sid)

    try:
        result = start_ffmpeg(rid, delay)
        return jsonify({"status": "running", **result})
    except Exception as e:
        import traceback
        traceback.print_exc()
        return jsonify({"error": str(e)}), 500

@app.route("/api/stop", methods=["POST"])
def api_stop():
    data = request.get_json(force=True)
    rid = str(data.get("room_id", "")).strip()
    _, sid = detect_source(rid)
    return jsonify(stop_ffmpeg(sid))

@app.route("/api/status")
def api_status():
    with lock:
        streams = {}
        for sid, info in active_streams.items():
            alive = info["process"].poll() is None
            streams[sid] = {
                "alive": alive,
                "delay_seconds": info["delay_seconds"],
                "source": info.get("source", "bilibili"),
                "source_id": info.get("source_id", sid),
                "started_at": info["started_at"],
                "uptime": time.time() - info["started_at"] if alive else 0,
                "hls_url": f"/hls/{sid}/live.m3u8"
            }
    return jsonify({"streams": streams})

# ================================================================
#  HLS 文件服务 (.m3u8 / .ts)  - 直接映射 /hls/ 到磁盘 hls/
# ================================================================
@app.route("/hls/<path:filepath>")
def serve_hls(filepath):
    full = HLS_DIR / filepath
    if not full.exists():
        # 检查是否是延迟 m3u8 请求
        m = re.match(r"(\d+)/delayed_(\d+)\.m3u8", filepath)
        if m:
            return serve_delayed_m3u8(m.group(1), int(m.group(2)))
        return "not found", 404
    resp = send_file(str(full), conditional=True)
    if filepath.endswith(".m3u8"):
        resp.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
        resp.headers["Pragma"] = "no-cache"
        resp.headers["Expires"] = "0"
    elif filepath.endswith(".ts"):
        resp.headers["Cache-Control"] = "public, max-age=5"
    resp.headers["Access-Control-Allow-Origin"] = "*"
    return resp


def serve_delayed_m3u8(room_id, delay_seconds):
    """生成延迟 m3u8：从 delay 秒前的分片开始"""
    stream_dir = HLS_DIR / str(room_id)
    live_m3u8 = stream_dir / "live.m3u8"
    if not live_m3u8.exists():
        return "stream not ready", 503

    # 读取当前 live.m3u8 获取所有分片
    lines = live_m3u8.read_text(encoding="utf-8", errors="ignore").splitlines()
    seg_time = 2  # 每分片秒数
    segments = []
    for line in lines:
        stripped = line.strip()
        if stripped and not stripped.startswith("#"):
            segments.append(stripped)
    if not segments:
        return "no segments", 503

    target_count = max(1, int(delay_seconds / seg_time))
    total_segments = len(segments)

    if total_segments <= target_count:
        # 缓冲区不够，返回所有分片（最小延迟）
        start_idx = 0
    else:
        # 从 delay 秒前的分片开始
        start_idx = total_segments - target_count - 3  # 留3个分片缓冲

    if start_idx < 0:
        start_idx = 0

    # 构建延迟 m3u8
    buf = io.StringIO()
    buf.write("#EXTM3U\n")
    buf.write("#EXT-X-VERSION:3\n")
    buf.write("#EXT-X-TARGETDURATION:2\n")
    buf.write(f"#EXT-X-MEDIA-SEQUENCE:{start_idx}\n")
    buf.write(f"#EXT-X-DISCONTINUITY\n")

    for seg in segments[start_idx:]:
        buf.write(f"#EXTINF:{seg_time}.000000,\n")
        buf.write(f"{seg}\n")

    result = buf.getvalue()
    resp = Response(result, mimetype="application/vnd.apple.mpegurl")
    resp.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
    resp.headers["Access-Control-Allow-Origin"] = "*"
    return resp

# ================================================================
#  优雅退出
# ================================================================
def cleanup():
    print("\n停止所有流...")
    with lock:
        for rid in list(active_streams.keys()):
            stop_ffmpeg(rid)

signal.signal(signal.SIGINT,  lambda s,f: (cleanup(), sys.exit(0)))
signal.signal(signal.SIGTERM, lambda s,f: (cleanup(), sys.exit(0)))

if __name__ == "__main__":
    print("=" * 55)
    print("  📡  B站直播延迟播放服务")
    print("  浏览器打开 →  http://localhost:8090")
    print("=" * 55)
    app.run(host="0.0.0.0", port=8090, debug=False, threaded=True)
