"""Subscription source resolution and merge helpers."""

from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Literal

try:
    import tomllib  # Python 3.11+
except ImportError:
    import tomli as tomllib  # type: ignore

from .models import Feed
from .sources.opml import load_opml_feeds

SubscriptionsMode = Literal["auto", "merge", "external-only", "builtin-only"]

_MODE_ALIASES: dict[str, SubscriptionsMode] = {
    "auto": "auto",
    "merge": "merge",
    "external": "external-only",
    "external-only": "external-only",
    "configured": "external-only",
    "configured-only": "external-only",
    "builtin": "builtin-only",
    "builtin-only": "builtin-only",
    "internal": "builtin-only",
    "internal-only": "builtin-only",
}


@dataclass
class SubscriptionsData:
    """Unified subscriptions data loaded from TOML or OPML."""

    feeds: list[Feed] = field(default_factory=list)
    sites: list[dict[str, Any]] = field(default_factory=list)
    sites_defaults: dict[str, Any] = field(default_factory=dict)
    hackernews: dict[str, Any] = field(default_factory=dict)
    x_defaults: dict[str, Any] = field(default_factory=dict)
    x_users: list[dict[str, Any]] = field(default_factory=list)

    def to_toml_data(self) -> dict[str, Any]:
        """Convert to the TOML-like structure expected by existing loaders."""
        data: dict[str, Any] = {
            "feeds": [
                {"name": feed.title, "feed_url": feed.xml_url}
                for feed in self.feeds
            ]
        }
        if self.sites:
            data["sites"] = [dict(site) for site in self.sites]
        if self.sites_defaults:
            data["sites_defaults"] = dict(self.sites_defaults)
        if self.hackernews:
            data["hackernews"] = dict(self.hackernews)
        if self.x_defaults:
            data["x_defaults"] = dict(self.x_defaults)
        if self.x_users:
            data["x_users"] = [dict(item) for item in self.x_users]
        return data


def normalize_subscriptions_mode(mode: str | None) -> SubscriptionsMode:
    """Normalize a subscriptions mode string to a canonical value."""
    key = (mode or "auto").strip().lower()
    normalized = _MODE_ALIASES.get(key)
    if normalized is None:
        valid = ", ".join(("auto", "merge", "external-only", "builtin-only"))
        raise ValueError(f"Invalid subscriptions mode: {mode!r}. Valid modes: {valid}")
    return normalized


def resolve_builtin_subscriptions_path() -> Path | None:
    """Resolve the packaged or repository built-in subscriptions file."""
    module_dir = Path(__file__).resolve().parent

    packaged_builtin = module_dir / "_builtin_subscriptions.toml"
    if packaged_builtin.exists():
        return packaged_builtin

    repo_builtin = module_dir.parent.parent / "subscriptions" / "subscriptions.toml"
    if repo_builtin.exists():
        return repo_builtin

    return None


def resolve_subscription_sources(
    configured_path: str,
    mode: str | None = None,
) -> list[Path]:
    """Resolve existing subscription source files for the selected mode."""
    normalized_mode = normalize_subscriptions_mode(mode)
    configured = Path(configured_path)
    builtin = resolve_builtin_subscriptions_path()

    if normalized_mode == "auto":
        if configured.exists():
            return [configured]
        return [builtin] if builtin is not None else []

    if normalized_mode == "external-only":
        return [configured] if configured.exists() else []

    if normalized_mode == "builtin-only":
        return [builtin] if builtin is not None else []

    sources: list[Path] = []
    if configured.exists():
        sources.append(configured)
    if builtin is not None and not _same_path(configured, builtin):
        sources.append(builtin)
    return sources


def load_effective_subscriptions(
    configured_path: str,
    mode: str | None = None,
) -> SubscriptionsData:
    """Load subscriptions according to the selected mode."""
    normalized_mode = normalize_subscriptions_mode(mode)
    sources = resolve_subscription_sources(configured_path, normalized_mode)
    if not sources:
        raise FileNotFoundError(configured_path)

    if normalized_mode != "merge" or len(sources) == 1:
        return load_subscriptions_data(sources[0])

    merged = load_subscriptions_data(sources[-1])
    for source in reversed(sources[:-1]):
        merged = merge_subscriptions_data(load_subscriptions_data(source), merged)
    return merged


def load_subscriptions_data(path: str | Path) -> SubscriptionsData:
    """Load subscriptions data from TOML or OPML."""
    p = Path(path)
    if p.suffix.lower() == ".toml":
        with open(p, "rb") as f:
            return subscriptions_from_toml_data(tomllib.load(f))
    return SubscriptionsData(feeds=load_opml_feeds(str(p)))


def subscriptions_from_toml_data(data: dict[str, Any]) -> SubscriptionsData:
    """Convert parsed TOML data into the unified subscriptions structure."""
    feeds: list[Feed] = []
    raw_feeds = data.get("feeds", [])
    if not isinstance(raw_feeds, list):
        raw_feeds = [raw_feeds]
    for item in raw_feeds:
        if isinstance(item, dict) and item.get("feed_url"):
            feeds.append(
                Feed(
                    title=item.get("name", item["feed_url"]),
                    xml_url=item["feed_url"],
                )
            )

    raw_sites = data.get("sites", [])
    if not isinstance(raw_sites, list):
        raw_sites = [raw_sites]
    sites = [dict(item) for item in raw_sites if isinstance(item, dict)]

    raw_x_users = data.get("x_users", [])
    if not isinstance(raw_x_users, list):
        raw_x_users = [raw_x_users]
    x_users = [dict(item) for item in raw_x_users if isinstance(item, dict)]

    return SubscriptionsData(
        feeds=feeds,
        sites=sites,
        sites_defaults=_as_dict(data.get("sites_defaults")),
        hackernews=_as_dict(data.get("hackernews")),
        x_defaults=_as_dict(data.get("x_defaults")),
        x_users=x_users,
    )


def merge_subscriptions_data(
    primary: SubscriptionsData,
    secondary: SubscriptionsData,
) -> SubscriptionsData:
    """Merge two subscription sets, with primary taking precedence."""
    return SubscriptionsData(
        feeds=_dedupe_feeds(primary.feeds + secondary.feeds),
        sites=_dedupe_dict_list(primary.sites + secondary.sites, "url", "name"),
        sites_defaults={**secondary.sites_defaults, **primary.sites_defaults},
        hackernews={**secondary.hackernews, **primary.hackernews},
        x_defaults={**secondary.x_defaults, **primary.x_defaults},
        x_users=_dedupe_dict_list(primary.x_users + secondary.x_users, "username"),
    )


def _dedupe_feeds(feeds: list[Feed]) -> list[Feed]:
    seen: set[str] = set()
    deduped: list[Feed] = []
    for feed in feeds:
        key = feed.xml_url.strip()
        if not key or key in seen:
            continue
        seen.add(key)
        deduped.append(feed)
    return deduped


def _dedupe_dict_list(
    items: list[dict[str, Any]],
    *identity_keys: str,
) -> list[dict[str, Any]]:
    seen: set[str] = set()
    deduped: list[dict[str, Any]] = []

    for item in items:
        identity = _identity_for_item(item, identity_keys)
        if identity is None:
            deduped.append(dict(item))
            continue
        if identity in seen:
            continue
        seen.add(identity)
        deduped.append(dict(item))

    return deduped


def _identity_for_item(item: dict[str, Any], keys: tuple[str, ...]) -> str | None:
    for key in keys:
        value = item.get(key)
        if value:
            return f"{key}:{str(value).strip().casefold()}"
    return None


def _as_dict(value: Any) -> dict[str, Any]:
    return dict(value) if isinstance(value, dict) else {}


def _same_path(left: Path, right: Path) -> bool:
    try:
        return left.resolve() == right.resolve()
    except FileNotFoundError:
        return str(left) == str(right)
