# #region BlockingExecutorsModule [C:4] [TYPE Module] [SEMANTICS async, blocking, executors, run_blocking, threadpool]
# @defgroup Core Module group.
# @BRIEF Named bounded executors for sync DB/file/git work. All production blocking operations use
#        loop.run_in_executor via the run_blocking helper instead of default asyncio.to_thread.
# @LAYER Infrastructure
# @RELATION DEPENDS_ON -> [EXT:concurrent.futures:ThreadPoolExecutor]
# @RELATION DEPENDS_ON -> [EXT:asyncio:AbstractEventLoop]
# @RATIONALE asyncio.to_thread uses default executor without backpressure. Named bounded executors
#   prevent thread pool exhaustion under load. Semaphore per kind limits queue depth.
# @REJECTED Default asyncio.to_thread — no backpressure, no bounded queue, can exhaust thread pool.

import asyncio
from collections.abc import Callable
from concurrent.futures import ThreadPoolExecutor
from functools import partial
from typing import Any

from ..logger import logger

# Module-level executors — one per kind, created once.
_db_executor: ThreadPoolExecutor | None = None
_file_executor: ThreadPoolExecutor | None = None
_git_executor: ThreadPoolExecutor | None = None

# Per-kind semaphores for queue backpressure.
_db_semaphore: asyncio.Semaphore | None = None
_file_semaphore: asyncio.Semaphore | None = None
_git_semaphore: asyncio.Semaphore | None = None

# Default config — overridden by init_executors().
_DEFAULT_MAX_WORKERS = 10
_DEFAULT_QUEUE_TIMEOUT = 30.0


# #region init_executors [C:2] [TYPE Function]
# @ingroup Core
# @BRIEF Initialize named executors with given config. Called once at application startup.
# @PRE event loop is running.
# @POST Executors and semaphores are ready for use.
# @SIDE_EFFECT Creates ThreadPoolExecutor instances.
def init_executors(
    db_workers: int = 10,
    file_workers: int = 10,
    git_workers: int = 5,
    queue_timeout: float = 30.0,
) -> None:
    global _db_executor, _file_executor, _git_executor
    global _db_semaphore, _file_semaphore, _git_semaphore

    _db_executor = ThreadPoolExecutor(max_workers=db_workers, thread_name_prefix="db")
    _file_executor = ThreadPoolExecutor(max_workers=file_workers, thread_name_prefix="file")
    _git_executor = ThreadPoolExecutor(max_workers=git_workers, thread_name_prefix="git")

    _db_semaphore = asyncio.Semaphore(db_workers * 2)
    _file_semaphore = asyncio.Semaphore(file_workers * 2)
    _git_semaphore = asyncio.Semaphore(git_workers * 2)

    logger.reason("init_executors",
                  extra={"payload": {"db": db_workers, "file": file_workers, "git": git_workers, "queue_timeout": queue_timeout}})


# #endregion init_executors


# #region shutdown_executors [C:2] [TYPE Function]
# @ingroup Core
# @BRIEF Shut down all executors. Called at application shutdown.
# @POST All executors are shut down, pending futures cancelled.
# @SIDE_EFFECT Waits for running tasks up to timeout.
def shutdown_executors(wait: bool = True, cancel_futures: bool = True) -> None:
    global _db_executor, _file_executor, _git_executor
    for executor in (_db_executor, _file_executor, _git_executor):
        if executor is not None:
            executor.shutdown(wait=wait, cancel_futures=cancel_futures)
    _db_executor = _file_executor = _git_executor = None
    logger.reason("shutdown_executors", extra={"payload": {}})


# #endregion shutdown_executors


# #region _get_executor [C:1] [TYPE Function]
# @BRIEF Return named executor and semaphore for given kind.
# @PRE  init_executors has been called.
# @POST Returns (executor, semaphore) tuple.
def _get_executor(kind: str) -> tuple[ThreadPoolExecutor, asyncio.Semaphore | None]:
    if kind == "db":
        return _db_executor, _db_semaphore
    if kind == "file":
        return _file_executor, _file_semaphore
    if kind == "git":
        return _git_executor, _git_semaphore
    raise ValueError(f"Unknown executor kind: {kind}. Use 'db', 'file', or 'git'.")


# #endregion _get_executor


# #region run_blocking [C:4] [TYPE Function]
# @ingroup Core
# @BRIEF Execute a blocking function in a named bounded executor.
# @PRE  init_executors has been called. kind is one of 'db', 'file', 'git'.
# @POST fn(*args) executed in named executor. Returns result or raises timeout/exception.
# @SIDE_EFFECT  Acquires/releases per-kind semaphore. Runs function in thread pool.
# @RATIONALE  asyncio.to_thread does not accept an executor parameter and uses default
#   unbounded pool. run_blocking uses loop.run_in_executor with named executors and
#   semaphore-based backpressure.
# @REJECTED  asyncio.to_thread — default executor without queue backpressure.
async def run_blocking(
    kind: str,
    fn: Callable[..., Any],
    *args: Any,
    timeout: float | None = None,
    **kwargs: Any,
) -> Any:
    executor, semaphore = _get_executor(kind)

    if semaphore is not None:
        try:
            await asyncio.wait_for(semaphore.acquire(), timeout=timeout or _DEFAULT_QUEUE_TIMEOUT)
        except TimeoutError:
            logger.explore(f"run_blocking.{kind}",
                           extra={"error": f"Queue timeout ({timeout or _DEFAULT_QUEUE_TIMEOUT}s)"})
            raise TimeoutError(f"Blocking executor '{kind}' queue timeout after {timeout or _DEFAULT_QUEUE_TIMEOUT}s")

    try:
        loop = asyncio.get_running_loop()
        bound_fn = partial(fn, *args, **kwargs)
        result = await loop.run_in_executor(executor, bound_fn)
        return result
    except asyncio.CancelledError:
        logger.explore(f"run_blocking.{kind}",
                       extra={"error": "Cancelled — function continues in thread pool"})
        raise
    finally:
        if semaphore is not None:
            semaphore.release()


# #endregion run_blocking


# #region run_cpu_blocking [C:2] [TYPE Function]
# @ingroup Core
# @BRIEF Execute CPU-bound work in default executor (no bounded pool needed).
# @POST fn(*args) executed in default thread pool.
# @SIDE_EFFECT  Runs function in thread pool. Does not acquire semaphore.
async def run_cpu_blocking(fn: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
    loop = asyncio.get_running_loop()
    bound_fn = partial(fn, *args, **kwargs)
    return await loop.run_in_executor(None, bound_fn)


# #endregion run_cpu_blocking
# #endregion BlockingExecutorsModule
