from __future__ import annotations

import asyncio
import logging
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any

import aiohttp

logger = logging.getLogger("captcha_solver")


def _normalize_task_kind(task_kind: str) -> str:
    return str(task_kind).strip().lower()


def captcha_task_handler(*task_kinds: str):
    """Decorator for provider methods that create tasks for specific captcha kinds."""

    if not task_kinds:
        raise ValueError("captcha_task_handler requires at least one task kind")

    normalized_kinds = tuple(_normalize_task_kind(kind) for kind in task_kinds)

    def decorator(func):
        setattr(func, "__captcha_task_kinds__", normalized_kinds)
        return func

    return decorator


@dataclass(slots=True)
class CaptchaTaskResult:
    """Normalized task result returned by providers."""

    status: str
    solution: Any | None = None
    error: str | None = None
    raw: dict[str, Any] | None = None

    @property
    def token(self) -> str | None:
        """Backward-compatible alias for token-based captchas."""
        return self.solution if isinstance(self.solution, str) else None


@dataclass(slots=True)
class CaptchaTaskSpec:
    """Provider-agnostic task definition."""

    kind: str
    params: dict[str, Any]


class CaptchaSolver(ABC):
    """Abstract base class for captcha solvers."""

    _task_handler_map: dict[str, str] = {}

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)

        # Inherit handler map and allow subclasses to override kinds.
        inherited_handlers: dict[str, str] = {}
        for base in cls.__mro__[1:]:
            base_handlers = getattr(base, "_task_handler_map", None)
            if base_handlers:
                inherited_handlers.update(base_handlers)

        declared_handlers: dict[str, str] = {}
        for attr_name, member in cls.__dict__.items():
            task_kinds = getattr(member, "__captcha_task_kinds__", None)
            if not task_kinds:
                continue
            for task_kind in task_kinds:
                existing_attr = declared_handlers.get(task_kind)
                if existing_attr and existing_attr != attr_name:
                    raise ValueError(
                        f"Duplicate captcha handler for kind '{task_kind}' in {cls.__name__}: "
                        f"{existing_attr} and {attr_name}"
                    )
                declared_handlers[task_kind] = attr_name

        inherited_handlers.update(declared_handlers)
        cls._task_handler_map = inherited_handlers

    def __init__(self, api_key: str, **kwargs):
        self.api_key = api_key
        self.config = kwargs
        self._session: aiohttp.ClientSession | None = None
        self._connector: aiohttp.TCPConnector | None = None
        self._task_kind_by_id: dict[str, str] = {}

        self.timeout = kwargs.get("timeout", 30.0)
        self.max_retries = kwargs.get("max_retries", 3)
        self.retry_delay = kwargs.get("retry_delay", 2.0)

    async def _get_session(self) -> aiohttp.ClientSession:
        if self._session is None or self._session.closed:
            if self._connector is None or self._connector.closed:
                self._connector = aiohttp.TCPConnector(limit=10, ttl_dns_cache=300)
            self._session = aiohttp.ClientSession(
                connector=self._connector,
                timeout=aiohttp.ClientTimeout(total=self.timeout),
            )
        return self._session

    async def solve_recaptcha(
        self,
        website_url: str,
        site_key: str,
        is_enterprise: bool = False,
        **kwargs,
    ) -> str | None:
        """Convenience helper for recaptcha token flow."""
        result = await self.solve_task(
            CaptchaTaskSpec(
                kind="recaptcha_v3" if str(kwargs.get("version", "v2")).lower() == "v3" else "recaptcha_v2",
                params={
                    "website_url": website_url,
                    "site_key": site_key,
                    "is_enterprise": is_enterprise,
                    **kwargs,
                },
            ),
            **kwargs,
        )
        return result.token

    async def solve_task(self, task: CaptchaTaskSpec, **kwargs) -> CaptchaTaskResult:
        """Create a generic task and wait for final result."""
        task_id = await self.create_task(task, **kwargs)
        if not task_id:
            return CaptchaTaskResult(status="failed", error="task_creation_failed")
        return await self.wait_for_task_result(task_id, **kwargs)

    async def create_recaptcha_task(
        self,
        website_url: str,
        site_key: str,
        is_enterprise: bool = False,
        **kwargs,
    ) -> str | None:
        """Compatibility helper that creates a recaptcha task."""
        return await self.create_task(
            CaptchaTaskSpec(
                kind="recaptcha_v3" if str(kwargs.get("version", "v2")).lower() == "v3" else "recaptcha_v2",
                params={
                    "website_url": website_url,
                    "site_key": site_key,
                    "is_enterprise": is_enterprise,
                    **kwargs,
                },
            ),
            **kwargs,
        )

    async def create_task(
        self,
        task: CaptchaTaskSpec,
        **kwargs,
    ) -> str | None:
        """Dispatch task creation to a decorated provider handler."""
        kind = _normalize_task_kind(task.kind)
        handler_name = self._task_handler_map.get(kind)

        if not handler_name:
            logger.error("Unsupported captcha kind for %s: %s", type(self).__name__, kind)
            return None

        handler = getattr(self, handler_name, None)
        if handler is None:
            logger.error("Captcha handler not found on %s: %s", type(self).__name__, handler_name)
            return None

        task_id = await handler(task, **kwargs)
        if not task_id:
            return None

        task_id_str = str(task_id)
        self._task_kind_by_id[task_id_str] = kind
        return task_id_str

    @classmethod
    def supported_task_kinds(cls) -> list[str]:
        """Return task kinds supported by this provider class."""
        return sorted(cls._task_handler_map.keys())

    @classmethod
    def can_solve_task_kind(cls, task_kind: str) -> bool:
        """Check whether provider class supports this task kind."""
        return _normalize_task_kind(task_kind) in cls._task_handler_map

    def get_task_kind(self, task_id: str) -> str | None:
        """Return tracked task kind for a created task id."""
        return self._task_kind_by_id.get(str(task_id))

    @abstractmethod
    async def get_task_result(self, task_id: str, **kwargs) -> CaptchaTaskResult:
        """Fetch current status/result for an existing task."""

    async def wait_for_task_result(self, task_id: str, **kwargs) -> CaptchaTaskResult:
        """Poll task status until ready, failed, or timeout."""

        initial_wait = float(kwargs.get("initial_wait", getattr(self, "DEFAULT_INITIAL_WAIT", 0.0)))
        polling_delay = float(
            kwargs.get("polling_delay", getattr(self, "DEFAULT_POLLING_DELAY", 2.0))
        )
        max_attempts = int(kwargs.get("max_attempts", getattr(self, "DEFAULT_MAX_ATTEMPTS", 60)))

        if initial_wait > 0:
            await asyncio.sleep(initial_wait)

        for _ in range(max_attempts):
            result = await self.get_task_result(task_id, **kwargs)

            if result.status == "ready":
                return result

            if result.status == "processing":
                await asyncio.sleep(polling_delay)
                continue

            logger.error("Captcha task failed: %s", result.error or "unknown_error")
            return result

        logger.error("Captcha task timeout")
        return CaptchaTaskResult(status="failed", error="timeout")

    @abstractmethod
    async def get_balance(self) -> float:
        """Get the current account balance."""

    async def close(self) -> None:
        """Clean up resources."""
        self._task_kind_by_id.clear()
        if self._session:
            await self._session.close()
        if self._connector:
            await self._connector.close()

    async def __aenter__(self):
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        await self.close()
