from __future__ import annotations

import json
import uuid
from datetime import datetime

import dataiku
from flask import Blueprint, current_app, g, jsonify, request

from backend.constants import DRAFT_ZONE
from backend.indexing_monitor import wake_monitor
from backend.schemas import schemas
from backend.services import agent_documents
from backend.services.agent_assets import (
    delete_agent_project,
    ensure_ua_project,
    get_visual_agent_in_zone,
    prepare_agent_tools,
    update_visual_agent,
    upload_documents,
)
from backend.socket import socketio
from backend.utils.logger_utils import log_http_request
from backend.utils.logging_utils import get_logger
from backend.utils.project_utils import (
    get_agent_details,
    get_agent_tool_display_name,
    get_kb_tool_in_zone,
    get_ua_project,
    update_agent_name_in_zone,
    update_project_name,
)
from backend.utils.utils import (
    _agent_version_rows,
    get_file_size,
    get_store,
    get_user_and_ent_agents,
    make_json_serializable,
)

logger = get_logger(__name__)

agenthub_agents_bp = Blueprint("agenthub_agents", __name__, url_prefix="/agents")
agenthub_search_bp = Blueprint("agenthub_search", __name__)


def enrich_agents_with_tools(agents: list[dict]) -> None:
    from backend.config import get_tools_details

    tool_details = get_tools_details(user=g.get("authIdentifier"))
    tool_map = {tool["dss_id"]: tool for tool in tool_details}

    def _to_ids(value):
        if not value:
            return []
        if isinstance(value[0], str):
            return value
        return [item.get("id") for item in value if isinstance(item, dict)]

    for agent in agents:
        raw_ids = _to_ids(agent.get("tools", []))
        agent["tools"] = raw_ids
        agent["toolDetails"] = [
            {
                "id": tool_id,
                "name": tool_map.get(tool_id, {}).get("name", "Unknown"),
                "description": tool_map.get(tool_id, {}).get("description", ""),
            }
            for tool_id in raw_ids
            if tool_id in tool_map
        ]

        # same treatment for the published snapshot
        pv = agent.get("published_version")
        if isinstance(pv, dict):
            pv_ids = _to_ids(pv.get("tools", []))
            pv["tools"] = pv_ids
            pv["toolDetails"] = [
                {
                    "id": tool_id,
                    "name": tool_map.get(tool_id, {}).get("name", "Unknown"),
                    "description": tool_map.get(tool_id, {}).get("description", ""),
                }
                for tool_id in pv_ids
                if tool_id in tool_map
            ]


def update_draft_agent(agent: schemas.AgentRead, tools_explicitly_sent: bool) -> schemas.AgentRead:
    store = get_store()
    logger.info("updating agent %s", agent.id)
    project = get_ua_project(agent.id)
    vis_agent = get_visual_agent_in_zone(project, DRAFT_ZONE)
    if tools_explicitly_sent:
        tools_ids = prepare_agent_tools(project, agent.model_dump(), DRAFT_ZONE)
    else:
        tools = get_agent_details(vis_agent)["tools"]
        tools_ids = prepare_agent_tools(project, {**agent.model_dump(), "tools": tools}, DRAFT_ZONE)

    update_visual_agent(
        agent=vis_agent,
        tools_ids=tools_ids,
        llm_id=agent.llmid,
        prompt=agent.system_prompt or "",
    )
    return store.update_agent(agent.id, agent.model_dump())


def _enrich_display_names(items: list[schemas.AgentShareRead]) -> list[dict]:
    client = dataiku.api_client()
    enriched: list[dict] = []

    for it in items:
        principal = it.principal
        type_ = it.principal_type
        dn = principal  # default fallback
        try:
            if type_ == "user":
                raw = client.get_user(principal).get_settings().get_raw()
                dn = raw.get("displayName") or raw.get("fullName") or dn
            else:  # group
                raw = client.get_group(principal).get_definition()
                dn = raw.get("displayName") or raw.get("name") or dn
        except Exception:
            # ignore lookup errors – keep fallback
            pass

        enriched.append(
            {
                **it.model_dump(mode="json", exclude={"displayName"}, by_alias=True),
                "displayName": dn,
            }
        )
    return enriched


def _get_agent_for_sharing(store, agent_id: str):
    agent = store.get_agent(agent_id)
    if agent and agent.published_version:

        return agent, None

    logger.error("Agent must be published before sharing: %s", agent_id)
    return None, (jsonify({"error": "Agent must be published before sharing"}), 403)


def _load_json_value(value):
    if isinstance(value, str):
        return json.loads(value)
    return value


def _resolve_datasource_fields(existing, raw_semantic, raw_datasources):
    if raw_semantic not in (None, ""):
        semantic_datasource = _load_json_value(raw_semantic)
    elif existing:
        semantic_datasource = existing.semantic_datasource
    else:
        semantic_datasource = None

    if raw_datasources is not None:
        datasources = _load_json_value(raw_datasources) if raw_datasources not in (None, "") else []
        datasources = datasources or []
    elif existing and existing.datasources is not None:
        datasources = existing.datasources
    else:
        datasources = [semantic_datasource] if semantic_datasource else []

    if semantic_datasource is None and datasources:
        semantic_datasource = datasources[0]

    return semantic_datasource, datasources


# ──────────────────────────────────────────────────────────────────────────────
#  Principal search  (users / groups)
# ──────────────────────────────────────────────────────────────────────────────


@agenthub_search_bp.route("/principals/search", methods=["GET"])
@log_http_request
def search_principals():
    """
    Query params:
    • q             (string, mandatory)
    • type          = 'user' | 'group' | ''  (optional filter)
    • excludeGroups (string, optional) - comma-separated list of group names to exclude
    Returns: [{ "principal": "alice", "displayName": "Alice Doe", "type":"user" }, …]
    Capped to 20 results.
    """
    term = (request.args.get("q") or "").lower()
    kind = (request.args.get("type") or "").lower()
    auth_identifier = g.get("authIdentifier")

    exclude_groups_param = request.args.get("excludeGroups") or ""
    excluded_groups = set(g.strip() for g in exclude_groups_param.split(",") if g.strip())

    from backend.config import get_config

    enable_groups_restriction_for_sharing = get_config().get("enable_groups_restriction_for_sharing", False)

    if not term:
        logger.error("Missing 'q' parameter in principals search")
        return jsonify({"error": "q parameter required"}), 400

    client = dataiku.api_client()
    hits = []
    filtered_users = []
    groups = []
    users = client.list_users_info()

    if enable_groups_restriction_for_sharing:
        # Fetch only the current user's groups
        user = client.get_user(auth_identifier)
        user_groups = user.get_info().groups

        if excluded_groups:
            # Filter out excluded groups
            user_groups = [g for g in user_groups if g not in excluded_groups]

        # Create a set of group names for efficient lookup
        group_names = set(user_groups)
        groups = [{"name": group_name} for group_name in user_groups]
        # Filter users to only those who share at least one group with the current user
        for u in users:
            user_data = u.get_raw()
            user_member_groups = set(user_data.get("groups", []))
            # Check if user belongs to any of our filtered groups
            if user_member_groups & group_names:
                filtered_users.append(u)
    else:
        filtered_users = users
        all_groups = client.list_groups_info()
        for group_data in all_groups:
            group_name = group_data.name
            if excluded_groups and group_name in excluded_groups:
                continue
            groups.append({"name": group_name})

    if kind in ("", "user"):
        for u in filtered_users:  # only search among filtered users
            user = u.get_raw()
            login = user["login"].lower()
            display = user.get("displayName") or user["login"]
            if term in login or term in display.lower():
                hits.append({"principal": user["login"], "displayName": display, "type": "user"})
            if len(hits) >= 20:
                break

    if kind in ("", "group") and len(hits) < 20:
        for group in groups:
            group_id = group["name"].lower()
            if term in group_id:
                hits.append({"principal": group["name"], "displayName": group["name"], "type": "group"})
            if len(hits) >= 20:
                break

    return jsonify({"results": hits[:20]})


@agenthub_search_bp.route("/groups/search", methods=["GET"])
@log_http_request
def search_groups():
    """
    Returns all groups the current user belongs to.
    Returns: [{ "principal": "group1", "displayName": "Group 1", "type":"group" }, …]
    """
    auth_identifier = g.get("authIdentifier")
    client = dataiku.api_client()
    hits = []

    # Fetch only the current user's groups
    user = client.get_user(auth_identifier)
    user_groups = user.get_info().groups

    for group_name in user_groups:
        try:
            # Try to get display name from group definition
            group = client.get_group(group_name)
            group_def = group.get_definition()
            display_name = group_def.get("displayName") or group_name
        except Exception:
            # Fallback to group name if lookup fails
            display_name = group_name

        # Return groups in the same format as search_principals
        hits.append({"principal": group_name, "displayName": display_name, "type": "group"})

    return jsonify({"results": hits[:50]})


# ──────────────────────────────────────────────────────────────────────────────
#  Agent-sharing CRUD
# ──────────────────────────────────────────────────────────────────────────────


@agenthub_agents_bp.route("/<agent_id>/shares", methods=["GET"])
@log_http_request
def get_agent_shares(agent_id):
    store = get_store()
    logger.info("GET /agents/%s/shares", agent_id)

    _, error_response = _get_agent_for_sharing(store, agent_id)
    if error_response:
        return error_response

    shares = store.get_agent_shares(agent_id)
    return jsonify({"shares": _enrich_display_names(shares)})


@agenthub_agents_bp.route("/<agent_id>/shares", methods=["PUT"])
@log_http_request
def replace_agent_shares(agent_id):
    store = get_store()
    logger.info("PUT /agents/%s/shares", agent_id)

    agent, error_response = _get_agent_for_sharing(store, agent_id)
    if error_response:
        return error_response

    # PUT → replace full list
    # TODO: SQLAlchemy/Pydantic use the new pydantic datamodel
    payload = request.get_json(force=True) or {}
    principals_list = payload.get("shares") or []
    if not isinstance(principals_list, list) or any(
        "principal" not in share or "type" not in share for share in principals_list
    ):
        logger.error(
            "Body must be a list of {principal,type} for agent shares update on %s by %s", agent_id, g.authIdentifier
        )
        return jsonify({"error": "Body must be a list of {principal,type}"}), 400

    try:
        old_shares = store.get_agent_shares(agent_id)
        store.replace_agent_shares(agent_id, principals_list)
        logger.info("Replaced shares for agent %s: %r", agent_id, principals_list)
        agent_name = agent.name or agent_id
        new_count = len(store.get_agent_shares(agent_id))

        for share in principals_list:
            principal = share["principal"]
            logger.info("emitting access granted notification to room : user:%s", principal)
            socketio.emit(
                "agent:access_granted",
                {"agentId": agent_id, "agentName": agent_name, "shareCount": new_count},
                room=f"user:{principal}",
            )

        # 3) you may also want to notify users who were removed:
        old_principals = {share.principal for share in old_shares}
        new_principals = {share["principal"] for share in principals_list}
        for revoked in old_principals - new_principals:
            logger.info("emitting agent:access_revoked notification to room : user:%s", revoked)
            socketio.emit(
                "agent:access_revoked",
                {"agentId": agent_id, "agentName": agent_name, "shareCount": new_count},
                room=f"user:{revoked}",
            )
    except PermissionError:
        logger.exception("Unauthorized share replace attempt on %s by %s", agent_id, g.authIdentifier)
        return jsonify({"error": "Only the owner can modify sharing"}), 403

    shares = store.get_agent_shares(agent_id)
    return jsonify({"shares": [share.model_dump(mode="json", by_alias=True) for share in shares]})


@agenthub_agents_bp.route("", methods=["GET"])
@log_http_request
def get_agents():
    logger.info("Listing all agents for user")
    result = get_user_and_ent_agents()

    # Enrich all user agents with tool details
    # enrich_agents_with_tools(result["userAgents"])

    return jsonify(result)


@agenthub_agents_bp.route("", methods=["POST"])
@log_http_request
def create_or_update_agent():
    from backend.schemas.schemas import AgentCreate

    store = get_store()
    # Check if the form submission explicitly includes tool data
    # This could be either selected tools OR an empty indicator
    tools_explicitly_sent = False
    semantic_datasource = None
    datasources = None

    # ───────────── multipart (draft autosave or doc upload) ──────────────
    if request.content_type and request.content_type.startswith("multipart/form-data"):
        form = request.form
        files = request.files.getlist("documents")

        # ---------------------------------------------------------------
        #  **Ignore** completely empty heart-beats
        #  • no id      (brand-new draft)
        #  • no name
        #  • no description
        #  • no files / tools
        # ---------------------------------------------------------------
        nothing_to_save = (
            not form.get("id")
            and not form.get("name")
            and not form.get("description")
            and not form.get("systemPrompt")
            and not form.get("llmid")
            and not form.get("kb_description")
            and not files
            and all(not tool for tool in form.getlist("tools") or [])
        )
        if nothing_to_save:
            # Tell the caller it was an intentional no–op
            return ("", 204)

        existing = store.get_agent(form.get("id")) if form.get("id") else None

        # Check for any field that indicates tools were part of the form submission
        # This includes checking for a hidden field or any tool-related field
        for key in form.keys():
            if key == "tools" or key == "tools[]" or key.startswith("tool_"):
                tools_explicitly_sent = True
                break

        # Also check if there's a special marker field (you may need to add this in frontend)
        if "tools_submitted" in form:
            tools_explicitly_sent = True

        if tools_explicitly_sent:
            # Tools were part of the form submission (even if empty)
            tool_ids = [tool for tool in form.getlist("tools") if tool]
        else:
            # Tools were not part of this form submission - preserve existing
            tool_ids = existing.tools if existing else []

        # Helper function to clean arrays - frontend sends [''] instead of [] for empty arrays
        def clean_array(arr):
            """Remove empty strings from array. If array contains only empty strings, return empty array."""
            cleaned = [item for item in arr if item and item.strip()]
            return cleaned

        # Handle sample questions - get list from form
        # If sample_questions field is present in form, use it; otherwise preserve existing
        sample_questions = []
        if "sample_questions" in form:
            sample_questions = clean_array(form.getlist("sample_questions"))
        elif existing:
            sample_questions = existing.sample_questions

        # ----------------------------------------------------------------
        # Preserve / create the frozen snapshot that everybody else sees
        # ----------------------------------------------------------------
        semantic_datasource, datasources = _resolve_datasource_fields(
            existing,
            form.get("semantic_datasource", None),
            form.get("datasources"),
        )

        agent_obj = AgentCreate(
            id=form.get("id") or str(uuid.uuid4()),
            owner=g.get("authIdentifier", "unknown"),
            name=form.get("name", existing.name if existing else ""),
            description=form.get("description", existing.description if existing else ""),
            system_prompt=form.get("systemPrompt")
            or form.get("system_prompt", existing.system_prompt if existing else ""),
            tools=tool_ids,
            llmid=form.get("llmid") or (existing.llmid if existing else ""),
            kb_description=form.get("kb_description", existing.kb_description if existing else ""),
            sample_questions=sample_questions,
            documents=existing.documents if existing else [],
            datasources=datasources,
            indexing=existing.indexing if existing else None,
            published_version=(
                existing.published_version if existing and hasattr(existing, "published_version") else None
            ),
            published_at=existing.published_at if existing and hasattr(existing, "published_at") else None,
            semantic_datasource=semantic_datasource,
            sem_models_llm=form.get("sem_models_llm", existing.sem_models_llm if existing else ""),
            sem_models_interaction_mode=form.get(
                "sem_models_interaction_mode",
                existing.sem_models_interaction_mode if existing else "simple",
            ),
        )

        # ── documents & indexing ─────────────────────────────────────────
        if files:
            existing_blob = agent_obj.indexing or {}
            if existing_blob and getattr(existing_blob, "status", None) in ("pending", "running"):
                logger.error(
                    "Indexing already in progress for agent %s, rejecting new upload %s", agent_obj.id, existing_blob
                )
                return jsonify({"error": "Indexing already in progress", "indexing": existing_blob}), 409

            new_entries = [{"name": file.filename, "active": False, "size": get_file_size(file)} for file in files]
            docs = (existing.documents if existing else []) + new_entries
            agent_with_doc = agent_obj.model_dump()
            agent_with_doc["documents"] = docs
            upload_documents(agent_with_doc, files)

            agent_obj.documents = docs
            agent_obj.indexing = None  # indexing_blob

    # ───────────── pure JSON (Edit Agent modal) ──────────────
    else:
        payload = request.get_json(force=True)
        logger.info("JSON POST /agents %s", payload.get("id"))
        missing = [field for field in ("id", "name") if not payload.get(field)]
        if missing:
            logger.error("Missing field(s): %s", ", ".join(missing))
            return jsonify({"error": f"Missing field(s): {', '.join(missing)}"}), 400

        existing = store.get_agent(payload["id"])
        semantic_datasource, datasources = _resolve_datasource_fields(
            existing,
            payload.get("semantic_datasource", None),
            payload.get("datasources", None),
        )
        if existing:
            agent_obj = AgentCreate(
                id=payload.get("id", existing.id),
                owner=g.get("authIdentifier", "unknown"),
                name=payload.get("name", existing.name),
                description=payload.get("description", existing.description),
                system_prompt=payload.get("system_prompt", existing.system_prompt),
                tools=payload.get("tools", existing.tools),
                llmid=payload.get("llmid", existing.llmid),
                kb_description=payload.get("kb_description", existing.kb_description),
                sample_questions=payload.get("sample_questions", existing.sample_questions),
                documents=existing.documents,
                datasources=datasources,
                indexing=existing.indexing,
                published_version=existing.published_version,
                published_at=existing.published_at,
                semantic_datasource=semantic_datasource,
                sem_models_llm=payload.get("sem_models_llm", existing.sem_models_llm),
                sem_models_interaction_mode=payload.get(
                    "sem_models_interaction_mode",
                    existing.sem_models_interaction_mode,
                ),
            )
        else:
            agent_obj = AgentCreate(
                id=payload["id"],
                owner=g.get("authIdentifier", "unknown"),
                name=payload["name"],
                description=payload.get("description", ""),
                system_prompt=payload.get("system_prompt", ""),
                tools=payload.get("tools", []),
                llmid=payload.get("llmid", ""),
                kb_description=payload.get("kb_description", ""),
                sample_questions=payload.get("sample_questions", []),
                documents=[],
                datasources=datasources,
                indexing=None,
                published_version=None,
                published_at=None,
                semantic_datasource=semantic_datasource,
                sem_models_llm=payload.get("sem_models_llm", ""),
                sem_models_interaction_mode=payload.get("sem_models_interaction_mode", "simple"),
            )

    # Check if agent exists in DB
    existing = store.get_agent(agent_obj.id)

    try:
        if existing:
            logger.info("Found agent: %s", agent_obj.id)
            saved = update_draft_agent(agent_obj, tools_explicitly_sent)

            if saved is None:
                # User doesn't own this agent
                logger.error("Unauthorized update attempt on agent %s by %s", agent_obj.id, g.authIdentifier)
                return jsonify({"error": "Unauthorized to update this agent"}), 403
            if agent_obj.name != existing.name:
                # Name changed - update project name too
                project = get_ua_project(agent_obj.id)
                if project:
                    update_project_name(project, agent_obj.name)
                    update_agent_name_in_zone(project, DRAFT_ZONE, agent_obj.name)
        else:
            logger.info("creating agent %s", agent_obj.id)
            # Create a project as soon as we have a new agent with name
            owner = g.get("run_as_login", "unknown")
            project = ensure_ua_project(agent_obj.model_dump(), owner)
            if not project:
                logger.error("Failed to create project for agent %s", agent_obj.id)
                return jsonify({"error": "Failed to create project for agent"}), 500
            agent_obj.id = project.project_key
            saved = store.create_agent(agent_obj.model_dump())
            if saved is None:
                logger.error("Failed to create agent %s", agent_obj.id)
                return jsonify({"error": "Failed to create agent"}), 500
    except ValueError as exc:
        # Handle race condition where agent was created between our check and create attempt
        logger.exception("Race condition detected for agent %s: %s", agent_obj.id, exc)
        saved = update_draft_agent(agent_obj, tools_explicitly_sent)
        if saved is None:
            return jsonify({"error": "Agent was created by another user"}), 403
    except Exception as exc:
        logger.exception("Unexpected error saving agent %s: %s", agent_obj.id, exc)
        return jsonify({"error": "Failed to save agent"}), 500

    if saved is None:
        # Extra safety check
        logger.error("Saved is None for agent %s - this shouldn't happen", agent_obj.id)
        return jsonify({"error": "Internal error: agent save failed"}), 500

    shares = store.get_agent_shares(saved.id)
    share_count = len([share for share in shares if share.principal_type == schemas.PrincipalTypeEnum.USER])

    # build the "version rows" once
    # TODO : SQLAlchemy/Pydantic use model directly
    version_rows = _agent_version_rows(saved.model_dump(mode="json"), share_count, g.authIdentifier)

    # owner
    for row in version_rows:
        socketio.emit("agent:updated", row, room=f"user:{saved.owner}")

    # every explicitly-shared user
    for share in shares:
        if share.principal_type == schemas.PrincipalTypeEnum.USER:
            for row in version_rows:
                socketio.emit("agent:updated", {**row, "isShared": True}, room=f"user:{share.principal}")
    return jsonify(saved.model_dump(mode="json")), 200


@agenthub_agents_bp.route("/<agent_id>/publish", methods=["POST"])
@log_http_request
def publish_agent(agent_id):
    publishing_service = current_app.config.get("PUBLISHING_SERVICE")

    try:
        result = publishing_service.start_publish(agent_id, g.authIdentifier)
        return jsonify(result), 200
    except PermissionError as exc:
        logger.exception("Permission denied to publish agent %s: %s", agent_id, exc)
        return jsonify({"error": str(exc)}), 403
    except ValueError as exc:
        logger.exception("Failed to publish agent %s: %s", agent_id, exc)
        return jsonify({"error": str(exc)}), 404


@agenthub_agents_bp.route("/<agent_id>/publishing_status", methods=["GET"])
@log_http_request
def get_publishing_status(agent_id):
    agent = get_store().get_agent(agent_id)
    if not agent:
        logger.error("Agent %s not found", agent_id)
        return jsonify({"error": "Agent not found"}), 404
    return jsonify(
        {
            "publishing_status": (
                agent.publishing_status.value if agent.publishing_status else schemas.PublishingStatusEnum.IDLE.value
            ),
            "published_at": agent.published_at,
            "has_published_version": bool(agent.published_version),
        }
    )


@agenthub_search_bp.route("/health/publishing", methods=["GET"])
@log_http_request
def publishing_health():
    publishing_service = current_app.config.get("PUBLISHING_SERVICE")
    if not publishing_service:
        return jsonify({"status": "error", "message": "Service not initialized"}), 503

    stats = publishing_service.get_stats()
    status = "healthy" if stats["monitor_running"] else "unhealthy"
    return jsonify({"status": status, "stats": stats, "timestamp": datetime.utcnow().isoformat()})


@agenthub_agents_bp.route("/<agent_id>", methods=["GET"])
@log_http_request
def get_agent(agent_id):
    store = get_store()
    agent = store.get_agent(agent_id)
    if not agent:
        logger.error("Agent %s not found", agent_id)
        return jsonify({"error": "Agent not found"}), 404

    project = get_ua_project(agent_id)
    agent_dict = agent.model_dump(mode="json")
    enrich_agents_with_tools([agent_dict])
    kb_tool = get_kb_tool_in_zone(project, DRAFT_ZONE)
    if kb_tool:
        try:
            kb_settings_raw = kb_tool.get_settings().get_raw()
            kb_description = kb_settings_raw.get("additionalDescriptionForLLM", "")
        except Exception:
            kb_settings_raw = {}
            kb_description = ""
        kb_tool_detail = {
            "id": kb_tool.id,
            "name": get_agent_tool_display_name(kb_tool),
            "description": kb_description,
        }
        if all(existing.get("id") != kb_tool.id for existing in (agent_dict.get("toolDetails") or [])):
            agent_dict["toolDetails"] = [*(agent_dict.get("toolDetails") or []), kb_tool_detail]
        published_version = agent_dict.get("published_version")
        if isinstance(published_version, dict) and all(
            existing.get("id") != kb_tool.id for existing in (published_version.get("toolDetails") or [])
        ):
            published_version["toolDetails"] = [*(published_version.get("toolDetails") or []), kb_tool_detail]
    agent_dict["shareCount"] = store.get_share_counts([agent_id]).get(agent_id, 0)
    return jsonify(agent_dict)


@agenthub_agents_bp.route("/<agent_id>/documents", methods=["POST"])
@log_http_request
def upload_agent_document(agent_id):
    files = request.files.getlist("documents")
    payload, status_code = agent_documents.upload_agent_document(agent_id, files)
    return jsonify(payload), status_code


@agenthub_agents_bp.route("/<agent_id>/documents/index", methods=["POST"])
@log_http_request
def index_agent_document(agent_id):
    payload, status_code = agent_documents.index_agent_documents(agent_id)
    return jsonify(payload), status_code


@agenthub_agents_bp.route("/<agent_id>/documents/sharepoint-folder", methods=["POST"])
@log_http_request
def configure_sharepoint_documents_folder(agent_id):
    payload = request.get_json(force=True) or {}
    payload, status_code = agent_documents.configure_sharepoint_documents_folder(agent_id, payload)
    return jsonify(payload), status_code


@agenthub_agents_bp.route("/<agent_id>/documents/<filename>", methods=["DELETE"])
@log_http_request
def delete_agent_document(agent_id, filename):
    payload, status_code = agent_documents.delete_agent_document(agent_id, filename)
    return jsonify(payload), status_code


@agenthub_agents_bp.route("/<agent_id>/semantic_datasource", methods=["DELETE"])
@log_http_request
def unbind_agent_semantic_datasource(agent_id):
    from backend.utils.semantic_models_utils import normalize_to_semantic_datasource_core

    store = get_store()
    agent = store.get_agent(agent_id)
    semantic_datasource = agent.semantic_datasource
    project = get_ua_project(agent.id)
    vis_agent = get_visual_agent_in_zone(project, DRAFT_ZONE)
    copy_agent = agent.model_dump()
    tools_ids = prepare_agent_tools(project, copy_agent, DRAFT_ZONE, with_semantic_tools=False)
    update_visual_agent(
        agent=vis_agent,
        tools_ids=tools_ids,
        llm_id=agent.llmid,
        prompt=agent.system_prompt,
    )

    remaining_datasources = [
        normalize_to_semantic_datasource_core(datasource)
        for datasource in (agent.datasources or [])
        if datasource.get("id") != semantic_datasource.get("id")
    ]

    updated = store.update_agent(agent_id, {"datasources": remaining_datasources})
    if updated is None:
        logger.error("Failed to update agent %s", agent_id)
        return jsonify({"error": "Unauthorized to update this agent"}), 403

    return jsonify({"unbinded_datasource": semantic_datasource, "datasources": updated.datasources or []}), 200


@agenthub_agents_bp.route("/<agent_id>/datasources", methods=["PUT"])
@log_http_request
def update_agent_datasources(agent_id):
    store = get_store()
    agent = store.get_agent(agent_id)
    if not agent:
        logger.error("Agent %s not found for datasource update", agent_id)
        return jsonify({"error": "Agent not found"}), 404

    payload = request.get_json(force=True) or {}
    datasources = payload.get("datasources", [])
    if datasources is None:
        datasources = []
    if not isinstance(datasources, list):
        logger.error("Invalid datasources format for agent %s: %s", agent_id, datasources)
        return jsonify({"error": "Field 'datasources' must be a list"}), 400

    updated = store.update_agent(agent_id, {"datasources": datasources})
    if updated is None:
        logger.error("Failed to update agent %s with new datasources", agent_id)
        return jsonify({"error": "Unauthorized to update this agent"}), 403

    return jsonify({"datasources": updated.datasources or []}), 200


@agenthub_agents_bp.route("/<agent_id>/documents", methods=["DELETE"])
@log_http_request
def delete_multiple_agent_documents(agent_id):
    payload = request.get_json(force=True)
    filenames = payload.get("filenames")
    if not filenames or not isinstance(filenames, list):
        logger.error("Missing or invalid 'filenames' for agent %s document deletion: %s", agent_id, filenames)
        return jsonify({"error": "Missing or invalid filenames"}), 400
    payload, status_code = agent_documents.delete_multiple_agent_documents(agent_id, filenames)
    return jsonify(payload), status_code


@agenthub_agents_bp.route("/<agent_id>/index_status", methods=["GET"])
@log_http_request
def get_index_status(agent_id):
    payload, status_code = agent_documents.get_index_status(agent_id)
    return jsonify(payload), status_code


@agenthub_agents_bp.route("/index_status_bulk", methods=["POST"])
@log_http_request
def index_status_bulk():
    agent_ids = request.get_json(force=True).get("agentIds", [])
    payload = agent_documents.index_status_bulk(agent_ids)
    return jsonify(payload)


@agenthub_agents_bp.route("/<agent_id>/discard", methods=["POST"])
@log_http_request
def discard_agent_changes(agent_id: str):
    store = get_store()
    agent = store.get_agent(agent_id)
    if not agent:
        logger.error("Agent %s not found for discard operation", agent_id)
        return jsonify({"error": "Agent not found"}), 404

    pv = agent.published_version
    if not pv:
        logger.error("Agent %s has no published version", agent_id)
        return jsonify({"error": "Agent has no published version"}), 400

    idx = agent.indexing or {}
    if idx.get("status") in ("pending", "running") and idx.get("jobId"):
        try:
            dataiku.api_client().get_project(agent_id).get_job(idx.get("jobId")).abort()
        except Exception:
            logger.exception("Could not abort indexing jobs for agent %s", agent_id)

    restored_docs, files_changed = agent_documents.restore_discarded_documents(agent, pv)

    now = datetime.utcnow().isoformat()

    payload = {
        "name": pv.name,
        "description": pv.description,
        "sample_questions": pv.sample_questions,
        "documents": restored_docs,
        "indexing": None,
        "updatedAt": now,
    }
    project = get_ua_project(agent_id=agent_id)
    draft_agent = get_visual_agent_in_zone(project, DRAFT_ZONE)
    prompt = pv.system_prompt or ""
    llm_id = pv.llmid or ""
    tools_ids = prepare_agent_tools(project, pv.model_dump(mode="json"), DRAFT_ZONE)
    update_visual_agent(draft_agent, tools_ids=tools_ids, prompt=prompt, llm_id=llm_id)
    agent = store.update_agent(agent_id, payload)

    if files_changed:
        try:
            indexing_blob = agent_documents.build_indexing_blob(agent_id, zone=DRAFT_ZONE)
            store.update_agent(
                agent_id,
                {"indexing": indexing_blob},
            )
            wake_monitor()
        except Exception:
            logger.exception("Failed to launch re-index job for %s", agent_id)

    shares = store.get_agent_shares(agent_id)
    share_count = len([share for share in shares if share.principal_type == schemas.PrincipalTypeEnum.USER])
    socketio.emit(
        "agent:updated",
        {
            "agentId": agent_id,
            "status": "active",
            "agentName": agent.name,
            "isShared": False,
            "shareCount": share_count,
            "hasUnpublishedChanges": False,
        },
        room=f"user:{agent.owner}",
    )

    if agent.name != pv.name:
        update_project_name(project, pv.name)
        update_agent_name_in_zone(project, DRAFT_ZONE, pv.name)

    return jsonify(make_json_serializable(store.get_agent(agent_id))), 200


@agenthub_agents_bp.route("/<agent_id>/favorite", methods=["POST"])
@log_http_request
def mark_agent_as_favorite(agent_id):
    store = get_store()
    agent = store.get_agent(agent_id)
    if not agent:
        logger.error("Agent %s not found for favorite operation", agent_id)
        return jsonify({"error": "Agent not found"}), 404

    payload = request.get_json(force=True)
    is_favorite = payload.get("favorite")
    if not isinstance(is_favorite, bool):
        logger.error("Invalid favorite value for agent %s: %s", agent_id, is_favorite)
        return jsonify({"error": "Invalid 'favorite' value, must be true or false"}), 400

    prefs = store.get_preferences() or {}
    favorite_prefs = prefs.setdefault("favorite", {})
    favorite_prefs[agent_id] = is_favorite
    store.update_preferences({"prefs": favorite_prefs})

    return jsonify({"message": "Favorite status updated", "agentId": agent_id, "favorite": is_favorite})


@agenthub_agents_bp.route("/<agent_id>", methods=["DELETE"])
@log_http_request
def delete_agent(agent_id):
    store = get_store()

    agent = store.get_agent(agent_id)
    if not agent:
        logger.error("DELETE missing agent %s", agent_id)
        return jsonify({"error": "Agent not found"}), 404

    agent_name = agent.name or agent_id
    owner = agent.owner
    shares = store.get_agent_shares(agent_id) or []
    shared_users = [share.principal for share in shares if share.principal_type == schemas.PrincipalTypeEnum.USER]

    success = store.delete_agent(agent_id)
    if not success:
        logger.error("DELETE missing agent %s", agent_id)
        return jsonify({"error": "Agent not found"}), 404

    delete_agent_project(agent_id)
    logger.info("Deleted agent %s and its project", agent_id)

    socketio.emit(
        "agent:deleted",
        {"agentId": agent_id, "agentName": agent_name, "deletedBy": g.authIdentifier},
        room=f"user:{owner}",
    )

    for user in shared_users:
        socketio.emit(
            "agent:deleted",
            {"agentId": agent_id, "agentName": agent_name, "deletedBy": g.authIdentifier},
            room=f"user:{user}",
        )

    return jsonify({"message": "Agent deleted", "agentId": agent_id})
