import threading
import time
import uuid
from collections import deque
from datetime import datetime, timezone


def _now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


class AIRunState:
    QUEUED = "queued"
    RUNNING = "running"
    COMPLETED = "completed"
    FAILED = "failed"
    TIMEOUT = "timeout"
    DROPPED = "dropped"


class AIExecutionPlane:
    """
    Minimal async run-plane skeleton.
    This is intentionally simple and acts as the contract anchor for future model workers.
    """

    def __init__(self, max_runs=1000, max_queue=300, queue_ttl_seconds=30, max_runtime_seconds=20):
        self._lock = threading.Lock()
        self._queue = deque()
        self._runs = {}
        self._max_runs = max_runs
        self._max_queue = max(1, int(max_queue))
        self._queue_ttl_seconds = max(1, int(queue_ttl_seconds))
        self._max_runtime_seconds = max(1, int(max_runtime_seconds))
        self._worker = None
        self._running = False
        self._stats = {
            "submitted": 0,
            "completed": 0,
            "failed": 0,
            "timed_out": 0,
            "dropped_queue_full": 0,
            "dropped_queue_ttl": 0,
            "dropped_trimmed": 0,
        }

    def start(self):
        with self._lock:
            if self._running:
                return
            self._running = True
        self._worker = threading.Thread(target=self._worker_loop, daemon=True)
        self._worker.start()

    def stop(self):
        with self._lock:
            self._running = False

    def submit_run(self, agent_id, task_id, chunk_id, payload):
        run_id = f"{chunk_id}:{uuid.uuid4().hex[:8]}"
        run = {
            "run_id": run_id,
            "agent_id": agent_id,
            "task_id": task_id,
            "chunk_id": chunk_id,
            "state": AIRunState.QUEUED,
            "submitted_at": _now_iso(),
            "started_at": None,
            "finished_at": None,
            "payload": payload,
            "output": None,
            "error": None,
        }
        with self._lock:
            if len(self._queue) >= self._max_queue:
                run["state"] = AIRunState.DROPPED
                run["finished_at"] = _now_iso()
                run["error"] = "queue_full"
                self._runs[run_id] = run
                self._stats["dropped_queue_full"] += 1
                self._trim_old_runs_locked()
                return run_id
            self._runs[run_id] = run
            self._queue.append(run_id)
            self._stats["submitted"] += 1
            self._trim_old_runs_locked()
        return run_id

    def get_run(self, run_id):
        with self._lock:
            run = self._runs.get(run_id)
            return dict(run) if run else None

    def get_stats(self):
        with self._lock:
            queued = sum(1 for r in self._runs.values() if r["state"] == AIRunState.QUEUED)
            running = sum(1 for r in self._runs.values() if r["state"] == AIRunState.RUNNING)
            timed_out = sum(1 for r in self._runs.values() if r["state"] == AIRunState.TIMEOUT)
            dropped = sum(1 for r in self._runs.values() if r["state"] == AIRunState.DROPPED)
            return {
                "total_runs": len(self._runs),
                "queued_runs": queued,
                "running_runs": running,
                "timed_out_runs": timed_out,
                "dropped_runs": dropped,
                "queue_depth": len(self._queue),
                "max_queue": self._max_queue,
                "queue_ttl_seconds": self._queue_ttl_seconds,
                "max_runtime_seconds": self._max_runtime_seconds,
                "stats": dict(self._stats),
            }

    def _worker_loop(self):
        while True:
            with self._lock:
                running = self._running
                run_id = self._queue.popleft() if self._queue else None
            if not running:
                return
            if not run_id:
                time.sleep(0.01)
                continue
            self._execute_run(run_id)

    def _execute_run(self, run_id):
        with self._lock:
            run = self._runs.get(run_id)
            if not run:
                return
            age_seconds = _age_seconds(run.get("submitted_at"))
            if age_seconds > self._queue_ttl_seconds:
                run["state"] = AIRunState.DROPPED
                run["error"] = "queue_ttl_expired"
                run["finished_at"] = _now_iso()
                self._stats["dropped_queue_ttl"] += 1
                return
            run["state"] = AIRunState.RUNNING
            run["started_at"] = _now_iso()
            payload = run["payload"]
        exec_started = time.monotonic()

        try:
            # Placeholder async model execution path.
            output = payload
            with self._lock:
                run = self._runs.get(run_id)
                if not run:
                    return
                elapsed = time.monotonic() - exec_started
                if elapsed > self._max_runtime_seconds:
                    run["state"] = AIRunState.TIMEOUT
                    run["error"] = "run_timeout_exceeded"
                    run["finished_at"] = _now_iso()
                    self._stats["timed_out"] += 1
                    return
                run["output"] = output
                run["state"] = AIRunState.COMPLETED
                run["finished_at"] = _now_iso()
                self._stats["completed"] += 1
        except Exception as exc:  # pragma: no cover - defensive
            with self._lock:
                run = self._runs.get(run_id)
                if not run:
                    return
                run["error"] = str(exc)
                run["state"] = AIRunState.FAILED
                run["finished_at"] = _now_iso()
                self._stats["failed"] += 1

    def _trim_old_runs_locked(self):
        if len(self._runs) <= self._max_runs:
            return
        ordered = sorted(
            self._runs.values(),
            key=lambda r: r.get("submitted_at") or "",
        )
        drop_count = len(self._runs) - self._max_runs
        for run in ordered[:drop_count]:
            self._runs.pop(run["run_id"], None)
            self._stats["dropped_trimmed"] += 1


def _age_seconds(ts_iso):
    if not ts_iso:
        return 0.0
    try:
        then = datetime.fromisoformat(str(ts_iso).replace("Z", "+00:00"))
    except Exception:
        return 0.0
    return max(0.0, (datetime.now(timezone.utc) - then).total_seconds())
