# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Browse & search helpers for connected services.

Extracted from ``familiar.dashboard.app`` so that both the Flask dashboard
and every channel adapter can reuse the same logic.

All functions are **synchronous** — they call skill functions that already
handle their own I/O internally.
"""

from __future__ import annotations

import json
import logging
import os
from pathlib import Path

from ..formatting import FormatMode, fmt_bold

logger = logging.getLogger(__name__)

# Resolve DATA_DIR the same way the dashboard does
_DATA_DIR = Path.home() / ".familiar" / "data"

# Domains recognised as video sites (yt-dlp supports many more,
# but this covers the main ones for quick detection).
_VIDEO_DOMAINS = {
    "youtube.com",
    "youtu.be",
    "vimeo.com",
    "dailymotion.com",
    "twitch.tv",
    "rumble.com",
    "odysee.com",
    "bitchute.com",
    "peertube.social",
}

_PLATFORM_SITES: dict[str, str] = {
    "youtube": "youtube.com",
    "vimeo": "vimeo.com",
    "dailymotion": "dailymotion.com",
    "twitch": "twitch.tv",
    "rumble": "rumble.com",
    "odysee": "odysee.com",
}


def is_video_url(url: str) -> bool:
    """Check if a URL points to a video site supported by yt-dlp."""
    try:
        from urllib.parse import urlparse

        host = urlparse(url).hostname or ""
        host = host.lstrip("www.").lstrip("m.")
        return host in _VIDEO_DOMAINS
    except Exception:
        return False


def _extract_video_id(url: str) -> str:
    """Best-effort extraction of a video ID from a URL."""
    try:
        import re
        from urllib.parse import parse_qs, urlparse

        parsed = urlparse(url)
        host = (parsed.hostname or "").lstrip("www.").lstrip("m.")
        if host in ("youtube.com", "youtu.be"):
            if host == "youtu.be":
                return parsed.path.lstrip("/").split("/")[0]
            qs = parse_qs(parsed.query)
            if "v" in qs:
                return qs["v"][0]
            m = re.search(r"/embed/([a-zA-Z0-9_-]+)", parsed.path)
            if m:
                return m.group(1)
        # Fallback: last non-empty path segment
        parts = [p for p in parsed.path.split("/") if p]
        return parts[-1] if parts else ""
    except Exception:
        return ""


def _format_duration(seconds: int) -> str:
    """Format seconds as H:MM:SS or M:SS."""
    if not seconds:
        return ""
    s = int(seconds)
    hrs, remainder = divmod(s, 3600)
    mins, secs = divmod(remainder, 60)
    if hrs:
        return f"{hrs}:{mins:02d}:{secs:02d}"
    return f"{mins}:{secs:02d}"


def search_video_online(
    query: str, max_results: int = 20, platforms: list[str] | None = None
) -> dict:
    """Search for videos across multiple platforms using DuckDuckGo.

    Runs a primary search, then supplements with platform-specific queries
    to ensure results span YouTube, Vimeo, Dailymotion, etc.

    When *platforms* is given (e.g. ``["youtube", "vimeo"]``), only those
    platforms are queried via ``site:`` prefixed searches.
    """
    try:
        from duckduckgo_search import DDGS

        ddgs = DDGS()
        seen_urls: set[str] = set()
        items: list[dict] = []

        def _collect(results):
            for r in results:
                url = r.get("content", "")
                if not url or url in seen_urls:
                    continue
                seen_urls.add(url)
                publisher = r.get("publisher", "")
                uploader = r.get("uploader", "")
                duration = r.get("duration", "")
                detail_parts = []
                if publisher:
                    detail_parts.append(publisher)
                if uploader:
                    detail_parts.append(uploader)
                if duration:
                    detail_parts.append(duration)
                images = r.get("images", {}) or {}
                thumbnail = images.get("medium", "") or images.get("small", "")
                items.append(
                    {
                        "name": r.get("title", ""),
                        "detail": " | ".join(detail_parts),
                        "url": url,
                        "thumbnail": thumbnail,
                        "video_id": _extract_video_id(url) if url else "",
                        "_platform": publisher,
                    }
                )

        # Filter to specific platforms when requested
        filtered = [p for p in (platforms or []) if p in _PLATFORM_SITES and p != "all"]
        if filtered:
            per_platform = max(max_results // len(filtered), 5)
            for plat in filtered:
                site = _PLATFORM_SITES[plat]
                try:
                    _collect(
                        ddgs.videos(
                            keywords=f"{query} site:{site}",
                            max_results=per_platform,
                        )
                    )
                except Exception as e:
                    logger.debug("Platform %s search failed: %s", plat, e)
            final = items[:max_results]
        else:
            # Broad search with supplemental platform queries
            _SUPPLEMENT_SITES = {
                "Vimeo": "vimeo.com",
                "Dailymotion": "dailymotion.com",
            }
            per_supplement = 4
            reserved = len(_SUPPLEMENT_SITES) * per_supplement
            _collect(ddgs.videos(keywords=query, max_results=max_results))

            found = {item["_platform"] for item in items if item.get("_platform")}
            supplement_items: list[dict] = []
            for platform, site in _SUPPLEMENT_SITES.items():
                if platform not in found:
                    try:
                        before = len(items)
                        _collect(
                            ddgs.videos(
                                keywords=f"{query} site:{site}",
                                max_results=per_supplement,
                            )
                        )
                        supplement_items.extend(items[before:])
                    except Exception as e:
                        logger.debug("Error suppressed: %s", e)
                        continue
            primary = [i for i in items if i not in supplement_items]
            primary_cap = max(max_results - len(supplement_items), max_results - reserved)
            final = primary[:primary_cap] + supplement_items
            final = final[:max_results]

        counts: dict[str, int] = {}
        for item in final:
            p = item.pop("_platform", "")
            if p:
                counts[p] = counts.get(p, 0) + 1
        breakdown = ", ".join(f"{p} {c}" for p, c in sorted(counts.items(), key=lambda x: -x[1]))
        summary = f"{len(final)} results"
        if breakdown:
            summary += f" ({breakdown})"
        return {"items": final, "summary": summary}
    except Exception as e:
        logger.debug(f"Online video search failed: {e}")
        return {"items": [], "summary": "", "error": str(e)}


# ============================================================
# Browse helpers (one per service)
# ============================================================


def browse_github() -> dict | None:
    """Browse GitHub repos via PyGithub."""
    token = os.environ.get("GITHUB_TOKEN", "")
    if not token:
        return None
    try:
        from github import Github

        g = Github(token)
        user = g.get_user()
        repos = []
        for repo in user.get_repos(sort="updated", direction="desc")[:8]:
            repos.append(
                {
                    "name": repo.full_name,
                    "detail": f"Updated {str(repo.updated_at)[:10]}"
                    + (f" | {repo.stargazers_count} stars" if repo.stargazers_count else ""),
                }
            )
        return {
            "connected": True,
            "display_name": "GitHub",
            "icon": "fab fa-github",
            "items": repos,
            "summary": f"{len(repos)} repos",
        }
    except Exception as e:
        logger.debug(f"Browse GitHub failed: {e}")
        return {
            "connected": True,
            "display_name": "GitHub",
            "icon": "fab fa-github",
            "items": [],
            "summary": "",
            "error": str(e),
        }


def browse_email() -> dict | None:
    """Browse unread emails via the email skill."""
    try:
        from familiar.skills.email.accounts import get_all_accounts

        accounts = get_all_accounts()
        if not accounts:
            return None
    except Exception:
        if not os.environ.get("EMAIL_ADDRESS"):
            return None

    try:
        from familiar.skills.email.skill import check_inbox

        raw = check_inbox({"limit": 5, "unread_only": True, "days_back": 3})
        items = []
        if raw and not raw.startswith("\u274c"):
            for block in raw.split("\n\n")[1:]:  # skip header line
                lines = block.strip().splitlines()
                sender = ""
                subject = ""
                for line in lines:
                    stripped = line.strip()
                    if stripped.startswith("From:"):
                        sender = stripped[5:].strip()
                    elif stripped.startswith("Subject:"):
                        subject = stripped[8:].strip()
                if sender or subject:
                    items.append(
                        {
                            "name": subject or "(no subject)",
                            "detail": f"From: {sender}" if sender else "",
                        }
                    )
        summary_match = raw.split(":\n")[0] if ":\n" in raw else raw[:60]
        return {
            "connected": True,
            "display_name": "Email",
            "icon": "fas fa-envelope",
            "items": items[:5],
            "summary": summary_match.lstrip("\U0001f4ec\U0001f4ed ").strip(),
        }
    except Exception as e:
        logger.debug(f"Browse email failed: {e}")
        return {
            "connected": True,
            "display_name": "Email",
            "icon": "fas fa-envelope",
            "items": [],
            "summary": "",
            "error": str(e),
        }


def browse_drive() -> dict | None:
    """Browse recent Google Drive files."""
    data_dir = _DATA_DIR if _DATA_DIR.exists() else Path.home() / ".familiar" / "data"
    if not (data_dir / "google_token_drive.json").exists():
        return None
    try:
        from familiar.skills.gdrive.skill import recent_files

        raw = recent_files({"limit": 5})
        items = []
        error = None
        if raw and (raw.startswith("\u274c") or "expired" in raw.lower() or "revoked" in raw.lower()):
            error = raw
        elif raw == "No recent files found":
            pass
        elif raw:
            for line in raw.splitlines():
                line = line.strip()
                if line.startswith("Modified:"):
                    if items:
                        items[-1]["detail"] = line
                elif line and not line.startswith("\U0001f4c2"):
                    name = line
                    if (
                        len(name) > 2
                        and name[0] not in "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
                        and name[0] not in "abcdefghijklmnopqrstuvwxyz"
                    ):
                        name = name.lstrip()
                        for i, c in enumerate(name):
                            if c.isalnum() or c in "._-([":
                                name = name[i:]
                                break
                    items.append({"name": name, "detail": ""})
        if error:
            if "expired" in error.lower() or "revoked" in error.lower():
                summary = "Token expired — re-authorize in Configure"
            else:
                summary = "Auth error"
        elif items:
            summary = f"{len(items)} recent files"
        elif raw == "No recent files found":
            summary = "No recent files found"
        else:
            summary = "Connected"
        result = {
            "connected": True,
            "display_name": "Google Drive",
            "icon": "fab fa-google-drive",
            "items": items[:5],
            "summary": summary,
        }
        if error:
            result["error"] = error
        return result
    except Exception as e:
        logger.debug(f"Browse Drive failed: {e}")
        return {
            "connected": True,
            "display_name": "Google Drive",
            "icon": "fab fa-google-drive",
            "items": [],
            "summary": "",
            "error": str(e),
        }


def browse_nextcloud() -> dict | None:
    """Browse Nextcloud root directory."""
    if not os.environ.get("NEXTCLOUD_URL"):
        return None
    try:
        from familiar.skills.nextcloud.skill import nc_list_files

        raw = nc_list_files({"path": "/"})
        items = []
        if raw and "not configured" not in raw.lower():
            for line in raw.splitlines()[1:]:  # skip header
                line = line.strip()
                if line:
                    name = line.split("(")[0].strip()
                    size = ""
                    if "(" in line and ")" in line:
                        size = line[line.index("(") : line.index(")") + 1]
                    items.append({"name": name, "detail": size})
        return {
            "connected": True,
            "display_name": "Nextcloud",
            "icon": "fas fa-cloud",
            "items": items[:8],
            "summary": f"{len(items)} items" if items else "Connected",
        }
    except Exception as e:
        logger.debug(f"Browse Nextcloud failed: {e}")
        return {
            "connected": True,
            "display_name": "Nextcloud",
            "icon": "fas fa-cloud",
            "items": [],
            "summary": "",
            "error": str(e),
        }


def browse_gitea() -> dict | None:
    """Browse Gitea repos."""
    if not os.environ.get("GITEA_URL"):
        return None
    try:
        from familiar.skills.gitea.skill import gitea_list_repos

        raw = gitea_list_repos({})
        items = []
        if raw and "not configured" not in raw.lower() and "No repositories" not in raw:
            for line in raw.splitlines()[1:]:  # skip header
                line = line.strip()
                if line:
                    parts = line.split("  ")
                    name = parts[0].strip()
                    detail = "  ".join(parts[1:]).strip() if len(parts) > 1 else ""
                    items.append({"name": name, "detail": detail})
        return {
            "connected": True,
            "display_name": "Gitea",
            "icon": "fas fa-code-branch",
            "items": items[:8],
            "summary": f"{len(items)} repos" if items else "Connected",
        }
    except Exception as e:
        logger.debug(f"Browse Gitea failed: {e}")
        return {
            "connected": True,
            "display_name": "Gitea",
            "icon": "fas fa-code-branch",
            "items": [],
            "summary": "",
            "error": str(e),
        }


def browse_jellyfin() -> dict | None:
    """Browse Jellyfin library."""
    if not os.environ.get("JELLYFIN_URL"):
        return None
    try:
        from familiar.skills.jellyfin.skill import jf_get_library

        raw = jf_get_library({})
        items = []
        if raw and "not configured" not in raw.lower() and "No items" not in raw:
            for line in raw.splitlines()[1:]:  # skip header
                line = line.strip()
                if line and line.startswith("["):
                    bracket_end = line.index("]") + 1
                    item_type = line[1 : bracket_end - 1]
                    title = line[bracket_end:].strip()
                    items.append({"name": title, "detail": item_type})
                elif line:
                    items.append({"name": line, "detail": ""})
        return {
            "connected": True,
            "display_name": "Jellyfin",
            "icon": "fas fa-film",
            "items": items[:8],
            "summary": f"{len(items)} items" if items else "Connected",
        }
    except Exception as e:
        logger.debug(f"Browse Jellyfin failed: {e}")
        return {
            "connected": True,
            "display_name": "Jellyfin",
            "icon": "fas fa-film",
            "items": [],
            "summary": "",
            "error": str(e),
        }


def browse_video() -> dict | None:
    """Browse downloaded videos from the video skill index."""
    video_index = Path.home() / ".familiar" / "video" / "index.json"
    if not video_index.exists():
        # Still show the card so online search is accessible
        return {
            "connected": True,
            "display_name": "Video Library",
            "icon": "fas fa-video",
            "items": [],
            "summary": "No downloads yet",
        }
    try:
        data = json.loads(video_index.read_text())
        if not data:
            return {
                "connected": True,
                "display_name": "Video Library",
                "icon": "fas fa-video",
                "items": [],
                "summary": "No downloads yet",
            }
        entries = sorted(
            data.values(),
            key=lambda v: v.get("downloaded_at", ""),
            reverse=True,
        )
        items = []
        total_size = 0
        for v in entries:
            total_size += v.get("file_size_bytes") or 0
        for v in entries[:8]:
            title = v.get("title", "(untitled)")
            dur = v.get("duration_seconds", 0)
            mins, secs = divmod(dur, 60)
            hrs, mins = divmod(mins, 60)
            dur_str = f"{hrs}:{mins:02d}:{secs:02d}" if hrs else f"{mins}:{secs:02d}"
            uploader = v.get("uploader", "")
            detail_parts = [dur_str]
            if uploader:
                detail_parts.append(uploader)
            items.append({"name": title, "detail": " | ".join(detail_parts)})
        count = len(entries)
        size_gb = total_size / (1024**3)
        summary = f"{count} video{'s' if count != 1 else ''}"
        if size_gb >= 0.01:
            summary += f" ({size_gb:.1f} GB)"
        return {
            "connected": True,
            "display_name": "Video Library",
            "icon": "fas fa-video",
            "items": items,
            "summary": summary,
        }
    except Exception as e:
        logger.debug(f"Browse video failed: {e}")
        return {
            "connected": True,
            "display_name": "Video Library",
            "icon": "fas fa-video",
            "items": [],
            "summary": "",
            "error": str(e),
        }


def browse_mealie() -> dict | None:
    """Browse recent recipes from Mealie."""
    if not os.environ.get("MEALIE_URL") or not os.environ.get("MEALIE_TOKEN"):
        return None
    try:
        from familiar.skills.mealie.skill import mealie_recipes

        raw = mealie_recipes({"per_page": 8})
        items = []
        if raw and "No recipes" not in raw and not raw.startswith("\u274c"):
            for line in raw.splitlines():
                line = line.strip()
                if not line:
                    continue
                # Lines like "1. **Recipe Name** (ID: ...)"
                if line[0].isdigit() and ". **" in line:
                    name = line.split("**")[1] if "**" in line else line
                    items.append({"name": name, "detail": ""})
                elif line.startswith("Tags:") and items:
                    items[-1]["detail"] = line
                elif (line.startswith("   ") and items
                      and not items[-1]["detail"] and "|" in line):
                    items[-1]["detail"] = line.strip()
        return {
            "connected": True,
            "display_name": "Mealie",
            "icon": "fas fa-utensils",
            "items": items[:8],
            "summary": f"{len(items)} recipes" if items else "Connected",
        }
    except Exception as e:
        logger.debug(f"Browse Mealie failed: {e}")
        return {
            "connected": True,
            "display_name": "Mealie",
            "icon": "fas fa-utensils",
            "items": [],
            "summary": "",
            "error": str(e),
        }


def browse_browser() -> dict | None:
    """Browse recent browser downloads and screenshots."""
    data_dir = _DATA_DIR if _DATA_DIR.exists() else Path.home() / ".familiar" / "data"
    downloads_dir = data_dir / "downloads"
    screenshots_dir = data_dir / "screenshots"
    has_downloads = downloads_dir.exists() and any(downloads_dir.iterdir())
    has_screenshots = screenshots_dir.exists() and any(screenshots_dir.iterdir())
    if not has_downloads and not has_screenshots:
        try:
            import playwright  # noqa: F401
        except Exception:
            return None
        return {
            "connected": True,
            "display_name": "Browser",
            "icon": "fas fa-globe",
            "items": [],
            "summary": "No downloads yet",
        }
    items = []
    if has_downloads:
        files = sorted(downloads_dir.iterdir(), key=lambda f: f.stat().st_mtime, reverse=True)
        for f in files[:5]:
            if f.is_file():
                size = f.stat().st_size
                if size > 1048576:
                    size_str = f"{size / 1048576:.1f} MB"
                elif size > 1024:
                    size_str = f"{size / 1024:.1f} KB"
                else:
                    size_str = f"{size} B"
                items.append({"name": f.name, "detail": f"Download | {size_str}"})
    if has_screenshots:
        files = sorted(screenshots_dir.iterdir(), key=lambda f: f.stat().st_mtime, reverse=True)
        for f in files[:3]:
            if f.is_file():
                items.append({"name": f.name, "detail": "Screenshot"})
    count_dl = len(list(downloads_dir.iterdir())) if has_downloads else 0
    count_ss = len(list(screenshots_dir.iterdir())) if has_screenshots else 0
    parts = []
    if count_dl:
        parts.append(f"{count_dl} download{'s' if count_dl != 1 else ''}")
    if count_ss:
        parts.append(f"{count_ss} screenshot{'s' if count_ss != 1 else ''}")
    return {
        "connected": True,
        "display_name": "Browser",
        "icon": "fas fa-globe",
        "items": items[:8],
        "summary": ", ".join(parts) if parts else "Active",
    }


# ============================================================
# Per-service search handlers
# ============================================================


def search_drive(query: str) -> dict:
    """Search Google Drive files."""
    from familiar.skills.gdrive.skill import search_files

    raw = search_files({"query": query, "limit": 10})
    if not raw or raw.startswith("\u274c") or raw.startswith("No files"):
        return {"items": [], "summary": raw or "No results"}
    items = []
    lines = raw.splitlines()
    for line in lines:
        line = line.strip()
        if not line or line.startswith("\U0001f50d") or line.startswith("ID:"):
            continue
        if line.startswith("Size:") or line.startswith("Modified:"):
            if items:
                items[-1]["detail"] = line
            continue
        name = line
        for i, c in enumerate(name):
            if c.isalnum() or c in "._-([":
                name = name[i:]
                break
        items.append({"name": name, "detail": ""})
    return {"items": items[:10], "summary": f"{len(items)} results"}


def search_email(query: str) -> dict:
    """Search emails across all accounts."""
    from familiar.skills.email.skill import search_emails

    raw = search_emails({"query": query, "limit": 10})
    if not raw or raw.startswith("\u274c"):
        return {"items": [], "summary": raw or "No results"}
    items = []
    for line in raw.splitlines():
        line = line.strip()
        if line.startswith("\u2022"):
            content = line.lstrip("\u2022 ").strip()
            if ": " in content:
                sender, subject = content.split(": ", 1)
                items.append({"name": subject, "detail": f"From: {sender}"})
            else:
                items.append({"name": content, "detail": ""})
    return {"items": items[:10], "summary": f"{len(items)} results"}


def search_nextcloud(query: str) -> dict:
    """Search Nextcloud files by name."""
    from familiar.skills.nextcloud.skill import nc_search_files

    raw = nc_search_files({"query": query})
    if not raw or "not configured" in raw.lower() or raw.startswith("No files"):
        return {"items": [], "summary": raw or "No results"}
    items = []
    for line in raw.splitlines():
        line = line.strip()
        if line and not line.startswith("Search results"):
            name = line.rstrip("/")
            is_dir = line.endswith("/")
            items.append({"name": name, "detail": "Folder" if is_dir else "File"})
    return {"items": items[:10], "summary": f"{len(items)} results"}


def search_gitea(query: str) -> dict:
    """Filter Gitea repos by name (no server-side search API)."""
    from familiar.skills.gitea.skill import gitea_list_repos

    raw = gitea_list_repos({})
    if not raw or "not configured" in raw.lower():
        return {"items": [], "summary": raw or "No results"}
    q = query.lower()
    items = []
    for line in raw.splitlines()[1:]:  # skip header
        line = line.strip()
        if not line:
            continue
        parts = line.split("  ")
        name = parts[0].strip()
        detail = "  ".join(parts[1:]).strip() if len(parts) > 1 else ""
        if q in name.lower():
            items.append({"name": name, "detail": detail})
    return {"items": items[:10], "summary": f"{len(items)} results"}


def search_jellyfin(query: str) -> dict:
    """Search Jellyfin media library."""
    from familiar.skills.jellyfin.skill import jf_search_media

    raw = jf_search_media({"query": query})
    if not raw or raw.startswith("No results") or "not configured" in raw.lower():
        return {"items": [], "summary": raw or "No results"}
    items = []
    for line in raw.splitlines():
        line = line.strip()
        if line.startswith("["):
            bracket_end = line.index("]") + 1
            item_type = line[1 : bracket_end - 1]
            title = line[bracket_end:].strip()
            items.append({"name": title, "detail": item_type})
        elif line and not line.startswith("Search results") and not line.startswith("..."):
            items.append({"name": line, "detail": ""})
    return {"items": items[:10], "summary": f"{len(items)} results"}


def search_video(query: str) -> dict:
    """Search local video library by title/uploader/description."""
    video_index = Path.home() / ".familiar" / "video" / "index.json"
    if not video_index.exists():
        return {"items": [], "summary": "No video index"}
    data = json.loads(video_index.read_text())
    if not data:
        return {"items": [], "summary": "No videos"}
    q = query.lower()
    items = []
    for v in data.values():
        title = v.get("title", "")
        uploader = v.get("uploader", "")
        desc = v.get("description", "")
        if q in title.lower() or q in uploader.lower() or q in desc.lower():
            dur = v.get("duration_seconds", 0)
            mins, secs = divmod(dur, 60)
            hrs, mins = divmod(mins, 60)
            dur_str = f"{hrs}:{mins:02d}:{secs:02d}" if hrs else f"{mins}:{secs:02d}"
            detail_parts = [dur_str]
            if uploader:
                detail_parts.append(uploader)
            items.append({"name": title or "(untitled)", "detail": " | ".join(detail_parts)})
    return {"items": items[:10], "summary": f"{len(items)} results"}


def search_mealie(query: str) -> dict:
    """Search Mealie recipes by keyword."""
    from familiar.skills.mealie.skill import mealie_recipes

    raw = mealie_recipes({"query": query, "per_page": 10})
    if not raw or "No recipes" in raw or raw.startswith("\u274c"):
        return {"items": [], "summary": raw or "No results"}
    items = []
    for line in raw.splitlines():
        line = line.strip()
        if not line:
            continue
        if line[0].isdigit() and ". **" in line:
            name = line.split("**")[1] if "**" in line else line
            items.append({"name": name, "detail": ""})
        elif line.startswith("Tags:") and items:
            items[-1]["detail"] = line
        elif line.startswith("   ") and items and not items[-1]["detail"] and "|" in line:
            items[-1]["detail"] = line.strip()
    return {"items": items[:10], "summary": f"{len(items)} results"}


def search_github(query: str) -> dict:
    """Search GitHub repos via PyGithub."""
    token = os.environ.get("GITHUB_TOKEN", "")
    if not token:
        return {"items": [], "summary": "No GitHub token"}
    from github import Github

    g = Github(token)
    repos = g.search_repositories(query)
    items = []
    for repo in repos[:10]:
        detail = f"Updated {str(repo.updated_at)[:10]}"
        if repo.stargazers_count:
            detail += f" | {repo.stargazers_count} stars"
        items.append({"name": repo.full_name, "detail": detail})
    return {"items": items, "summary": f"{len(items)} results"}


# ============================================================
# Aggregate dispatchers
# ============================================================

_BROWSE_DISPATCHERS: dict[str, callable] = {
    "github": browse_github,
    "email": browse_email,
    "drive": browse_drive,
    "nextcloud": browse_nextcloud,
    "gitea": browse_gitea,
    "jellyfin": browse_jellyfin,
    "mealie": browse_mealie,
    "video": browse_video,
    "browser": browse_browser,
}

_SEARCH_DISPATCHERS: dict[str, callable] = {
    "drive": search_drive,
    "email": search_email,
    "nextcloud": search_nextcloud,
    "gitea": search_gitea,
    "jellyfin": search_jellyfin,
    "mealie": search_mealie,
    "video": search_video,
    "github": search_github,
}

SEARCHABLE_SERVICES = sorted(_SEARCH_DISPATCHERS.keys())


def browse_all() -> dict[str, dict]:
    """Run all browse functions, returning ``{service_key: result}`` for connected services."""
    services: dict[str, dict] = {}
    for name, fn in _BROWSE_DISPATCHERS.items():
        try:
            result = fn()
            if result is not None:
                services[name] = result
        except Exception as e:
            logger.debug(f"Browse dispatcher {name} failed: {e}")
    return services


def search_service(service: str, query: str) -> dict:
    """Dispatch to the correct search handler.

    Returns ``{"items": [...], "summary": ...}`` or ``{"items": [], "error": ...}``.
    """
    handler = _SEARCH_DISPATCHERS.get(service)
    if not handler:
        return {"items": [], "error": f"Unknown service: {service}"}
    try:
        return handler(query)
    except Exception as e:
        logger.exception(f"Search {service} failed")
        return {"items": [], "error": str(e)}


# ============================================================
# Text formatters for channel output
# ============================================================


def format_browse_results(results: dict[str, dict], mode: FormatMode) -> str:
    """Format browse results as text suitable for a channel message."""
    if not results:
        return "No connected services with browsable data found.\nUse /connect to set up services."

    lines = [f"\U0001f4cb {fmt_bold('Connected Services', mode)}\n"]
    for _key, svc in results.items():
        display = svc.get("display_name", _key)
        summary = svc.get("summary", "")
        header = f"{fmt_bold(display, mode)}"
        if summary:
            header += f" ({summary})"
        lines.append(header)
        for item in svc.get("items", [])[:5]:
            detail = f" \u2014 {item['detail']}" if item.get("detail") else ""
            lines.append(f"  \u2022 {item['name']}{detail}")
        if svc.get("error"):
            lines.append(f"  \u26a0\ufe0f {svc['error']}")
        lines.append("")

    svc_list = ", ".join(SEARCHABLE_SERVICES)
    lines.append(f"Use /search <service> <query> to search.\nServices: {svc_list}")
    return "\n".join(lines)


def format_search_results(service: str, results: dict, mode: FormatMode) -> str:
    """Format search results as text suitable for a channel message."""
    if results.get("error"):
        return f"\u26a0\ufe0f Search error: {results['error']}"
    items = results.get("items", [])
    summary = results.get("summary", "")
    if not items:
        return f"No results for '{service}' search. {summary}"

    lines = [f"\U0001f50d {fmt_bold(f'Search: {service}', mode)} ({summary})\n"]
    for item in items:
        detail = f" \u2014 {item['detail']}" if item.get("detail") else ""
        lines.append(f"  \u2022 {item['name']}{detail}")
    return "\n".join(lines)
