"""Provider-neutral contracts and helpers for scraper tools."""

from __future__ import annotations

from dataclasses import asdict, dataclass
from typing import Any, Callable, Dict, List, Protocol


PROVIDER_DISPLAY_NAMES: Dict[str, str] = {
    "apify": "Apify",
    "nimbleway": "Nimbleway",
}

PROVIDER_CREDENTIAL_KEYS: Dict[str, tuple[str, ...]] = {
    "apify": ("api_token",),
    "nimbleway": ("api_key", "social_auth"),
}


def provider_display_name(provider: str) -> str:
    return PROVIDER_DISPLAY_NAMES.get(provider, provider.replace("_", " ").title())


def get_provider_credential_keys(provider: str) -> tuple[str, ...]:
    return PROVIDER_CREDENTIAL_KEYS.get(provider, ("api_key",))


@dataclass(frozen=True)
class ToolField:
    key: str
    label: str
    required: bool = False
    type: str = "text"
    default: bool = False


@dataclass(frozen=True)
class ToolCatalogEntry:
    id: str
    provider: str
    display_name: str
    description: str
    provider_tool_id: str
    input_schema: List[ToolField]
    output_schema: List[ToolField]
    output_column_prefix: str
    transform: str

    def required_keys(self) -> List[str]:
        return [field.key for field in self.input_schema if field.required]

    def to_api_dict(self) -> Dict[str, Any]:
        return {
            "id": self.id,
            "tool_id": self.id,
            "provider": self.provider,
            "provider_display_name": provider_display_name(self.provider),
            "display_name": self.display_name,
            "description": self.description,
            "actor_id": self.provider_tool_id,
            "provider_tool_id": self.provider_tool_id,
            "input_schema": {
                "fields": [asdict(field) for field in self.input_schema],
            },
            "output_schema": {
                "fields": [asdict(field) for field in self.output_schema],
            },
            "output_column_prefix": self.output_column_prefix,
            "transform": self.transform,
        }


class ScraperProvider(Protocol):
    provider_name: str

    def list_tools(self) -> List[ToolCatalogEntry]:
        ...

    def get_tool(self, tool_id: str) -> ToolCatalogEntry:
        ...

    def validate_rows(
        self,
        rows: List[Dict[str, Any]],
        input_mapping: Dict[str, str],
        tool_id: str,
    ) -> Dict[str, int]:
        ...

    def extract_inputs(
        self,
        row: Dict[str, Any],
        input_mapping: Dict[str, str],
        tool: ToolCatalogEntry,
    ) -> Dict[str, Any]:
        ...

    def run_single(
        self,
        inputs: Dict[str, Any],
        tool: ToolCatalogEntry,
        output_fields: List[str],
    ) -> Dict[str, Any]:
        ...

    def run_test(
        self,
        rows: List[Dict[str, Any]],
        input_mapping: Dict[str, str],
        output_fields: List[str],
        tool_id: str,
        max_rows: int = 5,
    ) -> List[Dict[str, Any]]:
        ...


class ScraperProviderRegistry:
    def __init__(self) -> None:
        self._providers: Dict[str, Callable[..., ScraperProvider]] = {}

    def register(
        self,
        provider_name: str,
        provider_factory: Callable[..., ScraperProvider],
    ) -> None:
        self._providers[provider_name] = provider_factory

    def get_provider(self, provider_name: str, **kwargs: Any) -> ScraperProvider:
        try:
            factory = self._providers[provider_name]
        except KeyError as exc:
            raise ValueError(f"Unknown scraper provider: {provider_name}") from exc
        if not kwargs:
            return factory()
        try:
            return factory(**kwargs)
        except TypeError:
            return factory()

    def get_provider_for_tool(self, tool_id: str, **kwargs: Any) -> ScraperProvider:
        for provider_name in self._providers:
            provider = self.get_provider(provider_name, **kwargs)
            try:
                provider.get_tool(tool_id)
            except ValueError:
                continue
            return provider
        raise ValueError(f"Unknown scraper tool: {tool_id}")

    def list_all_tools(self) -> List[ToolCatalogEntry]:
        tools: List[ToolCatalogEntry] = []
        for provider_name in sorted(self._providers):
            provider = self.get_provider(provider_name)
            tools.extend(provider.list_tools())
        return tools


def normalize_row_status(status: Any) -> str:
    normalized = str(status or "scraped").strip().lower()
    if normalized in {"scraped", "skipped", "error"}:
        return normalized
    return "scraped"


def extract_inputs_from_row(
    row: Dict[str, Any],
    input_mapping: Dict[str, str],
    fields: List[ToolField],
) -> Dict[str, Any]:
    inputs: Dict[str, Any] = {}
    for field in fields:
        column = input_mapping.get(field.key)
        if not column:
            continue
        if column.startswith("__preset__:"):
            inputs[field.key] = column[len("__preset__:"):]
            continue
        value = row.get(column)
        if value is None:
            continue
        text = str(value).strip()
        if text == "":
            continue
        inputs[field.key] = text
    return inputs


def filter_output_fields(result: Dict[str, Any], output_fields: List[str]) -> Dict[str, Any]:
    if not output_fields:
        return result
    filtered = {key: result.get(key, "") for key in output_fields}
    if "error_message" in result:
        filtered["error_message"] = result["error_message"]
    for key, value in result.items():
        if key.startswith("__"):
            filtered[key] = value
    return filtered


def get_row_index(row: Dict[str, Any], fallback: int) -> int:
    value = row.get("__fm_row_index__")
    if value is None:
        return fallback
    try:
        return int(value)
    except (TypeError, ValueError):
        return fallback


def run_test_generic(
    provider: ScraperProvider,
    rows: List[Dict[str, Any]],
    input_mapping: Dict[str, str],
    output_fields: List[str],
    tool_id: str,
    max_rows: int = 5,
) -> List[Dict[str, Any]]:
    tool = provider.get_tool(tool_id)
    results: List[Dict[str, Any]] = []
    for idx, row in enumerate(rows[:max_rows]):
        row_index = get_row_index(row, idx)
        inputs = provider.extract_inputs(row, input_mapping, tool)
        if not all(inputs.get(key) for key in tool.required_keys()):
            results.append(
                {
                    "row_index": row_index,
                    "status": "skipped",
                    "error_message": "Missing required input",
                }
            )
            continue
        try:
            result = provider.run_single(inputs, tool, output_fields)
        except Exception as exc:  # noqa: BLE001
            results.append(
                {
                    "row_index": row_index,
                    "status": "error",
                    "error_message": str(exc),
                }
            )
            continue
        row_status = normalize_row_status(result.pop("__row_status__", "scraped"))
        result["row_index"] = row_index
        result["status"] = row_status
        results.append(result)
    return results


SCRAPER_PROVIDER_REGISTRY = ScraperProviderRegistry()


def ensure_default_scraper_providers_registered() -> None:
    if {"apify", "nimbleway"}.issubset(SCRAPER_PROVIDER_REGISTRY._providers.keys()):
        return
    from . import apify_adapter  # noqa: F401
    from . import nimbleway_adapter  # noqa: F401
