#exonware/xwsystem/src/exonware/xwsystem/threading/async_primitives.py
"""
Company: eXonware.com
Author: eXonware Backend Team
Email: connect@exonware.com
Version: 0.9.0.23
Generation Date: September 04, 2025
Async-aware concurrency primitives and synchronization utilities.
"""

import asyncio
import logging
import time
import weakref
from contextlib import asynccontextmanager
from typing import Any, AsyncContextManager
from collections import defaultdict
from ..config.logging_setup import get_logger
logger = get_logger("xwsystem.threading.async_primitives")
# Performance-aware logging - only log if debug is enabled

def _debug_log(message: str) -> None:
    """Log debug message only if debug logging is enabled."""
    if logger.isEnabledFor(logging.DEBUG):
        logger.debug(message)


class AsyncRWLock:
    """
    Async read-write lock for concurrent read-heavy workloads.

    Allows multiple concurrent readers and exclusive writers.
    """

    def __init__(self):
        self._readers = 0
        self._writers_waiting = 0
        self._writer_active = False
        self._lock = asyncio.Lock()
        self._read_ready = asyncio.Condition(self._lock)
        self._write_ready = asyncio.Condition(self._lock)

    @asynccontextmanager
    async def read_lock(self):
        """Acquire read lock (concurrent reads allowed)."""
        async with self._read_ready:
            while self._writer_active or self._writers_waiting > 0:
                await self._read_ready.wait()
            self._readers += 1
        try:
            yield
        finally:
            async with self._read_ready:
                self._readers -= 1
                if self._readers == 0 and self._writers_waiting > 0:
                    self._write_ready.notify()

    @asynccontextmanager
    async def write_lock(self):
        """Acquire write lock (exclusive access)."""
        async with self._write_ready:
            self._writers_waiting += 1
            while self._readers > 0 or self._writer_active:
                await self._write_ready.wait()
            self._writer_active = True
            self._writers_waiting -= 1
        try:
            yield
        finally:
            async with self._write_ready:
                self._writer_active = False
                if self._writers_waiting > 0:
                    self._write_ready.notify()
                else:
                    self._read_ready.notify_all()

    async def acquire_read(self) -> None:
        """Acquire read lock manually (requires release_read())."""
        async with self._read_ready:
            while self._writer_active or self._writers_waiting > 0:
                await self._read_ready.wait()
            self._readers += 1

    async def release_read(self) -> None:
        """Release read lock acquired by acquire_read()."""
        async with self._read_ready:
            self._readers -= 1
            if self._readers == 0 and self._writers_waiting > 0:
                self._write_ready.notify()

    async def acquire_write(self) -> None:
        """Acquire write lock manually (requires release_write())."""
        async with self._write_ready:
            self._writers_waiting += 1
            while self._readers > 0 or self._writer_active:
                await self._write_ready.wait()
            self._writer_active = True
            self._writers_waiting -= 1

    async def release_write(self) -> None:
        """Release write lock acquired by acquire_write()."""
        async with self._write_ready:
            self._writer_active = False
            if self._writers_waiting > 0:
                self._write_ready.notify()
            else:
                self._read_ready.notify_all()

    def get_stats(self) -> dict[str, bool | int]:
        """Return current lock state for observability/debugging."""
        return {
            "readers": self._readers,
            "writers_waiting": self._writers_waiting,
            "writer_active": self._writer_active,
        }


class AsyncLock:
    """
    Enhanced async lock with timeout support and debugging capabilities.
    """

    def __init__(self, name: str | None = None):
        """
        Initialize async lock.
        Args:
            name: Optional name for debugging
        """
        self._lock = asyncio.Lock()
        self.name = name or f"AsyncLock-{id(self)}"
        self._acquired_at: float | None = None
        self._acquired_by: str | None = None

    async def acquire(self, timeout: float | None = None) -> bool:
        """
        Acquire the lock with optional timeout.
        Args:
            timeout: Maximum time to wait for lock (None = wait forever)
        Returns:
            True if lock was acquired, False if timeout
        """
        if timeout is None:
            await self._lock.acquire()
            self._acquired_at = time.time()
            self._acquired_by = f"Task-{id(asyncio.current_task())}"
            _debug_log(f"Lock {self.name} acquired by {self._acquired_by}")
            return True
        try:
            await asyncio.wait_for(self._lock.acquire(), timeout=timeout)
            self._acquired_at = time.time()
            self._acquired_by = f"Task-{id(asyncio.current_task())}"
            logger.debug(f"Lock {self.name} acquired by {self._acquired_by} (timeout: {timeout}s)")
            return True
        except TimeoutError:
            logger.warning(f"Lock {self.name} acquisition timeout after {timeout}s")
            return False

    def release(self) -> None:
        """Release the lock."""
        if self._acquired_at:
            duration = time.time() - self._acquired_at
            logger.debug(f"Lock {self.name} released by {self._acquired_by} (held for {duration:.3f}s)")
        self._lock.release()
        self._acquired_at = None
        self._acquired_by = None

    def locked(self) -> bool:
        """Check if lock is currently held."""
        return self._lock.locked()
    @asynccontextmanager

    async def acquire_with_timeout(self, timeout: float) -> AsyncContextManager[bool]:
        """
        Context manager for lock acquisition with timeout.
        Args:
            timeout: Maximum time to wait for lock
        Yields:
            True if lock was acquired, False if timeout
        """
        acquired = await self.acquire(timeout)
        try:
            yield acquired
        finally:
            if acquired:
                self.release()

    async def __aenter__(self):
        """Async context manager entry."""
        await self.acquire()
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        """Async context manager exit."""
        self.release()


class AsyncSemaphore:
    """
    Enhanced async semaphore with monitoring and debugging.
    """

    def __init__(self, value: int = 1, name: str | None = None):
        """
        Initialize async semaphore.
        Args:
            value: Initial semaphore value
            name: Optional name for debugging
        """
        self._semaphore = asyncio.Semaphore(value)
        self.name = name or f"AsyncSemaphore-{id(self)}"
        self._initial_value = value
        self._acquired_tasks: set[str] = set()

    async def acquire(self, timeout: float | None = None) -> bool:
        """
        Acquire semaphore with optional timeout.
        Args:
            timeout: Maximum time to wait (None = wait forever)
        Returns:
            True if acquired, False if timeout
        """
        task_id = f"Task-{id(asyncio.current_task())}"
        if timeout is None:
            await self._semaphore.acquire()
            self._acquired_tasks.add(task_id)
            logger.debug(f"Semaphore {self.name} acquired by {task_id} ({len(self._acquired_tasks)}/{self._initial_value})")
            return True
        try:
            await asyncio.wait_for(self._semaphore.acquire(), timeout=timeout)
            self._acquired_tasks.add(task_id)
            logger.debug(f"Semaphore {self.name} acquired by {task_id} (timeout: {timeout}s) ({len(self._acquired_tasks)}/{self._initial_value})")
            return True
        except TimeoutError:
            logger.warning(f"Semaphore {self.name} acquisition timeout after {timeout}s")
            return False

    def release(self) -> None:
        """Release semaphore."""
        task_id = f"Task-{id(asyncio.current_task())}"
        self._acquired_tasks.discard(task_id)
        self._semaphore.release()
        logger.debug(f"Semaphore {self.name} released by {task_id} ({len(self._acquired_tasks)}/{self._initial_value})")

    def locked(self) -> bool:
        """Check if semaphore is fully acquired."""
        return self._semaphore.locked()

    async def __aenter__(self):
        """Async context manager entry."""
        await self.acquire()
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        """Async context manager exit."""
        self.release()


class AsyncEvent:
    """
    Enhanced async event with timeout and debugging.
    """

    def __init__(self, name: str | None = None):
        """
        Initialize async event.
        Args:
            name: Optional name for debugging
        """
        self._event = asyncio.Event()
        self.name = name or f"AsyncEvent-{id(self)}"
        self._set_at: float | None = None
        self._waiting_tasks: set[str] = set()

    async def wait(self, timeout: float | None = None) -> bool:
        """
        Wait for event with optional timeout.
        Args:
            timeout: Maximum time to wait (None = wait forever)
        Returns:
            True if event was set, False if timeout
        """
        task_id = f"Task-{id(asyncio.current_task())}"
        self._waiting_tasks.add(task_id)
        try:
            if timeout is None:
                await self._event.wait()
                logger.debug(f"Event {self.name} received by {task_id}")
                return True
            try:
                await asyncio.wait_for(self._event.wait(), timeout=timeout)
                logger.debug(f"Event {self.name} received by {task_id} (timeout: {timeout}s)")
                return True
            except TimeoutError:
                logger.warning(f"Event {self.name} wait timeout after {timeout}s for {task_id}")
                return False
        finally:
            self._waiting_tasks.discard(task_id)

    def set(self) -> None:
        """Set the event."""
        self._set_at = time.time()
        waiting_count = len(self._waiting_tasks)
        self._event.set()
        logger.debug(f"Event {self.name} set, notifying {waiting_count} waiting tasks")

    def clear(self) -> None:
        """Clear the event."""
        self._event.clear()
        self._set_at = None
        logger.debug(f"Event {self.name} cleared")

    def is_set(self) -> bool:
        """Check if event is set."""
        return self._event.is_set()


class AsyncQueue:
    """
    Enhanced async queue with monitoring and timeout support.
    """

    def __init__(self, maxsize: int = 0, name: str | None = None):
        """
        Initialize async queue.
        Args:
            maxsize: Maximum queue size (0 = unlimited)
            name: Optional name for debugging
        """
        self._queue = asyncio.Queue(maxsize=maxsize)
        self.name = name or f"AsyncQueue-{id(self)}"
        self._maxsize = maxsize
        self._put_count = 0
        self._get_count = 0

    async def put(self, item: Any, timeout: float | None = None) -> bool:
        """
        Put item in queue with optional timeout.
        Args:
            item: Item to put in queue
            timeout: Maximum time to wait (None = wait forever)
        Returns:
            True if item was put, False if timeout
        """
        if timeout is None:
            await self._queue.put(item)
            self._put_count += 1
            logger.debug(f"Queue {self.name} put item #{self._put_count} (size: {self.qsize()})")
            return True
        try:
            await asyncio.wait_for(self._queue.put(item), timeout=timeout)
            self._put_count += 1
            logger.debug(f"Queue {self.name} put item #{self._put_count} (timeout: {timeout}s) (size: {self.qsize()})")
            return True
        except TimeoutError:
            logger.warning(f"Queue {self.name} put timeout after {timeout}s")
            return False

    async def get(self, timeout: float | None = None) -> Any | None:
        """
        Get item from queue with optional timeout.
        Args:
            timeout: Maximum time to wait (None = wait forever)
        Returns:
            Item from queue, or None if timeout
        """
        if timeout is None:
            item = await self._queue.get()
            self._get_count += 1
            logger.debug(f"Queue {self.name} got item #{self._get_count} (size: {self.qsize()})")
            return item
        try:
            item = await asyncio.wait_for(self._queue.get(), timeout=timeout)
            self._get_count += 1
            logger.debug(f"Queue {self.name} got item #{self._get_count} (timeout: {timeout}s) (size: {self.qsize()})")
            return item
        except TimeoutError:
            logger.warning(f"Queue {self.name} get timeout after {timeout}s")
            return None

    def put_nowait(self, item: Any) -> bool:
        """
        Put item in queue without waiting.
        Args:
            item: Item to put
        Returns:
            True if successful, False if queue is full
        """
        try:
            self._queue.put_nowait(item)
            self._put_count += 1
            logger.debug(f"Queue {self.name} put_nowait item #{self._put_count} (size: {self.qsize()})")
            return True
        except asyncio.QueueFull:
            logger.warning(f"Queue {self.name} is full, put_nowait failed")
            return False

    def get_nowait(self) -> Any | None:
        """
        Get item from queue without waiting.
        Returns:
            Item from queue, or None if queue is empty
        """
        try:
            item = self._queue.get_nowait()
            self._get_count += 1
            logger.debug(f"Queue {self.name} get_nowait item #{self._get_count} (size: {self.qsize()})")
            return item
        except asyncio.QueueEmpty:
            logger.debug(f"Queue {self.name} is empty, get_nowait returned None")
            return None

    def task_done(self) -> None:
        """Mark task as done."""
        self._queue.task_done()

    async def join(self) -> None:
        """Wait for all tasks to be done."""
        await self._queue.join()

    def qsize(self) -> int:
        """Get current queue size."""
        return self._queue.qsize()

    def empty(self) -> bool:
        """Check if queue is empty."""
        return self._queue.empty()

    def full(self) -> bool:
        """Check if queue is full."""
        return self._queue.full()


class AsyncCondition:
    """
    Async condition variable with timeout support.
    """

    def __init__(self, lock: AsyncLock | None = None, name: str | None = None):
        """
        Initialize async condition.
        Args:
            lock: Optional lock to use (creates new one if None)
            name: Optional name for debugging
        """
        self._lock = lock or AsyncLock()
        self._condition = asyncio.Condition(self._lock._lock)
        self.name = name or f"AsyncCondition-{id(self)}"
        self._waiting_tasks: set[str] = set()

    async def acquire(self, timeout: float | None = None) -> bool:
        """Acquire the underlying lock."""
        return await self._lock.acquire(timeout)

    def release(self) -> None:
        """Release the underlying lock."""
        self._lock.release()

    async def wait(self, timeout: float | None = None) -> bool:
        """
        Wait for condition with optional timeout.
        Args:
            timeout: Maximum time to wait (None = wait forever)
        Returns:
            True if notified, False if timeout
        """
        task_id = f"Task-{id(asyncio.current_task())}"
        self._waiting_tasks.add(task_id)
        try:
            if timeout is None:
                await self._condition.wait()
                logger.debug(f"Condition {self.name} notified to {task_id}")
                return True
            try:
                await asyncio.wait_for(self._condition.wait(), timeout=timeout)
                logger.debug(f"Condition {self.name} notified to {task_id} (timeout: {timeout}s)")
                return True
            except TimeoutError:
                logger.warning(f"Condition {self.name} wait timeout after {timeout}s for {task_id}")
                return False
        finally:
            self._waiting_tasks.discard(task_id)

    def notify(self, n: int = 1) -> None:
        """
        Notify n waiting tasks.
        Args:
            n: Number of tasks to notify
        """
        waiting_count = len(self._waiting_tasks)
        self._condition.notify(n)
        logger.debug(f"Condition {self.name} notified {min(n, waiting_count)} of {waiting_count} waiting tasks")

    def notify_all(self) -> None:
        """Notify all waiting tasks."""
        waiting_count = len(self._waiting_tasks)
        self._condition.notify_all()
        logger.debug(f"Condition {self.name} notified all {waiting_count} waiting tasks")

    async def __aenter__(self):
        """Async context manager entry."""
        await self.acquire()
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        """Async context manager exit."""
        self.release()


class AsyncResourcePool:
    """
    Async resource pool for managing limited resources.
    """

    def __init__(self, resources: list, name: str | None = None):
        """
        Initialize resource pool.
        Args:
            resources: List of resources to manage
            name: Optional name for debugging
        """
        self.name = name or f"AsyncResourcePool-{id(self)}"
        self._available = asyncio.Queue()
        self._in_use: dict[Any, str] = {}
        self._total_resources = len(resources)
        # Put all resources in the available queue
        for resource in resources:
            self._available.put_nowait(resource)
        logger.debug(f"Resource pool {self.name} initialized with {self._total_resources} resources")

    async def acquire(self, timeout: float | None = None) -> Any | None:
        """
        Acquire a resource from the pool.
        Args:
            timeout: Maximum time to wait (None = wait forever)
        Returns:
            Resource object, or None if timeout
        """
        task_id = f"Task-{id(asyncio.current_task())}"
        if timeout is None:
            resource = await self._available.get()
            self._in_use[resource] = task_id
            logger.debug(f"Resource pool {self.name} acquired resource by {task_id} ({len(self._in_use)}/{self._total_resources} in use)")
            return resource
        try:
            resource = await asyncio.wait_for(self._available.get(), timeout=timeout)
            self._in_use[resource] = task_id
            logger.debug(f"Resource pool {self.name} acquired resource by {task_id} (timeout: {timeout}s) ({len(self._in_use)}/{self._total_resources} in use)")
            return resource
        except TimeoutError:
            logger.warning(f"Resource pool {self.name} acquisition timeout after {timeout}s")
            return None

    def release(self, resource: Any) -> None:
        """
        Release a resource back to the pool.
        Args:
            resource: Resource to release
        """
        if resource in self._in_use:
            task_id = self._in_use.pop(resource)
            self._available.put_nowait(resource)
            logger.debug(f"Resource pool {self.name} released resource by {task_id} ({len(self._in_use)}/{self._total_resources} in use)")
        else:
            logger.warning(f"Resource pool {self.name} attempted to release unknown resource")
    @asynccontextmanager

    async def get_resource(self, timeout: float | None = None) -> AsyncContextManager[Any]:
        """
        Context manager for resource acquisition.
        Args:
            timeout: Maximum time to wait for resource
        Yields:
            Resource object, or None if timeout
        """
        resource = await self.acquire(timeout)
        try:
            yield resource
        finally:
            if resource is not None:
                self.release(resource)

    def available_count(self) -> int:
        """Get number of available resources."""
        return self._available.qsize()

    def in_use_count(self) -> int:
        """Get number of resources in use."""
        return len(self._in_use)

    def total_count(self) -> int:
        """Get total number of resources."""
        return self._total_resources
