"""Stream a constant-frame-rate recording to QDivZero and save crossing metrics.

Install websocket-client and opencv-python. Keep footfall_counter.py alongside.
YOLO and ByteTrack run in QDivZero; OpenCV only decodes and encodes video.
"""

import argparse
import json
import math
import os
from datetime import datetime, timedelta, timezone
from pathlib import Path
from urllib.parse import urlencode
from uuid import uuid4

from footfall_counter import LineCounter, forecast_request, hourly_history


def receive(ws, expected):
    event = json.loads(ws.recv())
    if event.get("type") == "error":
        raise RuntimeError(event["error"].get("message", "QDivZero stream error"))
    if event.get("type") != expected:
        raise RuntimeError(f"Expected {expected}, received {event.get('type')}")
    return event


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("video", type=Path)
    parser.add_argument("--recorded-at", required=True, help="First frame time, including UTC offset")
    parser.add_argument("--line-x", type=float, default=0.5)
    parser.add_argument("--y-min", type=float, default=0)
    parser.add_argument("--y-max", type=float, default=1)
    parser.add_argument("--margin", type=float, default=0.02)
    parser.add_argument("--output", type=Path, default=Path("camera-output"))
    args = parser.parse_args()
    start = datetime.fromisoformat(args.recorded_at)
    if start.utcoffset() is None or not args.video.is_file():
        parser.error("Use an existing local recording and a timestamp with UTC offset")
    start = start.astimezone(timezone.utc)
    counter = LineCounter(line_x=args.line_x, margin=args.margin, y_min=args.y_min, y_max=args.y_max)
    api_key, account = os.environ["QDIV0_API_KEY"], os.environ["QDIV0_ACCOUNT_ID"]
    model = os.environ.get("QDIV0_DETECTION_MODEL", "guia-restaurante-yolo26n")
    url = "wss://api.qdiv0.com/v1/images/detections/stream?" + urlencode({"model": model})

    import cv2
    import websocket

    capture = cv2.VideoCapture(str(args.video))
    ws = None
    events, frame_index = [], 0
    session = str(uuid4())
    try:
        fps = capture.get(cv2.CAP_PROP_FPS)
        total_frames = round(capture.get(cv2.CAP_PROP_FRAME_COUNT))
        if not capture.isOpened() or not math.isfinite(fps) or fps < 1 or total_frames < 1:
            raise ValueError("Use a seekable constant-frame-rate video with valid FPS and frame count")
        args.output.mkdir(parents=True, exist_ok=False)
        ws = websocket.create_connection(url, timeout=70, header=[
            f"Authorization: Bearer {api_key}", f"X-Account-Id: {account}",
        ])
        ready = receive(ws, "session.ready")
        ws.send(json.dumps({"type": "session.configure", "options": {
            "tracking": True, "classes": [0], "confidence": 0.25,
            "iou": 0.7, "image_size": 640, "max_detections": 300,
        }}))
        configured = receive(ws, "session.configured")
        if configured.get("options", {}).get("tracking") is not True:
            raise RuntimeError("The gateway did not acknowledge tracking; check rollout")
        with (args.output / "tracking-results.jsonl").open("w", encoding="utf-8") as raw, \
             (args.output / "crossings.jsonl").open("w", encoding="utf-8") as crossings:
            while True:
                ok, image = capture.read()
                if not ok:
                    break
                height, width = image.shape[:2]
                scale = min(1, 1280 / max(width, height))
                image = cv2.resize(image, (max(1, round(width * scale)), max(1, round(height * scale))))
                ok, encoded = cv2.imencode(".jpg", image, [cv2.IMWRITE_JPEG_QUALITY, 85])
                if not ok or len(encoded) > ready["max_frame_bytes"]:
                    raise ValueError("Frame cannot be encoded within the gateway limit")
                ws.send_binary(encoded.tobytes())
                event = receive(ws, "frame.result")
                if event["frame_id"] != frame_index + 1:
                    raise RuntimeError("Unexpected frame order; stop to protect trajectory continuity")
                seconds = frame_index / fps
                instant = start + timedelta(seconds=seconds)
                raw.write(json.dumps({"session": session, "source_seconds": seconds,
                                      "timestamp_utc": instant.isoformat(), "event": event}) + "\n")
                for crossing in counter.observe(event["result"], seconds):
                    crossing.update({"event_id": f"{session}:{event['frame_id']}:{crossing['track_id']}",
                                     "timestamp_utc": instant.isoformat()})
                    events.append(crossing)
                    crossings.write(json.dumps(crossing) + "\n")
                frame_index += 1
        if frame_index != total_frames:
            raise RuntimeError("Incomplete decoding; saved raw results are partial, not a complete history")
        history = hourly_history(events, start, start + timedelta(seconds=frame_index / fps))
        (args.output / "camera-history.json").write_text(json.dumps(history, indent=2) + "\n")
        print(f"{frame_index} frames; {sum(e['direction'] == 'in' for e in events)} entries; "
              f"{sum(e['direction'] == 'out' for e in events)} exits")
        try:
            payload = forecast_request(history)
        except ValueError as error:
            print(f"History saved; no forecast request created: {error}")
        else:
            (args.output / "camera-forecast-draft.json").write_text(json.dumps(payload, indent=2) + "\n")
            print("Use camera-forecast-draft.json in QDivZero's forecasting workbench")
    finally:
        capture.release()
        if ws is not None:
            ws.close()


if __name__ == "__main__":
    main()
