"""TRACE-01: bounded best-effort queue; no retry, spool, or per-channel worker."""
from collections import Counter
import json
import os
from pathlib import Path
import queue
import shutil
import threading
import time
from uuid import uuid4

from .sanitize import copy_safe

DEFAULT = {'enabled': True, 'queue_limit': 64, 'batch_size': 8,
           'min_free_bytes': 1073741824, 'auto_cleanup': False}


def policy(path):
    p = Path(path)
    if not p.is_absolute() or any(x.is_symlink() for x in (p, *p.parents)):
        raise ValueError('TRACE_POLICY_PATH')
    raw = p.read_bytes()
    if len(raw) > 4096:
        raise ValueError('TRACE_POLICY_SIZE')
    d = json.loads(raw)
    if set(d) != set(DEFAULT) or type(d['enabled']) is not bool or d['auto_cleanup'] is not False:
        raise ValueError('TRACE_POLICY_FIELDS')
    for key, low, high in [('queue_limit', 16, 256), ('batch_size', 1, 16),
                           ('min_free_bytes', 1073741824, 1024**4)]:
        if type(d[key]) is not int or not low <= d[key] <= high:
            raise ValueError('TRACE_POLICY_RANGE')
    return d


class Recorder:
    def __init__(self, sink, *, server, path=None, settings=None, start=True):
        self.sink, self.server, self.path = sink, server, path
        self.settings = dict(settings or DEFAULT)
        self.error = None
        if path:
            try:
                self.settings = policy(path)
            except Exception:
                self.settings['enabled'] = False
                self.error = 'TRACE_POLICY_INVALID'
        self.queue = queue.Queue(self.settings['queue_limit'])
        self.counts = Counter()
        self.lock = threading.Lock()
        self.stopping = threading.Event()
        self.thread = None
        self.pid = os.getpid()
        self.free = None
        self.last_check = 0.
        self.observer = None
        self.last_observed = 0.
        if start:
            self.thread = threading.Thread(target=self.run, name='contextra-trace', daemon=True)
            self.thread.start()

    def count(self, key, n=1):
        with self.lock:
            self.counts[key] += n

    def emit(self, value):
        if os.getpid() != self.pid or self.stopping.is_set() or not self.settings['enabled']:
            self.count('disabled')
            return False
        if self.queue.full():
            self.count('dropped_full')
            return False
        clean, marks = copy_safe(value)
        clean.update(traceId=uuid4().hex, server=self.server, recordedAtMs=time.time_ns()//1000000,
                     maskedFields=marks)
        try:
            self.queue.put_nowait(clean)
            self.count('queued')
            return True
        except queue.Full:
            self.count('dropped_full')
            return False

    def check(self):
        if self.path and time.monotonic() - self.last_check > 5:
            self.last_check = time.monotonic()
            d = policy(self.path)
            # Queue capacity changes require restart; never silently allocate another queue.
            if d['queue_limit'] != self.queue.maxsize:
                raise ValueError('TRACE_QUEUE_RESTART_REQUIRED')
            self.settings = d
            self.free = shutil.disk_usage(Path(self.path).parent).free
        if not self.settings['enabled']:
            raise ValueError('TRACE_DISABLED')
        if self.free is not None and self.free < self.settings['min_free_bytes']:
            raise ValueError('TRACE_SPACE_LOW')

    def flush(self, batch):
        try:
            self.check()
            self.sink(batch)
            self.count('stored_or_forwarded', len(batch))
            self.error = None
        except Exception as exc:
            self.count('dropped_sink', len(batch))
            self.error = str(exc) if isinstance(exc, ValueError) and str(exc).startswith('TRACE_') else 'TRACE_SINK_UNAVAILABLE'

    def run(self):
        while not self.stopping.is_set():
            if time.monotonic() - self.last_observed > 2:
                self.last_observed = time.monotonic()
                try:
                    self.check()
                    if self.observer:
                        self.observer()
                except Exception:
                    self.error = 'TRACE_POLICY_OR_SPACE_UNAVAILABLE'
            try:
                first = self.queue.get(timeout=.25)
            except queue.Empty:
                continue
            batch = [first]
            for _ in range(self.settings['batch_size'] - 1):
                try:
                    batch.append(self.queue.get_nowait())
                except queue.Empty:
                    break
            self.flush(batch)
        self.count('dropped_shutdown', self.queue.qsize())
        while True:
            try:
                self.queue.get_nowait()
            except queue.Empty:
                break

    def status(self):
        with self.lock:
            counts = dict(self.counts)
        return dict(effective=self.settings, counts=counts, queueDepth=self.queue.qsize(),
                    error=self.error, freeBytes=self.free, policyPath=self.path,
                    alive=bool(self.thread and self.thread.is_alive()))

    def close(self):
        self.stopping.set()
        if self.thread:
            self.thread.join(5)
        return not self.thread or not self.thread.is_alive()
