from __future__ import annotations

import hashlib
import json
import re
import sys
import time
import tempfile
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional, Callable, Tuple
from urllib.parse import urlparse

from API.HTTP import HTTPClient, download_direct_file
from plugins.alldebrid.api import AllDebridClient, parse_magnet_or_hash, is_torrent_file
from PluginCore.base import Provider, SearchResult
from SYS.plugin_helpers import TablePluginMixin
from SYS.item_accessors import get_field as _extract_value
from SYS.utils import sanitize_filename
from SYS.logger import log
from SYS.models import DownloadError, PipeObject

_HOSTS_CACHE_TTL_SECONDS = 24 * 60 * 60
_RUNTIME_HOSTS_PAYLOAD: Optional[Dict[str, Any]] = None
_RUNTIME_HOSTS_FETCHED_AT: float = 0.0


def _plugin_dir() -> Path:
    try:
        return Path(__file__).resolve().parent
    except Exception:
        return Path(".")


def _legacy_hosts_cache_paths() -> Tuple[Path, ...]:
    try:
        repo_root = Path(__file__).resolve().parents[2]
        plugins_root = Path(__file__).resolve().parents[1]
    except Exception:
        return tuple()
    return (
        plugins_root / "API" / "data" / "alldebrid.json",
        repo_root / "API" / "data" / "alldebrid.json",
    )


def _hosts_cache_path() -> Path:
    # Keep this local to the plugin so plugin-specific cache/state stays bundled
    # with the plugin itself in portable installs.
    #
    # This file is expected to be the JSON payload shape from AllDebrid:
    #   {"status":"success","data":{"hosts":[...],"streams":[...],"redirectors":[...]}}
    return _plugin_dir() / "alldebrid.json"


def _resolve_hosts_cache_path() -> Path:
    path = _hosts_cache_path()
    try:
        if path.exists() and path.is_file():
            return path
    except Exception:
        return path

    for legacy in _legacy_hosts_cache_paths():
        try:
            if legacy.exists() and legacy.is_file():
                return legacy
        except Exception:
            continue
    return path


def _runtime_hosts_payload_is_fresh() -> bool:
    if not isinstance(_RUNTIME_HOSTS_PAYLOAD, dict) or not _RUNTIME_HOSTS_PAYLOAD:
        return False
    try:
        return (time.time() - float(_RUNTIME_HOSTS_FETCHED_AT)) < _HOSTS_CACHE_TTL_SECONDS
    except Exception:
        return False


def _load_hosts_payload() -> Optional[Dict[str, Any]]:
    if _runtime_hosts_payload_is_fresh():
        return _RUNTIME_HOSTS_PAYLOAD

    path = _resolve_hosts_cache_path()
    try:
        if not path.exists() or not path.is_file():
            return None
        payload = json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return None

    return payload if isinstance(payload, dict) else None


def _load_cached_domains(category: str) -> List[str]:
    """Load domain list from the runtime cache or bundled alldebrid.json snapshot.

    category: "hosts" | "streams" | "redirectors"
    """

    wanted = str(category or "").strip().lower()
    if wanted not in {"hosts", "streams", "redirectors"}:
        return []

    payload = _load_hosts_payload()
    if not isinstance(payload, dict):
        return []

    data = payload.get("data")
    if not isinstance(data, dict):
        # Back-compat for older cache shapes.
        data = payload
        if not isinstance(data, dict):
            return []

    raw_list = data.get(wanted)
    if not isinstance(raw_list, (list, dict)):
        return []

    out: List[str] = []
    seen: set[str] = set()

    domain_candidates: List[Any] = []
    if isinstance(raw_list, list):
        domain_candidates.extend(raw_list)
    else:
        for entry in raw_list.values():
            if isinstance(entry, dict):
                nested_domains = entry.get("domains")
                if isinstance(nested_domains, list):
                    domain_candidates.extend(nested_domains)
                elif isinstance(nested_domains, str):
                    domain_candidates.append(nested_domains)
            elif isinstance(entry, str):
                domain_candidates.append(entry)

    for d in domain_candidates:
        try:
            dom = str(d or "").strip().lower()
        except Exception:
            continue
        if not dom:
            continue
        if dom.startswith("http://") or dom.startswith("https://"):
            # Accidentally stored as a URL; normalize to hostname.
            try:
                p = urlparse(dom)
                dom = str(p.hostname or "").strip().lower()
            except Exception:
                continue
        if dom.startswith("www."):
            dom = dom[4:]
        if not dom or dom in seen:
            continue
        seen.add(dom)
        out.append(dom)
    return out


def _load_cached_hoster_domains() -> List[str]:
    # For URL routing (download-file), we intentionally use only the "hosts" list.
    # The "streams" list is extremely broad and would steal URLs from other providers.
    return _load_cached_domains("hosts")


def _fetch_hosts_payload_v4_hosts() -> Optional[Dict[str, Any]]:
    """Fetch the public AllDebrid hosts payload.

    This intentionally does NOT require an API key.
    Endpoint referenced by user: https://api.alldebrid.com/v4/hosts
    """

    url = "https://api.alldebrid.com/v4/hosts"
    try:
        with HTTPClient(timeout=20.0) as client:
            resp = client.get(url)
            resp.raise_for_status()
            data = resp.json()
            return data if isinstance(data, dict) else None
    except Exception as exc:
        log(f"[alldebrid] Failed to fetch hosts list: {exc}", file=sys.stderr)
        return None


def refresh_alldebrid_hoster_cache(*, force: bool = False) -> None:
    """Refresh the in-memory host-domain cache for the current process."""
    global _RUNTIME_HOSTS_PAYLOAD, _RUNTIME_HOSTS_FETCHED_AT

    if (not force) and _runtime_hosts_payload_is_fresh():
        return

    payload = _fetch_hosts_payload_v4_hosts()
    if isinstance(payload, dict) and payload:
        _RUNTIME_HOSTS_PAYLOAD = payload
        _RUNTIME_HOSTS_FETCHED_AT = time.time()


def _get_debrid_api_key(config: Dict[str, Any]) -> Optional[str]:
    """Read the canonical AllDebrid API key from config."""
    try:
        from SYS.config import get_debrid_api_key

        key = get_debrid_api_key(config, service="All-debrid")
        return key.strip() if key else None
    except Exception:
        return None


def _consume_bencoded_value(data: bytes, pos: int) -> int:
    if pos >= len(data):
        raise ValueError("Unexpected end of bencode")
    token = data[pos:pos + 1]
    if token == b"i":
        end = data.find(b"e", pos + 1)
        if end == -1:
            raise ValueError("Unterminated integer")
        return end + 1
    if token == b"l" or token == b"d":
        cursor = pos + 1
        while cursor < len(data):
            if data[cursor:cursor + 1] == b"e":
                return cursor + 1
            cursor = _consume_bencoded_value(data, cursor)
        raise ValueError("Unterminated list/dict")
    if token and b"0" <= token <= b"9":
        colon = data.find(b":", pos)
        if colon == -1:
            raise ValueError("Invalid string length")
        length = int(data[pos:colon])
        return colon + 1 + length
    raise ValueError("Unknown bencode token")


def _info_hash_from_torrent_bytes(data: bytes) -> Optional[str]:
    needle = b"4:info"
    idx = data.find(needle)
    if idx == -1:
        return None

    start = idx + len(needle)
    try:
        end = _consume_bencoded_value(data, start)
    except ValueError:
        return None

    info_bytes = data[start:end]
    try:
        return hashlib.sha1(info_bytes).hexdigest()
    except Exception:
        return None


def _fetch_torrent_bytes(target: str) -> Optional[bytes]:
    path_obj = Path(str(target))
    try:
        if path_obj.exists() and path_obj.is_file():
            return path_obj.read_bytes()
    except Exception:
        pass

    try:
        parsed = urlparse(target)
    except Exception:
        parsed = None

    if parsed is None or not parsed.scheme or parsed.scheme.lower() not in {"http", "https"}:
        return None

    if not target.lower().endswith(".torrent"):
        return None

    try:
        with HTTPClient(timeout=30.0) as client:
            response = client.get(target)
            return response.content
    except Exception as exc:
        log(f"Failed to download .torrent from {target}: {exc}", file=sys.stderr)
        return None


_ALD_MAGNET_PREFIX = "alldebrid:magnet:"


def _parse_alldebrid_magnet_id(target: str) -> Optional[int]:
    candidate = str(target or "").strip()
    if not candidate:
        return None

    # Handle various prefix variations: alldebrid:magnet: (standard), alldebrid🧲, or alldebrid:
    id_part = None
    if candidate.lower().startswith(_ALD_MAGNET_PREFIX):
        id_part = candidate[len(_ALD_MAGNET_PREFIX):].strip()
    elif candidate.startswith("alldebrid🧲"):
        id_part = candidate[len("alldebrid🧲"):].strip()
    elif candidate.lower().startswith("alldebrid:"):
        id_part = candidate[len("alldebrid:"):].strip()

    if id_part:
        try:
            return int(id_part)
        except Exception:
            return None
    return None


def resolve_magnet_spec(target: str) -> Optional[str]:
    """Resolve a magnet/hash/torrent URL into a magnet/hash string."""
    candidate = str(target or "").strip()
    if not candidate:
        return None

    parsed = parse_magnet_or_hash(candidate)
    if parsed:
        return parsed

    if is_torrent_file(candidate):
        torrent_bytes = _fetch_torrent_bytes(candidate)
        if not torrent_bytes:
            return None
        hash_value = _info_hash_from_torrent_bytes(torrent_bytes)
        if hash_value:
            return hash_value
    return None


def _looks_like_torrent_source(candidate: str) -> bool:
    value = str(candidate or "").strip()
    if not value:
        return False

    def _clean_segment(text: str) -> str:
        cleaned = text.split("#", 1)[0].split("?", 1)[0]
        return cleaned.strip().lower()

    paths: list[str] = []
    try:
        parsed = urlparse(value)
    except Exception:
        parsed = None

    if parsed and parsed.path:
        paths.append(_clean_segment(parsed.path))
    paths.append(_clean_segment(value))

    for path in paths:
        if path and is_torrent_file(path):
            return True
    return False


def _build_queued_magnet_item(
    *,
    magnet_spec: str,
    magnet_id: int,
    magnet_info: Dict[str, Any],
) -> Dict[str, Any]:
    title = str(
        magnet_info.get("filename")
        or magnet_info.get("name")
        or magnet_info.get("hash")
        or f"magnet-{magnet_id}"
    ).strip() or f"magnet-{magnet_id}"
    status_label = str(magnet_info.get("status") or "queued").strip() or "queued"
    status_tag = re.sub(r"[^a-z0-9]+", "-", status_label.lower()).strip("-") or "queued"

    metadata: Dict[str, Any] = {
        "magnet_id": magnet_id,
        "plugin": "alldebrid",
        "plugin_view": "files",
        "magnet_spec": magnet_spec,
        "source_url": magnet_spec,
        "status": status_label,
    }
    for key in ("filename", "name", "hash", "size", "statusCode", "ready"):
        value = magnet_info.get(key)
        if value is not None:
            metadata[key] = value

    return {
        "table": "alldebrid",
        "plugin": "alldebrid",
        "path": f"{_ALD_MAGNET_PREFIX}{magnet_id}",
        "title": title,
        "detail": f"Queued in AllDebrid ({status_label})",
        "media_kind": "folder",
        "tag": ["folder", f"status:{status_tag}", "provider:alldebrid"],
        "columns": [
            ("Title", title),
            ("Status", status_label),
            ("Provider", "alldebrid"),
            ("Magnet ID", magnet_id),
        ],
        "full_metadata": metadata,
        "source_url": magnet_spec,
        "_selection_action": ["search-file", "-plugin", "alldebrid", f"ID={magnet_id}"],
    }


def prepare_magnet(
    magnet_spec: str,
    config: Dict[str, Any],
) -> tuple[Optional[AllDebridClient], Optional[int], Dict[str, Any]]:
    api_key = _get_debrid_api_key(config or {})
    if not api_key:
        log("AllDebrid API key not configured. Use .config to set it.", file=sys.stderr)
        return None, None, {}

    try:
        client = AllDebridClient(api_key)
    except Exception as exc:
        log(f"Failed to initialize AllDebrid client: {exc}", file=sys.stderr)
        return None, None, {}

    try:
        magnet_info = client.magnet_add(magnet_spec)
        magnet_id_val = magnet_info.get("id") or 0
        magnet_id = int(magnet_id_val)
        if magnet_id <= 0:
            log(f"AllDebrid magnet submission failed: {magnet_info}", file=sys.stderr)
            return None, None, {}
    except Exception as exc:
        log(f"Failed to submit magnet to AllDebrid: {exc}", file=sys.stderr)
        return None, None, {}

    return client, magnet_id, magnet_info if isinstance(magnet_info, dict) else {}


def _flatten_files_with_relpath(items: Any) -> Iterable[Dict[str, Any]]:
    for node in AllDebrid._flatten_files(items):
        enriched = dict(node)
        rel = node.get("_relpath") or node.get("relpath")
        if not rel:
            name = node.get("n") or node.get("name")
            rel = str(name or "").strip()
        enriched["relpath"] = rel
        yield enriched


def download_magnet(
    magnet_spec: str,
    original_url: str,
    final_output_dir: Path,
    config: Dict[str, Any],
    progress: Any,
    quiet_mode: bool,
    path_from_result: Callable[[Any], Path],
    on_emit: Callable[[Path, str, str, Dict[str, Any]], None],
) -> tuple[int, Optional[int]]:
    client, magnet_id, _magnet_info = prepare_magnet(magnet_spec, config)
    if client is None or magnet_id is None:
        return 0, None

    wait_timeout = 300
    try:
        streaming_config = config.get("streaming", {}) if isinstance(config, dict) else {}
        wait_timeout = int(streaming_config.get("wait_timeout", 300))
    except Exception:
        wait_timeout = 300

    elapsed = 0
    while elapsed < wait_timeout:
        try:
            status = client.magnet_status(magnet_id)
        except Exception as exc:
            log(f"Failed to read magnet status {magnet_id}: {exc}", file=sys.stderr)
            return 0, magnet_id
        ready = bool(status.get("ready")) or status.get("statusCode") == 4
        if ready:
            break
        time.sleep(5)
        elapsed += 5
    else:
        log(f"AllDebrid magnet {magnet_id} timed out after {wait_timeout}s", file=sys.stderr)
        return 0, magnet_id

    try:
        files_result = client.magnet_links([magnet_id])
    except Exception as exc:
        log(f"Failed to list AllDebrid magnet files: {exc}", file=sys.stderr)
        return 0, magnet_id

    magnet_files = files_result.get(str(magnet_id), {}) if isinstance(files_result, dict) else {}
    file_nodes = magnet_files.get("files") if isinstance(magnet_files, dict) else []
    if not file_nodes:
        log(f"AllDebrid magnet {magnet_id} produced no files", file=sys.stderr)
        return 0, magnet_id

    downloaded = 0
    for node in _flatten_files_with_relpath(file_nodes):
        file_url = str(node.get("link") or "").strip()
        file_name = str(node.get("name") or "").strip()
        relpath = str(node.get("relpath") or file_name).strip()
        if not file_url or not relpath:
            continue

        target_path = final_output_dir
        rel_path_obj = Path(relpath)
        output_dir = target_path
        if rel_path_obj.parent:
            output_dir = target_path / rel_path_obj.parent
        try:
            output_dir.mkdir(parents=True, exist_ok=True)
        except Exception:
            output_dir = target_path

        try:
            result_obj = download_direct_file(
                file_url,
                output_dir,
                quiet=quiet_mode,
                suggested_filename=rel_path_obj.name,
                pipeline_progress=progress,
            )
        except Exception as exc:
            log(f"Failed to download AllDebrid file {file_url}: {exc}", file=sys.stderr)
            continue

        downloaded_path = path_from_result(result_obj)
        metadata = {
            "magnet_id": magnet_id,
            "relpath": relpath,
            "name": file_name,
        }
        on_emit(downloaded_path, file_url or original_url, relpath, metadata)
        downloaded += 1

    return downloaded, magnet_id


def expand_folder_item(
    item: Any,
    get_plugin: Optional[Callable[[str, Dict[str, Any]], Any]],
    config: Dict[str, Any],
) -> Tuple[List[Any], Optional[str]]:
    table = getattr(item, "table", None) if not isinstance(item, dict) else item.get("table")
    media_kind = getattr(item, "media_kind", None) if not isinstance(item, dict) else item.get("media_kind")
    full_metadata = getattr(item, "full_metadata", None) if not isinstance(item, dict) else item.get("full_metadata")
    target = None
    if isinstance(item, dict):
        target = item.get("path") or item.get("url")
    else:
        target = getattr(item, "path", None) or getattr(item, "url", None)

    if (str(table or "").lower() != "alldebrid") or (str(media_kind or "").lower() != "folder"):
        return [], None

    magnet_id = None
    if isinstance(full_metadata, dict):
        magnet_id = full_metadata.get("magnet_id")
    if magnet_id is None and isinstance(target, str) and target.lower().startswith("alldebrid:magnet:"):
        try:
            magnet_id = int(target.split(":")[-1])
        except Exception:
            magnet_id = None

    if magnet_id is None or get_plugin is None:
        return [], None

    plugin = get_plugin("alldebrid", config) if get_plugin else None
    if plugin is None:
        return [], None

    try:
        files = plugin.search("*", limit=10_000, filters={"view": "files", "magnet_id": int(magnet_id)})
    except Exception:
        files = []

    if files and len(files) == 1 and getattr(files[0], "media_kind", "") == "folder":
        detail = getattr(files[0], "detail", "")
        return [], str(detail or "unknown")

    expanded: List[Any] = []
    for sr in files:
        expanded.append(sr.to_dict() if hasattr(sr, "to_dict") else sr)
    return expanded, None


def adjust_output_dir_for_alldebrid(
    base_output_dir: Path,
    full_metadata: Optional[Dict[str, Any]],
    item: Any,
) -> Path:
    from SYS.utils import sanitize_filename as _sf

    output_dir = base_output_dir
    md = full_metadata if isinstance(full_metadata, dict) else {}
    magnet_name = md.get("magnet_name") or md.get("folder")
    if not magnet_name:
        try:
            detail_val = getattr(item, "detail", None) if not isinstance(item, dict) else item.get("detail")
            magnet_name = str(detail_val or "").strip() or None
        except Exception:
            magnet_name = None

    magnet_dir_name = _sf(str(magnet_name)) if magnet_name else ""
    try:
        base_tail = str(Path(output_dir).name or "")
    except Exception:
        base_tail = ""
    base_tail_norm = _sf(base_tail).lower() if base_tail.strip() else ""
    magnet_dir_norm = magnet_dir_name.lower() if magnet_dir_name else ""

    if magnet_dir_name and (not base_tail_norm or base_tail_norm != magnet_dir_norm):
        output_dir = Path(output_dir) / magnet_dir_name

    relpath = md.get("relpath") if isinstance(md, dict) else None
    if (not relpath) and isinstance(md.get("file"), dict):
        relpath = md["file"].get("_relpath")

    if relpath:
        parts = [p for p in str(relpath).replace("\\", "/").split("/") if p and p not in {".", ".."}]
        if magnet_dir_name and parts:
            try:
                if _sf(parts[0]).lower() == magnet_dir_norm:
                    parts = parts[1:]
            except Exception:
                pass
        for part in parts[:-1]:
            output_dir = Path(output_dir) / _sf(part)

    try:
        Path(output_dir).mkdir(parents=True, exist_ok=True)
    except Exception:
        output_dir = base_output_dir

    return output_dir


class AllDebrid(TablePluginMixin, Provider):
    """AllDebrid account provider with magnet folder/file browsing and downloads.
    
    This provider uses the new table system (strict ResultTable adapter pattern) for
    consistent selection and auto-stage integration across all providers. It exposes
    magnets as folder rows and files as file rows, with metadata enrichment for:
    - magnet_id: For routing to _download_magnet_by_id
    - status/ready: For showing sync state
    - _selection_args/_selection_action: For @N expansion control
    - relpath: For proper file hierarchy in downloads
    
    KEY FEATURES:
    - Table system: Using ResultTable adapter for strict column/metadata handling
    - Selection override: Full metadata control via _selection_args/_selection_action
    - Auto-stages: download-file is auto-inserted when @N is used on magnet folders
    - File unlocking: URLs with /f/ paths are automatically unlocked via API before download
    - Drill-down: Selecting a folder row (@N) fetches and displays all files
    
    SELECTION FLOW:
    1. User runs: search-file -plugin alldebrid "ubuntu"
    2. Results show magnet folders and (optionally) files
    3. User selects a row: @1
    4. Selection metadata routes to download-file with -url alldebrid:magnet:<id>
    5. download-file invokes provider.download_items() via provider URL handling
    6. Provider fetches files, unlocks locked URLs, and downloads
    """
    # Magnet URIs should be routed through this provider.
    TABLE_AUTO_STAGES = {"alldebrid": ["download-file"]}
    AUTO_STAGE_USE_SELECTION_ARGS = True
    URL = ("magnet:", "alldebrid:magnet:", "alldebrid:", "alldebrid🧲", "alldebrid.com")
    URL_DOMAINS = ()
    SUPPORTED_CMDLETS = frozenset({"download-file", "search-file"})

    def extract_query_arguments(self, query: str) -> Tuple[str, Dict[str, Any]]:
        normalized = str(query or "").strip()
        filters: Dict[str, Any] = {}

        # Pull out id=123 or id:123
        match = re.search(r"\bid\s*[=:]\s*(\d+)", normalized, flags=re.IGNORECASE)
        if match:
            filters["magnet_id"] = int(match.group(1))
            normalized = re.sub(
                r"\bid\s*[=:]\s*\d+", "", normalized, flags=re.IGNORECASE
            ).strip()

        if not normalized:
            normalized = "*"

        return normalized, filters

    def get_table_title(self, query: str, filters: Optional[Dict[str, Any]] = None) -> str:
        f = filters or {}
        magnet_id = f.get("magnet_id")
        if magnet_id is not None:
            return f"{self.label} Files: {magnet_id}"
        q = str(query or "").strip() or "*"
        return f"{self.label}: {q}"

    def get_table_metadata(
        self, query: str, filters: Optional[Dict[str, Any]] = None
    ) -> Dict[str, Any]:
        meta = super().get_table_metadata(query, filters)
        f = filters or {}
        magnet_id = f.get("magnet_id")
        meta["view"] = "files" if magnet_id is not None else "folders"
        if magnet_id is not None:
            meta["magnet_id"] = magnet_id
        return meta

    @classmethod
    def config_schema(cls) -> List[Dict[str, Any]]:
        return [
            {
                "key": "api_key",
                "label": "API Key",
                "default": "",
                "required": True,
                "secret": True
            }
        ]

    @staticmethod
    def _resolve_magnet_spec_from_result(result: Any) -> Optional[str]:
        table = getattr(result, "table", None)
        media_kind = getattr(result, "media_kind", None)
        tags = getattr(result, "tag", None)
        full_metadata = getattr(result, "full_metadata", None)
        target = getattr(result, "path", None) or getattr(result, "url", None)

        if not table or str(table).strip().lower() != "alldebrid":
            return None

        kind_val = str(media_kind or "").strip().lower()
        is_folder = kind_val == "folder"
        if not is_folder and isinstance(tags, (list, set)):
            for tag in tags:
                if str(tag or "").strip().lower() == "folder":
                    is_folder = True
                    break
        if not is_folder:
            return resolve_magnet_spec(str(target or "")) if isinstance(target, str) else None

        metadata = full_metadata if isinstance(full_metadata, dict) else {}
        candidates: List[str] = []

        def _maybe_add(value: Any) -> None:
            if isinstance(value, str):
                cleaned = value.strip()
                if cleaned:
                    candidates.append(cleaned)

        magnet_block = metadata.get("magnet")
        if isinstance(magnet_block, dict):
            for inner in ("magnet", "magnet_link", "link", "url"):
                _maybe_add(magnet_block.get(inner))
            for inner in ("hash", "info_hash", "torrenthash", "magnethash"):
                _maybe_add(magnet_block.get(inner))
        else:
            _maybe_add(magnet_block)

        for extra in ("magnet_link", "magnet_url", "magnet_spec"):
            _maybe_add(metadata.get(extra))
        _maybe_add(metadata.get("hash"))
        _maybe_add(metadata.get("info_hash"))

        for candidate in candidates:
            spec = resolve_magnet_spec(candidate)
            if spec:
                return spec
        return resolve_magnet_spec(str(target)) if isinstance(target, str) else None

    def handle_url(self, url: str, *, output_dir: Optional[Path] = None) -> Tuple[bool, Optional[Path | Dict[str, Any]]]:
        magnet_id = _parse_alldebrid_magnet_id(url)
        if magnet_id is not None:
            return True, {
                "action": "download_items",
                "path": f"{_ALD_MAGNET_PREFIX}{magnet_id}",
                "title": f"magnet-{magnet_id}",
                "metadata": {
                    "magnet_id": magnet_id,
                    "plugin": "alldebrid",
                    "plugin_view": "files",
                },
            }

        spec = resolve_magnet_spec(url)
        if not spec:
            if _looks_like_torrent_source(url):
                log(f"[alldebrid] Torrent source ignored (unable to parse magnet/hash): {url}", file=sys.stderr)
            return False, None

        cfg = self.config if isinstance(self.config, dict) else {}
        try:
            _client, magnet_id, magnet_info = prepare_magnet(spec, cfg)
            if magnet_id is None:
                return False, None
            return True, {
                "action": "emit_items",
                "items": [
                    _build_queued_magnet_item(
                        magnet_spec=str(url),
                        magnet_id=int(magnet_id),
                        magnet_info=magnet_info,
                    )
                ],
                "exit_code": 0,
            }
        except Exception:
            return False, None

    @classmethod
    def url_patterns(cls) -> Tuple[str, ...]:
        # Combine static patterns with cached host domains.
        patterns = list(super().url_patterns())
        try:
            cached = _load_cached_hoster_domains()
            for d in cached:
                dom = str(d or "").strip().lower()
                if dom and dom not in patterns:
                    patterns.append(dom)
        except Exception:
            pass
        return tuple(patterns)

    """Search provider for AllDebrid account content.

    This provider lists and searches the files/magnets already present in the
    user's AllDebrid account.

    Query behavior:
    - "*" / "all" / "list": list recent files from ready magnets
    - otherwise: substring match on file name OR magnet name, or exact magnet id
    """

    def validate(self) -> bool:
        # Consider "available" when configured; actual API connectivity can vary.
        ok = bool(_get_debrid_api_key(self.config or {}))
        if ok:
            # Best-effort: refresh cached host domains so future URL routing can
            # route supported hosters through this provider.
            try:
                refresh_alldebrid_hoster_cache(force=False)
            except Exception:
                pass
        return ok

    def download(self, result: SearchResult, output_dir: Path) -> Optional[Path]:
        """Download an AllDebrid SearchResult into output_dir.

        AllDebrid magnet file listings often provide links that require an API
        "unlock" step to produce a true direct-download URL. Without unlocking,
        callers may download a small HTML/redirect page instead of file bytes.

        This is used by the download-file cmdlet when a provider item is piped.
        """
        try:
            api_key = _get_debrid_api_key(self.config or {})
            if not api_key:
                log("[alldebrid] download skipped: missing api_key", file=sys.stderr)
                return None

            target = str(getattr(result, "path", "") or "").strip()
            if not target.startswith(("http://", "https://")):
                log(f"[alldebrid] download skipped: target not http(s): {target}", file=sys.stderr)
                return None

            try:
                from plugins.alldebrid.api import AllDebridClient

                client = AllDebridClient(api_key)
            except Exception as exc:
                log(f"[alldebrid] Failed to init client: {exc}", file=sys.stderr)
                return None

            # Prefer provider title as the output filename; later we may override if unlocked URL has a better basename.
            suggested = sanitize_filename(str(getattr(result, "title", "") or "").strip())
            suggested_name = suggested if suggested else None

            # Quiet mode when download-file is mid-pipeline.
            quiet = bool(self.config.get("_quiet_background_output")) if isinstance(self.config, dict) else False

            def _html_guard(path: Path) -> bool:
                try:
                    if path.exists():
                        size = path.stat().st_size
                        if size > 0 and size <= 250_000 and path.suffix.lower() not in (".html", ".htm"):
                            head = path.read_bytes()[:512]
                            try:
                                text = head.decode("utf-8", errors="ignore").lower()
                            except Exception:
                                text = ""
                            if "<html" in text or "<!doctype html" in text:
                                return True
                except Exception:
                    return False
                return False

            def _download_unlocked(unlocked_url: str, *, allow_html: bool = False) -> Optional[Path]:
                # If this is an unlocked debrid link (allow_html=True), stream it directly and skip
                # the generic HTML guard to avoid falling back to the public hoster.
                if allow_html:
                    try:
                        from API.HTTP import HTTPClient

                        fname = suggested_name or sanitize_filename(Path(urlparse(unlocked_url).path).name)
                        if not fname:
                            fname = "download"
                        if not Path(fname).suffix:
                            fname = f"{fname}.bin"
                        dest = Path(output_dir) / fname
                        dest.parent.mkdir(parents=True, exist_ok=True)
                        with HTTPClient(timeout=30.0) as client:
                            with client._request_stream("GET", unlocked_url, follow_redirects=True) as resp:
                                resp.raise_for_status()
                                with dest.open("wb") as fh:
                                    for chunk in resp.iter_bytes():
                                        if not chunk:
                                            continue
                                        fh.write(chunk)
                        return dest if dest.exists() else None
                    except Exception:
                        return None

                # Otherwise, use standard downloader with guardrails.
                pipe_progress = None
                try:
                    if isinstance(self.config, dict):
                        pipe_progress = self.config.get("_pipeline_progress")
                except Exception:
                    pipe_progress = None

                try:
                    dl_res = download_direct_file(
                        unlocked_url,
                        Path(output_dir),
                        quiet=quiet,
                        suggested_filename=suggested_name,
                        pipeline_progress=pipe_progress,
                    )
                    downloaded_path = getattr(dl_res, "path", None)
                    if downloaded_path is None:
                        return None
                    downloaded_path = Path(str(downloaded_path))
                except DownloadError as exc:
                    log(
                        f"[alldebrid] download_direct_file rejected URL ({exc}); no further fallback", file=sys.stderr
                    )
                    return None

                try:
                    if _html_guard(downloaded_path):
                        log(
                            "[alldebrid] Download returned HTML page (not file bytes). Try again or check AllDebrid link status.",
                            file=sys.stderr,
                        )
                        return None
                except Exception:
                    pass

                return downloaded_path if downloaded_path.exists() else None

            unlocked_url = target
            try:
                unlocked = client.resolve_unlock_link(target, poll=True, max_wait_seconds=45, poll_interval_seconds=5)
                if isinstance(unlocked, str) and unlocked.strip().startswith(("http://", "https://")):
                    unlocked_url = unlocked.strip()
            except Exception:
                pass

            if unlocked_url != target:
                # Prefer filename from unlocked URL path.
                try:
                    unlocked_name = sanitize_filename(Path(urlparse(unlocked_url).path).name)
                    if unlocked_name:
                        suggested_name = unlocked_name
                except Exception:
                    pass

            # When using an unlocked URL different from the original hoster, stream it directly and do NOT fall back to the public URL.
            allow_html = unlocked_url != target
            downloaded = _download_unlocked(unlocked_url, allow_html=allow_html)
            if downloaded:
                return downloaded

            # If unlock failed entirely and we never changed URL, allow a single attempt on the original target.
            if unlocked_url == target:
                downloaded = _download_unlocked(target, allow_html=False)
                if downloaded:
                    return downloaded

            return None
        except Exception:
            return None

    def resolve_pipe_result_download(
        self,
        result: Any,
        pipe_obj: Optional[PipeObject],
    ) -> Tuple[Optional[Path], Optional[str], Optional[Path]]:
        """Download a remote provider result on behalf of add-file."""

        download_url = (
            _extract_value(result, "path")
            or (getattr(pipe_obj, "url", None) if pipe_obj is not None else None)
        )
        download_url = str(download_url or "").strip()
        if not download_url:
            return None, None, None

        metadata: Dict[str, Any] = {}
        maybe_meta = _extract_value(result, "full_metadata") or _extract_value(result, "metadata")
        if isinstance(maybe_meta, dict):
            metadata.update(maybe_meta)
        if pipe_obj is not None:
            pipe_meta = getattr(pipe_obj, "metadata", None)
            if isinstance(pipe_meta, dict):
                metadata.update(pipe_meta)

        table_name = str(
            _extract_value(result, "table")
            or (getattr(pipe_obj, "provider", None) if pipe_obj is not None else None)
            or "alldebrid"
        ).strip()
        if not table_name:
            table_name = "alldebrid"

        title = str(
            _extract_value(result, "title")
            or (getattr(pipe_obj, "title", None) if pipe_obj is not None else None)
            or metadata.get("name")
            or ""
        ).strip()
        if not title:
            fallback_name = None
            parsed_url = None
            try:
                parsed_url = urlparse(download_url)
            except Exception:
                parsed_url = None
            if parsed_url and parsed_url.path:
                try:
                    fallback_name = Path(parsed_url.path).stem
                except Exception:
                    fallback_name = None
            title = fallback_name or "alldebrid"

        media_kind = str(
            _extract_value(result, "media_kind")
            or metadata.get("media_kind")
            or "file"
        ).strip() or "file"

        search_result = SearchResult(
            table=table_name,
            title=title,
            path=download_url,
            detail=str(_extract_value(result, "detail") or ""),
            annotations=[],
            media_kind=media_kind,
            tag=set(getattr(pipe_obj, "tag", []) or []),
            columns=[],
            full_metadata=metadata,
        )

        if not download_url.startswith(("http://", "https://")):
            return None, None, None

        download_dir = Path(tempfile.mkdtemp(prefix="add-file-alldebrid-"))
        try:
            downloaded_path = self.download(search_result, download_dir)
            if not downloaded_path:
                shutil.rmtree(download_dir, ignore_errors=True)
                return None, None, None
            if pipe_obj is not None:
                pipe_obj.is_temp = True
            hash_hint = (
                _extract_value(result, "hash")
                or _extract_value(result, "file_hash")
                or (getattr(pipe_obj, "hash", None) if pipe_obj is not None else None)
            )
            if not hash_hint:
                for candidate in ("hash", "file_hash", "info_hash"):
                    candidate_val = metadata.get(candidate)
                    if isinstance(candidate_val, str) and candidate_val.strip():
                        hash_hint = candidate_val.strip()
                        break
            return downloaded_path, hash_hint, download_dir
        except Exception as exc:
            log(f"[alldebrid] add-file download failed: {exc}", file=sys.stderr)
            shutil.rmtree(download_dir, ignore_errors=True)
            return None, None, None

    def status_summary(self) -> Dict[str, Any]:
        try:
            api_key = _get_debrid_api_key(self.config)
            if not api_key:
                return {
                    "status": "DISABLED",
                    "name": self.label,
                    "plugin": self.name,
                    "detail": "Not configured",
                }
            client = AllDebridClient(api_key)
            base_url = str(getattr(client, "base_url", "") or "").strip()
            return {
                "status": "ENABLED",
                "name": self.label,
                "plugin": self.name,
                "detail": base_url or "Connected",
            }
        except Exception as exc:
            return {
                "status": "DISABLED",
                "name": self.label,
                "plugin": self.name,
                "detail": str(exc),
            }

    def resolve_url(self, url: str, **_kwargs: Any) -> str:
        target = str(url or "").strip()
        if not target.startswith(("http://", "https://")):
            return target

        try:
            parsed = urlparse(target)
            host = str(parsed.netloc or "").lower()
            path = str(parsed.path or "")
        except Exception:
            return target

        if host != "alldebrid.com" or not path.startswith("/f/"):
            return target

        api_key = _get_debrid_api_key(self.config)
        if not api_key:
            return target

        try:
            client = AllDebridClient(str(api_key))
            unlocked = client.unlock_link(target)
            if isinstance(unlocked, str) and unlocked.strip():
                return unlocked.strip()
        except Exception:
            pass

        return target

    def download_items(
        self,
        result: SearchResult,
        output_dir: Path,
        *,
        emit: Callable[[Path, str, str, Dict[str, Any]], None],
        progress: Any,
        quiet_mode: bool,
        path_from_result: Callable[[Any], Path],
        config: Optional[Dict[str, Any]] = None,
    ) -> int:
        # Check if this is a direct magnet_id from the account (from selector or custom URL scheme)
        path_id = _parse_alldebrid_magnet_id(getattr(result, "path", ""))
        full_metadata = getattr(result, "full_metadata", None) or {}
        magnet_id_direct = None
        if isinstance(full_metadata, dict):
            magnet_id_direct = full_metadata.get("magnet_id")

        active_id = magnet_id_direct if magnet_id_direct is not None else path_id

        if active_id is not None:
            try:
                magnet_id = int(active_id)
                cfg = config if isinstance(config, dict) else (self.config or {})
                count = self._download_magnet_by_id(
                    magnet_id,
                    output_dir,
                    cfg,
                    emit,
                    progress,
                    quiet_mode,
                    path_from_result,
                )
                return count
            except Exception:
                pass
        
        spec = self._resolve_magnet_spec_from_result(result)
        if not spec:
            return 0

        cfg = config if isinstance(config, dict) else (self.config or {})

        def _on_emit(path: Path, file_url: str, relpath: str, metadata: Dict[str, Any]) -> None:
            emit(path, file_url, relpath, metadata)

        downloaded, _ = download_magnet(
            spec,
            str(getattr(result, "path", "") or ""),
            output_dir,
            cfg,
            progress,
            quiet_mode,
            path_from_result,
            _on_emit,
        )
        return downloaded

    @staticmethod
    def _flatten_files(items: Any,
                       *,
                       _prefix: Optional[List[str]] = None) -> Iterable[Dict[str, Any]]:
        """Flatten AllDebrid magnet file tree into file dicts, preserving relative paths.

        API commonly returns:
          - file: {n: name, s: size, l: link}
          - folder: {n: name, e: [sub_items]}

        This flattener attaches a best-effort relative path to each yielded file node
        as `_relpath` using POSIX separators (e.g., "Season 1/E01.mkv").

        Some call sites in this repo also expect {name, size, link}, so we accept both.
        """
        prefix = list(_prefix or [])

        if not items:
            return
        if isinstance(items, dict):
            items = [items]
        if not isinstance(items, list):
            return

        for node in items:
            if not isinstance(node, dict):
                continue

            children = node.get("e") or node.get("children")
            if isinstance(children, list):
                folder_name = node.get("n") or node.get("name")
                next_prefix = prefix
                if isinstance(folder_name, str) and folder_name.strip():
                    next_prefix = prefix + [folder_name.strip()]
                yield from AllDebrid._flatten_files(children, _prefix=next_prefix)
                continue

            name = node.get("n") or node.get("name")
            link = node.get("l") or node.get("link")
            if isinstance(name, str) and name.strip() and isinstance(link, str) and link.strip():
                rel_parts = prefix + [name.strip()]
                relpath = "/".join([p for p in rel_parts if p])
                enriched = dict(node)
                enriched["_relpath"] = relpath
                yield enriched

    def _download_magnet_by_id(
        self,
        magnet_id: int,
        output_dir: Path,
        config: Dict[str, Any],
        emit: Callable[[Path, str, str, Dict[str, Any]], None],
        progress: Any,
        quiet_mode: bool,
        path_from_result: Callable[[Any], Path],
    ) -> int:
        """Download files from an existing magnet ID (already in account)."""
        api_key = _get_debrid_api_key(config or {})
        if not api_key:
            log("AllDebrid API key not configured", file=sys.stderr)
            return 0

        try:
            client = AllDebridClient(api_key)
        except Exception as exc:
            log(f"Failed to init AllDebrid client: {exc}", file=sys.stderr)
            return 0

        try:
            files_result = client.magnet_links([magnet_id])
        except Exception as exc:
            log(f"Failed to list files for magnet {magnet_id}: {exc}", file=sys.stderr)
            return 0

        magnet_files = files_result.get(str(magnet_id), {}) if isinstance(files_result, dict) else {}
        file_nodes = magnet_files.get("files") if isinstance(magnet_files, dict) else []
        if not file_nodes:
            log(f"AllDebrid magnet {magnet_id} has no files", file=sys.stderr)
            return 0

        file_entries = list(self._flatten_files(file_nodes))
        total_files = len(file_entries)
        if total_files <= 0:
            log(f"AllDebrid magnet {magnet_id} has no downloadable files", file=sys.stderr)
            return 0

        magnet_path_metadata: Dict[str, Any] = {}
        magnet_folder_name = str(
            magnet_files.get("filename")
            or magnet_files.get("name")
            or magnet_files.get("hash")
            or f"magnet-{magnet_id}"
        ).strip()
        if magnet_folder_name:
            magnet_path_metadata["folder"] = magnet_folder_name

        downloaded = 0
        for file_idx, node in enumerate(file_entries, 1):
            locked_url = str(node.get("l") or node.get("link") or "").strip()
            file_name = str(node.get("n") or node.get("name") or "").strip()
            relpath = str(node.get("_relpath") or file_name or "").strip()

            try:
                if progress is not None and hasattr(progress, "set_status"):
                    progress.set_status(
                        f"downloading file {file_idx}/{total_files}: {relpath or file_name or 'download'}"
                    )
            except Exception:
                pass
            
            if not locked_url or not relpath:
                continue

            # Unlock the URL if it's restricted (contains /f/)
            file_url = locked_url
            if "/f/" in locked_url:
                try:
                    unlocked = client.unlock_link(locked_url)
                    if unlocked:
                        file_url = unlocked
                except Exception:
                    pass

            rel_path_obj = Path(relpath)
            target_path = adjust_output_dir_for_alldebrid(
                output_dir,
                {**magnet_path_metadata, "relpath": relpath},
                magnet_files,
            )

            suggested_name = sanitize_filename(rel_path_obj.name) or sanitize_filename(file_name)
            if not suggested_name:
                suggested_name = rel_path_obj.name or file_name or f"file-{file_idx}"

            try:
                result_obj = download_direct_file(
                    file_url,
                    target_path,
                    quiet=quiet_mode,
                    suggested_filename=suggested_name,
                    pipeline_progress=progress,
                )
            except Exception:
                continue

            downloaded_path = path_from_result(result_obj)
            metadata = {
                "magnet_id": magnet_id,
                "relpath": relpath,
                "name": file_name,
            }
            emit(downloaded_path, file_url, relpath, metadata)
            downloaded += 1

        if downloaded == 0:
            log(f"AllDebrid magnet {magnet_id} produced no downloads", file=sys.stderr)

        try:
            if progress is not None and hasattr(progress, "clear_status"):
                progress.clear_status()
        except Exception:
            pass

        return downloaded

    def search(
        self,
        query: str,
        limit: int = 50,
        filters: Optional[Dict[str,
                               Any]] = None,
        **kwargs: Any,
    ) -> List[SearchResult]:
        q = (query or "").strip()
        if not q:
            return []

        api_key = _get_debrid_api_key(self.config or {})
        if not api_key:
            return []

        view = None
        if isinstance(filters, dict):
            view = str(filters.get("view") or "").strip().lower() or None
        view = view or "folders"

        try:
            from plugins.alldebrid.api import AllDebridClient

            client = AllDebridClient(api_key)
        except Exception as exc:
            log(f"[alldebrid] Failed to init client: {exc}", file=sys.stderr)
            return []

        q_lower = q.lower()
        needle = "" if q_lower in {"*",
                                   "all",
                                   "list"} else q_lower

        # Second-stage: list files for a specific magnet id.
        if view == "files":
            magnet_id_val = None
            if isinstance(filters, dict):
                magnet_id_val = filters.get("magnet_id")
            if magnet_id_val is None:
                magnet_id_val = kwargs.get("magnet_id")

            if magnet_id_val is None:
                return []

            try:
                magnet_id = int(magnet_id_val)
            except (TypeError, ValueError):
                return []

            magnet_status: Dict[str,
                                Any] = {}
            try:
                magnet_status = client.magnet_status(magnet_id)
            except Exception:
                magnet_status = {}

            magnet_name = str(
                magnet_status.get("filename") or magnet_status.get("name")
                or magnet_status.get("hash") or f"magnet-{magnet_id}"
            )
            status_code = magnet_status.get("statusCode")
            status_text = str(magnet_status.get("status") or "").strip() or "unknown"
            ready = status_code == 4 or bool(magnet_status.get("ready"))

            if not ready:
                return [
                    SearchResult(
                        table="alldebrid",
                        title=magnet_name,
                        path=f"alldebrid:magnet:{magnet_id}",
                        detail=status_text,
                        annotations=["folder",
                                     "not-ready"],
                        media_kind="folder",
                        tag={"alldebrid",
                             "folder",
                             str(magnet_id),
                             "not-ready"},
                        columns=[
                            ("Folder",
                             magnet_name),
                            ("ID",
                             str(magnet_id)),
                            ("Status",
                             status_text),
                            ("Ready",
                             "no"),
                        ],
                        full_metadata={
                            "magnet": magnet_status,
                            "magnet_id": magnet_id,
                            "plugin": "alldebrid",
                            "plugin_view": "files",
                            "magnet_name": magnet_name,
                        },
                    )
                ]

            try:
                files_result = client.magnet_links([magnet_id])
                magnet_files = (
                    files_result.get(str(magnet_id),
                                     {}) if isinstance(files_result,
                                                       dict) else {}
                )
                file_tree = magnet_files.get("files",
                                             []) if isinstance(magnet_files,
                                                               dict) else []
            except Exception as exc:
                log(
                    f"[alldebrid] Failed to list files for magnet {magnet_id}: {exc}",
                    file=sys.stderr,
                )
                file_tree = []

            results: List[SearchResult] = []
            for file_node in self._flatten_files(file_tree):
                file_name = str(file_node.get("n") or file_node.get("name")
                                or "").strip()
                file_url = str(file_node.get("l") or file_node.get("link")
                               or "").strip()
                relpath = str(file_node.get("_relpath") or file_name or "").strip()
                file_size = file_node.get("s") or file_node.get("size")
                if not file_name or not file_url:
                    continue

                if needle and needle not in file_name.lower():
                    continue

                size_bytes: Optional[int] = None
                try:
                    if isinstance(file_size, (int, float)):
                        size_bytes = int(file_size)
                    elif isinstance(file_size, str) and file_size.isdigit():
                        size_bytes = int(file_size)
                except Exception:
                    size_bytes = None

                metadata = {
                    "magnet": magnet_status,
                    "magnet_id": magnet_id,
                    "magnet_name": magnet_name,
                    "relpath": relpath,
                    "file": file_node,
                    "plugin": "alldebrid",
                    "plugin_view": "files",
                    # Selection metadata for table system
                    "_selection_args": ["-url", f"{_ALD_MAGNET_PREFIX}{magnet_id}"],
                    "_selection_action": ["download-file", "-plugin", "alldebrid", "-url", f"{_ALD_MAGNET_PREFIX}{magnet_id}"],
                }

                results.append(
                    SearchResult(
                        table="alldebrid",
                        title=file_name,
                        path=file_url,
                        detail=magnet_name,
                        annotations=["file"],
                        media_kind="file",
                        size_bytes=size_bytes,
                        tag={"alldebrid",
                             "file",
                             str(magnet_id)},
                        columns=[
                            ("File",
                             file_name),
                            ("Folder",
                             magnet_name),
                            ("ID",
                             str(magnet_id)),
                        ],
                        full_metadata=metadata,
                    )
                )
                if len(results) >= max(1, limit):
                    break

            return results

        # Default: folders view (magnets)
        try:
            magnets = client.magnet_list() or []
        except Exception as exc:
            log(f"[alldebrid] Failed to list account magnets: {exc}", file=sys.stderr)
            return []

        wanted_id: Optional[int] = None
        if needle.isdigit():
            try:
                wanted_id = int(needle)
            except Exception:
                wanted_id = None

        results: List[SearchResult] = []
        for magnet in magnets:
            if not isinstance(magnet, dict):
                continue

            magnet_id_val = magnet.get("id")
            if magnet_id_val is None:
                continue
            try:
                magnet_id = int(magnet_id_val)
            except (TypeError, ValueError):
                continue

            magnet_name = str(
                magnet.get("filename") or magnet.get("name") or magnet.get("hash")
                or f"magnet-{magnet_id}"
            )
            magnet_name_lower = magnet_name.lower()

            status_text = str(magnet.get("status") or "").strip() or "unknown"
            status_code = magnet.get("statusCode")
            ready = status_code == 4 or bool(magnet.get("ready"))

            if wanted_id is not None:
                if magnet_id != wanted_id:
                    continue
            elif needle and (needle not in magnet_name_lower):
                continue

            size_bytes: Optional[int] = None
            try:
                size_val = magnet.get("size")
                if isinstance(size_val, (int, float)):
                    size_bytes = int(size_val)
                elif isinstance(size_val, str) and size_val.isdigit():
                    size_bytes = int(size_val)
            except Exception:
                size_bytes = None

            results.append(
                SearchResult(
                    table="alldebrid",
                    title=magnet_name,
                    path=f"alldebrid:magnet:{magnet_id}",
                    detail=status_text,
                    annotations=["folder"],
                    media_kind="folder",
                    size_bytes=size_bytes,
                    tag={"alldebrid",
                         "folder",
                         str(magnet_id)}
                    | ({"ready"} if ready else {"not-ready"}),
                    columns=[
                        ("Folder",
                         magnet_name),
                        ("ID",
                         str(magnet_id)),
                        ("Status",
                         status_text),
                        ("Ready",
                         "yes" if ready else "no"),
                    ],
                    full_metadata={
                        "magnet": magnet,
                        "magnet_id": magnet_id,
                        "plugin": "alldebrid",
                        "plugin_view": "folders",
                        "magnet_name": magnet_name,
                        # Selection metadata: allow @N expansion to drive downloads directly
                        "_selection_args": ["-url", f"{_ALD_MAGNET_PREFIX}{magnet_id}"],
                        "_selection_action": ["download-file", "-plugin", "alldebrid", "-url", f"{_ALD_MAGNET_PREFIX}{magnet_id}"],
                    },
                )
            )

            if len(results) >= max(1, limit):
                break

        return results

    def selector(
        self,
        selected_items: List[Any],
        *,
        ctx: Any,
        stage_is_last: bool = True,
        **_kwargs: Any,
    ) -> bool:
        """Handle AllDebrid `@N` selection by drilling into magnet files."""
        if not stage_is_last:
            return False

        def _as_payload(item: Any) -> Dict[str, Any]:
            if isinstance(item, dict):
                return dict(item)
            try:
                if hasattr(item, "to_dict"):
                    maybe = item.to_dict()  # type: ignore[attr-defined]
                    if isinstance(maybe, dict):
                        return maybe
            except Exception:
                pass
            payload: Dict[str, Any] = {}
            try:
                payload = {
                    "title": getattr(item, "title", None),
                    "path": getattr(item, "path", None),
                    "table": getattr(item, "table", None),
                    "annotations": getattr(item, "annotations", None),
                    "media_kind": getattr(item, "media_kind", None),
                    "full_metadata": getattr(item, "full_metadata", None),
                }
            except Exception:
                payload = {}
            return payload

        chosen: List[Dict[str, Any]] = []
        for item in selected_items or []:
            payload = _as_payload(item)
            meta = payload.get("full_metadata") or payload.get("metadata") or {}
            if not isinstance(meta, dict):
                meta = {}

            ann_set: set[str] = set()
            for ann_source in (payload.get("annotations"), meta.get("annotations")):
                if isinstance(ann_source, (list, tuple, set)):
                    for ann in ann_source:
                        ann_text = str(ann or "").strip().lower()
                        if ann_text:
                            ann_set.add(ann_text)

            media_kind = str(payload.get("media_kind") or meta.get("media_kind") or "").strip().lower()
            is_folder = (media_kind == "folder") or ("folder" in ann_set)
            magnet_id = meta.get("magnet_id")
            if magnet_id is None or (not is_folder):
                continue

            title = str(payload.get("title") or meta.get("magnet_name") or meta.get("name") or "").strip()
            if not title:
                title = f"magnet-{magnet_id}"

            chosen.append({
                "magnet_id": magnet_id,
                "title": title,
            })

        if not chosen:
            return False

        target = chosen[0]
        magnet_id = target.get("magnet_id")
        title = target.get("title") or f"magnet-{magnet_id}"

        try:
            files = self.search("*", limit=200, filters={"view": "files", "magnet_id": magnet_id})
        except Exception as exc:
            print(f"alldebrid selector failed: {exc}\n")
            return True

        try:
            from SYS.result_table import Table
            from SYS.rich_display import stdout_console
        except Exception:
            return True

        table = Table(f"AllDebrid Files: {title}")._perseverance(True)
        table.set_table("alldebrid")
        try:
            table.set_table_metadata({"plugin": "alldebrid", "view": "files", "magnet_id": magnet_id})
        except Exception:
            pass
        table.set_source_command("download-file", ["-plugin", "alldebrid"])

        results_payload: List[Dict[str, Any]] = []
        for r in files or []:
            table.add_result(r)
            try:
                results_payload.append(r.to_dict())
            except Exception:
                results_payload.append(
                    {
                        "table": getattr(r, "table", "alldebrid"),
                        "title": getattr(r, "title", ""),
                        "path": getattr(r, "path", ""),
                        "full_metadata": getattr(r, "full_metadata", None),
                    }
                )

        try:
            ctx.set_last_result_table(table, results_payload)
            ctx.set_current_stage_table(table)
        except Exception:
            pass

        try:
            stdout_console().print()
            stdout_console().print(table)
        except Exception:
            pass

        return True

    def show_selection_details(
        self,
        selected_items: List[Any],
        *,
        ctx: Any,
        stage_is_last: bool = True,
        source_command: str = "",
        table_type: str = "",
        table_metadata: Optional[Dict[str, Any]] = None,
        **_kwargs: Any,
    ) -> bool:
        _ = table_type
        _item, payload, meta = self.resolve_selection_detail_subject(
            selected_items,
            stage_is_last=stage_is_last,
            source_command=source_command,
            require_media_kind="file",
        )
        if not isinstance(payload, dict):
            return False

        title = str(payload.get("title") or meta.get("name") or "").strip() or "AllDebrid Item"
        magnet_name = str(meta.get("magnet_name") or payload.get("detail") or "").strip()
        magnet_id = meta.get("magnet_id")
        relpath = str(meta.get("relpath") or "").strip()
        direct_url = str(payload.get("path") or "").strip()
        selection_url = ""
        action = meta.get("_selection_action") or meta.get("selection_action")
        if isinstance(action, (list, tuple)):
            action_tokens = [str(x) for x in action if x is not None]
            for idx, token in enumerate(action_tokens):
                if str(token).strip().lower() in {"-url", "--url"} and idx + 1 < len(action_tokens):
                    selection_url = str(action_tokens[idx + 1] or "").strip()
                    break

        try:
            from SYS.detail_view_helpers import prepare_detail_metadata, render_selection_detail_view
        except Exception:
            return super().show_selection_details(
                selected_items,
                ctx=ctx,
                stage_is_last=stage_is_last,
                source_command=source_command,
                table_type=table_type,
                table_metadata=table_metadata,
            )

        detail_metadata = prepare_detail_metadata(
            payload,
            title=title,
            store=self.name,
            path=direct_url or None,
            tags=meta.get("tag") or meta.get("tags"),
            extra_fields={
                "Plugin": self.name,
                "Magnet": magnet_name or None,
                "Magnet ID": magnet_id,
                "Relative Path": relpath or None,
                "View": str(meta.get("plugin_view") or meta.get("view") or (table_metadata or {}).get("view") or "").strip() or None,
                "Direct Url": direct_url or None,
                "Selection Url": selection_url or None,
            },
        )

        return render_selection_detail_view(
            ctx=ctx,
            item=payload,
            title=f"AllDebrid Item: {title}",
            metadata=detail_metadata,
            table_name=self.name,
            detail_order=["Title", "Instance", "Magnet", "Magnet ID", "Relative Path", "View", "Path", "File", "Folder", "ID", "Direct URL", "Selection URL", "Plugin"],
            value_case="preserve",
        )


try:
    from SYS.result_table_adapters import register_plugin
    from SYS.result_table_api import ColumnSpec, ResultModel, metadata_column, title_column

    def _as_payload(item: Any) -> Dict[str, Any]:
        if isinstance(item, dict):
            return dict(item)
        try:
            if hasattr(item, "to_dict"):
                result = item.to_dict()  # type: ignore[attr-defined]
                if isinstance(result, dict):
                    return result
        except Exception:
            pass
        payload: Dict[str, Any] = {}
        for attr in ("title", "path", "columns", "full_metadata", "table", "source", "size_bytes", "size", "ext"):
            try:
                val = getattr(item, attr, None)
            except Exception:
                val = None
            if val is not None:
                payload.setdefault(attr, val)
        return payload


    def _coerce_size(value: Any) -> Optional[int]:
        if value is None:
            return None
        if isinstance(value, (int, float)):
            try:
                return int(value)
            except Exception:
                return None
        try:
            return int(float(str(value).strip()))
        except Exception:
            return None


    def _normalize_columns(columns: Any, metadata: Dict[str, Any]) -> None:
        if not isinstance(columns, list):
            return
        for entry in columns:
            if not isinstance(entry, (list, tuple)) or len(entry) < 2:
                continue
            key, value = entry[0], entry[1]
            if not key:
                continue
            normalized = str(key).replace(" ", "_").strip().lower()
            if not normalized:
                continue
            metadata.setdefault(normalized, value)


    def _convert_to_model(item: Any) -> ResultModel:
        payload = _as_payload(item)
        title = str(payload.get("title") or payload.get("name") or "").strip()
        if not title:
            candidate = payload.get("path") or payload.get("detail") or payload.get("magnet_name")
            title = str(candidate or "").strip()
        if not title:
            title = "alldebrid"

        path_val = payload.get("path")
        if path_val is not None and not isinstance(path_val, str):
            try:
                path_val = str(path_val)
            except Exception:
                path_val = None

        size_bytes = _coerce_size(payload.get("size_bytes") or payload.get("size") or payload.get("file_size"))
        metadata: Dict[str, Any] = {}
        full_metadata = payload.get("full_metadata")
        if isinstance(full_metadata, dict):
            metadata.update(full_metadata)
        _normalize_columns(payload.get("columns"), metadata)

        table_name = str(payload.get("table") or payload.get("source") or "alldebrid").strip().lower()
        if table_name:
            metadata.setdefault("table", table_name)
            metadata.setdefault("source", table_name)
            metadata.setdefault("plugin", table_name)

        ext = payload.get("ext")
        if not ext and isinstance(path_val, str):
            try:
                suffix = Path(path_val).suffix
                if suffix:
                    ext = suffix.lstrip(".")
            except Exception:
                ext = None

        return ResultModel(
            title=title,
            path=path_val,
            ext=str(ext) if ext is not None else None,
            size_bytes=size_bytes,
            metadata=metadata,
            source="alldebrid",
        )


    def _adapter(items: Iterable[Any]) -> Iterable[ResultModel]:
        for item in items or []:
            try:
                model = _convert_to_model(item)
            except Exception:
                continue
            yield model


    def _has_metadata(rows: List[ResultModel], key: str) -> bool:
        for row in rows:
            md = row.metadata or {}
            if key in md:
                val = md[key]
                if val is None:
                    continue
                if isinstance(val, str) and not val.strip():
                    continue
                return True
        return False


    def _columns_factory(rows: List[ResultModel]) -> List[ColumnSpec]:
        """Build column specifications from available metadata in rows.
        
        This factory inspects all rows and creates ColumnSpec entries only
        for metadata that is actually present in the result set. This avoids
        empty columns in the display.
        """
        cols = [title_column()]
        if _has_metadata(rows, "magnet_name"):
            cols.append(metadata_column("magnet_name", "Magnet"))
        if _has_metadata(rows, "magnet_id"):
            cols.append(metadata_column("magnet_id", "Magnet ID"))
        if _has_metadata(rows, "status"):
            cols.append(metadata_column("status", "Status"))
        if _has_metadata(rows, "ready"):
            cols.append(metadata_column("ready", "Ready"))
        if _has_metadata(rows, "relpath"):
            cols.append(metadata_column("relpath", "File Path"))
        if _has_metadata(rows, "plugin_view"):
            cols.append(metadata_column("plugin_view", "View"))
        if _has_metadata(rows, "size"):
            cols.append(metadata_column("size", "Size"))
        return cols


    def _selection_fn(row: ResultModel) -> List[str]:
        """Return selection args for @N expansion and auto-download integration.
        
        Selection precedence:
        1. Explicit _selection_action (full command args)
        2. Explicit _selection_args (URL-specific args)
        3. Magic routing based on plugin_view (files vs folders)
        4. Magnet ID routing for folder-type rows (via alldebrid:magnet:<id>)
        5. Direct URL for file rows
        
        This ensures that selector overrides all pre-codes and gives users full power.
        """
        metadata = row.metadata or {}
        
        # First try explicit action (full command)
        action = metadata.get("_selection_action") or metadata.get("selection_action")
        if isinstance(action, (list, tuple)) and action:
            return [str(x) for x in action if x is not None]
        
        # Next try explicit args (typically URL-based)
        args = metadata.get("_selection_args") or metadata.get("selection_args")
        if isinstance(args, (list, tuple)) and args:
            return [str(x) for x in args if x is not None]
        
        # Magic routing by view type
        view = metadata.get("plugin_view") or metadata.get("view") or ""
        if view == "files":
            # File rows: pass direct URL for immediate download
            if row.path:
                return ["-url", row.path]
        
        # Folder rows: use magnet_id to fetch and download all files
        magnet_id = metadata.get("magnet_id")
        if magnet_id is not None:
            return ["-url", f"{_ALD_MAGNET_PREFIX}{magnet_id}"]
        
        # Fallback: try direct URL
        if row.path:
            return ["-url", row.path]
        
        return ["-title", row.title or ""]


    register_plugin(
        "alldebrid",
        _adapter,
        columns=_columns_factory,
        selection_fn=_selection_fn,
        metadata={"description": "AllDebrid account provider"},
    )
except Exception:
    pass
