import requests
from flask import (
    Blueprint,
    current_app,
    jsonify,
    request,
)
from loguru import logger

from ...database.models import QueuedResearch, ResearchHistory
from ...database.session_context import get_user_db_session
from ...config.constants import DEFAULT_OLLAMA_URL
from ...constants import ResearchStatus
from ...utilities.url_utils import normalize_url
from ...security.decorators import require_json_body
from ..auth.decorators import login_required
from ..routes.research_routes import _research_not_found
from ..services.research_service import (
    cancel_research,
)
from ..services.resource_service import (
    add_resource,
    delete_resource,
    get_resources_for_research,
)
from local_deep_research.settings import SettingsManager
from ...llm.providers.base import normalize_provider
from ...security import safe_get, strip_settings_snapshot

# Create blueprint
api_bp = Blueprint("api", __name__)

# NOTE: Routes use session["username"] (not .get()) intentionally.
# @login_required guarantees the key exists; direct access fails fast
# if the decorator is ever removed.


@api_bp.route("/settings/current-config", methods=["GET"])
@login_required
def get_current_config():
    """Get the current configuration from database settings."""
    try:
        with get_user_db_session() as session:
            settings_manager = SettingsManager(session)
            config = {
                "provider": settings_manager.get_setting(
                    "llm.provider", "Not configured"
                ),
                "model": settings_manager.get_setting(
                    "llm.model", "Not configured"
                ),
                "search_tool": settings_manager.get_setting(
                    "search.tool", "searxng"
                ),
                "iterations": settings_manager.get_setting(
                    "search.iterations", 8
                ),
                "questions_per_iteration": settings_manager.get_setting(
                    "search.questions_per_iteration", 5
                ),
                "search_strategy": settings_manager.get_setting(
                    "search.search_strategy", "focused_iteration"
                ),
            }

        return jsonify({"success": True, "config": config})

    except Exception:
        logger.exception("Error getting current config")
        return jsonify(
            {"success": False, "error": "An internal error occurred"}
        ), 500


# API Routes
@api_bp.route("/start", methods=["POST"])
@login_required
def api_start_research():
    """
    Start a new research process.

    Delegates to the full-featured start_research() in research_routes,
    which reads settings from the database, handles queueing, and starts
    the research thread.
    """
    from ..routes.research_routes import start_research

    return start_research()


@api_bp.route("/status/<string:research_id>", methods=["GET"])
@login_required
def api_research_status(research_id):
    """
    Get the status of a research process
    """
    try:
        # Get a fresh session to avoid conflicts with the research process

        with get_user_db_session() as db_session:
            research = (
                db_session.query(ResearchHistory)
                .filter_by(id=research_id)
                .first()
            )

            if research is None:
                return _research_not_found(research_id)

            # Extract attributes while session is active
            # to avoid DetachedInstanceError after the with block exits
            result = {
                "status": research.status,
                "progress": research.progress,
                "completed_at": research.completed_at,
                "report_path": research.report_path,
                "metadata": strip_settings_snapshot(research.research_meta),
            }

            # Include queue position for queued research
            if research.status == ResearchStatus.QUEUED:
                queued = (
                    db_session.query(QueuedResearch)
                    .filter_by(research_id=research_id)
                    .first()
                )
                if queued:
                    result["queue_position"] = queued.position

        return jsonify(result)
    except Exception:
        logger.exception("Error getting research status")
        return jsonify(
            {"status": "error", "message": "Failed to get research status"}
        ), 500


@api_bp.route("/terminate/<string:research_id>", methods=["POST"])
@login_required
def api_terminate_research(research_id):
    """
    Terminate a research process
    """
    try:
        from flask import session

        username = session["username"]
        result = cancel_research(research_id, username)
        if result:
            return jsonify(
                {
                    "status": "success",
                    "message": "Research terminated",
                    "result": result,
                }
            )
        return jsonify(
            {
                "status": "success",
                "message": "Research not found or already completed",
                "result": result,
            }
        )
    except Exception:
        logger.exception("Error terminating research")
        return (
            jsonify({"status": "error", "message": "Failed to stop research."}),
            500,
        )


@api_bp.route("/resources/<string:research_id>", methods=["GET"])
@login_required
def api_get_resources(research_id):
    """
    Get resources for a specific research
    """
    try:
        resources = get_resources_for_research(research_id)
        return jsonify({"status": "success", "resources": resources})
    except Exception:
        logger.exception("Error getting resources for research")
        return jsonify(
            {"status": "error", "message": "Failed to get resources"}
        ), 500


@api_bp.route("/resources/<string:research_id>", methods=["POST"])
@login_required
@require_json_body(error_format="status")
def api_add_resource(research_id):
    """
    Add a new resource to a research project
    """
    try:
        data = request.json
        # Required fields
        title = data.get("title")
        url = data.get("url")

        # Optional fields
        content_preview = data.get("content_preview")
        source_type = data.get("source_type", "web")
        metadata = data.get("metadata", {})

        # Validate required fields
        if not title or not url:
            return (
                jsonify(
                    {"status": "error", "message": "Title and URL are required"}
                ),
                400,
            )

        # Security: Validate URL to prevent SSRF attacks
        from ...security.ssrf_validator import validate_url

        is_valid = validate_url(url)
        if not is_valid:
            logger.warning(f"SSRF protection: Rejected URL {url}")
            return (
                jsonify({"status": "error", "message": "Invalid URL"}),
                400,
            )

        # Check if the research exists
        with get_user_db_session() as db_session:
            research = (
                db_session.query(ResearchHistory)
                .filter_by(id=research_id)
                .first()
            )

            if not research:
                return _research_not_found(research_id)

        # Add the resource
        resource_id = add_resource(
            research_id=research_id,
            title=title,
            url=url,
            content_preview=content_preview,
            source_type=source_type,
            metadata=metadata,
        )

        return jsonify(
            {
                "status": "success",
                "message": "Resource added successfully",
                "resource_id": resource_id,
            }
        )
    except Exception:
        logger.exception("Error adding resource")
        return jsonify(
            {"status": "error", "message": "Failed to add resource"}
        ), 500


@api_bp.route(
    "/resources/<string:research_id>/delete/<int:resource_id>",
    methods=["DELETE"],
)
@login_required
def api_delete_resource(research_id, resource_id):
    """
    Delete a resource from a research project
    """
    try:
        # Delete the resource
        success = delete_resource(resource_id)

        if success:
            return jsonify(
                {
                    "status": "success",
                    "message": "Resource deleted successfully",
                }
            )
        return jsonify(
            {"status": "error", "message": "Resource not found"}
        ), 404
    except Exception:
        logger.exception("Error deleting resource")
        return jsonify(
            {
                "status": "error",
                "message": "An internal error occurred while deleting the resource.",
            }
        ), 500


def _ollama_base_url_from_config(llm_config):
    """Resolve the Ollama base URL from the LLM config (normalized, with the
    default fallback). Single source so every Ollama probe targets the same
    URL."""
    raw = llm_config.get("ollama_base_url", DEFAULT_OLLAMA_URL)
    return normalize_url(raw) if raw else DEFAULT_OLLAMA_URL


def _probe_ollama_tags(base_url, timeout=5):
    """Probe Ollama ``/api/tags`` once and classify the outcome.

    Single source for the resolve→fetch→new/old-format-parse→error logic that
    was previously copy-pasted across the status and model-availability checks
    (so "is Ollama up?" can no longer answer differently per caller). Returns
    ``(outcome, probe_result)`` where outcome is one of:

    - ``"ok"`` → probe_result is the list of model dicts (both API formats handled)
    - ``"bad_status"`` → probe_result is the non-200 status code
    - ``"invalid_json"`` → probe_result is None (200 but unparseable body)
    - ``"connection_error"`` / ``"timeout"`` → probe_result is None

    Callers map the outcome onto their own response shape.
    """
    try:
        response = safe_get(
            f"{base_url}/api/tags",
            timeout=timeout,
            allow_localhost=True,
            allow_private_ips=True,
        )
    except requests.exceptions.ConnectionError:
        return "connection_error", None
    except requests.exceptions.Timeout:
        return "timeout", None

    if response.status_code != 200:
        return "bad_status", response.status_code

    try:
        data = response.json()
    except ValueError:
        return "invalid_json", None

    # New Ollama API nests the list under "models"; the older format is a
    # bare list. Mirror the previous inline check exactly (a bare ``"models"
    # in data`` membership test, no isinstance guard) so behavior is identical
    # to the pre-refactor endpoints — a malformed non-dict/non-list body
    # raises here and is handled by each caller's outer except, as before.
    if "models" in data:
        models = data.get("models", [])
    else:
        models = data
    return "ok", models


@api_bp.route("/check/ollama_status", methods=["GET"])
@login_required
def check_ollama_status():
    """
    Check if Ollama API is running
    """
    try:
        # Get Ollama URL from config
        llm_config = current_app.config.get("LLM_CONFIG", {})
        provider = normalize_provider(llm_config.get("provider", "ollama"))

        if provider != "ollama":
            return jsonify(
                {
                    "running": True,
                    "message": f"Using provider: {provider}, not Ollama",
                }
            )

        ollama_base_url = _ollama_base_url_from_config(llm_config)
        logger.info(f"Checking Ollama status at: {ollama_base_url}")

        outcome, probe_result = _probe_ollama_tags(ollama_base_url)

        if outcome == "ok":
            model_count = len(probe_result)
            logger.info(f"Ollama service is running with {model_count} models")
            return jsonify(
                {
                    "running": True,
                    "message": f"Ollama service is running with {model_count} models",
                    "model_count": model_count,
                }
            )
        if outcome == "invalid_json":
            logger.warning("Ollama returned invalid JSON")
            return jsonify(
                {
                    "running": True,
                    "message": "Ollama service is running but returned invalid data format",
                    "error_details": "Invalid response format from the service.",
                }
            )
        if outcome == "bad_status":
            logger.warning(
                f"Ollama returned non-200 status code: {probe_result}"
            )
            return jsonify(
                {
                    "running": False,
                    "message": f"Ollama service returned status code: {probe_result}",
                    "status_code": probe_result,
                }
            )
        if outcome == "connection_error":
            logger.warning("Ollama connection error")
            return jsonify(
                {
                    "running": False,
                    "message": "Ollama service is not running or not accessible",
                    "error_type": "connection_error",
                    "error_details": "Unable to connect to the service. Please check if the service is running.",
                }
            )
        # outcome == "timeout"
        logger.warning("Ollama request timed out")
        return jsonify(
            {
                "running": False,
                "message": "Ollama service request timed out after 5 seconds",
                "error_type": "timeout",
                "error_details": "Request timed out. The service may be overloaded.",
            }
        )

    except Exception:
        logger.exception("Error checking Ollama status")
        return jsonify(
            {
                "running": False,
                "message": "An internal error occurred while checking Ollama status.",
                "error_type": "exception",
                "error_details": "An internal error occurred.",
            }
        )


@api_bp.route("/check/ollama_model", methods=["GET"])
@login_required
def check_ollama_model():
    """
    Check if the configured Ollama model is available
    """
    try:
        # Get Ollama configuration
        llm_config = current_app.config.get("LLM_CONFIG", {})
        provider = normalize_provider(llm_config.get("provider", "ollama"))

        if provider != "ollama":
            return jsonify(
                {
                    "available": True,
                    "message": f"Using provider: {provider}, not Ollama",
                    "provider": provider,
                }
            )

        # Get model name from request or use config default
        model_name = request.args.get("model")
        if not model_name:
            model_name = llm_config.get("model", "")

        if not model_name:
            logger.warning(
                "/api/check/ollama_model called with no model name and "
                "llm.model is not configured"
            )
            return jsonify(
                {
                    "available": False,
                    "model": "",
                    "message": (
                        "Model is required. Pass ?model=<name> in the "
                        "query string, or configure llm.model in Settings."
                    ),
                    "error_type": "model_not_configured",
                }
            ), 400

        # Log which model we're checking for debugging
        logger.info(f"Checking availability of Ollama model: {model_name}")

        ollama_base_url = _ollama_base_url_from_config(llm_config)

        outcome, probe_result = _probe_ollama_tags(ollama_base_url)

        if outcome == "bad_status":
            logger.warning(
                f"Ollama API returned non-200 status: {probe_result}"
            )
            return jsonify(
                {
                    "available": False,
                    "model": model_name,
                    "message": f"Could not access Ollama service - status code: {probe_result}",
                    "status_code": probe_result,
                }
            )
        if outcome == "invalid_json":
            logger.warning("Failed to parse Ollama API response")
            return jsonify(
                {
                    "available": False,
                    "model": model_name,
                    "message": "Invalid response from Ollama API",
                    "error_type": "json_parse_error",
                }
            )
        if outcome == "connection_error":
            logger.warning("Connection error to Ollama API")
            return jsonify(
                {
                    "available": False,
                    "model": model_name,
                    "message": "Could not connect to Ollama service",
                    "error_type": "connection_error",
                    "error_details": "Unable to connect to the service. Please check if the service is running.",
                }
            )
        if outcome == "timeout":
            logger.warning("Timeout connecting to Ollama API")
            return jsonify(
                {
                    "available": False,
                    "model": model_name,
                    "message": "Connection to Ollama service timed out",
                    "error_type": "timeout",
                }
            )

        # outcome == "ok"
        models = probe_result
        model_names = [m.get("name", "") for m in models]
        logger.debug(
            f"Available Ollama models: {', '.join(model_names[:10])}"
            + (
                f" and {len(model_names) - 10} more"
                if len(model_names) > 10
                else ""
            )
        )

        # Case-insensitive model name comparison
        model_exists = any(
            m.get("name", "").lower() == model_name.lower() for m in models
        )

        if model_exists:
            logger.info(f"Ollama model {model_name} is available")
            return jsonify(
                {
                    "available": True,
                    "model": model_name,
                    "message": f"Model {model_name} is available",
                    "all_models": model_names,
                }
            )
        # Check if models were found at all
        if not models:
            logger.warning("No models found in Ollama")
            message = "No models found in Ollama. Please pull models first."
        else:
            logger.warning(
                f"Model {model_name} not found among {len(models)} available models"
            )
            # Don't expose available models for security reasons
            message = f"Model {model_name} is not available"

        return jsonify(
            {
                "available": False,
                "model": model_name,
                "message": message,
                # Remove all_models to prevent information disclosure
            }
        )

    except Exception:
        # General exception
        logger.exception("Error checking Ollama model")

        return jsonify(
            {
                "available": False,
                "model": (
                    model_name
                    if "model_name" in locals()
                    else llm_config.get("model", "")
                ),
                "message": "An internal error occurred while checking the model.",
                "error_type": "exception",
                "error_details": "An internal error occurred.",
            }
        )
