# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# Licensed under the MIT License

"""Connect wizard + Google OAuth routes — /api/connect/*."""

from __future__ import annotations

from datetime import datetime
import json
import logging
import os
import threading
import time

from flask import Blueprint, Response, jsonify, request, send_file

from ..auth import require_auth
from ..helpers import get_agent, get_service_manager, get_wizard_host, DATA_DIR, AGENT_DIR, safe_error

logger = logging.getLogger(__name__)

bp = Blueprint("connect", __name__)




# ============================================================
# Connect Wizard Routes
# ============================================================


@bp.route("/api/connect/wizard/start", methods=["POST"])
@require_auth
def api_wizard_start():
    """Start (or restart) the connect wizard."""
    data = request.json or {}
    args = data.get("args", [])
    host = get_wizard_host()
    messages = host.start_wizard(args)
    return jsonify({"wizard_messages": messages})


@bp.route("/api/connect/wizard/select", methods=["POST"])
@require_auth
def api_wizard_select():
    """Handle a wizard button click."""
    data = request.json or {}
    key = data.get("key", "")
    if not key:
        return jsonify({"error": "No key provided"}), 400
    # Google OAuth keys are handled by the dedicated panel — bypass the wizard
    if key.startswith("google_auth_") or key == "google_upload_creds":
        return jsonify({"redirect": "google_oauth"})
    host = get_wizard_host()
    messages = host.select_menu(key)
    return jsonify({"wizard_messages": messages})


@bp.route("/api/connect/wizard/tab", methods=["POST"])
@require_auth
def api_wizard_tab():
    """Switch to a wizard tab."""
    data = request.json or {}
    tab = data.get("tab", "")
    if not tab:
        return jsonify({"error": "No tab provided"}), 400
    host = get_wizard_host()
    messages = host.switch_tab(tab)
    return jsonify({"wizard_messages": messages})


@bp.route("/api/connect/wizard/input", methods=["POST"])
@require_auth
def api_wizard_input():
    """Send text input to the wizard (e.g. API key, URL)."""
    data = request.json or {}
    text = data.get("text", "")
    if not text:
        return jsonify({"error": "No text provided"}), 400
    host = get_wizard_host()
    messages = host.send_text(text)
    return jsonify({"wizard_messages": messages})


@bp.route("/api/connect/upload/google-credentials", methods=["POST"])
@require_auth
def api_upload_google_credentials():
    """Upload Google OAuth credentials.json file."""
    if "file" not in request.files:
        return jsonify({"error": "No file uploaded"}), 400
    f = request.files["file"]
    if not f.filename:
        return jsonify({"error": "Empty filename"}), 400
    import json as _json

    try:
        content = f.read()
        data = _json.loads(content)
        # Validate it looks like a Google credentials file
        if "installed" not in data and "web" not in data:
            return jsonify({"error": "Not a valid Google OAuth credentials file"}), 400
    except _json.JSONDecodeError:
        return jsonify({"error": "Invalid JSON file"}), 400
    dest = DATA_DIR / "google_credentials.json"
    dest.parent.mkdir(parents=True, exist_ok=True)
    dest.write_bytes(content)
    cred_type = "Desktop app" if "installed" in data else "Web app"
    return jsonify({"success": True, "type": cred_type, "path": str(dest)})


# ── Google OAuth (dashboard-native, no wizard) ────────────────────────────────
# Keyed by service name ("gmail", "calendar", "drive", "contacts").
# Each entry: {"done": bool, "success": bool, "error": str|None, "auth_url": str|None}
_google_oauth_state: dict = {}
_google_oauth_lock = threading.Lock()

def _google_accounts_file() -> Path:
    return DATA_DIR / "google_accounts.json"


def _load_google_accounts() -> list:
    path = _google_accounts_file()
    if path.exists():
        try:
            import json as _json
            return _json.loads(path.read_text()).get("accounts", [])
        except Exception:
            pass
    return []


def _register_google_account(email: str, service: str):
    """Register a Google account with the service it was authorized for."""
    import json as _json
    path = _google_accounts_file()
    data = {"accounts": _load_google_accounts()}

    # Find or create account entry
    existing = None
    for acct in data["accounts"]:
        if acct.get("email", "").lower() == email.lower():
            existing = acct
            break

    if existing:
        if service not in existing.get("services", []):
            existing.setdefault("services", []).append(service)
    else:
        data["accounts"].append({
            "email": email,
            "label": "",
            "services": [service],
            "added_at": datetime.now().isoformat(),
        })

    path.parent.mkdir(parents=True, exist_ok=True)
    from familiar.core.paths import save_secure_file
    save_secure_file(path, _json.dumps(data, indent=2))


_GOOGLE_SERVICES = {
    "calendar": {
        "token_file": "google_token.json",
        "scopes": ["https://www.googleapis.com/auth/calendar"],
    },
    "gmail": {
        "token_file": "google_token_gmail.json",
        "scopes": [
            "https://www.googleapis.com/auth/gmail.readonly",
            "https://www.googleapis.com/auth/gmail.send",
            "https://www.googleapis.com/auth/gmail.modify",
        ],
    },
    "drive": {
        "token_file": "google_token_drive.json",
        "scopes": [
            "https://www.googleapis.com/auth/drive.readonly",
            "https://www.googleapis.com/auth/drive.file",
        ],
    },
    "contacts": {
        "token_file": "google_token_contacts.json",
        "scopes": ["https://www.googleapis.com/auth/contacts.readonly"],
    },
}


def _run_google_oauth_thread(service: str, creds_path: Path, token_path: Path, scopes: list):
    """Background thread: bind port → build auth URL → wait for redirect → save token."""
    import http.server
    import os as _os
    import urllib.parse

    # Allow Google to return broader scopes than requested (it includes all
    # previously-granted scopes in the token response; without this flag
    # requests_oauthlib raises a scope mismatch error).
    _os.environ["OAUTHLIB_RELAX_TOKEN_SCOPE"] = "1"

    with _google_oauth_lock:
        state = _google_oauth_state[service]
    try:
        from google_auth_oauthlib.flow import Flow

        flow = Flow.from_client_secrets_file(
            str(creds_path),
            scopes=scopes,
            redirect_uri=None,  # set after port bind
        )

        # Bind a free port
        server_address = ("127.0.0.1", 0)
        httpd = http.server.HTTPServer(server_address, http.server.BaseHTTPRequestHandler)
        port = httpd.server_address[1]
        redirect_uri = f"http://127.0.0.1:{port}/"
        flow.redirect_uri = redirect_uri

        auth_url, _ = flow.authorization_url(
            access_type="offline",
            prompt="consent",
        )
        with _google_oauth_lock:
            state["auth_url"] = auth_url

        # Capture the redirect request
        received: dict = {}

        class _Handler(http.server.BaseHTTPRequestHandler):
            def log_message(self, fmt, *args):  # silence server logs
                pass

            def do_GET(self):
                received["path"] = self.path
                self.send_response(200)
                self.send_header("Content-Type", "text/html")
                self.end_headers()
                self.wfile.write(
                    b"<html><body><h2>Authorization complete!</h2>"
                    b"<p>You can close this tab and return to Familiar.</p>"
                    b"<script>window.close();</script></body></html>"
                )

        httpd.RequestHandlerClass = _Handler
        httpd.handle_request()  # blocks until browser redirects

        # Exchange code for token
        parsed = urllib.parse.urlparse(received.get("path", "/"))
        params = dict(urllib.parse.parse_qsl(parsed.query))
        if "error" in params:
            with _google_oauth_lock:
                state["error"] = params["error"]
                state["done"] = True
            return

        flow.fetch_token(code=params["code"])
        from familiar.core.paths import save_secure_file

        # Detect which Google account was authorized
        account_email = ""
        try:
            import json as _json
            token_json = flow.credentials.to_json()
            # Try to get email from userinfo
            import urllib.request
            _req = urllib.request.Request(
                "https://www.googleapis.com/oauth2/v1/userinfo?alt=json",
                headers={"Authorization": f"Bearer {flow.credentials.token}"},
            )
            with urllib.request.urlopen(_req, timeout=5) as _resp:
                _info = _json.loads(_resp.read())
                account_email = _info.get("email", "")
        except Exception:
            pass

        # Save to legacy path (backward compat)
        save_secure_file(token_path, flow.credentials.to_json())

        # Also save to per-account directory if we know the email
        if account_email:
            safe_name = account_email.replace("@", "_").replace(".", "_")
            acct_dir = DATA_DIR / "google_accounts" / safe_name
            acct_dir.mkdir(parents=True, exist_ok=True)
            acct_token = acct_dir / f"token_{service}.json"
            save_secure_file(acct_token, flow.credentials.to_json())

            # Register in google_accounts.json
            _register_google_account(account_email, service)

        with _google_oauth_lock:
            state["success"] = True
            state["account_email"] = account_email
            state["done"] = True
    except Exception as exc:
        with _google_oauth_lock:
            state["error"] = str(exc)
            state["done"] = True
    finally:
        try:
            httpd.server_close()
        except Exception:
            pass


@bp.route("/api/connect/google/start/<service>", methods=["POST"])
@require_auth
def api_google_oauth_start(service: str):
    """Start Google OAuth for the given service. Returns auth_url for the browser."""
    if service not in _GOOGLE_SERVICES:
        return jsonify({"error": f"Unknown service: {service}"}), 400

    creds_path = DATA_DIR / "google_credentials.json"
    if not creds_path.exists():
        return jsonify({"error": "google_credentials.json not found — upload it first"}), 400

    cfg = _GOOGLE_SERVICES[service]
    token_path = DATA_DIR / cfg["token_file"]

    # Reset state for this service
    with _google_oauth_lock:
        _google_oauth_state[service] = {
            "done": False,
            "success": False,
            "error": None,
            "auth_url": None,
        }

    t = threading.Thread(
        target=_run_google_oauth_thread,
        args=(service, creds_path, token_path, cfg["scopes"]),
        daemon=False,
        name=f"google-oauth-{service}",
    )
    t.start()

    # Wait up to 3 s for the auth URL to appear (the port bind is nearly instant)
    deadline = time.time() + 3.0
    while time.time() < deadline:
        with _google_oauth_lock:
            st = _google_oauth_state[service]
            if st["auth_url"] or st["done"]:
                break
        time.sleep(0.05)

    with _google_oauth_lock:
        st = dict(_google_oauth_state[service])
    if st.get("error"):
        return jsonify({"error": st["error"]}), 500
    return jsonify({"auth_url": st.get("auth_url"), "service": service})


@bp.route("/api/connect/google/status/<service>")
@require_auth
def api_google_oauth_status(service: str):
    """Poll whether the OAuth flow for a service has completed."""
    with _google_oauth_lock:
        st = _google_oauth_state.get(service)
        if st is None:
            return jsonify({"done": False, "success": False, "error": "No active flow"})
        return jsonify(
            {
                "done": st["done"],
                "success": st["success"],
                "error": st["error"],
            }
        )


@bp.route("/api/connect/google/reset", methods=["POST"])
@require_auth
def api_google_reset():
    """Delete Google credential and/or token files."""
    data = request.json or {}
    target = data.get("target", "tokens")  # "tokens" | "all"
    data_dir = DATA_DIR
    removed = []
    files = [cfg["token_file"] for cfg in _GOOGLE_SERVICES.values()]
    if target == "all":
        files.append("google_credentials.json")
    for fname in files:
        p = data_dir / fname
        if p.exists():
            p.unlink()
            removed.append(fname)
    return jsonify({"removed": removed})


@bp.route("/api/connect/google/token-status")
@require_auth
def api_google_token_status():
    """Return which Google service tokens exist on disk."""
    data_dir = DATA_DIR
    result = {}
    for svc, cfg in _GOOGLE_SERVICES.items():
        result[svc] = (data_dir / cfg["token_file"]).exists()
    result["credentials"] = (data_dir / "google_credentials.json").exists()
    return jsonify(result)


@bp.route("/api/connect/wizard/cancel", methods=["POST"])
@require_auth
def api_wizard_cancel():
    """Cancel any active wizard session so chat routes to the agent."""
    host = get_wizard_host()
    host.cancel()
    return jsonify({"success": True})


@bp.route("/api/connect/wizard/state")
@require_auth
def api_wizard_state():
    """Check whether a wizard session is active."""
    host = get_wizard_host()
    return jsonify({"active": host.is_active()})


@bp.route("/api/connect/wizard/context")
@require_auth
def api_wizard_context():
    """Return context about the active wizard step (for Helper Hat handoff)."""
    host = get_wizard_host()
    return jsonify(host.get_current_context())


@bp.route("/api/wizard/handoff", methods=["POST"])
@require_auth
def api_wizard_handoff():
    """Hand off an active wizard to the Helper agent for completion.

    The wizard is cancelled and a structured prompt is sent to the agent
    so it can finish the setup using its host_setup / shell tools.
    """
    data = request.json or {}
    service_name = data.get("service_name", "unknown service")
    step = data.get("step", "")
    context = data.get("context", "")
    source = data.get("source", "connect")

    # Cancel the active wizard so chat routes to the agent
    host = get_wizard_host()
    host.cancel()

    # Build a prompt that gives the agent full context
    prompt_parts = [
        f"The user needs help setting up {service_name}.",
    ]
    if step:
        prompt_parts.append(f"They were on step '{step}' of the {source} wizard.")
    if context:
        prompt_parts.append(f"Context: {context}")
    prompt_parts.append(
        "Help them complete the installation using your host_setup, "
        "shell, and file tools. Detect the system first, then install "
        "and configure what's needed. Ask for any credentials or values "
        "you don't have."
    )
    prompt = " ".join(prompt_parts)

    agent = get_agent()
    result = agent.chat_with_results(prompt, user_id="dashboard", channel="web")

    return jsonify(
        {
            "response": result.text,
            "panel": "chat",
            "timestamp": datetime.now().isoformat(),
        }
    )


@bp.route("/api/config")
@require_auth
def api_config():
    """Get current configuration."""
    agent = get_agent()
    config = {
        "agent_name": agent.config.agent.name,
        "provider": agent.provider.name,
        "memory_enabled": agent.config.agent.memory_enabled,
        "skills_enabled": agent.config.agent.skills_enabled,
        "scheduler_enabled": agent.config.agent.scheduler_enabled,
    }
    return jsonify(config)


@bp.route("/api/config", methods=["POST"])
@require_auth
def api_config_update():
    """Update configuration."""
    data = request.json or {}
    # For now just switch provider
    if "provider" in data:
        agent = get_agent()
        result = agent.switch_provider(data["provider"])
        return jsonify({"result": result})

    return jsonify({"error": "No valid config option"}), 400


@bp.route("/api/config/routing")
@require_auth
def api_config_routing():
    """Get current model routing configuration."""
    agent = get_agent()
    return jsonify(agent.get_routing_config())


@bp.route("/api/config/routing", methods=["POST"])
@require_auth
def api_config_routing_update():
    """Update model routing mode and manual route assignments."""
    data = request.json or {}
    mode = data.get("mode", "auto")
    routes = data.get("routes")
    agent = get_agent()
    try:
        result = agent.set_routing_config(mode, routes)
        return jsonify(result)
    except ValueError:
        return jsonify({"error": "Invalid routing configuration"}), 400


@bp.route("/api/config/routing/presets")
@require_auth
def api_routing_presets():
    """Get all routing preset slots."""
    agent = get_agent()
    return jsonify(agent.get_presets())


@bp.route("/api/config/routing/presets/save", methods=["POST"])
@require_auth
def api_routing_presets_save():
    """Save current routing config to a preset slot."""
    data = request.json or {}
    slot = data.get("slot")
    name = data.get("name")
    agent = get_agent()
    try:
        result = agent.save_preset(slot, name)
        return jsonify(result)
    except (ValueError, TypeError) as e:
        return jsonify({"error": safe_error(e)}), 400


@bp.route("/api/config/routing/presets/load", methods=["POST"])
@require_auth
def api_routing_presets_load():
    """Load a preset into the active routing config."""
    data = request.json or {}
    slot = data.get("slot")
    agent = get_agent()
    try:
        result = agent.load_preset(slot)
        return jsonify(result)
    except (ValueError, TypeError) as e:
        return jsonify({"error": safe_error(e)}), 400


@bp.route("/api/config/routing/presets/default", methods=["POST"])
@require_auth
def api_routing_presets_default():
    """Set which preset auto-loads on startup."""
    data = request.json or {}
    slot = data.get("slot")
    agent = get_agent()
    try:
        result = agent.set_active_preset(slot)
        return jsonify(result)
    except (ValueError, TypeError) as e:
        return jsonify({"error": safe_error(e)}), 400


@bp.route("/api/config/routing/presets/rename", methods=["POST"])
@require_auth
def api_routing_presets_rename():
    """Rename a preset slot."""
    data = request.json or {}
    slot = data.get("slot")
    name = data.get("name", "")
    agent = get_agent()
    try:
        result = agent.rename_preset(slot, name)
        return jsonify(result)
    except (ValueError, TypeError) as e:
        return jsonify({"error": safe_error(e)}), 400


@bp.route("/api/config/routing/presets/delete", methods=["POST"])
@require_auth
def api_routing_presets_delete():
    """Clear a preset slot."""
    data = request.json or {}
    slot = data.get("slot")
    agent = get_agent()
    try:
        result = agent.delete_preset(slot)
        return jsonify(result)
    except (ValueError, TypeError) as e:
        return jsonify({"error": safe_error(e)}), 400


@bp.route("/api/config/job-class")
@require_auth
def api_job_class():
    """Get available job classes and the currently active one."""
    from familiar.creature.state import load_creature
    from familiar.jobs import list_job_classes

    creature = load_creature()
    active = creature.job_class if creature else ""
    classes = [
        {"key": jc.key, "name": jc.name, "icon": jc.icon, "description": jc.description}
        for jc in list_job_classes()
    ]
    return jsonify({"active": active, "classes": classes})


@bp.route("/api/config/job-class", methods=["POST"])
@require_auth
def api_job_class_update():
    """Switch the active job class."""
    data = request.json or {}
    key = data.get("job_class", "")
    agent = get_agent()
    result = agent.switch_job_class(key)
    return jsonify({"result": result, "active": key})


@bp.route("/api/connect/status")
@require_auth
def api_connect_status():
    """Get connection status of all supported services."""
    agent = get_agent()
    current_provider = agent.provider.name if agent else "unknown"

    services = []

    def _add(name, display_name, category, connected, detail=None, cmd=None):
        services.append(
            {
                "name": name,
                "display_name": display_name,
                "category": category,
                "connected": connected,
                "detail": detail,
                "connect_command": cmd or f"/connect {name}",
            }
        )

    # ── LLM Providers ────────────────────────────────────────
    anthropic_key = os.environ.get("ANTHROPIC_API_KEY", "")
    _add(
        "anthropic",
        "Anthropic (Claude)",
        "llm",
        bool(anthropic_key),
        f"...{anthropic_key[-4:]}" if len(anthropic_key) >= 4 else None,
    )
    openai_key = os.environ.get("OPENAI_API_KEY", "")
    _add(
        "openai",
        "OpenAI (GPT)",
        "llm",
        bool(openai_key),
        f"...{openai_key[-4:]}" if len(openai_key) >= 4 else None,
    )
    gemini_key = os.environ.get("GEMINI_API_KEY", "")
    _add(
        "gemini",
        "Gemini (Google AI)",
        "llm",
        bool(gemini_key),
        f"...{gemini_key[-4:]}" if len(gemini_key) >= 4 else None,
    )
    # Ollama — reachable if the API responds on localhost
    has_ollama = False
    ollama_detail = None
    try:
        import socket as _sock

        with _sock.create_connection(("127.0.0.1", 11434), timeout=2.0):
            has_ollama = True
        ollama_detail = current_provider if "Ollama" in current_provider else "Running"
    except (OSError, TimeoutError) as e:
        logger.debug("I/O error suppressed: %s", e)
    _add("ollama", "Ollama (local)", "llm", has_ollama, ollama_detail)

    # ── Integrations ─────────────────────────────────────────
    data_dir = DATA_DIR

    has_google = (data_dir / "google_credentials.json").exists()
    _add("google", "Google Workspace", "integrations", has_google, cmd="/connect google")

    has_gmail = (data_dir / "google_token_gmail.json").exists()
    _add("gmail", "Gmail API", "integrations", has_gmail, cmd="/connect google auth gmail")

    has_calendar_token = (data_dir / "google_token.json").exists()
    has_caldav = bool(os.environ.get("CALDAV_URL"))
    has_calendar = has_calendar_token or has_caldav
    _add(
        "calendar",
        "Calendar",
        "integrations",
        has_calendar,
        "CalDAV" if has_caldav else ("Google OAuth" if has_calendar_token else None),
        "/connect calendar",
    )

    has_drive = (data_dir / "google_token_drive.json").exists()
    _add("drive", "Google Drive", "integrations", has_drive, cmd="/connect google auth drive")

    try:
        from familiar.skills.email.accounts import get_all_accounts

        email_accounts = get_all_accounts()
        has_email = bool(email_accounts)
        email_detail = ", ".join(a["address"] for a in email_accounts) if email_accounts else None
    except Exception:
        has_email = bool(os.environ.get("EMAIL_ADDRESS"))
        email_detail = os.environ.get("EMAIL_ADDRESS") if has_email else None
    _add("email", "Email IMAP/SMTP", "integrations", has_email, email_detail, "/connect email")

    has_twilio = bool(os.environ.get("TWILIO_ACCOUNT_SID"))
    _add("sms", "SMS (Twilio)", "integrations", has_twilio, cmd="/connect sms")

    has_browser = False
    try:
        import playwright  # noqa: F401

        has_browser = True
    except Exception as e:
        logger.debug("Error suppressed: %s", e)
    _add("browser", "Browser (Playwright)", "integrations", has_browser, cmd="/connect browser")

    has_voice = False
    try:
        import faster_whisper  # noqa: F401

        has_voice = True
    except Exception:
        try:
            import whisper  # noqa: F401

            has_voice = True
        except Exception as e:
            logger.debug("Error suppressed: %s", e)
    _add("voice", "Voice (Whisper)", "integrations", has_voice, cmd="/connect voice")

    _add("websearch", "WebSearch (DuckDuckGo)", "integrations", True, "Built-in")

    _add(
        "github",
        "GitHub (issues & PRs)",
        "integrations",
        bool(os.environ.get("GITHUB_TOKEN")),
        cmd="/connect github",
    )
    _add(
        "slack",
        "Slack (Socket Mode)",
        "integrations",
        bool(os.environ.get("SLACK_BOT_TOKEN") and os.environ.get("SLACK_APP_TOKEN")),
        cmd="/connect slack",
    )

    # ── Self-Hosted ──────────────────────────────────────────
    for env_name, svc_name, display in [
        ("JELLYFIN_URL", "jellyfin", "Jellyfin (media)"),
        ("GITEA_URL", "gitea", "Gitea (code)"),
        ("HOMEASSISTANT_URL", "homeassistant", "Home Assistant"),
        ("JOPLIN_TOKEN", "joplin", "Joplin (notes)"),
        ("PIHOLE_URL", "pihole", "Pi-hole / AdGuard"),
        ("NEXTCLOUD_URL", "nextcloud", "Nextcloud"),
    ]:
        val = os.environ.get(env_name, "")
        _add(svc_name, display, "selfhosted", bool(val), val[:40] if val else None)

    # Stalwart Mail — TCP check on admin port or env var
    import socket

    _has_stalwart = bool(os.environ.get("EMAIL_SERVER_DOMAIN"))
    if not _has_stalwart:
        try:
            with socket.create_connection(("127.0.0.1", 8010), timeout=2.0):
                _has_stalwart = True
        except (OSError, TimeoutError):
            pass
    _stalwart_url = os.environ.get("EMAIL_SERVER_DOMAIN", "localhost:8010")
    _add(
        "email_server",
        "Stalwart Mail",
        "selfhosted",
        _has_stalwart,
        _stalwart_url if _has_stalwart else None,
    )

    # Proton Bridge — TCP check
    try:
        with socket.create_connection(("127.0.0.1", 1143), timeout=2.0):
            has_proton = True
    except (OSError, TimeoutError):
        has_proton = False
    _add("proton", "Proton Bridge", "selfhosted", has_proton, cmd="/connect proton")

    # ── Security ─────────────────────────────────────────────
    has_vaultwarden = bool(os.environ.get("VAULTWARDEN_URL"))
    _add(
        "vaultwarden",
        "Vaultwarden (passwords)",
        "security",
        has_vaultwarden,
        cmd="/connect security",
    )

    key_dir = AGENT_DIR / "keys"
    has_encryption = any(key_dir.glob("*.key")) if key_dir.exists() else False
    _add("encryption", "Encryption (at rest)", "security", has_encryption, cmd="/connect security")

    categories = ["llm", "integrations", "selfhosted", "security"]

    return jsonify(
        {
            "services": services,
            "categories": categories,
            "active_provider": current_provider,
        }
    )


