import json
import os
import queue
import random
import threading
import time
import uuid
from collections import deque
from datetime import datetime, timezone

from hiveos.config import (
    TRACE_DB_ASYNC,
    TRACE_DB_QUEUE_MAX,
    TRACE_LOG_PATH,
    TRACE_MAX_BYTES,
    TRACE_MAX_ROTATIONS,
    TRACE_READ_MAX_LINES,
    TRACE_SAMPLING_ALWAYS_EVENT_TYPES,
    TRACE_SAMPLING_BASE_RATE,
    TRACE_SAMPLING_DEGRADED_QUEUE_PCT,
    TRACE_SAMPLING_DEGRADED_RATE,
    TRACE_SAMPLING_ENABLED,
    TRACE_SAMPLING_SEVERE_QUEUE_PCT,
    TRACE_SAMPLING_SEVERE_RATE,
    TRACE_SAMPLING_AUTOTUNE_DROP_RATIO_PCT,
    TRACE_SAMPLING_AUTOTUNE_ENABLED,
    TRACE_SAMPLING_AUTOTUNE_HIGH_QUEUE_PCT,
    TRACE_SAMPLING_AUTOTUNE_INTERVAL_SEC,
    TRACE_SAMPLING_AUTOTUNE_MIN_RATE,
    TRACE_SAMPLING_AUTOTUNE_STEP,
    TRACE_SAMPLING_AUTOTUNE_TARGET_QUEUE_PCT,
)
from hiveos.intelligence.contracts import validate_trace_event
from hiveos.metrics import log_event

_TRACE_LOCK = threading.Lock()
_TRACE_DB_QUEUE = queue.Queue(maxsize=max(1, TRACE_DB_QUEUE_MAX))
_TRACE_DB_WORKER = None
_TRACE_DB_WORKER_LOCK = threading.Lock()
_TRACE_STATS_LOCK = threading.Lock()
_TRACE_STATS = {
    "total": 0,
    "sampled_out": 0,
    "persisted": 0,
    "db_enqueued": 0,
    "db_dropped": 0,
    "db_written": 0,
    "db_failed": 0,
}
_TRACE_AUTOTUNE_LOCK = threading.Lock()
_TRACE_AUTOTUNE_STATE = {
    "degraded_rate": float(TRACE_SAMPLING_DEGRADED_RATE),
    "severe_rate": float(TRACE_SAMPLING_SEVERE_RATE),
    "last_eval_monotonic": 0.0,
    "last_db_enqueued": 0,
    "last_db_dropped": 0,
    "last_drop_ratio": 0.0,
    "last_queue_pct": 0,
    "last_action": "init",
}


def _ensure_trace_log_parent():
    try:
        path = os.path.abspath(str(TRACE_LOG_PATH or "trace_events.log"))
        parent = os.path.dirname(path)
        if parent:
            os.makedirs(parent, exist_ok=True)
    except OSError:
        return


def now_iso():
    return datetime.now(timezone.utc).isoformat()


def build_trace_event(event_type, run_id, outcome="success", payload=None, **kwargs):
    model_meta = _normalize_model_meta(kwargs.get("model"))
    event = {
        "trace_id": kwargs.get("trace_id") or str(uuid.uuid4()),
        "span_id": kwargs.get("span_id") or str(uuid.uuid4())[:12],
        "parent_span_id": kwargs.get("parent_span_id"),
        "run_id": run_id,
        "timestamp": kwargs.get("timestamp") or now_iso(),
        "event_type": event_type,
        "agent_id": kwargs.get("agent_id"),
        "task_id": kwargs.get("task_id"),
        "chunk_id": kwargs.get("chunk_id"),
        "zone_id": kwargs.get("zone_id"),
        "payload": payload or {},
        "timing": kwargs.get("timing") or {},
        "model": model_meta,
        "outcome": outcome,
    }
    return validate_trace_event(event)


def _normalize_model_meta(model_meta):
    if not isinstance(model_meta, dict):
        return {}
    normalized = dict(model_meta)
    name = normalized.get("name")
    legacy = normalized.get("model")
    if not isinstance(name, str) or not name.strip():
        if isinstance(legacy, str) and legacy.strip():
            normalized["name"] = legacy.strip()
    provider = normalized.get("provider")
    if isinstance(provider, str):
        normalized["provider"] = provider.strip()
    if isinstance(normalized.get("name"), str):
        normalized["name"] = normalized["name"].strip()
    return normalized


def emit_trace(event, persist_db=True, persist_file=True):
    validated = validate_trace_event(event)
    _increment_trace_stat("total")
    _maybe_auto_tune()

    sample = _sample_decision(validated)
    if not sample["keep"]:
        _increment_trace_stat("sampled_out")
        return validated

    if persist_file:
        line = json.dumps(validated, separators=(",", ":")) + "\n"
        _ensure_trace_log_parent()
        with _TRACE_LOCK:
            _rotate_trace_files_if_needed(incoming_bytes=len(line.encode("utf-8")))
            with open(TRACE_LOG_PATH, "a", encoding="utf-8") as handle:
                handle.write(line)

    if persist_db:
        if TRACE_DB_ASYNC:
            _ensure_db_worker_started()
            job = {
                "agent_id": validated.get("agent_id"),
                "chunk_id": validated.get("chunk_id"),
                "event_type": validated.get("event_type"),
                "zone": validated.get("zone_id"),
                "task_id": validated.get("task_id"),
                "result": validated.get("outcome"),
                "reason": json.dumps(validated.get("payload", {}), separators=(",", ":")),
                "tick": None,
            }
            try:
                _TRACE_DB_QUEUE.put_nowait(job)
                _increment_trace_stat("db_enqueued")
            except queue.Full:
                _increment_trace_stat("db_dropped")
        else:
            _write_trace_to_db(
                agent_id=validated.get("agent_id"),
                chunk_id=validated.get("chunk_id"),
                event_type=validated.get("event_type"),
                zone=validated.get("zone_id"),
                task_id=validated.get("task_id"),
                result=validated.get("outcome"),
                reason=json.dumps(validated.get("payload", {}), separators=(",", ":")),
                tick=None,
            )

    _increment_trace_stat("persisted")
    return validated


def read_trace_events(limit=200, filters=None):
    filters = filters or {}
    if limit <= 0:
        return []
    matched = deque(maxlen=limit)
    scanned = 0

    for path in _iter_trace_files_oldest_first():
        try:
            with open(path, "r", encoding="utf-8") as handle:
                for line in handle:
                    if scanned >= TRACE_READ_MAX_LINES:
                        return list(matched)
                    scanned += 1

                    line = line.strip()
                    if not line:
                        continue
                    try:
                        event = json.loads(line)
                    except json.JSONDecodeError:
                        continue
                    if _matches_filters(event, filters):
                        matched.append(event)
        except OSError:
            continue

    return list(matched)


def read_trace_events_ndjson(limit=200, filters=None):
    events = read_trace_events(limit=limit, filters=filters)
    if not events:
        return ""
    return "\n".join(json.dumps(event, separators=(",", ":")) for event in events) + "\n"


def _matches_filters(event, filters):
    for key in ("run_id", "agent_id", "event_type", "outcome", "task_id", "chunk_id", "zone_id"):
        wanted = filters.get(key)
        if wanted is None:
            continue
        if str(event.get(key)) != str(wanted):
            return False
    model = event.get("model") or {}
    payload = event.get("payload") or {}
    wanted_provider = filters.get("model_provider")
    if wanted_provider is not None and str(model.get("provider")) != str(wanted_provider):
        return False
    wanted_name = filters.get("model_name")
    if wanted_name is not None and str(model.get("name")) != str(wanted_name):
        return False
    wanted_quality = filters.get("quality_status")
    if wanted_quality is not None and str(payload.get("quality_status")) != str(wanted_quality):
        return False
    return True


def _rotate_trace_files_if_needed(incoming_bytes):
    if TRACE_MAX_BYTES <= 0:
        return
    try:
        current_size = os.path.getsize(TRACE_LOG_PATH)
    except OSError:
        current_size = 0

    if current_size + incoming_bytes <= TRACE_MAX_BYTES:
        return

    max_rotations = max(0, int(TRACE_MAX_ROTATIONS))
    if max_rotations == 0:
        try:
            os.remove(TRACE_LOG_PATH)
        except OSError:
            pass
        return

    oldest = f"{TRACE_LOG_PATH}.{max_rotations}"
    if os.path.exists(oldest):
        try:
            os.remove(oldest)
        except OSError:
            pass

    for idx in range(max_rotations - 1, 0, -1):
        src = f"{TRACE_LOG_PATH}.{idx}"
        dst = f"{TRACE_LOG_PATH}.{idx + 1}"
        if os.path.exists(src):
            try:
                os.replace(src, dst)
            except OSError:
                continue

    if os.path.exists(TRACE_LOG_PATH):
        try:
            os.replace(TRACE_LOG_PATH, f"{TRACE_LOG_PATH}.1")
        except OSError:
            pass


def _iter_trace_files_oldest_first():
    max_rotations = max(0, int(TRACE_MAX_ROTATIONS))
    for idx in range(max_rotations, 0, -1):
        candidate = f"{TRACE_LOG_PATH}.{idx}"
        if os.path.exists(candidate):
            yield candidate
    if os.path.exists(TRACE_LOG_PATH):
        yield TRACE_LOG_PATH


def get_trace_sink_stats():
    with _TRACE_STATS_LOCK:
        stats = dict(_TRACE_STATS)
    queue_depth = _TRACE_DB_QUEUE.qsize() if TRACE_DB_ASYNC else 0
    try:
        current_log_bytes = os.path.getsize(TRACE_LOG_PATH)
    except OSError:
        current_log_bytes = 0
    queue_pct = _queue_pressure_pct()
    autotune = _get_autotune_state()
    active_rates = _get_active_rates()
    sample = _sample_decision(None)
    return {
        "file_path": TRACE_LOG_PATH,
        "file_bytes": current_log_bytes,
        "max_bytes": TRACE_MAX_BYTES,
        "max_rotations": TRACE_MAX_ROTATIONS,
        "read_max_lines": TRACE_READ_MAX_LINES,
        "db_async": TRACE_DB_ASYNC,
        "db_queue_max": TRACE_DB_QUEUE_MAX,
        "db_queue_depth": queue_depth,
        "db_queue_pressure_pct": queue_pct,
        "sampling": {
            "enabled": TRACE_SAMPLING_ENABLED,
            "mode": sample["mode"],
            "active_rate": sample["rate"],
            "base_rate": _clamp_rate(TRACE_SAMPLING_BASE_RATE),
            "degraded_rate": active_rates["degraded_rate"],
            "severe_rate": active_rates["severe_rate"],
            "degraded_queue_pct": TRACE_SAMPLING_DEGRADED_QUEUE_PCT,
            "severe_queue_pct": TRACE_SAMPLING_SEVERE_QUEUE_PCT,
            "always_event_types": list(TRACE_SAMPLING_ALWAYS_EVENT_TYPES),
            "always_outcomes": ["denied", "failed"],
            "autotune": autotune,
        },
        "stats": stats,
    }


def _ensure_db_worker_started():
    global _TRACE_DB_WORKER
    if _TRACE_DB_WORKER is not None and _TRACE_DB_WORKER.is_alive():
        return
    with _TRACE_DB_WORKER_LOCK:
        if _TRACE_DB_WORKER is not None and _TRACE_DB_WORKER.is_alive():
            return
        worker = threading.Thread(target=_db_worker_loop, daemon=True, name="hive-trace-db-writer")
        worker.start()
        _TRACE_DB_WORKER = worker


def _db_worker_loop():
    while True:
        job = _TRACE_DB_QUEUE.get()
        try:
            _write_trace_to_db(**job)
        finally:
            _TRACE_DB_QUEUE.task_done()


def _write_trace_to_db(agent_id, chunk_id, event_type, zone, task_id, result, reason, tick):
    try:
        log_event(
            agent_id=agent_id,
            chunk_id=chunk_id,
            event_type=event_type,
            zone=zone,
            task_id=task_id,
            result=result,
            reason=reason,
            tick=tick,
        )
        _increment_trace_stat("db_written")
    except Exception:
        _increment_trace_stat("db_failed")


def _increment_trace_stat(key):
    with _TRACE_STATS_LOCK:
        _TRACE_STATS[key] = int(_TRACE_STATS.get(key, 0)) + 1


def _sample_decision(event):
    if not TRACE_SAMPLING_ENABLED:
        return {"keep": True, "mode": "disabled", "rate": 1.0}

    if event is not None:
        outcome = str(event.get("outcome") or "").lower()
        event_type = str(event.get("event_type") or "")
        if outcome in {"denied", "failed"}:
            return {"keep": True, "mode": "critical", "rate": 1.0}
        if event_type in set(TRACE_SAMPLING_ALWAYS_EVENT_TYPES):
            return {"keep": True, "mode": "critical", "rate": 1.0}

    queue_pct = _queue_pressure_pct()
    active_rates = _get_active_rates()
    mode = "normal"
    rate = active_rates["base_rate"]
    if queue_pct >= int(TRACE_SAMPLING_SEVERE_QUEUE_PCT):
        mode = "severe"
        rate = active_rates["severe_rate"]
    elif queue_pct >= int(TRACE_SAMPLING_DEGRADED_QUEUE_PCT):
        mode = "degraded"
        rate = active_rates["degraded_rate"]

    if rate >= 1.0:
        return {"keep": True, "mode": mode, "rate": rate}
    return {"keep": random.random() <= rate, "mode": mode, "rate": rate}


def _queue_pressure_pct():
    if not TRACE_DB_ASYNC or TRACE_DB_QUEUE_MAX <= 0:
        return 0
    try:
        depth = _TRACE_DB_QUEUE.qsize()
    except Exception:
        depth = 0
    return int((depth / max(1, TRACE_DB_QUEUE_MAX)) * 100)


def _clamp_rate(value):
    return max(0.0, min(1.0, float(value)))


def _get_active_rates():
    base_rate = _clamp_rate(TRACE_SAMPLING_BASE_RATE)
    if not TRACE_SAMPLING_AUTOTUNE_ENABLED:
        return {
            "base_rate": base_rate,
            "degraded_rate": _clamp_rate(TRACE_SAMPLING_DEGRADED_RATE),
            "severe_rate": _clamp_rate(TRACE_SAMPLING_SEVERE_RATE),
        }
    with _TRACE_AUTOTUNE_LOCK:
        degraded = _clamp_rate(_TRACE_AUTOTUNE_STATE["degraded_rate"])
        severe = _clamp_rate(_TRACE_AUTOTUNE_STATE["severe_rate"])
    if severe > degraded:
        severe = degraded
    return {
        "base_rate": base_rate,
        "degraded_rate": degraded,
        "severe_rate": severe,
    }


def _get_autotune_state():
    with _TRACE_AUTOTUNE_LOCK:
        state = dict(_TRACE_AUTOTUNE_STATE)
    return {
        "enabled": TRACE_SAMPLING_AUTOTUNE_ENABLED,
        "interval_sec": max(1, int(TRACE_SAMPLING_AUTOTUNE_INTERVAL_SEC)),
        "step": _clamp_rate(TRACE_SAMPLING_AUTOTUNE_STEP),
        "min_rate": _clamp_rate(TRACE_SAMPLING_AUTOTUNE_MIN_RATE),
        "target_queue_pct": int(TRACE_SAMPLING_AUTOTUNE_TARGET_QUEUE_PCT),
        "high_queue_pct": int(TRACE_SAMPLING_AUTOTUNE_HIGH_QUEUE_PCT),
        "drop_ratio_pct": float(TRACE_SAMPLING_AUTOTUNE_DROP_RATIO_PCT),
        "last_action": state.get("last_action"),
        "last_drop_ratio": round(float(state.get("last_drop_ratio", 0.0)), 4),
        "last_queue_pct": int(state.get("last_queue_pct", 0)),
        "last_eval_monotonic": round(float(state.get("last_eval_monotonic", 0.0)), 3),
    }


def _maybe_auto_tune():
    if not TRACE_SAMPLING_AUTOTUNE_ENABLED:
        return

    now_mono = time.monotonic()
    interval = max(1, int(TRACE_SAMPLING_AUTOTUNE_INTERVAL_SEC))
    queue_pct = _queue_pressure_pct()

    with _TRACE_AUTOTUNE_LOCK:
        last_eval = float(_TRACE_AUTOTUNE_STATE.get("last_eval_monotonic", 0.0))
        if now_mono - last_eval < interval:
            return
        _TRACE_AUTOTUNE_STATE["last_eval_monotonic"] = now_mono
        _TRACE_AUTOTUNE_STATE["last_queue_pct"] = int(queue_pct)

    with _TRACE_STATS_LOCK:
        enqueued = int(_TRACE_STATS.get("db_enqueued", 0))
        dropped = int(_TRACE_STATS.get("db_dropped", 0))

    with _TRACE_AUTOTUNE_LOCK:
        prev_enqueued = int(_TRACE_AUTOTUNE_STATE.get("last_db_enqueued", 0))
        prev_dropped = int(_TRACE_AUTOTUNE_STATE.get("last_db_dropped", 0))
        delta_enqueued = max(0, enqueued - prev_enqueued)
        delta_dropped = max(0, dropped - prev_dropped)
        denom = max(1, delta_enqueued + delta_dropped)
        drop_ratio = float(delta_dropped) / float(denom)
        _TRACE_AUTOTUNE_STATE["last_db_enqueued"] = enqueued
        _TRACE_AUTOTUNE_STATE["last_db_dropped"] = dropped
        _TRACE_AUTOTUNE_STATE["last_drop_ratio"] = drop_ratio

        base_degraded = _clamp_rate(TRACE_SAMPLING_DEGRADED_RATE)
        base_severe = _clamp_rate(TRACE_SAMPLING_SEVERE_RATE)
        step = max(0.001, _clamp_rate(TRACE_SAMPLING_AUTOTUNE_STEP))
        min_rate = _clamp_rate(TRACE_SAMPLING_AUTOTUNE_MIN_RATE)
        high_queue = int(TRACE_SAMPLING_AUTOTUNE_HIGH_QUEUE_PCT)
        target_queue = int(TRACE_SAMPLING_AUTOTUNE_TARGET_QUEUE_PCT)
        drop_ratio_threshold = float(TRACE_SAMPLING_AUTOTUNE_DROP_RATIO_PCT) / 100.0

        degraded = _clamp_rate(_TRACE_AUTOTUNE_STATE.get("degraded_rate", base_degraded))
        severe = _clamp_rate(_TRACE_AUTOTUNE_STATE.get("severe_rate", base_severe))

        if queue_pct >= high_queue or drop_ratio >= drop_ratio_threshold:
            degraded = max(min_rate, degraded - step)
            severe = max(min_rate, severe - step)
            _TRACE_AUTOTUNE_STATE["last_action"] = "tighten"
        elif queue_pct <= target_queue and drop_ratio <= 0.0:
            recover_step = step * 0.5
            degraded = min(base_degraded, degraded + recover_step)
            severe = min(base_severe, severe + recover_step)
            _TRACE_AUTOTUNE_STATE["last_action"] = "relax"
        else:
            _TRACE_AUTOTUNE_STATE["last_action"] = "hold"

        if severe > degraded:
            severe = degraded
        _TRACE_AUTOTUNE_STATE["degraded_rate"] = degraded
        _TRACE_AUTOTUNE_STATE["severe_rate"] = severe
