"""Preferences satellite routes + feeds extracted from [mugi_backend._build_app](mugi_backend.py)."""
from __future__ import annotations

import asyncio
import json
import logging
import os
import re
import sys
import urllib.error
import urllib.request
from collections.abc import Callable
from typing import Any

from fastapi import Request as FRequest
from fastapi.responses import JSONResponse

from backend.bootstrap import _sync_backend_venv_packages
from utils.llm_token_normalize import normalize_hex_id_token


def register_preferences_satellite_routes(
    fastapi_app: Any,
    *,
    delegate: Any,
    load_app_prefs: Callable[[], dict],
    persist_searxng_setup_success: Callable[[], None],
    persist_searxng_setup_failure: Callable[[], None],
    plugins_for_api: Callable[..., Any],
    max_json_body_bytes: int,
    gui_brew_formula_re: re.Pattern[str],
    gui_brew_tap_re: re.Pattern[str],
    gui_brew_install_lock: asyncio.Lock,
    log: logging.Logger,
) -> None:
    """Register feeds, SearXNG prefs, Discord test, advanced tools, mugi brew, dashboard-plugins list."""

    @fastapi_app.get("/feeds")
    async def get_feeds():
        """Return list of subscribed RSS/Atom feeds from workspace memory/feeds.json."""
        try:
            from core import get_config, get_workspace_path
            workspace = get_workspace_path()
            if not workspace:
                cfg = get_config()
                workspace = (cfg.get("general") or {}).get("workspace_path")
            if workspace:
                workspace = os.path.expanduser(workspace)
            if not workspace or not os.path.isdir(workspace):
                return JSONResponse(content={"feeds": []})
            feeds_path = os.path.join(workspace, "memory", "feeds.json")
            if not os.path.isfile(feeds_path):
                return JSONResponse(content={"feeds": []})
            with open(feeds_path, "r", encoding="utf-8") as f:
                data = json.load(f)
            feeds = data if isinstance(data, list) else []
            return JSONResponse(content={"feeds": feeds})
        except Exception as e:
            log.warning("get_feeds failed", exc_info=True)
            return JSONResponse(content={"feeds": [], "error": str(e)}, status_code=500)

    @fastapi_app.post("/feeds")
    async def add_feed_route(request: FRequest):
        """Add an RSS/Atom feed. Body: { "url": "...", "label": "..." }. Uses feed_tools.add_feed."""
        try:
            body = await request.json()
        except Exception:
            body = {}
        url = (body.get("url") or "").strip()
        if not url:
            return JSONResponse(content={"ok": False, "message": "url is required"}, status_code=400)
        label = (body.get("label") or "").strip()
        try:
            from tools.feed_tools import add_feed
            loop = asyncio.get_event_loop()
            result = await loop.run_in_executor(None, lambda: add_feed.invoke({"url": url, "label": label}))
        except Exception as e:
            return JSONResponse(content={"ok": False, "message": str(e)}, status_code=500)
        if result.startswith("ERROR:") or result.strip().startswith("Already"):
            return JSONResponse(content={"ok": False, "message": result}, status_code=400)
        return JSONResponse(content={"ok": True, "message": result})

    @fastapi_app.delete("/feeds/{feed_id}")
    async def remove_feed_route(feed_id: str):
        """Remove an RSS/Atom feed by id. Uses feed_tools.remove_feed."""
        fid = normalize_hex_id_token((feed_id or "").strip(), allowed_lengths=(8,))
        if not fid:
            return JSONResponse(content={"ok": False, "message": "feed_id required"}, status_code=400)
        try:
            from tools.feed_tools import remove_feed
            result = remove_feed.invoke({"feed_id": fid})
        except Exception as e:
            return JSONResponse(content={"ok": False, "message": str(e)}, status_code=500)
        if "ID not found" in result:
            return JSONResponse(content={"ok": False, "message": result}, status_code=404)
        return JSONResponse(content={"ok": True, "message": result})

    @fastapi_app.post("/preferences/searxng/{action}")
    async def searxng_action(action: str):
        try:
            from tools.searxng_docker import (
                run_setup,
                searxng_start,
                searxng_stop,
                searxng_restart,
                remove_searxng_setup,
            )
            from core.config import get_search_config
            cfg = get_search_config()
            port = int(cfg.get("port", 8888))
            if port < 1024 or port > 65535:
                port = 8888

            actions = {
                "setup": lambda: run_setup(port),
                "start": lambda: searxng_start(port),
                "stop": searxng_stop,
                "restart": lambda: searxng_restart(port),
                "remove": remove_searxng_setup,
            }
            fn = actions.get(action)
            if fn is None:
                return JSONResponse(content={"error": "unknown action"}, status_code=400)
            ok, msg = fn()
        except ImportError:
            return JSONResponse(content={"error": "searxng_docker not available"}, status_code=500)
        except Exception as exc:
            return JSONResponse(content={"error": str(exc)}, status_code=500)

        try:
            from tools.web_tools import clear_health_cache
            clear_health_cache()
        except Exception:
            log.debug("Failed to clear web health cache", exc_info=True)
        if ok and action == "setup":
            persist_searxng_setup_success()
        elif not ok and action == "setup":
            persist_searxng_setup_failure()
        elif ok and action == "remove":
            try:
                from core import get_config, save_config, reload_config
                config = get_config()
                config.setdefault("search", {})["use_local_docker"] = False
                save_config(config)
                reload_config()
            except Exception:
                log.debug("Failed to save SearXNG remove config", exc_info=True)

        if not ok:
            return JSONResponse(
                content={"ok": False, "message": msg or "Action failed."},
                status_code=502,
            )
        return JSONResponse(content={"ok": True, "message": msg or ""})

    @fastapi_app.get("/preferences/searxng/status")
    async def searxng_status_route():
        error = ""
        try:
            from tools.searxng_docker import searxng_container_status
            status = searxng_container_status()
        except Exception as exc:
            status = "unknown"
            error = str(exc)
        return JSONResponse(content={"status": status, "error": error})

    @fastapi_app.post("/preferences/messaging/discord/test")
    async def test_discord_connection(request: FRequest):
        try:
            body = await request.json()
        except Exception:
            body = {}
        token = (body.get("token") or "").strip()
        if not token:
            return JSONResponse(content={"ok": False, "error": "Token is empty."}, status_code=400)
        auth_value = token if token.lower().startswith("bot ") else ("Bot " + token)
        try:
            import ssl as _ssl
            try:
                import certifi
                _ctx = _ssl.create_default_context(cafile=certifi.where())
            except ImportError:
                _ctx = _ssl.create_default_context()
            req = urllib.request.Request("https://discord.com/api/v10/users/@me")
            req.add_header("Authorization", auth_value)
            req.add_header("User-Agent", "Mugi-Settings/1.0 (Discord token check)")
            loop = asyncio.get_running_loop()

            def _open_discord_user() -> int:
                with urllib.request.urlopen(req, timeout=10, context=_ctx) as resp:
                    return int(resp.getcode())

            code = await loop.run_in_executor(None, _open_discord_user)
            if 200 <= code < 300:
                return JSONResponse(content={"ok": True})
            return JSONResponse(content={"ok": False, "error": "Discord returned HTTP %s" % code})
        except urllib.error.HTTPError as e:
            if e.code == 401:
                return JSONResponse(
                    content={
                        "ok": False,
                        "error": "Invalid token (401). Check the token in the Discord Developer Portal.",
                    }
                )
            return JSONResponse(content={"ok": False, "error": "Discord returned HTTP %s" % e.code})
        except Exception as e:
            err_text = str(getattr(e, "reason", None) or e)
            if not err_text or "token" in err_text.lower():
                err_text = "Network or connection error."
            return JSONResponse(content={"ok": False, "error": err_text})

    @fastapi_app.post("/preferences/advanced/kraken/install")
    async def kraken_install():
        try:
            from tools import ensure_kraken_venv
        except Exception:
            return JSONResponse(content={"ok": False, "error": "Kraken OCR support is not available."}, status_code=500)
        loop = asyncio.get_event_loop()
        try:
            ok, msg = await loop.run_in_executor(None, ensure_kraken_venv)
            return JSONResponse(content={"ok": ok, "message": msg or ""})
        except Exception as e:
            return JSONResponse(content={"ok": False, "message": str(e)})

    @fastapi_app.post("/preferences/advanced/kraken/remove")
    async def kraken_remove():
        try:
            from tools import remove_kraken_venv
        except Exception:
            return JSONResponse(content={"ok": False, "error": "Kraken OCR support is not available."}, status_code=500)
        if remove_kraken_venv is None:
            return JSONResponse(content={"ok": False, "error": "Remove not available."}, status_code=500)
        loop = asyncio.get_event_loop()
        try:
            ok, msg = await loop.run_in_executor(None, remove_kraken_venv)
            return JSONResponse(content={"ok": ok, "message": msg or ""})
        except Exception as e:
            return JSONResponse(content={"ok": False, "message": str(e)})

    @fastapi_app.get("/preferences/advanced/playwright/status")
    async def playwright_status():
        try:
            from tools.playwright_screenshot import playwright_available
            from utils.playwright_runner import playwright_browsers_ready

            available = playwright_available()
            browsers_ready, _detail = playwright_browsers_ready() if available else (False, "")
        except Exception:
            available = False
            browsers_ready = False
        return JSONResponse(content={"available": available, "browsers_ready": browsers_ready})

    @fastapi_app.post("/preferences/advanced/playwright/install")
    async def playwright_install():
        try:
            from tools.playwright_screenshot import install_playwright
        except Exception:
            return JSONResponse(content={"ok": False, "message": "Playwright support is not available."}, status_code=500)
        loop = asyncio.get_event_loop()
        try:
            ok, msg = await loop.run_in_executor(None, lambda: install_playwright())
            return JSONResponse(content={"ok": ok, "message": msg or ""})
        except Exception as e:
            return JSONResponse(content={"ok": False, "message": str(e)})

    @fastapi_app.post("/preferences/advanced/backend/sync")
    async def backend_sync():
        """Install or upgrade packages in the backend venv (~/.mugi/venv) to match current requirements. Does not touch Kraken or Playwright venvs."""
        loop = asyncio.get_event_loop()
        try:
            ok, msg = await loop.run_in_executor(None, _sync_backend_venv_packages)
            return JSONResponse(content={"ok": ok, "message": msg or ""})
        except Exception as e:
            return JSONResponse(content={"ok": False, "message": str(e)})

    @fastapi_app.post("/preferences/mugi_brew/install")
    async def mugi_brew_install_route(req: FRequest):
        """Install a formula via Mugi Homebrew (GUI-triggered; auth enforced by middleware)."""
        raw = await req.body()
        if len(raw) > max_json_body_bytes:
            return JSONResponse(content={"error": "body too large"}, status_code=413)
        try:
            payload = json.loads(raw.decode("utf-8") if isinstance(raw, (bytes, bytearray)) else str(raw or "{}"))
        except Exception:
            return JSONResponse(content={"error": "invalid JSON body"}, status_code=400)
        if not isinstance(payload, dict):
            return JSONResponse(content={"error": "invalid JSON body"}, status_code=400)

        formula = str(payload.get("formula") or "").strip()
        tap = str(payload.get("tap") or "").strip()
        if payload.get("user_confirmed") is not True:
            return JSONResponse(content={"error": "user confirmation required"}, status_code=400)
        if not formula:
            return JSONResponse(content={"error": "formula is required"}, status_code=400)
        if not gui_brew_formula_re.fullmatch(formula):
            return JSONResponse(content={"error": "invalid formula name"}, status_code=400)
        if tap and not gui_brew_tap_re.fullmatch(tap):
            return JSONResponse(content={"error": "invalid tap format (expected user/repo)"}, status_code=400)

        prefs = load_app_prefs()
        if not prefs.get("allow_shell_execute", False):
            return JSONResponse(
                content={"ok": False, "message": "Shell execution is disabled. Enable it in Advanced settings first."},
                status_code=200,
            )

        from tools.mugi_brew_bootstrap import _run_brew, ensure_mugi_homebrew_core, get_mugi_brew_bin

        async with gui_brew_install_lock:
            def _run_install() -> tuple[bool, str]:
                if not get_mugi_brew_bin():
                    ok_boot, msg_boot = ensure_mugi_homebrew_core(python_for_dulwich=sys.executable)
                    if not ok_boot:
                        return False, msg_boot or "Failed to initialize Mugi Homebrew."
                if tap:
                    ok_tap, msg_tap = _run_brew(["tap", tap], timeout=300)
                    if not ok_tap:
                        return False, msg_tap or f"Failed to tap {tap}."
                ok_install, msg_install = _run_brew(["install", formula], timeout=900)
                return ok_install, msg_install or ""

            try:
                ok, msg = await asyncio.to_thread(_run_install)
            except Exception as exc:
                return JSONResponse(content={"ok": False, "message": str(exc)})
        return JSONResponse(content={"ok": ok, "message": msg})

    @fastapi_app.get("/preferences/dashboard-plugins")
    async def list_dashboard_plugins():
        if not callable(plugins_for_api):
            return JSONResponse(content=[])
        try:
            return JSONResponse(content=plugins_for_api(delegate._plugin_manager, dashboard_only=True))
        except Exception as exc:
            log.warning("list_dashboard_plugins failed", exc_info=True)
            return JSONResponse(content={"error": str(exc)[:200]}, status_code=500)
