import asyncio
import logging

from quart import Blueprint, jsonify, request

from fabledassistant.auth import login_required, get_current_user_id
from fabledassistant.config import Config
from fabledassistant.services.caldav import CALDAV_SETTING_KEYS, get_caldav_config, test_connection
from fabledassistant.services.llm import get_installed_models, _is_private_url
from fabledassistant.services.settings import delete_setting, get_all_settings, get_setting, set_settings_batch

logger = logging.getLogger(__name__)


async def _prime_kv_cache_bg(user_id: int, model: str) -> None:
    """Fire-and-forget: prime Ollama's KV cache with the user's system prompt."""
    import httpx
    from fabledassistant.services.llm import build_context, pick_num_ctx
    from fabledassistant.services.tools import get_tools_for_user
    try:
        messages, _ = await build_context(
            user_id=user_id,
            history=[],
            current_note_id=None,
            user_message=" ",
        )
        # Size the prime to match what real chat requests will use, including
        # tool schemas — otherwise Ollama reloads the model on the first real
        # request and throws away the cache we just built.
        tools = await get_tools_for_user(user_id)
        num_ctx = pick_num_ctx(messages, tools=tools)
        from fabledassistant.services.llm import keep_alive_for
        async with httpx.AsyncClient(timeout=120.0) as client:
            await client.post(
                f"{Config.OLLAMA_URL}/api/chat",
                json={
                    "model": model,
                    "messages": messages,
                    "stream": False,
                    "options": {"num_predict": 1, "num_ctx": num_ctx},
                    "keep_alive": keep_alive_for(model),
                },
            )
        logger.info("Primed KV cache for user %d with model '%s' (num_ctx=%d)", user_id, model, num_ctx)
    except Exception:
        logger.warning("Failed to prime KV cache for user %d", user_id, exc_info=True)

settings_bp = Blueprint("settings", __name__, url_prefix="/api/settings")


@settings_bp.route("", methods=["GET"])
@login_required
async def get_settings_route():
    uid = get_current_user_id()
    settings = await get_all_settings(uid)
    return jsonify(settings)


@settings_bp.route("", methods=["PUT"])
@login_required
async def update_settings_route():
    uid = get_current_user_id()
    data = await request.get_json()
    if not isinstance(data, dict):
        return jsonify({"error": "Expected a JSON object"}), 400

    # Normalize model names to lowercase before validation. Ollama's /api/tags
    # preserves whatever casing was used at pull time, but /api/chat rejects
    # mixed-case tags — lowercasing here keeps the two paths consistent and
    # means the stored setting is always in a form Ollama will actually accept.
    _MODEL_KEYS = frozenset({"default_model", "background_model"})
    for _key in _MODEL_KEYS:
        if _key in data and data[_key]:
            data[_key] = str(data[_key]).lower()

    if "default_model" in data:
        installed = await get_installed_models()
        if data["default_model"]:
            model = str(data["default_model"])
            if installed and model not in installed:
                return jsonify({"error": f"Model '{model}' is not installed"}), 400

    # Empty string for model keys means "reset to system default".
    # Delete the DB row so get_setting() falls back to Config defaults
    # rather than returning "" and breaking model resolution everywhere.
    to_save = {}
    for k, v in data.items():
        str_v = str(v)
        if k in _MODEL_KEYS and not str_v:
            await delete_setting(uid, k)
        else:
            to_save[k] = str_v

    if to_save:
        await set_settings_batch(uid, to_save)

    # Live-reschedule the journal daily-prep job when the timezone changes.
    if "user_timezone" in to_save:
        from fabledassistant.services.journal_scheduler import update_user_schedule as _update_journal_schedule
        await _update_journal_schedule(uid)

    if "default_model" in to_save and to_save["default_model"]:
        asyncio.create_task(_prime_kv_cache_bg(uid, to_save["default_model"]))

    settings = await get_all_settings(uid)
    return jsonify(settings)


@settings_bp.route("/models", methods=["GET"])
@login_required
async def get_models_route():
    """Return installed Ollama models and the configured defaults."""
    models = sorted(await get_installed_models())
    return jsonify({
        "models": models,
        "default_chat_model": Config.OLLAMA_MODEL,
    })


@settings_bp.route("/caldav", methods=["GET"])
@login_required
async def get_caldav():
    uid = get_current_user_id()
    config = await get_caldav_config(uid)
    if config.get("caldav_password"):
        config["caldav_password"] = "********"
    return jsonify(config)


@settings_bp.route("/caldav", methods=["PUT"])
@login_required
async def update_caldav():
    uid = get_current_user_id()
    data = await request.get_json()

    # Validate CalDAV URL before saving — block internal/private addresses
    if "caldav_url" in data:
        url = str(data.get("caldav_url") or "").strip()
        if url:
            parsed_scheme = url.split("://")[0].lower() if "://" in url else ""
            if parsed_scheme not in ("http", "https"):
                return jsonify({"error": "CalDAV URL must use http or https"}), 400
            if _is_private_url(url):
                return jsonify({"error": "CalDAV URL must not point to an internal or private address"}), 400

    settings_to_save = {}
    for key in CALDAV_SETTING_KEYS:
        if key in data:
            if key == "caldav_password" and data[key] == "********":
                continue
            settings_to_save[key] = str(data[key])

    if settings_to_save:
        await set_settings_batch(uid, settings_to_save)
    return jsonify({"status": "ok"})


@settings_bp.route("/caldav/test", methods=["POST"])
@login_required
async def test_caldav():
    uid = get_current_user_id()
    result = await test_connection(uid)
    return jsonify(result)


@settings_bp.route("/search", methods=["GET"])
@login_required
async def test_search():
    """Test SearXNG connectivity and preview results for a query."""
    if not Config.searxng_enabled():
        return jsonify({"configured": False, "results": [], "searxng_url": ""})
    q = request.args.get("q", "").strip()
    if not q:
        return jsonify({"configured": True, "results": [], "searxng_url": Config.SEARXNG_URL})
    from fabledassistant.services.research import _search_searxng
    results = await _search_searxng(q)
    return jsonify({"configured": True, "results": results, "query": q, "searxng_url": Config.SEARXNG_URL})
