"""Count crossings from QDivZero track_id responses. No model runs here."""

import math
from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo


class LineCounter:
    """Vertical entrance segment: left -> right is in; right -> left is out."""

    def __init__(self, line_x=0.5, margin=0.02, y_min=0.0, y_max=1.0, max_gap=1.0):
        if not (0 < line_x < 1 and 0 < margin < min(line_x, 1 - line_x)):
            raise ValueError("Line and margin must be inside the normalized frame")
        if not (0 <= y_min < y_max <= 1 and math.isfinite(max_gap) and max_gap > 0):
            raise ValueError("Invalid entrance segment or maximum observation gap")
        self.line_x, self.margin = line_x, margin
        self.y_min, self.y_max, self.max_gap = y_min, y_max, max_gap
        self.tracks = {}
        self.dimensions = None
        self.last_time = None

    def observe(self, result, source_seconds):
        if not math.isfinite(source_seconds) or (self.last_time is not None and source_seconds <= self.last_time):
            raise ValueError("Source timestamps must increase")
        dimensions = (result["width"], result["height"])
        if min(dimensions) <= 0 or (self.dimensions is not None and dimensions != self.dimensions):
            raise ValueError("Frame dimensions must stay fixed within a session")
        self.dimensions, self.last_time = dimensions, source_seconds
        self.tracks = {key: value for key, value in self.tracks.items()
                       if source_seconds - value["last_seen"] <= self.max_gap}
        events, seen = [], set()
        for detection in result["detections"]:
            track_id = detection.get("track_id")
            if detection["class_name"] != "person" or type(track_id) is not int or not (1 <= track_id <= 2**53 - 1):
                continue
            if track_id in seen:
                raise ValueError("A frame contains a duplicate track ID")
            seen.add(track_id)
            box = detection["box"]
            x = (box["x1"] + box["x2"]) / (2 * dimensions[0])
            y = box["y2"] / dimensions[1]
            if not (math.isfinite(x) and math.isfinite(y)):
                raise ValueError("Invalid bounding box")
            side = -1 if x < self.line_x - self.margin else 1 if x > self.line_x + self.margin else 0
            previous = self.tracks.get(track_id)
            if previous is None:
                self.tracks[track_id] = {"side": side, "point": (x, y), "last_seen": source_seconds,
                                         "crossing_valid": False}
                continue
            px, py = previous["point"]
            # Keep every observed point, including the hysteresis band, so a curved
            # path outside the entrance is not replaced by a shortcut through it.
            if (px < self.line_x <= x) or (x <= self.line_x < px):
                crossing_y = py + (y - py) * (self.line_x - px) / (x - px)
                previous["crossing_valid"] = self.y_min <= crossing_y <= self.y_max
            if side != 0:
                if previous["side"] != 0 and previous["side"] != side and previous["crossing_valid"]:
                    events.append({"track_id": track_id, "direction": "in" if side == 1 else "out",
                                   "source_seconds": source_seconds})
                previous["side"], previous["crossing_valid"] = side, False
            previous["point"], previous["last_seen"] = (x, y), source_seconds
        return events


def hourly_history(events, start, end):
    """Continuous, fully processed recording only; partial edge hours retain coverage."""
    if start.utcoffset() is None or end.utcoffset() is None or end <= start:
        raise ValueError("Use increasing, timezone-aware recording boundaries")
    start, end = start.astimezone(timezone.utc), end.astimezone(timezone.utc)
    hour = timedelta(hours=1)
    cursor = start.replace(minute=0, second=0, microsecond=0)
    rows = {}
    while cursor < end:
        coverage = (min(cursor + hour, end) - max(cursor, start)).total_seconds() / 3600
        rows[cursor] = {"timestamp_utc": cursor.isoformat(),
                        "timestamp_local": cursor.astimezone(ZoneInfo("Europe/Madrid")).isoformat(),
                        "entries": 0, "exits": 0, "coverage": coverage}
        cursor += hour
    seen = set()
    for event in events:
        event_id = event.get("event_id")
        if event_id is not None:
            if event_id in seen:
                continue
            seen.add(event_id)
        instant = datetime.fromisoformat(event["timestamp_utc"])
        if instant.utcoffset() is None or not start <= instant < end or event["direction"] not in ("in", "out"):
            raise ValueError("Crossing outside recording or invalid direction")
        bucket = instant.astimezone(timezone.utc).replace(minute=0, second=0, microsecond=0)
        rows[bucket]["entries" if event["direction"] == "in" else "exits"] += 1
    return {"synthetic": False, "source": "QDivZero tracking; client-side entrance crossings on a continuous recording",
            "timezone": "Europe/Madrid", "interval_seconds": 3600, "observations": list(rows.values())}


def forecast_request(history, horizon=24):
    """Keep complete edge hours, reject internal gaps; future opening hours are optional."""
    rows = [row for row in history["observations"] if row["coverage"] == 1]
    if not rows:
        raise ValueError("No complete observed hour; collect a longer recording")
    times = [datetime.fromisoformat(row["timestamp_utc"]) for row in rows]
    if any(b - a != timedelta(hours=1) for a, b in zip(times, times[1:])):
        raise ValueError("The history has internal gaps; do not compress missing hours")
    return {"horizon": horizon, "return_quantiles": True, "use_symmetric_averaging": False,
            "inputs": [{"id": "camera-entries", "target": [[row["entries"] for row in rows]]}]}
