"""EVPIN-01: single-owner packet retention; Event pins never block inference."""
from collections import Counter, deque
import sys
import time


class ProtectedPacketCache:
    def __init__(self, *, base_bytes=32*1024**2, total_bytes=64*1024**2,
                 history_ns=12_000_000_000, max_packets=32768, max_events=8,
                 hold_seconds=8.0, clock=time.monotonic):
        if not 0 < base_bytes <= total_bytes or min(history_ns, max_packets, max_events, hold_seconds) <= 0:
            raise ValueError('Invalid protected cache limits.')
        self.base_bytes, self.total_bytes = base_bytes, total_bytes
        self.history_ns, self.max_packets = history_ns, max_packets
        self.max_events, self.hold_seconds, self.clock = max_events, hold_seconds, clock
        self.packets, self.keys, self.costs = deque(), deque(), deque()
        self.first = self.next = self.bytes = self.accounted = 0
        self.latest_pts = None
        self.pins, self.failures = {}, {}
        self.metrics = Counter()
        self.last_key_pts = None

    def append(self, pts, raw, keyframe):
        row = (pts, raw)
        # Raw bytes are held once, including across overlapping Event pins.
        cost = sys.getsizeof(raw) + sys.getsizeof(row) + 256
        self.packets.append(row)
        self.costs.append(cost)
        self.bytes += len(raw)
        self.accounted += cost
        if keyframe:
            self.keys.append((self.next, pts))
            self.metrics['keyframes'] += 1
            if self.last_key_pts is not None and pts > self.last_key_pts:
                gap = pts-self.last_key_pts
                self.metrics['keyframe_gap_last_ns'] = gap
                self.metrics['keyframe_gap_max_ns'] = max(self.metrics['keyframe_gap_max_ns'], gap)
            self.last_key_pts = pts
        self.next += 1
        self.latest_pts = pts
        self.expire()
        self.trim()
        self.metrics['peak_payload_bytes'] = max(self.metrics['peak_payload_bytes'], self.bytes)
        self.metrics['peak_accounted_bytes'] = max(self.metrics['peak_accounted_bytes'], self.accounted)

    def protect(self, event_id, pts):
        if event_id in self.pins or event_id in self.failures:
            return 'PROTECTED_CACHE_DUPLICATE'
        if len(self.pins)+len(self.failures) >= self.max_events:
            self.metrics['event_limit'] += 1
            return 'PROTECTED_CACHE_EVENT_LIMIT'
        if type(pts) is not int or not self.packets:
            return 'NO_COMPRESSED_VIDEO'
        keys = [(seq, p) for seq, p in self.keys if p <= max(0, pts-3_000_000_000)]
        if not keys:
            keys = [(seq, p) for seq, p in self.keys if p <= pts]
        if not keys:
            self.metrics['missing_key_on_admission'] += 1
            return 'NO_PRE_EVENT_KEYFRAME'
        seq, _ = keys[-1]
        self.pins[event_id] = (seq, self.clock())
        self.metrics['protected'] += 1
        self.metrics['peak_events'] = max(self.metrics['peak_events'], len(self.pins))
        return None

    def _fail(self, event_id, reason):
        _, created = self.pins.pop(event_id)
        self.metrics['max_hold_ms'] = max(self.metrics['max_hold_ms'], (self.clock()-created)*1000)
        self.failures[event_id] = reason
        self.metrics[reason] += 1

    def expire(self):
        now = self.clock()
        for event_id, (_, created) in list(self.pins.items()):
            if now-created >= self.hold_seconds:
                self._fail(event_id, 'PROTECTED_CACHE_EXPIRED')

    def trim(self):
        while self.packets:
            ordinary = self.bytes > self.base_bytes or self.latest_pts-self.packets[0][0] > self.history_ns
            hard = self.accounted > self.total_bytes or len(self.packets) > self.max_packets
            if not ordinary and not hard:
                break
            owners = [eid for eid, (seq, _) in self.pins.items() if seq <= self.first]
            if owners and not hard:
                break
            for event_id in owners:
                self._fail(event_id, 'PROTECTED_CACHE_CAPACITY')
            _, raw = self.packets.popleft()
            self.bytes -= len(raw)
            self.accounted -= self.costs.popleft()
            self.first += 1
            while self.keys and self.keys[0][0] < self.first:
                self.keys.popleft()
            self.metrics['evicted_packets'] += 1

    def rows(self, event_id, end_pts):
        seq, _ = self.pins[event_id]
        return [(pts, raw) for i, (pts, raw) in enumerate(self.packets, self.first)
                if i >= seq and pts <= end_pts]

    def release(self, event_id):
        pin = self.pins.pop(event_id, None)
        self.failures.pop(event_id, None)
        if pin is not None:
            self.metrics['max_hold_ms'] = max(self.metrics['max_hold_ms'], (self.clock()-pin[1])*1000)
            self.metrics['released'] += 1
        self.trim()

    def status(self):
        return dict(self.metrics, payload_bytes=self.bytes, accounted_bytes=self.accounted,
                    packets=len(self.packets), protected_events=len(self.pins), failed_events=len(self.failures),
                    span_ns=max(0, self.latest_pts-self.packets[0][0]) if self.packets else 0,
                    base_bytes=self.base_bytes, total_bytes=self.total_bytes,
                    max_packets=self.max_packets, max_events=self.max_events, hold_seconds=self.hold_seconds)

    def clear(self):
        self.pins.clear()
        self.failures.clear()
        self.packets.clear()
        self.keys.clear()
        self.costs.clear()
        self.bytes = self.accounted = 0
