import asyncio
import inspect
import json
import logging
import os
import tempfile
import time
import uuid
from contextlib import suppress
from dataclasses import dataclass
from pathlib import Path
from typing import Any, AsyncIterator, Awaitable, Callable, Optional

logger = logging.getLogger("job_system")

try:  # pragma: no cover - optional dependency in tests
    from redis import asyncio as redis_asyncio  # type: ignore
except Exception:  # pragma: no cover - optional dependency in tests
    redis_asyncio = None


TERMINAL_STATUSES = {"completed", "failed", "cancelled"}
TERMINAL_EVENTS = {"done", "error", "cancelled"}

JOB_TYPES = (
    "completion",
    "pro_completion",
    "web_search",
    "compress",
    "ocr",
    "convert",
    "tts",
    "asr",
)


def _int_env(name: str, default: int) -> int:
    try:
        return max(1, int(os.getenv(name, str(default))))
    except (TypeError, ValueError):
        return default


DEFAULT_CONCURRENCY = {
    "completion": 2,
    "pro_completion": 1,
    "web_search": 1,
    "compress": 1,
    "ocr": 1,
    "convert": 1,
    "tts": _int_env("JOB_TTS_CONCURRENCY", 2),
    "asr": _int_env("JOB_ASR_CONCURRENCY", 1),
}

DEFAULT_QUEUE_SIZE = {
    "completion": 16,
    "pro_completion": 8,
    "web_search": 4,
    "compress": 8,
    "ocr": 8,
    "convert": 8,
    "tts": _int_env("JOB_TTS_MAX_QUEUE", 8),
    "asr": _int_env("JOB_ASR_MAX_QUEUE", 8),
}


class JobSystemError(RuntimeError):
    """任务系统内部错误"""

    def __init__(self, message: str = "任务系统错误", error_code: str = "job_system_error") -> None:
        super().__init__(message)
        self.message = message
        self.error_code = error_code

    def __str__(self) -> str:
        return f"{self.error_code}: {self.message}"


class QueueFullError(JobSystemError):
    """任务队列已满"""

    def __init__(self, job_type: str, max_queue: int) -> None:
        super().__init__(f"{job_type} 队列已满 (当前: {max_queue}/{max_queue})", "queue_full")
        self.job_type = job_type
        self.max_queue = max_queue


@dataclass(frozen=True)
class QueueConfig:
    job_type: str
    concurrency: int
    max_queue: int


Handler = Callable[[dict[str, Any], Callable[[str, dict[str, Any]], Awaitable[None]], Callable[[], bool]], Awaitable[dict[str, Any]]]


def _bool_env(name: str, default: bool) -> bool:
    value = os.getenv(name)
    if value is None:
        return default
    return value.strip().lower() in {"1", "true", "yes", "on"}


def _float_env(name: str, default: float) -> float:
    try:
        return float(os.getenv(name, str(default)))
    except (TypeError, ValueError):
        return default


def _now_ms() -> int:
    return int(time.time() * 1000)


def _busy_level(ratio: float) -> str:
    if ratio >= _float_env("JOB_BUSY_FULL_THRESHOLD", 1.0):
        return "full"
    if ratio >= _float_env("JOB_BUSY_HIGH_THRESHOLD", 0.75):
        return "busy"
    if ratio >= _float_env("JOB_BUSY_NORMAL_THRESHOLD", 0.25):
        return "normal"
    return "idle"


def _json_dumps(value: Any) -> str:
    return json.dumps(value, ensure_ascii=False)


def _json_loads(value: str | bytes | None, default: Any = None) -> Any:
    if value is None:
        return default
    if isinstance(value, bytes):
        value = value.decode("utf-8")
    if not value:
        return default
    return json.loads(value)


def _queue_config(job_type: str) -> QueueConfig:
    upper = job_type.upper()
    concurrency = _int_env(f"JOB_{upper}_CONCURRENCY", DEFAULT_CONCURRENCY[job_type])
    max_queue = _int_env(f"JOB_{upper}_MAX_QUEUE", DEFAULT_QUEUE_SIZE[job_type])
    return QueueConfig(job_type=job_type, concurrency=concurrency, max_queue=max_queue)


def get_job_backend_name() -> str:
    value = (os.getenv("JOB_BACKEND") or "").strip().lower()
    if value:
        return value
    if redis_asyncio is not None:
        return "redis"
    return "memory"


def _shared_temp_dir() -> Path:
    path = Path(os.getenv("JOB_SHARED_TEMP_DIR", tempfile.gettempdir()) or tempfile.gettempdir())
    path.mkdir(parents=True, exist_ok=True)
    return path


def persist_temp_input(raw_bytes: bytes, suffix: str) -> str:
    directory = _shared_temp_dir()
    fd, path = tempfile.mkstemp(prefix="job-input-", suffix=suffix, dir=directory)
    os.close(fd)
    with open(path, "wb") as handle:
        handle.write(raw_bytes)
    return path


async def _maybe_await(value: Any) -> Any:
    if inspect.isawaitable(value):
        return await value
    return value


class BaseJobManager:
    def __init__(self) -> None:
        self.handlers: dict[str, Handler] = {}

    def register_handler(self, job_type: str, handler: Handler) -> None:
        self.handlers[job_type] = handler

    async def submit(self, job_type: str, payload: dict[str, Any], request_id: str | None = None) -> str:
        raise NotImplementedError

    async def cancel(self, job_id: str, reason: str = "abort") -> dict[str, Any]:
        raise NotImplementedError

    async def get_status(self, job_id: str) -> dict[str, Any] | None:
        raise NotImplementedError

    async def stream_events(self, job_id: str) -> AsyncIterator[dict[str, Any]]:
        raise NotImplementedError

    async def close(self) -> None:
        return None


class InMemoryJobManager(BaseJobManager):
    def __init__(self) -> None:
        super().__init__()
        self.jobs: dict[str, dict[str, Any]] = {}
        self.event_history: dict[str, list[dict[str, Any]]] = {}
        self.subscribers: dict[str, list[asyncio.Queue]] = {}
        self.queues = {job_type: asyncio.Queue() for job_type in JOB_TYPES}
        self.semaphores = {job_type: asyncio.Semaphore(_queue_config(job_type).concurrency) for job_type in JOB_TYPES}
        self.queue_counts = {job_type: 0 for job_type in JOB_TYPES}
        self.running_counts = {job_type: 0 for job_type in JOB_TYPES}
        self.running_tasks: dict[str, asyncio.Task] = {}
        self.worker_tasks: list[asyncio.Task] = []
        self.started = False
        self.lock = asyncio.Lock()

    async def _ensure_started(self) -> None:
        if self.started:
            return
        self.started = True
        for job_type in JOB_TYPES:
            self.worker_tasks.append(asyncio.create_task(self._worker_loop(job_type)))

    def _metrics(self, job_type: str) -> dict[str, Any]:
        config = _queue_config(job_type)
        queued = self.queue_counts[job_type]
        running = self.running_counts[job_type]
        capacity = max(config.max_queue + config.concurrency, 1)
        ratio = min((queued + running) / capacity, 1.0)
        return {
            "queue_position": queued if queued > 0 else 0,
            "queued_count": queued,
            "running_count": running,
            "concurrency_limit": config.concurrency,
            "max_queue": config.max_queue,
            "busy_ratio": round(ratio, 4),
            "busy_level": _busy_level(ratio),
        }

    async def _publish(self, job_id: str, event: str, data: dict[str, Any]) -> None:
        event_payload = {"event": event, **data}
        self.event_history.setdefault(job_id, []).append(event_payload)
        for queue in self.subscribers.get(job_id, []):
            await queue.put(event_payload)

    async def submit(self, job_type: str, payload: dict[str, Any], request_id: str | None = None) -> str:
        await self._ensure_started()
        if job_type not in self.handlers:
            raise JobSystemError(f"missing handler for job type: {job_type}")

        async with self.lock:
            config = _queue_config(job_type)
            if self.queue_counts[job_type] >= config.max_queue:
                raise QueueFullError(job_type, config.max_queue)
            job_id = request_id or str(uuid.uuid4())
            self.jobs[job_id] = {
                "job_id": job_id,
                "request_id": job_id,
                "job_type": job_type,
                "status": "queued",
                "payload": payload,
                "result": None,
                "error": "",
                "cancel_requested": False,
                "created_at": _now_ms(),
                "updated_at": _now_ms(),
                "started_at": 0,
                "completed_at": 0,
                "queue_ms": 0,
                "run_ms": 0,
                "total_ms": 0,
            }
            self.event_history[job_id] = []
            self.queue_counts[job_type] += 1
            metrics = self._metrics(job_type)

        await self._publish(job_id, "queued", {"job_id": job_id, "type": job_type, "status": "queued", **metrics})
        await self.queues[job_type].put(job_id)
        return job_id

    async def cancel(self, job_id: str, reason: str = "abort") -> dict[str, Any]:
        async with self.lock:
            job = self.jobs.get(job_id)
            if not job:
                return {"cancelled": False, "status": "not_found"}
            if job["status"] in TERMINAL_STATUSES:
                return {"cancelled": False, "status": job["status"]}
            job["cancel_requested"] = True
            job["updated_at"] = _now_ms()
            task = self.running_tasks.get(job_id)
            if task and not task.done():
                task.cancel()
            if job["status"] == "queued":
                job["status"] = "cancelled"
                self.queue_counts[job["job_type"]] = max(0, self.queue_counts[job["job_type"]] - 1)
                metrics = self._metrics(job["job_type"])
            else:
                job["status"] = "cancelled"
                metrics = self._metrics(job["job_type"])

        await self._publish(job_id, "cancelled", {"job_id": job_id, "type": job["job_type"], "status": "cancelled", "reason": reason, **metrics})
        return {"cancelled": True, "status": "ok"}

    async def get_status(self, job_id: str) -> dict[str, Any] | None:
        job = self.jobs.get(job_id)
        if not job:
            return None
        metrics = self._metrics(job["job_type"])
        return {
            "job_id": job_id,
            "request_id": job["request_id"],
            "type": job["job_type"],
            "status": job["status"],
            "result": job["result"],
            "error": job["error"],
            "created_at": job.get("created_at", 0),
            "started_at": job.get("started_at", 0),
            "completed_at": job.get("completed_at", 0),
            "queue_ms": job.get("queue_ms", 0),
            "run_ms": job.get("run_ms", 0),
            "total_ms": job.get("total_ms", 0),
            **metrics,
        }

    async def stream_events(self, job_id: str) -> AsyncIterator[dict[str, Any]]:
        queue: asyncio.Queue = asyncio.Queue()
        self.subscribers.setdefault(job_id, []).append(queue)
        last_index = 0
        try:
            while True:
                history = list(self.event_history.get(job_id, []))
                while last_index < len(history):
                    item = history[last_index]
                    last_index += 1
                    yield item
                    if item.get("event") in TERMINAL_EVENTS:
                        return
                event = await queue.get()
                last_index = len(self.event_history.get(job_id, []))
                yield event
                if event["event"] in TERMINAL_EVENTS:
                    break
        finally:
            with suppress(ValueError):
                self.subscribers.get(job_id, []).remove(queue)

    async def _worker_loop(self, job_type: str) -> None:
        queue = self.queues[job_type]
        sem = self.semaphores[job_type]
        while True:
            job_id = await queue.get()
            async with self.lock:
                job = self.jobs.get(job_id)
                if not job or job["status"] == "cancelled":
                    continue
            await sem.acquire()
            task = asyncio.create_task(self._run_job(job_id))
            self.running_tasks[job_id] = task

    async def _run_job(self, job_id: str) -> None:
        job = self.jobs[job_id]
        job_type = job["job_type"]
        try:
            async with self.lock:
                if job["status"] == "cancelled":
                    return
                self.queue_counts[job_type] = max(0, self.queue_counts[job_type] - 1)
                self.running_counts[job_type] += 1
                job["status"] = "running"
                started_at = _now_ms()
                job["updated_at"] = started_at
                job["started_at"] = started_at
                job["queue_ms"] = max(0, started_at - int(job.get("created_at", started_at)))
                metrics = self._metrics(job_type)
            await self._publish(job_id, "started", {"job_id": job_id, "type": job_type, "status": "running", **metrics})

            async def emit(event: str, data: dict[str, Any]) -> None:
                metrics_now = self._metrics(job_type)
                await self._publish(job_id, event, {"job_id": job_id, "type": job_type, "status": job["status"], **metrics_now, **data})

            def is_cancelled() -> bool:
                return bool(job.get("cancel_requested"))

            job_payload = dict(job["payload"])
            job_payload["job_context"] = {
                "job_id": job_id,
                "created_at": int(job.get("created_at", 0) or 0),
                "started_at": int(job.get("started_at", 0) or 0),
                "queue_ms": int(job.get("queue_ms", 0) or 0),
            }
            result = await self.handlers[job_type](job_payload, emit, is_cancelled)
            async with self.lock:
                if job["cancel_requested"]:
                    job["status"] = "cancelled"
                    metrics = self._metrics(job_type)
                    await emit("cancelled", {"reason": "abort"})
                    return
                job["status"] = "completed"
                job["result"] = result
                completed_at = _now_ms()
                job["updated_at"] = completed_at
                job["completed_at"] = completed_at
                job["run_ms"] = max(0, completed_at - int(job.get("started_at", completed_at)))
                job["total_ms"] = max(0, completed_at - int(job.get("created_at", completed_at)))
                metrics = self._metrics(job_type)
            await self._publish(
                job_id,
                "done",
                {
                    "job_id": job_id,
                    "type": job_type,
                    "status": "completed",
                    "result": result,
                    "queue_ms": job.get("queue_ms", 0),
                    "run_ms": job.get("run_ms", 0),
                    "total_ms": job.get("total_ms", 0),
                    **metrics,
                },
            )
        except asyncio.CancelledError:
            async with self.lock:
                job["status"] = "cancelled"
                job["cancel_requested"] = True
                completed_at = _now_ms()
                job["updated_at"] = completed_at
                job["completed_at"] = completed_at
                job["run_ms"] = max(0, completed_at - int(job.get("started_at", completed_at)))
                job["total_ms"] = max(0, completed_at - int(job.get("created_at", completed_at)))
                metrics = self._metrics(job_type)
            await self._publish(job_id, "cancelled", {"job_id": job_id, "type": job_type, "status": "cancelled", **metrics})
            raise
        except Exception as exc:
            logger.exception("in-memory job failed job_id=%s type=%s", job_id, job_type)
            async with self.lock:
                job["status"] = "failed"
                job["error"] = str(exc)
                completed_at = _now_ms()
                job["updated_at"] = completed_at
                job["completed_at"] = completed_at
                job["run_ms"] = max(0, completed_at - int(job.get("started_at", completed_at)))
                job["total_ms"] = max(0, completed_at - int(job.get("created_at", completed_at)))
                metrics = self._metrics(job_type)
            await self._publish(
                job_id,
                "error",
                {
                    "job_id": job_id,
                    "type": job_type,
                    "status": "failed",
                    "error": str(exc),
                    "queue_ms": job.get("queue_ms", 0),
                    "run_ms": job.get("run_ms", 0),
                    "total_ms": job.get("total_ms", 0),
                    **metrics,
                },
            )
        finally:
            async with self.lock:
                self.running_counts[job_type] = max(0, self.running_counts[job_type] - 1)
            self.running_tasks.pop(job_id, None)
            self.semaphores[job_type].release()

    async def close(self) -> None:
        for task in self.worker_tasks:
            task.cancel()
        for task in self.running_tasks.values():
            task.cancel()
        for task in self.worker_tasks:
            with suppress(asyncio.CancelledError):
                await task
        self.worker_tasks.clear()
        self.running_tasks.clear()
        self.started = False


class RedisJobManager(BaseJobManager):
    def __init__(self) -> None:
        super().__init__()
        if redis_asyncio is None:
            raise JobSystemError("redis package is not installed")
        self.redis = redis_asyncio.from_url(
            os.getenv("REDIS_URL", "redis://localhost:6379/0"),
            encoding="utf-8",
            decode_responses=True,
        )
        self.prefix = (os.getenv("JOB_REDIS_PREFIX") or "llmtext:jobs").strip() or "llmtext:jobs"
        self.state_ttl = _int_env("JOB_STATE_TTL_SECONDS", 600)
        self.event_ttl = _int_env("JOB_EVENT_TTL_SECONDS", 600)

    def _queue_key(self, job_type: str) -> str:
        return f"{self.prefix}:queue:{job_type}"

    def _event_key(self, job_id: str) -> str:
        return f"{self.prefix}:events:{job_id}"

    def _state_key(self, job_id: str) -> str:
        return f"{self.prefix}:state:{job_id}"

    def _metrics_key(self, job_type: str) -> str:
        return f"{self.prefix}:metrics:{job_type}"

    def _group_name(self, job_type: str) -> str:
        return f"{self.prefix}:group:{job_type}"

    async def ensure_groups(self) -> None:
        for job_type in JOB_TYPES:
            stream = self._queue_key(job_type)
            group = self._group_name(job_type)
            try:
                await self.redis.xgroup_create(stream, group, id="0-0", mkstream=True)
            except Exception as exc:  # pragma: no cover - redis-specific
                if "BUSYGROUP" not in str(exc):
                    raise

    async def close(self) -> None:
        await self.redis.aclose()

    async def _metrics(self, job_type: str) -> dict[str, Any]:
        raw = await self.redis.hgetall(self._metrics_key(job_type))
        queued = int(raw.get("queued_count", "0") or 0)
        running = int(raw.get("running_count", "0") or 0)
        config = _queue_config(job_type)
        capacity = max(config.max_queue + config.concurrency, 1)
        ratio = min((queued + running) / capacity, 1.0)
        return {
            "queued_count": queued,
            "running_count": running,
            "concurrency_limit": config.concurrency,
            "max_queue": config.max_queue,
            "busy_ratio": round(ratio, 4),
            "busy_level": _busy_level(ratio),
        }

    async def _emit_event(self, job_id: str, event: str, data: dict[str, Any]) -> None:
        key = self._event_key(job_id)
        payload = {k: _json_dumps(v) if not isinstance(v, str) else v for k, v in data.items()}
        payload["event"] = event
        await self.redis.xadd(key, payload, maxlen=_int_env("JOB_EVENT_STREAM_MAXLEN", 512), approximate=True)
        await self.redis.expire(key, self.event_ttl)

    async def _set_state(self, job_id: str, state: dict[str, Any]) -> None:
        serializable = {k: _json_dumps(v) if isinstance(v, (dict, list)) else str(v) for k, v in state.items()}
        await self.redis.hset(self._state_key(job_id), mapping=serializable)
        await self.redis.expire(self._state_key(job_id), self.state_ttl)

    async def submit(self, job_type: str, payload: dict[str, Any], request_id: str | None = None) -> str:
        config = _queue_config(job_type)
        metrics = await self._metrics(job_type)
        if metrics["queued_count"] >= config.max_queue:
            raise QueueFullError(job_type, config.max_queue)

        job_id = request_id or str(uuid.uuid4())
        created_at = _now_ms()
        state = {
            "job_id": job_id,
            "request_id": job_id,
            "type": job_type,
            "status": "queued",
            "error": "",
            "created_at": created_at,
            "updated_at": created_at,
            "started_at": 0,
            "completed_at": 0,
            "queue_ms": 0,
            "run_ms": 0,
            "total_ms": 0,
            "cancel_requested": "0",
        }
        await self._set_state(job_id, state)
        await self.redis.hincrby(self._metrics_key(job_type), "queued_count", 1)
        await self.redis.expire(self._metrics_key(job_type), self.state_ttl)
        metrics = await self._metrics(job_type)
        await self._emit_event(job_id, "queued", {"job_id": job_id, "type": job_type, "status": "queued", **metrics})
        await self.redis.xadd(self._queue_key(job_type), {"job_id": job_id, "payload": _json_dumps(payload), "request_id": job_id})
        return job_id

    async def cancel(self, job_id: str, reason: str = "abort") -> dict[str, Any]:
        state = await self.get_status(job_id)
        if not state:
            return {"cancelled": False, "status": "not_found"}
        if state["status"] in TERMINAL_STATUSES:
            return {"cancelled": False, "status": state["status"]}
        await self.redis.hset(self._state_key(job_id), mapping={"cancel_requested": "1", "status": "cancelled", "updated_at": _now_ms(), "cancel_reason": reason})
        metrics = await self._metrics(state["type"])
        await self._emit_event(job_id, "cancelled", {"job_id": job_id, "type": state["type"], "status": "cancelled", "reason": reason, **metrics})
        return {"cancelled": True, "status": "ok"}

    async def get_status(self, job_id: str) -> dict[str, Any] | None:
        state = await self.redis.hgetall(self._state_key(job_id))
        if not state:
            return None
        job_type = state.get("type", "")
        metrics = await self._metrics(job_type) if job_type else {}
        result = state.get("result")
        error = state.get("error", "")
        return {
            "job_id": state.get("job_id", job_id),
            "request_id": state.get("request_id", job_id),
            "type": job_type,
            "status": state.get("status", "queued"),
            "error": error,
            "result": _json_loads(result, result),
            "cancel_requested": state.get("cancel_requested") == "1",
            "created_at": int(state.get("created_at", "0") or 0),
            "started_at": int(state.get("started_at", "0") or 0),
            "completed_at": int(state.get("completed_at", "0") or 0),
            "queue_ms": int(state.get("queue_ms", "0") or 0),
            "run_ms": int(state.get("run_ms", "0") or 0),
            "total_ms": int(state.get("total_ms", "0") or 0),
            **metrics,
        }

    async def stream_events(self, job_id: str) -> AsyncIterator[dict[str, Any]]:
        stream = self._event_key(job_id)
        last_id = "0-0"
        while True:
            events = await self.redis.xread({stream: last_id}, block=1000, count=20)
            if not events:
                state = await self.get_status(job_id)
                if state and state["status"] in TERMINAL_STATUSES:
                    break
                continue
            for _, entries in events:
                for entry_id, fields in entries:
                    last_id = entry_id
                    event_payload: dict[str, Any] = {}
                    for key, value in fields.items():
                        if key == "event":
                            event_payload[key] = value
                            continue
                        try:
                            event_payload[key] = json.loads(value)
                        except Exception:
                            event_payload[key] = value
                    yield event_payload
                    if event_payload.get("event") in TERMINAL_EVENTS:
                        return


class RedisWorker:
    def __init__(self, manager: RedisJobManager) -> None:
        self.manager = manager
        self.running_tasks: dict[str, asyncio.Task] = {}
        self.queue_semaphores = {job_type: asyncio.Semaphore(_queue_config(job_type).concurrency) for job_type in JOB_TYPES}
        self.poll_interval = _float_env("JOB_CANCEL_POLL_SECONDS", 0.5)
        self.consumer_name = (os.getenv("JOB_CONSUMER_NAME") or f"worker-{uuid.uuid4().hex[:8]}").strip()

    async def run_forever(self) -> None:
        await self.manager.ensure_groups()
        cancel_task = asyncio.create_task(self._cancel_watch_loop())
        consumers = [asyncio.create_task(self._consume_loop(job_type)) for job_type in JOB_TYPES]
        try:
            await asyncio.gather(*consumers)
        finally:
            cancel_task.cancel()
            with suppress(asyncio.CancelledError):
                await cancel_task

    async def _cancel_watch_loop(self) -> None:
        while True:
            await asyncio.sleep(self.poll_interval)
            for job_id, task in list(self.running_tasks.items()):
                state = await self.manager.get_status(job_id)
                if state and state.get("cancel_requested") and not task.done():
                    task.cancel()

    async def _consume_loop(self, job_type: str) -> None:
        queue_key = self.manager._queue_key(job_type)
        group = self.manager._group_name(job_type)
        semaphore = self.queue_semaphores[job_type]
        while True:
            try:
                streams = await self.manager.redis.xreadgroup(group, self.consumer_name, {queue_key: ">"}, count=1, block=1000)
            except asyncio.CancelledError:
                raise
            except Exception as exc:
                logger.warning("redis worker consume loop retrying type=%s error=%s", job_type, exc)
                await asyncio.sleep(1)
                continue
            if not streams:
                continue
            for _, messages in streams:
                for message_id, fields in messages:
                    await semaphore.acquire()
                    task = asyncio.create_task(self._run_message(job_type, queue_key, group, message_id, fields, semaphore))
                    self.running_tasks[fields["job_id"]] = task

    async def _run_message(
        self,
        job_type: str,
        queue_key: str,
        group: str,
        message_id: str,
        fields: dict[str, str],
        semaphore: asyncio.Semaphore,
    ) -> None:
        job_id = fields["job_id"]
        started_at = 0
        created_at = 0
        queue_ms = 0
        try:
            state = await self.manager.get_status(job_id)
            if not state or state["status"] == "cancelled":
                await self.manager.redis.xack(queue_key, group, message_id)
                return

            await self.manager.redis.hincrby(self.manager._metrics_key(job_type), "queued_count", -1)
            await self.manager.redis.hincrby(self.manager._metrics_key(job_type), "running_count", 1)
            started_at = _now_ms()
            created_at = int(state.get("created_at", 0) or 0)
            queue_ms = max(0, started_at - created_at)
            await self.manager._set_state(job_id, {
                "job_id": job_id,
                "request_id": state["request_id"],
                "type": job_type,
                "status": "running",
                "updated_at": started_at,
                "created_at": created_at or started_at,
                "started_at": started_at,
                "completed_at": 0,
                "queue_ms": queue_ms,
                "run_ms": 0,
                "total_ms": 0,
                "cancel_requested": "1" if state.get("cancel_requested") else "0",
                "error": "",
            })
            metrics = await self.manager._metrics(job_type)
            await self.manager._emit_event(job_id, "started", {"job_id": job_id, "type": job_type, "status": "running", **metrics})

            payload = _json_loads(fields["payload"], {})
            payload["job_context"] = {
                "job_id": job_id,
                "created_at": created_at,
                "started_at": started_at,
                "queue_ms": queue_ms,
            }

            async def emit(event: str, data: dict[str, Any]) -> None:
                live_state = await self.manager.get_status(job_id) or {"status": "running"}
                live_metrics = await self.manager._metrics(job_type)
                await self.manager._emit_event(job_id, event, {"job_id": job_id, "type": job_type, "status": live_state["status"], **live_metrics, **data})

            def is_cancelled() -> bool:
                task = self.running_tasks.get(job_id)
                return bool(task and task.cancelled())

            result = await self.manager.handlers[job_type](payload, emit, is_cancelled)
            current = await self.manager.get_status(job_id)
            if current and current["status"] == "cancelled":
                await self.manager.redis.xack(queue_key, group, message_id)
                return

            await self.manager._set_state(job_id, {
                "job_id": job_id,
                "request_id": state["request_id"],
                "type": job_type,
                "status": "completed",
                "updated_at": _now_ms(),
                "created_at": created_at or started_at,
                "started_at": started_at,
                "completed_at": _now_ms(),
                "queue_ms": queue_ms,
                "run_ms": max(0, _now_ms() - started_at),
                "total_ms": max(0, _now_ms() - (created_at or started_at)),
                "cancel_requested": "0",
                "error": "",
                "result": _json_dumps(result),
            })
            metrics = await self.manager._metrics(job_type)
            final_state = await self.manager.get_status(job_id) or {}
            await self.manager._emit_event(
                job_id,
                "done",
                {
                    "job_id": job_id,
                    "type": job_type,
                    "status": "completed",
                    "result": result,
                    "queue_ms": final_state.get("queue_ms", queue_ms),
                    "run_ms": final_state.get("run_ms", 0),
                    "total_ms": final_state.get("total_ms", 0),
                    **metrics,
                },
            )
            await self.manager.redis.xack(queue_key, group, message_id)
        except asyncio.CancelledError:
            cancelled_at = _now_ms()
            await self.manager.redis.hset(
                self.manager._state_key(job_id),
                mapping={
                    "status": "cancelled",
                    "cancel_requested": "1",
                    "updated_at": cancelled_at,
                    "completed_at": cancelled_at,
                    "run_ms": max(0, cancelled_at - started_at),
                    "total_ms": max(0, cancelled_at - (created_at or started_at)),
                },
            )
            metrics = await self.manager._metrics(job_type)
            await self.manager._emit_event(job_id, "cancelled", {"job_id": job_id, "type": job_type, "status": "cancelled", **metrics})
            await self.manager.redis.xack(queue_key, group, message_id)
            raise
        except Exception as exc:
            logger.exception("redis worker failed job_id=%s type=%s", job_id, job_type)
            state = await self.manager.get_status(job_id)
            request_id = state["request_id"] if state else job_id
            await self.manager._set_state(job_id, {
                "job_id": job_id,
                "request_id": request_id,
                "type": job_type,
                "status": "failed",
                "updated_at": _now_ms(),
                "created_at": created_at or (_now_ms() if state else _now_ms()),
                "started_at": started_at,
                "completed_at": _now_ms(),
                "queue_ms": queue_ms,
                "run_ms": max(0, _now_ms() - started_at),
                "total_ms": max(0, _now_ms() - (created_at or started_at)),
                "cancel_requested": "0",
                "error": str(exc),
            })
            metrics = await self.manager._metrics(job_type)
            final_state = await self.manager.get_status(job_id) or {}
            await self.manager._emit_event(
                job_id,
                "error",
                {
                    "job_id": job_id,
                    "type": job_type,
                    "status": "failed",
                    "error": str(exc),
                    "queue_ms": final_state.get("queue_ms", queue_ms),
                    "run_ms": final_state.get("run_ms", 0),
                    "total_ms": final_state.get("total_ms", 0),
                    **metrics,
                },
            )
            await self.manager.redis.xack(queue_key, group, message_id)
        finally:
            self.running_tasks.pop(job_id, None)
            await self.manager.redis.hincrby(self.manager._metrics_key(job_type), "running_count", -1)
            semaphore.release()


_job_manager: BaseJobManager | None = None


def get_job_manager() -> BaseJobManager:
    global _job_manager
    if _job_manager is None:
        backend = get_job_backend_name()
        if backend == "redis":
            _job_manager = RedisJobManager()
        else:
            _job_manager = InMemoryJobManager()
    return _job_manager


def reset_job_manager() -> None:
    global _job_manager
    manager = _job_manager
    if manager is not None:
        close = getattr(manager, "close", None)
        if close is not None:
            try:
                loop = asyncio.get_running_loop()
            except RuntimeError:
                try:
                    asyncio.run(close())
                except Exception:
                    pass
            else:
                loop.create_task(close())
    _job_manager = None
