"""
REST API for Local Deep Research.
Provides HTTP access to programmatic search and research capabilities.
"""

import inspect
import time
from functools import wraps
from typing import Dict, Any, Optional, Tuple

from flask import Blueprint, jsonify, request, Response
from loguru import logger

from ..api.research_functions import analyze_documents
from ..database.session_context import get_user_db_session
from ..security.decorators import require_json_body
from ..utilities.db_utils import get_settings_manager
from ..security.rate_limiter import (
    API_RATE_LIMIT_DEFAULT,
    api_rate_limit,
    get_current_username,
)

# Create a blueprint for the API
api_blueprint = Blueprint("api_v1", __name__, url_prefix="/api/v1")

# Body params /analyze_documents accepts beyond the positional
# query/collection_name. Derived from the real signature so the two can't
# drift: analyze_documents (unlike quick_summary/generate_report) has no
# **kwargs, so an unknown key would TypeError at call time — surfacing as
# an opaque 500. Validating up front turns that into a clear 400.
# username/settings_snapshot are excluded because they are server-set by
# _load_user_context_into_params (and overwritten if a body supplied
# them); programmatic_mode stays accepted as the documented body
# override.
_ANALYZE_DOCUMENTS_PARAMS = frozenset(
    inspect.signature(analyze_documents).parameters
) - {"query", "collection_name", "username", "settings_snapshot"}


# Contract enforced by the test_user_context_loaded /
# test_settings_snapshot_loaded tests in tests/web/test_api_coverage.py.
# Each authed REST endpoint that calls a research function must invoke
# this helper. Each endpoint's contract test must assert that username,
# settings_snapshot (with the tracer key from _mock_access_control), and
# programmatic_mode=False reach the underlying research function.
def _load_user_context_into_params(
    params: Dict[str, Any],
    username: str | None,
    allow_default_settings: bool = False,
) -> Optional[Tuple[Response, int]]:
    """Mutate ``params`` in place to thread the authenticated user's context
    down to the research-function call.

    All authenticated REST endpoints share the same shape: the user has an
    encrypted DB whose settings snapshot must be loaded and passed through,
    so calls honor the user's stored API keys, model preference, search
    tool, and other config — not just the application defaults plus
    ``LDR_*`` env vars that the programmatic-API fallback would produce.

    Sets ``username``, ``settings_snapshot``, and (for authenticated
    requests) ``programmatic_mode=False`` so DB-backed rate-limit
    estimates persist across requests. Uses ``setdefault`` for
    ``programmatic_mode`` so an explicit override in the request body
    is respected.

    Returns ``None`` on success. If the settings snapshot cannot be loaded,
    fails CLOSED: returns a ``(response, 503)`` tuple the endpoint must
    return to the caller. Continuing with an empty snapshot would resolve
    to the permissive default egress scope, silently downgrading a
    configured PRIVATE_ONLY / require-local user — bypassing the very
    boundary they configured. ``allow_default_settings=True`` is the
    caller's CONSCIOUS opt-in to proceed with defaults (empty snapshot,
    no egress policy) instead; it is logged loudly so it is never silent.
    """
    if not username:
        logger.debug("No username in session, skipping settings snapshot")
        params["settings_snapshot"] = {}
        return None

    params["username"] = username
    params.setdefault("programmatic_mode", False)
    try:
        with get_user_db_session(username) as db_session:
            if db_session is None:
                logger.warning(f"No database session for user: {username}")
                params["settings_snapshot"] = {}
                return None
            settings_manager = get_settings_manager(db_session, username)
            snapshot = settings_manager.get_settings_snapshot()
            params["settings_snapshot"] = snapshot
            logger.debug(
                f"Loaded settings snapshot for user '{username}' "
                f"with {len(snapshot)} settings"
            )
            return None
    except Exception:
        # logger.exception captures the traceback so the root cause
        # (e.g. SQLCipher decrypt failure, settings table corruption,
        # missing column after a migration) is visible. Without this
        # the downstream error misleads — looks like "no provider",
        # really was "couldn't read user settings".
        logger.exception("Failed to load user settings snapshot")
        if allow_default_settings:
            # Caller explicitly opted in to run without their settings.
            # Proceed with defaults (empty snapshot → permissive scope).
            # Logged loudly so it's never a silent downgrade.
            logger.bind(policy_audit=True).warning(
                "Settings snapshot failed to load; proceeding with "
                "DEFAULT settings because allow_default_settings=true "
                "— this run is NOT bound by the user's egress policy",
                user=username,
            )
            params["settings_snapshot"] = {}
            return None
        return (
            jsonify(
                {
                    "error": (
                        "Your settings could not be loaded, so the "
                        "research was REFUSED to avoid silently "
                        "running without your privacy/egress policy "
                        "(which could send your data to the cloud "
                        "when you meant to keep it local)."
                    ),
                    "how_to_fix": (
                        "This is usually transient — try again. If "
                        "it persists, your encrypted settings "
                        "database may be unavailable (e.g. a session "
                        "/ password issue), so re-authenticate. To "
                        "deliberately run with default settings and "
                        "NO egress policy, resend the request with "
                        '"allow_default_settings": true.'
                    ),
                    "reason": "settings_unavailable",
                }
            ),
            503,
        )


def api_access_control(f):
    """
    Decorator to enforce API access control:
    - Check if user is authenticated
    - Check if API is enabled for the user
    - Pre-cache api_rate_limit on g so the rate limiter avoids a second DB read
    """

    @wraps(f)
    def decorated_function(*args, **kwargs):
        from flask import g

        username = get_current_username()

        if not username:
            return jsonify({"error": "Authentication required"}), 401

        # Read both settings in a single DB session
        api_enabled = True
        with get_user_db_session(username) as db_session:
            if db_session:
                settings_manager = get_settings_manager(db_session, username)
                api_enabled = settings_manager.get_setting(
                    "app.enable_api", True
                )
                # Pre-cache for _get_user_api_rate_limit() to avoid a second DB read
                g._api_rate_limit = settings_manager.get_setting(
                    "app.api_rate_limit", API_RATE_LIMIT_DEFAULT
                )

        if not api_enabled:
            return jsonify({"error": "API access is disabled"}), 403

        return f(*args, **kwargs)

    return decorated_function


@api_blueprint.route("/", methods=["GET"])
@api_access_control
@api_rate_limit
def api_documentation():
    """
    Provide documentation on the available API endpoints.
    """
    api_docs = {
        "api_version": "v1",
        "description": "REST API for Local Deep Research",
        "endpoints": [
            {
                "path": "/api/v1/quick_summary",
                "method": "POST",
                "description": "Generate a quick research summary",
                "parameters": {
                    "query": "Research query (required)",
                    "search_tool": "Search engine to use (optional)",
                    "iterations": "Number of search iterations (optional)",
                    "temperature": "LLM temperature (optional)",
                    "allow_default_settings": "Set to true to proceed with default settings (and NO egress policy) when your stored settings cannot be loaded; default is to refuse with 503 (optional)",
                },
            },
            {
                "path": "/api/v1/generate_report",
                "method": "POST",
                "description": "Generate a comprehensive research report",
                "parameters": {
                    "query": "Research query (required)",
                    "output_file": "Path to save report (optional)",
                    "searches_per_section": "Searches per report section (optional)",
                    "model_name": "LLM model to use (optional)",
                    "temperature": "LLM temperature (optional)",
                    "allow_default_settings": "Set to true to proceed with default settings (and NO egress policy) when your stored settings cannot be loaded; default is to refuse with 503 (optional)",
                },
            },
            {
                "path": "/api/v1/analyze_documents",
                "method": "POST",
                "description": "Search and analyze documents in a local collection",
                "parameters": {
                    "query": "Search query (required)",
                    "collection_name": "Local collection name (required)",
                    "max_results": "Maximum results to return (optional)",
                    "temperature": "LLM temperature (optional)",
                    "force_reindex": "Force collection reindexing (optional)",
                    "allow_default_settings": "Set to true to proceed with default settings (and NO egress policy) when your stored settings cannot be loaded; default is to refuse with 503 (optional)",
                },
            },
        ],
    }

    return jsonify(api_docs)


@api_blueprint.route("/health", methods=["GET"])
def health_check():
    """Simple health check endpoint."""
    return jsonify(
        {"status": "ok", "message": "API is running", "timestamp": time.time()}
    )


def _serialize_results(results: Dict[str, Any]) -> Response:
    """
    Converts the results dictionary into a JSON string.

    Args:
        results: The results dictionary.

    Returns:
        The JSON string.

    """
    # The main thing that needs to be handled here is the `Document` instances.
    converted_results = results.copy()
    for finding in converted_results.get("findings", []):
        for i, document in enumerate(finding.get("documents", [])):
            finding["documents"][i] = {
                "metadata": document.metadata,
                "content": document.page_content,
            }

    return jsonify(converted_results)


@api_blueprint.route("/quick_summary", methods=["POST"])
@api_access_control
@api_rate_limit
@require_json_body(error_message="Query parameter is required")
def api_quick_summary():
    """
    Generate a quick research summary via REST API.

    POST /api/v1/quick_summary
    {
        "query": "Advances in fusion energy research",
        "search_tool": "searxng",  # Optional: search engine to use (defaults to your configured search.tool setting)
        "iterations": 2,        # Optional: number of search iterations
        "temperature": 0.7      # Optional: LLM temperature
    }
    """
    logger.debug("API quick_summary endpoint called")
    data = request.json
    logger.debug(f"Request data keys: {list(data.keys())}")

    if "query" not in data:
        logger.debug("Missing query parameter")
        return jsonify({"error": "Query parameter is required"}), 400

    # Extract query and validate type
    query = data.get("query")
    if not isinstance(query, str):
        return jsonify({"error": "Query must be a string"}), 400
    # Opt-in escape hatch for programmatic callers: when settings can't be
    # loaded, proceed with defaults (empty snapshot → permissive scope) instead
    # of failing closed (503). Default False (fail closed) so a configured
    # PRIVATE_ONLY user is never silently downgraded; setting it true is a
    # CONSCIOUS "I'm fine running without my settings/egress policy" choice.
    # Excluded from ``params`` so it isn't forwarded to quick_summary().
    # Strict ``is True`` (not bool()): for a security-boundary flag we only opt
    # in on a real JSON ``true`` — not on a truthy string like "false"/"0".
    allow_default_settings = data.get("allow_default_settings") is True
    params = {
        k: v
        for k, v in data.items()
        if k not in ("query", "allow_default_settings")
    }
    logger.debug(
        f"Query length: {len(query) if query else 0}, params keys: {list(params.keys()) if params else 'None'}"
    )

    username = get_current_username()

    try:
        # Import here to avoid circular imports. NOTE: get_user_db_session and
        # get_settings_manager are NOT re-imported here — both are bound at
        # module level (top of file). A local re-import would shadow the
        # module-level name and silently defeat ``patch("...web.api.<name>")``
        # in tests (the function would fetch the real, encrypted-DB-requiring
        # implementation instead of the mock), so keep them module-level for one
        # consistent patch target. Only quick_summary genuinely needs the local
        # import (it pulls in the research stack, which would cycle).
        from ..api.research_functions import quick_summary

        logger.info(
            f"Processing quick_summary request: query='{query}' for user='{username}'"
        )

        # Set reasonable defaults for API use. search_tool deliberately has
        # no default here: when omitted, quick_summary reads the user's
        # configured search.tool from the settings snapshot.
        params.setdefault("temperature", 0.7)
        params.setdefault("iterations", 1)

        error = _load_user_context_into_params(
            params, username, allow_default_settings
        )
        if error is not None:
            return error

        # Call the actual research function
        result = quick_summary(query, **params)

        return _serialize_results(result)
    except TimeoutError:
        logger.exception("Request timed out")
        return (
            jsonify(
                {
                    "error": "Request timed out. Please try with a simpler query or fewer iterations."
                }
            ),
            504,
        )
    except Exception:
        logger.exception("Error in quick_summary API")
        return (
            jsonify(
                {
                    "error": "An internal error has occurred. Please try again later."
                }
            ),
            500,
        )


@api_blueprint.route("/generate_report", methods=["POST"])
@api_access_control
@api_rate_limit
@require_json_body(error_message="Query parameter is required")
def api_generate_report():
    """
    Generate a comprehensive research report via REST API.

    POST /api/v1/generate_report
    {
        "query": "Impact of climate change on agriculture",
        "output_file": "/path/to/save/report.md",  # Optional
        "searches_per_section": 2,                 # Optional
        "model_name": "gpt-4",                     # Optional
        "temperature": 0.5                         # Optional
    }
    """
    data = request.json
    if "query" not in data:
        return jsonify({"error": "Query parameter is required"}), 400

    query = data.get("query")
    # See api_quick_summary for the allow_default_settings semantics
    # (opt-in escape hatch, strict ``is True``, excluded from params).
    allow_default_settings = data.get("allow_default_settings") is True
    params = {
        k: v
        for k, v in data.items()
        if k not in ("query", "allow_default_settings")
    }

    username = get_current_username()

    try:
        # Import here to avoid circular imports
        from ..api.research_functions import generate_report

        # Set reasonable defaults for API use
        params.setdefault("searches_per_section", 1)
        params.setdefault("temperature", 0.7)

        error = _load_user_context_into_params(
            params, username, allow_default_settings
        )
        if error is not None:
            return error

        logger.info(
            f"Processing generate_report request: query='{query}' for user='{username}'"
        )

        result = generate_report(query, **params)

        # Don't return the full content for large reports
        if (
            result
            and "content" in result
            and isinstance(result["content"], str)
            and len(result["content"]) > 10000
        ):
            # Include a summary of the report content
            content_preview = (
                result["content"][:2000] + "... [Content truncated]"
            )
            result["content"] = content_preview
            result["content_truncated"] = True

        return jsonify(result)
    except TimeoutError:
        logger.exception("Request timed out")
        return (
            jsonify(
                {"error": "Request timed out. Please try with a simpler query."}
            ),
            504,
        )
    except Exception:
        logger.exception("Error in generate_report API")
        return (
            jsonify(
                {
                    "error": "An internal error has occurred. Please try again later."
                }
            ),
            500,
        )


@api_blueprint.route("/analyze_documents", methods=["POST"])
@api_access_control
@api_rate_limit
@require_json_body(
    error_message="Both query and collection_name parameters are required"
)
def api_analyze_documents():
    """
    Search and analyze documents in a local collection via REST API.

    POST /api/v1/analyze_documents
    {
        "query": "neural networks in medicine",
        "collection_name": "my_collection",          # Required: local collection name
        "max_results": 20,                         # Optional: max results to return
        "temperature": 0.7,                        # Optional: LLM temperature
        "force_reindex": false                     # Optional: force reindexing
    }
    """
    data = request.json
    if "query" not in data or "collection_name" not in data:
        return (
            jsonify(
                {
                    "error": "Both query and collection_name parameters are required"
                }
            ),
            400,
        )

    query = data.get("query")
    collection_name = data.get("collection_name")
    # See api_quick_summary for the allow_default_settings semantics
    # (opt-in escape hatch, strict ``is True``, excluded from params).
    allow_default_settings = data.get("allow_default_settings") is True
    params = {
        k: v
        for k, v in data.items()
        if k not in ("query", "collection_name", "allow_default_settings")
    }

    unknown_params = sorted(set(params) - _ANALYZE_DOCUMENTS_PARAMS)
    if unknown_params:
        return (
            jsonify(
                {
                    "error": (
                        f"Unknown parameter(s) for analyze_documents: "
                        f"{', '.join(unknown_params)}"
                    ),
                    "allowed_parameters": sorted(_ANALYZE_DOCUMENTS_PARAMS),
                }
            ),
            400,
        )

    username = get_current_username()

    try:
        error = _load_user_context_into_params(
            params, username, allow_default_settings
        )
        if error is not None:
            return error
        result = analyze_documents(query, collection_name, **params)
        return jsonify(result)
    except Exception:
        logger.exception("Error in analyze_documents API")
        return (
            jsonify(
                {
                    "error": "An internal error has occurred. Please try again later."
                }
            ),
            500,
        )
