"""Dependency injection for FastAPI.

Provides access to service-specific Jellyfin/Jellyseerr clients and
machine-specific SSH clients via FastAPI's request context.

- Jellyfin/Jellyseerr are selected with a ``jellyfin_service_id`` query
  parameter (resolved against the service registry); the backend falls back to
  the first enabled ``jellyfin``/``jellyseerr`` service instance.
- SSH/Files transport is selected with ``machine_id`` as before.
"""

from __future__ import annotations

import logging
from functools import lru_cache
from typing import Any

from fastapi import HTTPException, Request

from media_library_viewer_api.clients.jellyfin import JellyfinClient
from media_library_viewer_api.clients.local import LocalCommandClient
from media_library_viewer_api.clients.ssh import RemoteSSHClient
from media_library_viewer_api.config import get_settings
from media_library_viewer_api.services.mail_queue import MailQueue
from media_library_viewer_api.services.mail_queue import get_mail_queue as _get_mail_queue
from media_library_viewer_api.services.settings_store import SettingsStore
from media_library_viewer_api.services.settings_store import get_settings_store as _get_settings_store

logger = logging.getLogger(__name__)


def _request_machine_id(request: Request | None) -> str | None:
    if request is None:
        return None
    machine_id = request.query_params.get("machine_id")
    return machine_id or None


def _request_jellyfin_service_id(request: Request | None) -> str | None:
    if request is None:
        return None
    service_id = request.query_params.get("jellyfin_service_id")
    return service_id or None


def _service_record(store: SettingsStore, service_type: str, service_id: str | None) -> dict[str, Any] | None:
    """Return a service row for a type, preferring the requested id.

    The row carries an in-memory decrypted ``secrets`` dict. Returns None if no
    enabled instance of the type exists.
    """
    from media_library_viewer_api.services.secrets import decrypt_secrets

    row = None
    if service_id:
        candidate = store.get_service(service_id)
        if candidate and candidate.get("service_type") == service_type and candidate.get("enabled", True):
            row = candidate
    if row is None:
        instances = [s for s in store.list_services(service_type) if s.get("enabled", True)]
        row = instances[0] if instances else None
    if row is None:
        return None
    decrypted = {}
    blob = row.get("secrets") or {}
    if blob:
        try:
            decrypted = decrypt_secrets(blob)
        except Exception:
            logger.exception("Failed to decrypt service secrets service_id=%s", row.get("id"))
    return {**row, "secrets": decrypted}


@lru_cache(maxsize=32)
def _jellyfin_client_for(cache_key: tuple[str, str, str]) -> JellyfinClient:
    machine_id, url, api_key = cache_key
    logger.info(
        "Creating Jellyfin client machine_id=%s url=%s", machine_id or "<default>", url.rstrip("/") or "<unset>"
    )
    return JellyfinClient(url, api_key)


@lru_cache(maxsize=32)
def _ssh_client_for(
    cache_key: tuple[str, str, str, int, str, str | None, str | None, str | None, str | None],
) -> RemoteSSHClient:
    machine_id, host, username, port, key_filename, password, private_key, private_key_passphrase, known_hosts_path = (
        cache_key
    )
    logger.info(
        "Creating SSH client machine_id=%s host=%s user=%s port=%s key=%s password=%s private_key=%s passphrase=%s",
        machine_id or "<default>",
        host or "<unset>",
        username or "<unset>",
        port,
        key_filename or "<unset>",
        "set" if password else "missing",
        "set" if private_key else "missing",
        "set" if private_key_passphrase else "missing",
    )
    client = RemoteSSHClient(
        host=host,
        username=username,
        port=port,
        key_filename=key_filename or None,
        private_key=private_key or None,
        private_key_passphrase=private_key_passphrase or None,
        password=password or None,
        known_hosts_path=known_hosts_path or None,
    )
    try:
        client.connect()
    except RuntimeError as exc:
        message = str(exc)
        lowered = message.lower()
        logger.exception("Failed to establish SSH connection to %s", host or "<unset>")
        if "banner" in lowered:
            raise HTTPException(
                status_code=502,
                detail=(
                    f"SSH banner not received from {host}:{port}. "
                    "Confirm the host, port, and firewall; the backend could not complete the SSH handshake."
                ),
            ) from exc
        if "authentication failed" in lowered or "no authentication methods available" in lowered:
            raise HTTPException(
                status_code=401,
                detail=(
                    f"SSH authentication failed for {host}:{port}. "
                    "Check the selected key, passphrase, username, or password."
                ),
            ) from exc
        raise HTTPException(status_code=502, detail=message) from exc
    except Exception:
        logger.exception("Failed to establish SSH connection to %s", host or "<unset>")
        raise
    return client


def _resolve_machine(service: str, request: Request | None = None) -> dict[str, Any] | None:
    """Resolve an SSH/Files machine for the given transport service.

    Jellyfin/Jellyseerr are resolved against the service registry, not here.
    """
    store = get_settings_store()
    machine_id = _request_machine_id(request)
    if machine_id:
        machine = store.get_machine(machine_id)
        if machine and (service in machine.get("services", []) or service == "ssh"):
            return machine
        return machine
    if service == "ssh":
        machines = store.list_machines_for_service("files") or store.list_machines_for_service("monitoring")
    else:
        machines = store.list_machines_for_service(service)
    return machines[0] if machines else None


def get_jellyfin_client(request: Request = None) -> JellyfinClient:
    """Return a Jellyfin client for the selected Jellyfin service instance."""
    store = get_settings_store()
    service_id = _request_jellyfin_service_id(request)
    service = _service_record(store, "jellyfin", service_id)
    if service is None:
        raise HTTPException(
            status_code=503,
            detail="No Jellyfin service is configured. Add a Jellyfin service on the Services page.",
        )
    base_url = str(service.get("config", {}).get("base_url") or "")
    api_key = str(service.get("secrets", {}).get("api_key") or "")
    if not base_url or not api_key:
        raise HTTPException(
            status_code=503,
            detail="Jellyfin service is missing base_url or api_key. Edit it on the Services page.",
        )
    cache_key = (service["id"], base_url, api_key)
    return _jellyfin_client_for(cache_key)


def _ssh_client_from_machine_config(machine: dict[str, Any], store: SettingsStore | None = None) -> RemoteSSHClient:
    """Build a RemoteSSHClient from a machine config dict."""
    store = store or get_settings_store()
    known_hosts_path = get_settings().ssh_known_hosts_file
    key_data = None
    key_passphrase = None
    ssh_key_id = str(machine.get("ssh_key_id") or "").strip()
    if ssh_key_id:
        ssh_key = store.get_ssh_key(ssh_key_id)
        if ssh_key:
            key_data = ssh_key.get("private_key") or None
            key_passphrase = ssh_key.get("passphrase") or None
    if not key_data and machine.get("ssh_private_key"):
        key_data = machine.get("ssh_private_key") or None
        key_passphrase = machine.get("ssh_private_key_passphrase") or None
    cache_key = (
        machine["id"],
        machine["host"],
        machine["username"],
        int(machine.get("port") or 22),
        f"{machine.get('key_directory')}/{machine.get('key_name')}"
        if machine.get("key_directory") and machine.get("key_name")
        else "",
        machine.get("password") or None,
        key_data,
        key_passphrase,
        str(known_hosts_path),
    )
    return _ssh_client_for(cache_key)


def get_ssh_client(request: Request = None):
    """Return a command client for the selected machine or legacy env fallback."""
    store = get_settings_store()
    machine_id = _request_machine_id(request)
    machine = store.get_machine_config(machine_id) if machine_id else None
    if machine is None:
        machine_ref = _resolve_machine("ssh", request)
        machine = store.get_machine_config(machine_ref["id"]) if machine_ref else None
    if machine and str(machine.get("mode") or "local").strip().lower() == "local":
        logger.info("Creating LocalCommandClient machine_id=%s", machine["id"])
        return LocalCommandClient()
    if machine and machine.get("host") and machine.get("username"):
        return _ssh_client_from_machine_config(machine, store)

    settings = get_settings()
    logger.info(
        "Creating SSH client from legacy env host=%s user=%s port=%s key_dir=%s key_name=%s password=%s",
        settings.ssh_host or "<unset>",
        settings.ssh_username or "<unset>",
        settings.ssh_port,
        settings.ssh_key_directory or "<unset>",
        settings.ssh_key_name or "<unset>",
        "set" if settings.ssh_password else "missing",
    )
    if not settings.ssh_key_path:
        raise HTTPException(
            status_code=503,
            detail="No SSH machine is configured and SSH key settings must be configured",
        )
    return _ssh_client_for(
        (
            "legacy",
            settings.ssh_host,
            settings.ssh_username,
            settings.ssh_port,
            settings.ssh_key_path,
            settings.ssh_password or None,
            None,
            None,
            str(settings.ssh_known_hosts_file),
        )
    )


def get_mail_queue() -> MailQueue:
    """Return the singleton background email queue."""
    return _get_mail_queue()


def get_settings_store() -> SettingsStore:
    """Return the singleton persistent settings store."""
    return _get_settings_store()


def get_user_id(request: Request = None) -> str:
    """Return the configured Jellyfin user ID or discover the first available one."""
    store = get_settings_store()
    service_id = _request_jellyfin_service_id(request)
    service = _service_record(store, "jellyfin", service_id)
    if service and service.get("config", {}).get("user_id"):
        return str(service["config"]["user_id"])
    client = get_jellyfin_client(request)
    users = client.users()
    if not users:
        raise HTTPException(
            status_code=503,
            detail="No Jellyfin users found and no user_id configured on the service",
        )
    return users[0]["Id"]
