"""Load tracker definitions from YAML data."""

import dataclasses

import yaml

from mariner import exceptions
from mariner.models import html, json, tracker, url
from mariner.types import YAML


def from_yaml(yaml_data: YAML) -> tracker.Definition:
    """Parse a YAML string into a tracker Definition.

    Args:
        yaml_data: Raw YAML content describing a tracker.

    Returns:
        A fully constructed Definition.

    """
    data = yaml.safe_load(yaml_data)

    metadata = tracker.Metadata(
        id=data["id"],
        name=data["name"],
        description=data["description"],
    )

    search = data["search"]
    response_type = _detect_response_type(search)

    if response_type == tracker.ResponseType.JSON:
        spec = _build_json_spec(data, search)
    else:
        spec = _build_html_spec(data, search)

    return tracker.Definition(metadata, spec)


def _detect_response_type(search: dict) -> tracker.ResponseType:
    """Detect the response type from the search paths."""
    for path in search.get("paths", []):
        response = path.get("response", {})
        if response.get("type") == "json":
            return tracker.ResponseType.JSON
    return tracker.ResponseType.HTML


def _build_html_spec(data: dict, search: dict) -> tracker.SearchSpec:
    """Build a SearchSpec for HTML-based trackers."""
    fields = search["fields"]

    tracker_id = data["id"]
    title = _require_html_field(tracker_id, fields, "title", "title_default")
    size = _require_html_field(tracker_id, fields, "size")
    date = _require_html_field(tracker_id, fields, "date")
    seeders = _require_html_field(tracker_id, fields, "seeders")
    leechers = _require_html_field(tracker_id, fields, "leechers")

    config = _extract_config(data)

    return tracker.SearchSpec(
        links=[url.Site(link) for link in data["links"]],
        paths=[
            tracker.SearchPath(
                path=url.Pathname(path["path"]),
                inputs=path.get("inputs") or search.get("inputs") or {},
            )
            for path in search["paths"]
        ],
        rows_selector=search["rows"]["selector"],
        rows_after=int(search["rows"].get("after", 0)),
        request_delay=float(data.get("requestDelay", 0)),
        fields=html.SearchFields(
            title=title,
            size=size,
            date=date,
            seeders=seeders,
            leechers=leechers,
            magnet=_extract_html_field(fields, "magnet"),
            download=(
                _extract_html_field(fields, "download_optional")
                or _extract_html_field(fields, "download")
            ),
            details=_extract_html_field(fields, "details"),
        ),
        config=config,
        download=_extract_download_spec(data),
    )


def _build_json_spec(data: dict, search: dict) -> tracker.SearchSpec:
    """Build a SearchSpec for JSON-based trackers."""
    raw_fields = search["fields"]
    all_fields = {
        name: _extract_json_field(field) for name, field in raw_fields.items()
    }

    known_names = {
        f.name for f in dataclasses.fields(json.SearchFields) if f.name != "extras"
    }
    known = {k: v for k, v in all_fields.items() if k in known_names}
    extras = {k: v for k, v in all_fields.items() if k not in known_names}

    for required in ("title", "size", "date", "seeders", "leechers"):
        if required not in known:
            raise exceptions.DefinitionFieldMissingError(data["id"], required)

    config = _extract_config(data)

    return tracker.SearchSpec(
        links=[url.Site(link) for link in data["links"]],
        paths=[
            tracker.SearchPath(
                path=url.Pathname(path["path"]),
                inputs=path.get("inputs") or search.get("inputs") or {},
                response_type=tracker.ResponseType.JSON,
            )
            for path in search["paths"]
        ],
        rows_selector=search["rows"]["selector"],
        rows_after=int(search["rows"].get("after", 0)),
        fields=json.SearchFields(**known, extras=extras),
        request_delay=float(data.get("requestDelay", 0)),
        config=config,
    )


def _extract_config(data: dict) -> dict[str, str]:
    """Extract default setting values from the definition's settings block."""
    config: dict[str, str] = {}
    for setting in data.get("settings", []):
        name = setting.get("name")
        default = setting.get("default")
        if name is not None and default is not None:
            config[name] = str(default)
    return config


def _extract_download_spec(data: dict) -> tracker.DownloadSpec | None:
    """Extract the download section from a tracker definition.

    The download section describes CSS selectors for resolving
    download/magnet links from a tracker's details page.
    """
    raw = data.get("download")
    if not raw:
        return None
    selectors_data = raw.get("selectors", [])
    if not selectors_data:
        return None
    selectors = [
        tracker.DownloadSelector(
            selector=s["selector"],
            attribute=s.get("attribute", "href"),
        )
        for s in selectors_data
    ]
    return tracker.DownloadSpec(selectors=selectors)


def _extract_json_field(field_data: dict) -> json.Field:
    """Extract a JSON field spec from raw YAML field data."""
    raw_filters = field_data.get("filters", [])
    parsed_filters = _extract_filters(raw_filters)

    selector = field_data.get("selector")

    return json.Field(
        selector=json.Selector(selector) if selector else None,
        text=str(raw_text)
        if (raw_text := field_data.get("text")) is not None
        else None,
        optional=bool(field_data.get("optional", False)),
        default=field_data.get("default"),
        filters=parsed_filters,
    )


def _require_html_field(
    tracker_id: str,
    fields: dict,
    name: str,
    *fallback_names: str,
) -> html.Field:
    """Extract a required HTML field, trying fallbacks if missing."""
    result = _extract_html_field(fields, name)
    for fallback in fallback_names:
        if result is not None:
            break
        result = _extract_html_field(fields, fallback)
    if result is None:
        raise exceptions.DefinitionFieldMissingError(tracker_id, name)
    return result


def _extract_html_field(fields: dict, name: str) -> html.Field | None:
    """Extract a field from raw YAML field data.

    Returns None if the field is not defined or uses a computed text
    template (no CSS selector).
    """
    field_data = fields.get(name)
    if not field_data:
        return None
    if "selector" not in field_data:
        return None
    raw_filters = field_data.get("filters", [])
    parsed_filters = _extract_filters(raw_filters)
    raw_remove = field_data.get("remove")
    return html.Field(
        attribute=field_data.get("attribute"),
        selector=field_data["selector"],
        remove=html.Selector(raw_remove) if raw_remove else None,
        filters=parsed_filters,
    )


def _extract_filters(
    raw_filters: list[dict[str, object]],
) -> list[html.FieldFilter]:
    """Extract a list of FieldFilters from raw YAML filter definitions."""
    filters = []
    for f in raw_filters:
        raw_args = f.get("args", [])
        if not isinstance(raw_args, list):
            raw_args = [raw_args]
        filters.append(
            html.FieldFilter(
                name=str(f["name"]),
                args=[str(a) for a in raw_args],
            ),
        )
    return filters
