"""Anonymous visitor-role classification from configured zones and dwell.

The detector's short-lived tracker IDs never leave the player. Only aggregate
one-time counters are returned to the API:

* activeShoppers: remained in a SHOPPER zone for its configured threshold
* fleetingVisitors: left a SHOPPER zone before that threshold
* staff/security: entered a dedicated role zone
* unclassified: a confirmed track ended without crossing a configured zone

No names, face embeddings, biometric templates or persistent identities exist
in this module.
"""

SEGMENT_KEYS = (
    "activeShoppers",
    "fleetingVisitors",
    "staff",
    "security",
    "unclassified",
)

PASSAGE_KIND = "FOOTFALL"


def empty_segments():
    return {key: 0 for key in SEGMENT_KEYS}


def _kind(zone):
    return str(zone.get("kind") or "COUNTING").upper()


def _threshold(zone):
    try:
        return max(5, min(3600, int(zone.get("dwellThresholdSeconds") or 30)))
    except (TypeError, ValueError):
        return 30


class ZonePassageTracker:
    """Count each confirmed anonymous track once per FOOTFALL area.

    A gate is drawn as a polygon rather than a directional line. Counting both
    polygon edges would turn one person walking through a narrow gate into two
    crossings (outside -> inside -> outside), so the product's combined
    entry/exit measurement is deliberately one passage per tracked person while
    that person remains visible to this camera. No direction or identity leaves
    the player.
    """

    def __init__(self):
        self._confirmed = set()
        self._counted = set()  # (zone_id, track_id)

    def reset(self):
        self._confirmed.clear()
        self._counted.clear()

    def update(self, zones, inside_now, newly_confirmed, surviving_track_ids):
        self._confirmed.update(newly_confirmed or ())
        flows = {}

        active_zone_ids = {
            zone.get("id")
            for zone in zones
            if _kind(zone) == PASSAGE_KIND and zone.get("id")
        }
        for zone_id in active_zone_ids:
            for track_id in inside_now.get(zone_id, set()):
                key = (zone_id, track_id)
                if track_id not in self._confirmed or key in self._counted:
                    continue
                self._counted.add(key)
                flows[zone_id] = flows.get(zone_id, 0) + 1

        survivors = set(surviving_track_ids or ())
        self._confirmed.intersection_update(survivors)
        self._counted = {
            (zone_id, track_id)
            for zone_id, track_id in self._counted
            if zone_id in active_zone_ids and track_id in survivors
        }
        return flows


class VisitorSegmentTracker:
    """Classify each confirmed, anonymous tracker id at most once."""

    def __init__(self):
        self._confirmed = set()
        self._segment_by_track = {}
        self._shopper_entered_at = {}  # (zone_id, track_id) -> monotonic seconds

    def reset(self):
        self._confirmed.clear()
        self._segment_by_track.clear()
        self._shopper_entered_at.clear()

    def update(self, now, zones, inside_now, newly_confirmed, surviving_track_ids):
        flows = empty_segments()
        self._confirmed.update(newly_confirmed or ())

        # Dedicated role zones win before customer-area dwell. This is reliable
        # when staff/security have their own entrance or work area; the UI warns
        # that a shared entrance needs a future badge/roster signal.
        role_priority = (("SECURITY", "security"), ("STAFF", "staff"))
        for zone_kind, segment in role_priority:
            for zone in zones:
                if _kind(zone) != zone_kind:
                    continue
                for tid in inside_now.get(zone.get("id"), set()):
                    if tid not in self._confirmed or tid in self._segment_by_track:
                        continue
                    self._segment_by_track[tid] = segment
                    flows[segment] += 1

        thresholds = []
        current_shopper_keys = set()
        for zone in zones:
            if _kind(zone) != "SHOPPER":
                continue
            zid = zone.get("id")
            threshold = _threshold(zone)
            thresholds.append(threshold)
            for tid in inside_now.get(zid, set()):
                if tid not in self._confirmed or tid in self._segment_by_track:
                    continue
                key = (zid, tid)
                current_shopper_keys.add(key)
                entered = self._shopper_entered_at.setdefault(key, now)
                if now - entered >= threshold:
                    self._segment_by_track[tid] = "activeShoppers"
                    flows["activeShoppers"] += 1
                    self._shopper_entered_at.pop(key, None)

        # Leaving a customer zone before the threshold is a fleeting visit. A
        # track is classified only once even if zones overlap.
        for key, entered in list(self._shopper_entered_at.items()):
            if key in current_shopper_keys:
                continue
            zid, tid = key
            self._shopper_entered_at.pop(key, None)
            if tid in self._confirmed and tid not in self._segment_by_track:
                self._segment_by_track[tid] = "fleetingVisitors"
                flows["fleetingVisitors"] += 1

        # Tracker ids disappear only after the tracker's missed-frame grace
        # period. Anything confirmed but never classified is explicitly
        # "unclassified", not silently forced into shopper or staff.
        survivors = set(surviving_track_ids or ())
        for tid in list(self._confirmed):
            if tid in survivors:
                continue
            if tid not in self._segment_by_track:
                flows["unclassified"] += 1
            self._confirmed.discard(tid)
            self._segment_by_track.pop(tid, None)
            for key in [key for key in self._shopper_entered_at if key[1] == tid]:
                self._shopper_entered_at.pop(key, None)

        return flows, (min(thresholds) if thresholds else None)
