"""Settings API: read/write config, manage credentials, factory reset."""

from __future__ import annotations

import logging

import httpx
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import StreamingResponse
from pydantic import BaseModel

log = logging.getLogger(__name__)

router = APIRouter(prefix="/settings", tags=["settings"])


class UpdateSectionRequest(BaseModel):
    values: dict


class LocaleRequest(BaseModel):
    locale: str


class CredentialRequest(BaseModel):
    service: str
    credentials: dict


# ---- Config ----


@router.get("")
async def get_settings(request: Request) -> dict:
    """Get all settings from config.toml."""
    svc = request.app.state.settings_service
    return svc.get_all()


@router.get("/locale")
async def get_locale() -> dict:
    """Get current backend locale."""
    from agntspace.infra.i18n import _CURRENT_LOCALE
    return {"locale": _CURRENT_LOCALE}


@router.put("/locale")
async def set_locale_endpoint(body: LocaleRequest) -> dict:
    """Set backend locale."""
    from agntspace.infra import i18n
    i18n.set_locale(body.locale)
    return {"locale": body.locale}


class TimezoneRequest(BaseModel):
    timezone: str


@router.get("/timezone")
async def get_timezone() -> dict:
    """Get current backend timezone (IANA name)."""
    from agntspace.infra.timezone import get_tz_name
    return {"timezone": get_tz_name()}


@router.put("/timezone")
async def set_timezone(request: Request, body: TimezoneRequest) -> dict:
    """Set backend timezone (IANA name like 'Europe/Stockholm').

    Persists to config.toml and reconfigures the runtime timezone.
    Empty string resets to auto-detect.
    """
    from agntspace.infra import timezone as tz_mod

    tz_name = body.timezone.strip()
    # Validate non-empty timezone names
    if tz_name:
        try:
            from zoneinfo import ZoneInfo
            ZoneInfo(tz_name)
        except Exception as exc:
            raise HTTPException(status_code=422, detail=f"Unknown timezone: {tz_name}") from exc

    # Persist to config.toml under [schedule]
    svc = request.app.state.settings_service
    svc.set_value("schedule", "timezone", tz_name)

    # Reconfigure runtime
    tz_mod.configure(tz_name)

    return {"timezone": tz_mod.get_tz_name()}


@router.get("/model-tiers")
async def get_model_tiers(request: Request) -> dict:
    """Get current local tier→model mappings."""
    model_router = getattr(request.app.state, "model_router", None)
    if not model_router:
        return {"reasoning": "llama3:70b", "capable": "llama3:70b", "fast": "llama3"}
    # Always return the local profile's ollama mappings, regardless of current profile
    local_data = model_router._profiles.get("local", {}).get("ollama", {})  # noqa: SLF001
    if not local_data:
        local_data = model_router.get_profile_summary()
    return {
        "reasoning": local_data.get("reasoning", ""),
        "capable": local_data.get("capable", ""),
        "fast": local_data.get("fast", ""),
    }


class TierMappingRequest(BaseModel):
    reasoning: str
    capable: str
    fast: str


@router.put("/model-tiers")
async def set_model_tiers(request: Request, body: TierMappingRequest) -> dict:
    """Update local tier→model mappings in models.yaml and hot-swap."""
    settings = request.app.state.settings
    skills_dir = settings.skills_dir
    yaml_path = skills_dir / "_meta" / "model-routing" / "config" / "models.yaml"

    if not yaml_path.is_file():
        raise HTTPException(status_code=404, detail="models.yaml not found")

    try:
        import yaml

        data = yaml.safe_load(yaml_path.read_text(encoding="utf-8"))
        profiles = data.setdefault("profiles", {})
        local = profiles.setdefault("local", {})
        local["ollama"] = {
            "reasoning": body.reasoning,
            "capable": body.capable,
            "fast": body.fast,
        }
        yaml_path.write_text(yaml.dump(data, default_flow_style=False, sort_keys=False), encoding="utf-8")

        # Hot-reload the model router
        model_router = getattr(request.app.state, "model_router", None)
        if model_router:
            model_router._profiles = profiles  # noqa: SLF001
            log.info("Model tiers updated: reasoning=%s, capable=%s, fast=%s",
                     body.reasoning, body.capable, body.fast)

        return {"ok": True}
    except Exception as exc:
        raise HTTPException(status_code=500, detail=str(exc)[:200]) from exc


# ---- Autostart (system service) ----


@router.get("/autostart")
async def get_autostart(request: Request) -> dict:
    """Return whether AgntSpace is registered to auto-start on boot.

    Merges actual OS-level daemon state with the user preference stored in
    config.toml under [system].autostart — so the UI can show 'enabled but not
    yet installed' or 'installed but disabled' states truthfully.
    """
    from agntspace.automation.daemon import daemon_status

    info = daemon_status()
    svc = request.app.state.settings_service
    enabled_pref = svc.get_value("system", "autostart", False)
    return {
        "platform": info["platform"],
        "method": info["method"],
        "installed": info["installed"],
        "running": info["running"],
        "enabled": bool(enabled_pref),
    }


@router.post("/autostart")
async def enable_autostart(request: Request) -> dict:
    """Register AgntSpace as a system service that auto-starts on boot.

    On Windows requires nssm; on Linux requires sudo permission to write the
    systemd unit. The installer surfaces a clear error message when a
    prerequisite is missing — we never silently swallow it.
    """
    from agntspace.automation.daemon import daemon_status, install_daemon

    settings = request.app.state.settings
    host = getattr(settings.server, "host", "127.0.0.1")
    port = getattr(settings.server, "port", 8000)

    message = install_daemon(host=host, port=port)
    info = daemon_status()

    # Persist the user's intent even when the OS-level install failed — so a
    # later attempt (after installing nssm, rerunning with sudo) can simply
    # retry without the user having to re-flip the toggle.
    svc = request.app.state.settings_service
    svc.set_value("system", "autostart", True)

    return {
        "message": message,
        "installed": info["installed"],
        "running": info["running"],
        "enabled": True,
        "platform": info["platform"],
        "method": info["method"],
    }


@router.delete("/autostart")
async def disable_autostart(request: Request) -> dict:
    """Remove the AgntSpace system service and clear the user preference."""
    from agntspace.automation.daemon import daemon_status, uninstall_daemon

    message = uninstall_daemon()
    info = daemon_status()

    svc = request.app.state.settings_service
    svc.set_value("system", "autostart", False)

    return {
        "message": message,
        "installed": info["installed"],
        "running": info["running"],
        "enabled": False,
        "platform": info["platform"],
        "method": info["method"],
    }


# ---- Watcher (inbox auto-ingestion pause/resume) ----


class WatcherToggleRequest(BaseModel):
    paused: bool


@router.get("/watcher")
async def get_watcher_state(request: Request) -> dict:
    """Return whether automatic inbox ingestion is currently paused.

    Reads ``[watcher].paused`` from config.toml and merges with the live
    watcher status (running, last_cycle_*) so the UI can render an honest
    "running" / "paused" / "not_wired" state.
    """
    svc = request.app.state.settings_service
    paused = bool(svc.get_value("watcher", "paused", False))
    watcher = getattr(request.app.state, "raw_watcher", None)
    if watcher is None:
        return {
            "paused": paused,
            "running": False,
            "wired": False,
        }
    status = watcher.get_status()
    return {
        "paused": paused,
        "running": bool(status.get("running")),
        "wired": True,
        "interval_seconds": status.get("interval_seconds"),
        "last_cycle_at": status.get("last_cycle_at"),
        "last_cycle_seconds_ago": status.get("last_cycle_seconds_ago"),
        "last_cycle_ingested": status.get("last_cycle_ingested"),
        "cycle_count": status.get("cycle_count"),
    }


@router.put("/watcher")
async def set_watcher_state(request: Request, body: WatcherToggleRequest) -> dict:
    """Pause or resume automatic inbox ingestion.

    Writes ``[watcher].paused`` to config.toml so the choice survives restarts.
    The watcher reads the flag on every loop tick + on every ``check_and_ingest``
    entry, so the change takes effect immediately without restarting the server.
    """
    svc = request.app.state.settings_service
    svc.set_value("watcher", "paused", bool(body.paused))
    watcher = getattr(request.app.state, "raw_watcher", None)
    running = False
    if watcher is not None:
        try:
            running = bool(watcher.get_status().get("running"))
        except Exception:
            pass
    return {
        "paused": bool(body.paused),
        "running": running,
        "message": (
            "Inbox ingestion paused. Drop files anywhere — they will be picked up when you resume."
            if body.paused
            else "Inbox ingestion resumed. Pending files will be processed on the next cycle."
        ),
    }


# ---- Pipeline freeze (global stop on ALL ingestion paths) ----


class PipelineFreezeRequest(BaseModel):
    frozen: bool


@router.get("/pipeline-freeze")
async def get_pipeline_freeze(request: Request) -> dict:
    """Return whether ALL ingestion paths are currently frozen.

    Distinct from the watcher pause: pause only stops the watcher's auto-scan
    loop, while freeze gates EVERY entry to ``KnowledgeBaseService.ingest()``
    — work queue retries, agent tools, drag-drop, plugin imports, the lot.
    Reads ``[pipeline].frozen`` from config.toml on every call so the choice
    survives restarts and can be flipped from the UI without restarting.
    """
    svc = request.app.state.settings_service
    frozen = bool(svc.get_value("pipeline", "frozen", False))
    return {"frozen": frozen}


@router.put("/pipeline-freeze")
async def set_pipeline_freeze(request: Request, body: PipelineFreezeRequest) -> dict:
    """Freeze or thaw the entire ingestion pipeline.

    When frozen:
      * ``kb.ingest()`` raises ``IngestFrozenError`` immediately on entry —
        no scopes opened, no locks taken, no LLM calls.
      * Work queue ingest jobs catch the error, log a deferred-completed
        marker, and re-enqueue (deduplicated) for when freeze lifts.
      * The /api/pipeline/queued/ingest-now endpoint returns 423 Locked.
      * Plugin-driven ingest (OneNote etc.) sees the same gate.
    When thawed: queued retries process on the next work-queue tick.
    """
    svc = request.app.state.settings_service
    svc.set_value("pipeline", "frozen", bool(body.frozen))
    return {
        "frozen": bool(body.frozen),
        "message": (
            "Ingestion pipeline FROZEN. Every path (watcher, work queue, agent tools, "
            "drag-drop, plugins) is now blocked. Pending files queue up; nothing leaves "
            "the inbox until you thaw."
            if body.frozen
            else "Ingestion pipeline thawed. Work queue retries will resume on the next tick."
        ),
    }


@router.get("/{section}")
async def get_section(request: Request, section: str) -> dict:
    """Get a config section."""
    svc = request.app.state.settings_service
    return svc.get_section(section)


@router.put("/{section}")
async def update_section(request: Request, section: str, body: UpdateSectionRequest) -> dict:
    """Update a config section."""
    svc = request.app.state.settings_service
    result = svc.update_section(section, body.values)

    # Hot-swap LLM provider when llm settings change
    if section == "llm":
        _hot_swap_llm(request)

    return result


def _hot_swap_llm(request: Request) -> None:
    """Recreate LLM provider and update all cached references after settings change."""
    from agntspace.infra.config import load_settings, reset_settings
    from agntspace.llm.factory import get_provider

    try:
        # Reload settings from disk
        reset_settings()
        settings = load_settings()
        request.app.state.settings = settings

        # Recreate provider with new settings
        credential_store = getattr(request.app.state, "credential_store", None)
        new_llm = get_provider(settings, credential_store=credential_store)

        # Update ModelRouter. In local mode, resolve the active_model against
        # the Ollama tag list so the router never hands out "llama3" when only
        # "llama3:latest" is installed.
        model_router = getattr(request.app.state, "model_router", None)
        if model_router:
            active_provider = settings.llm.local_provider if settings.llm.mode == "local" else settings.llm.cloud_provider
            active_profile = "local" if settings.llm.mode == "local" else settings.llm.quality_profile
            if settings.llm.mode == "local":
                from agntspace.llm.factory import _resolve_ollama_model
                resolved_default = _resolve_ollama_model(settings.llm.local_model)
            else:
                resolved_default = settings.active_model
            model_router._provider = active_provider  # noqa: SLF001
            model_router._profile = active_profile  # noqa: SLF001
            model_router._default_model = resolved_default  # noqa: SLF001
            model_router._mode = settings.llm.mode  # noqa: SLF001
            # Reload the YAML profiles in case reconcile ran
            model_router._load(getattr(settings, "skills_dir", None))  # noqa: SLF001
            # Mode flip → invalidate the local-stack reachability cache so
            # the next resolve() re-probes (Ollama may have just started).
            model_router.invalidate_local_cache()

        # Update all services that cache the provider
        services_with_llm = [
            "agent", "orchestrator", "memory_engine", "kb_service",
            "heartbeat", "daily_cycle", "reflection_service",
            "skillschool", "supervisor",
        ]
        for svc_name in services_with_llm:
            svc = getattr(request.app.state, svc_name, None)
            if svc and hasattr(svc, "_llm"):
                svc._llm = new_llm  # noqa: SLF001

        # Knowledge graph and memory bridge (nested in memory_engine)
        mem = getattr(request.app.state, "memory_engine", None)
        if mem:
            graph = getattr(mem, "_graph", None)
            if graph and hasattr(graph, "_llm"):
                graph._llm = new_llm  # noqa: SLF001

        # Memory-wiki bridge (nested inside daily_cycle)
        dc = getattr(request.app.state, "daily_cycle", None)
        if dc:
            bridge = getattr(dc, "_memory_bridge", None)
            if bridge and hasattr(bridge, "_llm"):
                bridge._llm = new_llm  # noqa: SLF001

        log.info(
            "LLM hot-swap: mode=%s, model=%s",
            settings.llm.mode,
            settings.active_model,
        )
    except Exception as exc:
        log.error("LLM hot-swap failed: %s", exc)


# ---- Health & Reset ----


class ResetRequest(BaseModel):
    categories: list[str] | None = None


class ResetFileRequest(BaseModel):
    path: str


@router.get("/health")
async def vault_health(request: Request) -> dict:
    """Validate system files and return per-file health status."""
    from agntspace.setup.init import validate_vault

    settings = request.app.state.settings
    try:
        result = validate_vault(settings.root_dir)
        return result
    except FileNotFoundError as exc:
        raise HTTPException(status_code=404, detail=str(exc)) from exc


@router.post("/migrate-frontmatter")
async def migrate_frontmatter(request: Request) -> dict:
    """Backfill date: and agent: fields on existing vault files."""
    from agntspace.vault.migrate_frontmatter import migrate_vault

    settings = request.app.state.settings
    vault = getattr(request.app.state, "vault", None)
    vault_paths = vault.paths if vault else None
    result = migrate_vault(settings.vault_dir, paths=vault_paths)
    return result


@router.post("/reset")
async def factory_reset(request: Request, body: ResetRequest | None = None) -> dict:
    """Reset system files to factory defaults.

    Body: { "categories": ["identity", "memory", "agents", "skills", "security"] }
    Omit categories to reset all. User content is always preserved.
    """
    from agntspace.setup.init import reset_vault

    settings = request.app.state.settings
    categories = body.categories if body else None
    try:
        result = reset_vault(settings.root_dir, categories=categories)
    except FileNotFoundError as exc:
        raise HTTPException(status_code=404, detail=str(exc)) from exc

    # Re-initialize agent with fresh files
    agent = getattr(request.app.state, "agent", None)
    if agent:
        await agent.initialize()
        log.info("Agent re-initialized after reset (%s)", result.get("categories"))

    return result


@router.post("/reset-file")
async def reset_single_file(request: Request, body: ResetFileRequest) -> dict:
    """Reset a single system file to its factory default."""
    from agntspace.setup.init import reset_file

    settings = request.app.state.settings
    try:
        result = reset_file(settings.root_dir, body.path)
    except FileNotFoundError as exc:
        raise HTTPException(status_code=404, detail=str(exc)) from exc

    # Re-initialize agent
    agent = getattr(request.app.state, "agent", None)
    if agent:
        await agent.initialize()

    return result


# ---- Ollama ----

OLLAMA_BASE = "http://localhost:11434"


@router.get("/ollama/models")
async def list_ollama_models() -> dict:
    """List locally available Ollama models.

    If Ollama is not reachable, attempts auto-start before returning offline.
    """
    async def _try_fetch() -> dict | None:
        try:
            async with httpx.AsyncClient(timeout=5) as client:
                resp = await client.get(f"{OLLAMA_BASE}/api/tags")
                resp.raise_for_status()
                data = resp.json()
                models = [m["name"] for m in data.get("models", [])]
                return {"online": True, "models": sorted(models)}
        except Exception:
            return None

    # First attempt
    result = await _try_fetch()
    if result:
        return result

    # Ollama not reachable — try auto-start in a thread (blocking I/O)
    import asyncio

    from agntspace.llm.ollama_discovery import ensure_ollama_running

    started = await asyncio.to_thread(ensure_ollama_running)
    if started:
        result = await _try_fetch()
        if result:
            return {**result, "auto_started": True}

    import shutil

    return {
        "online": False,
        "models": [],
        "installed": shutil.which("ollama") is not None,
    }


@router.post("/ollama/test/{model}")
async def test_ollama_model(model: str) -> StreamingResponse:
    """Interactive health check: stream a short response from the model."""

    prompt = (
        "You are being tested by a user who just installed you. "
        "Say hi, introduce yourself in one SHORT sentence (who you are, what you're good at), "
        "then confirm you're working. Be friendly and a little playful. Keep it under 30 words total."
    )

    async def _stream():
        try:
            async with httpx.AsyncClient(timeout=httpx.Timeout(60, connect=10)) as client:
                async with client.stream(
                    "POST",
                    f"{OLLAMA_BASE}/api/generate",
                    json={"model": model, "prompt": prompt, "stream": True,
                          "options": {"num_predict": 60}},
                ) as resp:
                    async for line in resp.aiter_lines():
                        if line.strip():
                            yield line + "\n"
        except Exception as exc:
            import json as _json
            yield _json.dumps({"error": str(exc)[:200]}) + "\n"

    return StreamingResponse(_stream(), media_type="application/x-ndjson")


class OllamaPullRequest(BaseModel):
    name: str


@router.post("/ollama/pull")
async def pull_ollama_model(body: OllamaPullRequest) -> StreamingResponse:
    """Pull a model from the Ollama library. Streams progress as JSON lines."""

    async def _stream():
        async with httpx.AsyncClient(timeout=httpx.Timeout(600, connect=10)) as client:
            async with client.stream(
                "POST",
                f"{OLLAMA_BASE}/api/pull",
                json={"name": body.name, "stream": True},
            ) as resp:
                async for line in resp.aiter_lines():
                    if line.strip():
                        yield line + "\n"

    return StreamingResponse(_stream(), media_type="application/x-ndjson")


@router.delete("/ollama/models/{model}")
async def delete_ollama_model(model: str) -> dict:
    """Delete an Ollama model."""
    try:
        async with httpx.AsyncClient(timeout=30) as client:
            resp = await client.request(
                "DELETE",
                f"{OLLAMA_BASE}/api/delete",
                json={"name": model},
            )
            resp.raise_for_status()
            return {"ok": True}
    except Exception as exc:
        return {"ok": False, "error": str(exc)[:200]}


# ---- Credentials ----


@router.get("/credentials/list")
async def list_credentials(request: Request) -> list[str]:
    """List services with stored credentials."""
    creds = request.app.state.credential_store
    return creds.list_services()


@router.post("/credentials/save")
async def save_credentials(request: Request, body: CredentialRequest) -> dict:
    """Save credentials for a service."""
    creds = request.app.state.credential_store
    creds.set(body.service, body.credentials)
    return {"saved": True, "service": body.service}


@router.delete("/credentials/{service}")
async def delete_credentials(request: Request, service: str) -> dict:
    """Delete credentials for a service."""
    creds = request.app.state.credential_store
    creds.delete(service)
    return {"deleted": True, "service": service}


