"""TRACKCFG-01: 검증된 ByteTrack core를 감싼 run-local, class-aware adapter."""

from dataclasses import asdict, dataclass
from importlib.metadata import version
import math
import time

import numpy as np
from app.tracking_kernel import TRACKING_KERNEL


def _prepare_observations(observations):
    groups = {}
    for index, observation in enumerate(observations):
        if not 0 <= observation.class_id < 80:
            raise ValueError('COCO tracking class out of range.')
        groups.setdefault(observation.class_id, []).append(index)
    # Preserve the old float64 multiply -> float32 conversion without per-object arrays.
    boxes = np.asarray([o.bbox_xyxy for o in observations], dtype=np.float64).reshape(-1, 4)
    dimensions = np.asarray([(o.frame.frame_width, o.frame.frame_height) for o in observations],
                            dtype=np.float64).reshape(-1, 2)
    boxes[:, 0::2] *= dimensions[:, 0:1]
    boxes[:, 1::2] *= dimensions[:, 1:2]
    scores = np.asarray([o.score for o in observations], dtype=np.float32)
    return groups, boxes.astype(np.float32), scores


@dataclass(frozen=True)
class TrackingSettings:
    # 설치 reference 기본값. 표시/Scenario threshold 또는 서비스 FPS 보장이 아니다.
    activation: float = .25
    lost_buffer: int = 30
    matching: float = .8
    frame_rate: float = 30
    consecutive: int = 1
    enabled: bool = True

    def __post_init__(self):
        if (type(self.enabled) is not bool
                or not all(math.isfinite(v) for v in (self.activation, self.matching, self.frame_rate))
                or not .1 < self.activation <= 1 or not 0 < self.matching <= 1
                or self.frame_rate <= 0 or type(self.lost_buffer) is not int
                or not 1 <= self.lost_buffer <= 300 or type(self.consecutive) is not int
                or not 1 <= self.consecutive <= 300):
            raise ValueError('Invalid tracking settings.')

    @classmethod
    def parse(cls, value):
        if type(value) is not dict:
            raise ValueError('Invalid tracking settings.')
        try:
            return cls(**value)
        except (TypeError, OverflowError) as error:
            raise ValueError('Invalid tracking settings.') from error

    def payload(self):
        return asdict(self)


class ByteTracker:
    def __init__(self, settings=TrackingSettings(), metrics=None):
        try:
            if version('supervision') != '0.30.2':
                raise RuntimeError('Unvalidated ByteTrack version.')
            import supervision as sv
        except (ImportError, RuntimeError) as error:
            raise RuntimeError('ByteTrack requires validated supervision 0.30.2.') from error
        self.sv, self.settings = sv, settings
        self._trackers = {}
        self.metrics = metrics

    def update(self, observations):
        revision = self.metrics.reserve_tracking_profile() if self.metrics is not None else None
        if revision is not None:
            from app.tracking_probe import profile_update
        # Only compute is sequenced. Metrics publication, queue handling and I/O stay outside.
        with TRACKING_KERNEL.turn() as wait_ns:
            if revision is not None:
                result, profile = profile_update(self, observations)
                profile['kernel_wait_ms'] = wait_ns / 1e6
            else:
                result = self._update(observations)
        if revision is not None:
            self.metrics.record_tracking_profile(revision, profile)
        return result

    def _update(self, observations, class_samples=None):
        ids = [None] * len(observations)
        groups, boxes, scores = _prepare_observations(observations)
        empty_tensors = np.empty((0, 5), dtype=np.float32)
        for class_id in sorted(groups.keys() | self._trackers.keys()):
            if class_id not in self._trackers:
                s = self.settings
                self._trackers[class_id] = self.sv.ByteTrack(
                    track_activation_threshold=s.activation, lost_track_buffer=s.lost_buffer,
                    minimum_matching_threshold=s.matching, frame_rate=s.frame_rate,
                    minimum_consecutive_frames=s.consecutive)
            core = self._trackers[class_id]
            indices = np.asarray(groups.get(class_id, ()), dtype=int)
            if class_samples is not None:
                before = (len(core.tracked_tracks), len(core.lost_tracks), len(core.removed_tracks))
                wall, cpu = time.monotonic_ns(), time.thread_time_ns()
            fast = self._advance_class(core, class_id, indices, boxes, scores, ids, empty_tensors)
            if class_samples is not None:
                class_samples.append(dict(class_id=class_id, candidates=len(indices), before=before,
                    after=(len(core.tracked_tracks), len(core.lost_tracks), len(core.removed_tracks)),
                    fast_path=fast, wall_ms=(time.monotonic_ns()-wall)/1e6,
                    thread_cpu_ms=(time.thread_time_ns()-cpu)/1e6))
        return ids

    def _advance_class(self, core, class_id, indices, boxes, scores, ids, empty_tensors):
        # supervision 0.30.2: no state and no high-score candidate changes only frame_id.
        # Preserve the instance/ID counters and all candidates; activation depends on this clock.
        if (not (core.tracked_tracks or core.lost_tracks or core.removed_tracks)
                and (not len(indices) or not np.any(scores[indices] >= core.track_activation_threshold))):
            core.frame_id += 1
            return True
        if not len(indices):
            core.update_with_tensors(empty_tensors)
            return False
        detections = self.sv.Detections(xyxy=boxes[indices], confidence=scores[indices],
            class_id=np.full(len(indices), class_id, dtype=int), data={'candidate': indices})
        tracked = core.update_with_detections(detections)
        for index, track_id in zip(tracked.data.get('candidate', ()), tracked.tracker_id):
            ids[int(index)] = (int(track_id) - 1) * 80 + class_id + 1
        return False

    def reset(self):
        for tracker in self._trackers.values():
            tracker.reset()
        self._trackers.clear()

    def active_tracks(self):
        return sum(len(t.tracked_tracks) for t in self._trackers.values())
