"""EVART-01: child-owned compressed cache and asynchronous durable Event admission."""
from collections import Counter, deque
import ctypes
import json
import os
from pathlib import Path
import queue
import struct
import threading
import time

from app.event_artifact_contract import ArtifactConfig
from app.event_artifact_store import ArtifactStore

PACKET = struct.Struct('<QQQIIII')
MAX_PACKET = 2 * 1024 ** 2 + 65536 + PACKET.size
MAX_CACHE = 32 * 1024 ** 2
LIBRARY = '/opt/nvidia/deepstream/deepstream/lib/gst-plugins/libcontextra_rtp_time.so'


class EncodedCache:
    def __init__(self):
        self.lib = ctypes.CDLL(LIBRARY)
        self.lib.contextra_encoded_enable.argtypes = [ctypes.c_int]
        self.lib.contextra_encoded_pull.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
        self.lib.contextra_encoded_pull.restype = ctypes.c_long
        self.lib.contextra_encoded_dropped.restype = ctypes.c_ulonglong
        self.buffer = ctypes.create_string_buffer(MAX_PACKET)
        self.lib.contextra_encoded_enable(1)

    def pull(self):
        length = self.lib.contextra_encoded_pull(self.buffer, len(self.buffer))
        if length < 0:
            raise RuntimeError('Compressed cache packet exceeded contract.')
        return self.buffer.raw[:length] if length else None

    def close(self):
        self.lib.contextra_encoded_enable(0)


class EventCapture:
    def __init__(self, config, camera_id):
        self.config, self.camera_id = config, camera_id
        self.store = ArtifactStore(config)
        self.native = EncodedCache()
        self.results = queue.Queue(maxsize=128)
        self.packets = deque()
        self.metadata = deque(maxlen=300)
        self.pending = {}
        self.bytes = 0
        self.metrics = Counter()
        self.error = None
        self.stop = threading.Event()
        self.worker = threading.Thread(target=self._run, name='event-capture', daemon=True)
        self.worker.start()

    @classmethod
    def from_environment(cls, spec):
        config = ArtifactConfig.from_environment()
        region_enabled = any((spec.get(name) or {}).get('enabled', False)
                             for name in ('intrusion', 'loitering', 'fall', 'fight'))
        from app.fire_config import scenario_enabled
        if not config or not config.enabled or not (region_enabled or scenario_enabled(spec)):
            return None
        return cls(config, spec.get('camera_id', spec['source_id']))

    def submit(self, result):
        if self.error or self.stop.is_set():
            self.metrics['unaccepted_events'] += len(result.events)
            return False
        try:
            self.results.put_nowait(result)
            return True
        except queue.Full:
            self.metrics['metadata_queue_drop'] += 1
            self.metrics['unaccepted_events'] += len(result.events)
            return False

    def status(self):
        return dict(self.metrics, error=self.error, pending=len(self.pending),
                    queued=self.results.qsize(), native_drop=self.native.lib.contextra_encoded_dropped())

    def _collect(self):
        for _ in range(128):
            packet = self.native.pull()
            if packet is None:
                break
            pts = PACKET.unpack_from(packet)[0]
            if self.packets and pts < self.packets[-1][0]:
                # PTS reordering is legal for B-frames; decoder order is retained.
                self.metrics['packet_pts_reordered'] += 1
            self.packets.append((pts, packet))
            self.bytes += len(packet)
            while self.packets and (self.bytes > MAX_CACHE or pts-self.packets[0][0] > 12_000_000_000):
                self.bytes -= len(self.packets.popleft()[1])
        for _ in range(128):
            try:
                result = self.results.get_nowait()
            except queue.Empty:
                break
            snapshot = result.snapshot()
            snapshot['cameraId'] = self.camera_id
            self.metadata.append(snapshot)
            for event in result.events:
                payload = event.payload()
                payload['cameraId'] = self.camera_id
                payload['scenarioConfig'] = snapshot.get('scenarioConfig')
                accepted, reason = self.store.accept(payload)
                self.metrics['duplicate' if not accepted else 'accepted'] += 1
                if accepted and reason:
                    self.metrics['artifact_admission_failed'] += 1
                if accepted and not reason:
                    self.pending[event.event_id] = (payload, time.monotonic()+6)

    def _save(self, event, reason):
        event_id, pts = event['eventId'], event['ptsNs']
        if type(pts) is not int or not self.packets:
            self.store.captured(event_id, 'NO_COMPRESSED_VIDEO')
            return
        lo, hi = max(0, pts-3_000_000_000), pts+3_000_000_000
        rows = list(self.packets)
        keys = [i for i, (p, raw) in enumerate(rows) if PACKET.unpack_from(raw)[5] and p <= lo]
        if not keys:
            keys = [i for i, (p, raw) in enumerate(rows) if PACKET.unpack_from(raw)[5] and p <= pts]
        if not keys:
            self.store.captured(event_id, 'NO_PRE_EVENT_KEYFRAME')
            return
        chosen = rows[keys[-1]:]
        chosen = [(p, raw) for p, raw in chosen if p <= hi]
        stage = self.config.root / '.staging' / event_id
        stage.mkdir(mode=0o700, exist_ok=False)
        with (stage / 'capture.bin').open('xb') as target:
            for _, raw in chosen:
                target.write(struct.pack('<I', len(raw)))
                target.write(raw)
            target.flush()
            os.fsync(target.fileno())
        job = dict(event=event, startPtsNs=lo, endPtsNs=hi,
                   captureReason=reason, nativeDrop=self.native.lib.contextra_encoded_dropped(),
                   metadataQueueDrop=self.metrics['metadata_queue_drop'],
                   metadata=[s for s in self.metadata if s['ptsNs'] is not None and lo <= s['ptsNs'] <= hi])
        with (stage / 'job.json').open('x') as target:
            json.dump(job, target, ensure_ascii=False, separators=(',', ':'))
            target.flush()
            os.fsync(target.fileno())
        self.store.captured(event_id)
        self.metrics['capture_ready'] += 1

    def _run(self):
        try:
            while True:
                self._collect()
                stopped = self.stop.is_set()
                last_pts = max((p for p, _ in self.packets), default=-1)
                for event_id, (event, deadline) in list(self.pending.items()):
                    complete = event['ptsNs'] is not None and last_pts >= event['ptsNs']+3_000_000_000
                    if stopped or complete or time.monotonic() >= deadline:
                        self._save(event, 'COMPLETE' if complete else 'STOP_PARTIAL' if stopped else 'POST_TIMEOUT_PARTIAL')
                        del self.pending[event_id]
                if stopped:
                    return
                self.stop.wait(.05)
        except Exception as exc:
            self.error = 'EVENT_CAPTURE_' + type(exc).__name__.upper()
            print('[ARTIFACT] ' + self.error, flush=True)
            for event_id in self.pending:
                try:
                    self.store.captured(event_id, self.error)
                except Exception:
                    pass  # Durable CAPTURING expires in the parent, never vanishes.
        finally:
            self.native.close()
            self.store.close()

    def close(self, timeout):
        self.stop.set()
        self.worker.join(max(0, timeout))
        return not self.worker.is_alive()
