#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Glass curtain wall adhesive detection - backend
- Start/stop capture via POST /start_capture and /stop_capture
- Push parsed samples to WebSocket /ws
- Push raw log lines to WebSocket /log_ws
- Camera MJPEG stream at /video_feed
"""
import os
import time
import json
import threading
import asyncio
import subprocess
from datetime import datetime
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request
from fastapi.responses import StreamingResponse, FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
import cv2

# ---------------- CONFIG ----------------
CSV_DIR = "/home/pi/acc_data"
CAPTURE_CWD = "/home/pi/acc_module/AI0"
CAPTURE_CMD = ["sudo", "./main"]
CAPTURE_LOG = "/home/pi/capture.log"
CAMERA_INDEX = 0

SAMPLE_QUEUE_MAX = 8000
LOG_QUEUE_MAX = 2000
# ----------------------------------------

app = FastAPI(title="Glass Curtain Wall Adhesive Detection")
app.mount("/static", StaticFiles(directory="static"), name="static")


@app.get("/")
def index():
    return FileResponse(os.path.join("static", "index.html"))


# ---------------- Queues ----------------
sample_queue: asyncio.Queue = asyncio.Queue(maxsize=SAMPLE_QUEUE_MAX)
log_queue: asyncio.Queue = asyncio.Queue(maxsize=LOG_QUEUE_MAX)


def _put_to_queue(q: asyncio.Queue, item: any):
    """
    一个在 event loop 中运行的辅助函数，
    安全地将项目放入队列，如果队列满了，则移除最旧的项目。
    """
    try:
        q.put_nowait(item)
    except asyncio.QueueFull:
        try:
            q.get_nowait()
        except asyncio.QueueEmpty:
            pass
        try:
            q.put_nowait(item)
        except asyncio.QueueFull:
            pass


# ---- helper: parse a capture stdout line into sample dict ----
def try_parse_sample_from_line(line: str):
    s = line.strip()
    if not s:
        return None
    parts = [p.strip() for p in s.replace(';', ',').split(',') if p.strip() != '']
    if len(parts) < 4:
        parts = [p for p in s.split() if p != '']
    if len(parts) < 4:
        return None
    try:
        t = float(parts[0])
        volt = float(parts[1])
        accel_g = float(parts[2])
        accel_mps2 = float(parts[3])
        return {"t": t, "volt": volt, "accel_g": accel_g, "accel_mps2": accel_mps2}
    except Exception:
        return None


# ---- capture reader thread (已修复 "tail" 逻辑) ----
def capture_reader(proc: subprocess.Popen, loop: asyncio.AbstractEventLoop, stop_event: threading.Event):
    real_csv_path = None
    logf = None

    # --- 阶段 1: 从 stdout 读取状态和 CSV 路径 ---
    try:
        logf = open(CAPTURE_LOG, "a", encoding="utf-8", errors="ignore")
        stdout = proc.stdout
        print("[capture_reader] Reading stdout for CSV path...")

        for raw in iter(stdout.readline, b''):
            if stop_event.is_set():
                print("[capture_reader] Stopped during stdout read.")
                return
            try:
                line = raw.decode("utf-8", errors="ignore").rstrip("\r\n")
            except Exception:
                line = raw.decode("latin1", errors="ignore").rstrip("\r\n")

            if logf:
                try:
                    logf.write(line + "\n")
                    logf.flush()
                except Exception:
                    pass

            loop.call_soon_threadsafe(_put_to_queue, log_queue, line)

            if line.startswith("Output file:"):
                real_csv_path = line.split(":", 1)[1].strip()
                print(f"[capture_reader] Found output file: {real_csv_path}")
                break

            if proc.poll() is not None:
                print("[capture_reader] Process exited before outputting CSV path.")
                return

    except Exception as e:
        print(f"[capture_reader] Exception reading stdout: {e}")
        return
    finally:
        if logf:
            try:
                logf.close()
            except:
                pass

    if not real_csv_path:
        print("[capture_reader] Did not find CSV path, stopping thread.")
        return

    app.state.capture_csv_path = real_csv_path

    # --- 阶段 2: 实时 "tail" CSV 文件 ---
    csvf = None
    try:
        timeout_at = time.time() + 10.0
        while not os.path.exists(real_csv_path) and time.time() < timeout_at:
            if stop_event.is_set(): return
            time.sleep(0.05)

        if not os.path.exists(real_csv_path):
            print(f"[capture_reader] CSV file never appeared: {real_csv_path}")
            return

        # ++++++++++++++++++++++++ 终极修复 ++++++++++++++++++++++++
        #
        # 强制使用 `buffering=1` (行缓冲)
        # 这会关闭 Python 自己的大型内部缓冲区，
        # 迫使它更频繁地从操作系统获取新数据。
        #
        csvf = open(real_csv_path, "r", encoding="utf-8", errors="ignore", buffering=1)
        #
        # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++

        # 跳过表头 (t,volt,...)
        header_read = False
        timeout_at = time.time() + 5.0
        while not header_read and time.time() < timeout_at:
            if stop_event.is_set(): return
            line = csvf.readline()
            if line:  # 读到了表头
                header_read = True
            else:  # 文件刚创建，表头还没 flush
                csvf.seek(csvf.tell())  # 清除 EOF 缓存
                time.sleep(0.01)

        if not header_read:
            print(f"[capture_reader] CSV file header never appeared: {real_csv_path}")
            return

        print(f"[capture_reader] Tailing CSV file: {real_csv_path}")

        while not stop_event.is_set():
            if proc.poll() is not None:
                break

            line = csvf.readline()
            if not line:
                # 读到了文件末尾, 清除 EOF 缓存并等待新数据
                csvf.seek(csvf.tell())
                time.sleep(0.01)  # 10ms 轮询, 匹配 C++ 的刷新率
                continue

            # 找到了新的一行
            sample = try_parse_sample_from_line(line)
            if sample:
                loop.call_soon_threadsafe(_put_to_queue, sample_queue, sample)

    except Exception as e:
        print(f"[capture_reader] Tailing error: {e}")
    finally:
        if csvf:
            try:
                csvf.close()
            except:
                pass
        print(f"[capture_reader] stopped, csv: {real_csv_path}")


# ---- capture control (无变化) ----
def _is_capture_running():
    p = getattr(app.state, "capture_proc", None)
    return (p is not None) and (p.poll() is None)


@app.post("/start_capture")
async def start_capture(req: Request):
    if _is_capture_running():
        return JSONResponse({"status": "already_running", "pid": app.state.capture_proc.pid}, status_code=200)

    if not os.path.isdir(CAPTURE_CWD):
        return JSONResponse({"status": "failed", "reason": f"CAPTURE_CWD not found: {CAPTURE_CWD}"}, status_code=500)

    os.makedirs(CSV_DIR, exist_ok=True)

    try:
        proc = subprocess.Popen(CAPTURE_CMD, cwd=CAPTURE_CWD,
                                stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE)
    except Exception as e:
        return JSONResponse({"status": "failed", "reason": f"start process failed: {e}"}, status_code=500)

    app.state.capture_proc = proc
    app.state.capture_stop_event = threading.Event()
    loop = asyncio.get_running_loop()

    t = threading.Thread(target=capture_reader, args=(proc, loop, app.state.capture_stop_event), daemon=True)

    app.state.capture_thread = t
    app.state.capture_csv_path = None
    t.start()

    print("[start_capture] started pid:", proc.pid)
    return JSONResponse({"status": "started", "pid": proc.pid, "csv": "pending..."}, status_code=200)


@app.post("/stop_capture")
async def stop_capture():
    proc = getattr(app.state, "capture_proc", None)
    if not proc:
        return JSONResponse({"status": "not_running"}, status_code=200)

    if proc.poll() is None:
        try:
            if proc.stdin:
                proc.stdin.write(b"\n")
                proc.stdin.flush()
                proc.wait(timeout=2.0)
        except Exception:
            pass

    if proc.poll() is None:
        try:
            proc.terminate()
            try:
                proc.wait(timeout=3.0)
            except subprocess.TimeoutExpired:
                proc.kill()
        except Exception as e:
            print("[stop_capture] terminate/kill error:", e)

    stop_event = getattr(app.state, "capture_stop_event", None)
    if stop_event:
        stop_event.set()
    thread = getattr(app.state, "capture_thread", None)
    if thread and thread.is_alive():
        thread.join(timeout=1.0)

    csv_path = getattr(app.state, "capture_csv_path", None)
    app.state.capture_proc = None
    app.state.capture_thread = None
    app.state.capture_stop_event = None

    print("[stop_capture] stopped, csv:", csv_path)
    return JSONResponse({"status": "stopped", "csv": csv_path}, status_code=200)


# ---- WebSocket: parsed samples (无变化) ----
@app.websocket("/ws")
async def websocket_samples(websocket: WebSocket):
    await websocket.accept()
    print("[ws] client connected:", websocket.client)
    try:
        while True:
            try:
                sample = await asyncio.wait_for(sample_queue.get(), timeout=0.05)
                await websocket.send_text(json.dumps(sample, ensure_ascii=False))
            except asyncio.TimeoutError:
                continue
    except WebSocketDisconnect:
        print("[ws] disconnected:", websocket.client)


# ---- WebSocket: raw logs (无变化) ----
@app.websocket("/log_ws")
async def log_ws(websocket: WebSocket):
    await websocket.accept()
    print("[log_ws] client connected:", websocket.client)
    try:
        while True:
            try:
                line = await asyncio.wait_for(log_queue.get(), timeout=0.05)
                await websocket.send_text(line)
            except asyncio.TimeoutError:
                continue
    except WebSocketDisconnect:
        print("[log_ws] disconnected:", websocket.client)


# ---- camera feed (无变化) ----
def gen_frames():
    cap = cv2.VideoCapture(CAMERA_INDEX)
    if not cap.isOpened():
        print("[camera] cannot open index", CAMERA_INDEX)
        err_img = cv2.imencode('.jpg', cv2.UMat(cv2.Mat(240, 320, cv2.CV_8UC3, (0, 0, 50))))[1].tobytes()
        while True:
            yield (b'--frame\r\n'
                   b'Content-Type: image/jpeg\r\n\r\n' + err_img + b'\r\n')
            time.sleep(1.0)

    while True:
        success, frame = cap.read()
        if not success:
            time.sleep(0.05)
            cap.release()
            cap = cv2.VideoCapture(CAMERA_INDEX)
            if not cap.isOpened():
                print("[camera] reconnect failed, sleeping")
                time.sleep(1.0)
            continue

        ret, buf = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 80])
        if not ret:
            continue

        frame_bytes = buf.tobytes()
        yield (b'--frame\r\n'
               b'Content-Type: image/jpeg\r\n\r\n' + frame_bytes + b'\r\n')
        time.sleep(0.03)

    cap.release()


@app.get("/video_feed")
def video_feed():
    return StreamingResponse(gen_frames(), media_type='multipart/x-mixed-replace; boundary=frame')


@app.on_event("startup")
async def on_startup():
    if not hasattr(app.state, "capture_proc"):
        app.state.capture_proc = None
    print("backend started")