from flask import Blueprint, jsonify, request

from backend.config import get_config
from backend.schemas import schemas
from backend.services.generated_document.generated_document_revision_review import (
    RevisionReviewError,
    review_generated_document_revision,
)
from backend.services.stream_link_enrichment_service import serialize_messages_with_enriched_links
from backend.utils.connection_credentials_utils import (
    resolve_connection_credentials_issue_for_draft_message,
    resolve_connection_credentials_issue_for_message,
)
from backend.utils.logger_utils import log_http_request
from backend.utils.logging_utils import get_logger
from backend.utils.utils import get_store

logger = get_logger(__name__)

convs_blueprint = Blueprint("convs_blueprint", __name__, url_prefix="/conversations")


# --- Global Conversations Endpoints (all user-scoped) ---


@convs_blueprint.route("", methods=["GET"])
@log_http_request
def get_conversations():
    store = get_store()
    conversations = store.get_conversations()
    conv_list = []
    for conv in conversations:
        if conv.messages is not None:
            conv_list.append(
                {
                    "id": conv.conversation_id,
                    "title": conv.title or "New Chat",
                    "messages": serialize_messages_with_enriched_links(conv.messages or []),
                    "lastUpdated": conv.last_modified.isoformat() if conv.last_modified else "",
                    "agentCount": len(conv.agent_ids or []),
                    "agentIds": conv.agent_ids or [],
                }
            )
    conv_list.sort(key=lambda c: c["lastUpdated"], reverse=True)
    return jsonify({"conversations": conv_list})


def make_aware(dt):
    from datetime import timezone

    if dt.tzinfo is None:
        return dt.replace(tzinfo=timezone.utc)
    return dt


@convs_blueprint.route("/metadata", methods=["GET"])
@log_http_request
def get_conversation_metadata():
    """
    Return only the metadata for each conversation:
      - id
      - title
      - lastUpdated
      - agentCount
      - agentIds
    (no messages)
    """
    store = get_store()
    metas = store.get_conversations_metadata()
    # print("Metas before sort:", [   m.last_updated for m in metas])
    # sort by lastUpdated desc
    # TODO check if we can correctly store timezone info in the DB
    metas.sort(key=lambda c: make_aware(c.last_modified), reverse=True)
    # metas.sort(key=lambda c: c.last_updated, reverse=True)
    # strip userId if we don’t want to expose it
    return jsonify({"conversations": [m.model_dump(mode="json", exclude={"userId"}, by_alias=True) for m in metas]})


@convs_blueprint.route("/<conversation_id>", methods=["GET"])
@log_http_request
def get_conversation(conversation_id):
    store = get_store()
    conv = store.get_conversation(conversation_id)
    if not conv:
        logger.error(f"Conversation {conversation_id} not found")
        return jsonify({"error": "Conversation not found"}), 404
    try:
        docs = store.get_derived_documents(conversation_id)
        uploaded_attachments = []
        has_attachments = len(docs) > 0  # True if conversation has ANY derived documents (regardless of status)
        for doc in docs:
            meta = doc.document_metadata or {}
            if meta.get("status") in ("uploaded", "ready"):
                attachment_info = {
                    "id": doc.id,
                    "document_name": doc.document_name,
                    "document_path": doc.document_path,
                    "uploadStatus": meta.get("status"),
                    "guardrailsStatus": meta.get("guardrails_status"),
                }
                if "size" in meta:
                    attachment_info["size"] = meta["size"]
                if "type" in meta:
                    attachment_info["type"] = meta["type"]
                uploaded_attachments.append(attachment_info)
    except Exception:
        uploaded_attachments = []
        has_attachments = False

    # Get extraction mode from latest message with attachments
    extraction_mode = None
    messages = conv.messages or []
    latest_messages = messages[-2:] if len(messages) >= 2 else messages
    # TODO seem there is a pb when migrating existing dbs where extraction_mode was not stored and we have a new msg with attachments but no extraction_mode
    for msg in reversed(latest_messages):  # Only check the two latest messages
        if msg.extraction_mode:
            extraction_mode = msg.extraction_mode
            break
    # TODO SQLAlchemy/Pydantic: improve response structure by using FullConversationRead schema
    return jsonify(
        {
            "id": conversation_id,
            "title": conv.title or "New Chat",
            "messages": serialize_messages_with_enriched_links(conv.messages),
            "lastUpdated": conv.last_modified,
            "agentIds": conv.agent_ids or [],
            "selectedLLM": conv.llm_id,
            "modeAgents": conv.agents_enabled,
            "documentGenerationEnabled": conv.doc_gen_enabled,
            "agentCount": len(conv.agent_ids or []),
            "conversation_attachments": uploaded_attachments,
            "has_attachments": has_attachments,
            "extractionMode": extraction_mode,
        }
    )


@convs_blueprint.route("/message_events/<message_id>", methods=["GET"])
@log_http_request
def get_message_events(message_id):
    store = get_store()
    if request.args.get("draft") == "1":
        aid = request.args.get("agentId")
        if not aid:
            logger.error("agentId is required when draft=1 for message events retrieval")
            return jsonify({"error": "agentId required when draft=1"}), 400
        convo = store.get_draft_conversation(aid)
        if not convo or not convo.convo:
            convo = {}
        else:
            convo = convo.convo

        for m in convo.get("messages", []):
            if m["id"] == message_id:
                return jsonify({"messageId": message_id, "events": m.get("eventLog", [])})
        return jsonify({"messageId": message_id, "events": []})

    events = store.get_message_events(message_id)
    return jsonify({"messageId": message_id, "events": events})


@convs_blueprint.route("/<conversation_id>", methods=["DELETE"])
@log_http_request
def delete_conversation(conversation_id):
    store = get_store()
    permanent_delete = get_config().get("permanent_delete_messages", False)
    success = store.delete_conversation(conversation_id, permanent_delete)
    if not success:
        logger.error(f"Failed to delete conversation {conversation_id}")
        return jsonify({"error": "Conversation not found"}), 404
    return jsonify({"message": "Conversation deleted", "conversationId": conversation_id})


@convs_blueprint.route("", methods=["DELETE"])
@log_http_request
def delete_all_conversations():
    store = get_store()
    permanent_delete = get_config().get("permanent_delete_messages", False)
    store.delete_all_conversations(permanent_delete)
    return jsonify({"message": "All conversations deleted"})


@convs_blueprint.route("", methods=["POST"])
@log_http_request
def create_conversation():
    """
    Create or update a conversation depending on if id is provided.
    Request body (JSON):
        {
            "id": <optional>, // use to update if present, otherwise create
            "title": "",
            "agentIds": ["agent-id1", ...],
            "modeAgents": true,
            "selectedLLM": "llm-id"
        }
    """
    store = get_store()
    payload = request.get_json(force=True, silent=True) or {}

    from uuid import uuid4

    conv_id = payload.get("id") or str(uuid4())
    title = payload.get("title") or "New Chat"
    agent_ids = payload.get("agentIds") or []
    mode_agents = payload.get("modeAgents", True)
    selected_llm = payload.get("selectedLLM") or ""
    doc_gen_enabled = payload.get("documentGenerationEnabled", True) is not False
    # TODO SQLAlchemy/Pydantic: use ConversationCreate schema
    conversation = schemas.ConversationCreate(
        conversation_id=conv_id,
        title=title,
        agent_ids=agent_ids,
        agents_enabled=mode_agents,
        llm_id=selected_llm,
        doc_gen_enabled=doc_gen_enabled,
        user_id="",
    )
    # {
    #     "id": conv_id,
    #     "title": title,
    #     "messages": [],
    #     "agentIds": agent_ids,
    #     "lastUpdated": None,
    # }
    # If id exists (update), else create.
    # TODO SQLAlchemy/Pydantic: previous comment `If id exists (update), else create.` is not accurate based on the store. Is this expected?
    store.create_conversation(conv_id, conversation)
    return (
        jsonify(
            {
                "id": conv_id,
                "title": title,
                "agentIds": agent_ids,
                "modeAgents": mode_agents,
                "selectedLLM": selected_llm,
                "documentGenerationEnabled": doc_gen_enabled,
            }
        ),
        201,
    )


@convs_blueprint.route("/<conversation_id>", methods=["PUT"])
@log_http_request
def update_conversation_title(conversation_id):
    payload = request.get_json()
    new_title = payload.get("title", "").strip()
    if not new_title:
        logger.error("Title cannot be empty")
        return jsonify({"error": "Title cannot be empty"}), 400

    store = get_store()
    conv = store.get_conversation(conversation_id=conversation_id)
    if not conv:
        logger.error(f"Conversation {conversation_id} not found")
        return jsonify({"error": "Conversation not found"}), 404

    store.update_conversation_meta(conversation_id, title=new_title)
    return jsonify({"message": "Conversation title updated", "title": new_title})


@convs_blueprint.route("/draft/<agent_id>", methods=["DELETE"])
@log_http_request
def reset_draft_chat(agent_id):
    store = get_store()
    store.delete_draft_conversation(agent_id)
    return jsonify({"ok": True})


@convs_blueprint.route("/draft/<agent_id>", methods=["GET"])
@log_http_request
def get_draft_conversation(agent_id):
    """
    Return the saved draft conversation (if any) for *this* user /
    *this* agent.  Shape is identical to what the frontend already
    receives on chat_end, e.g.
      { id, title, messages, lastUpdated, agentIds }
    If nothing exists we return `{}` so the UI knows to start fresh.
    """
    store = get_store()
    convo = store.get_draft_conversation(agent_id)
    if not convo:
        logger.error(f"Draft conversation for agent {agent_id} not found")
        return jsonify({"error": "Draft conversation not found"}), 404
    convo_data = dict(convo.convo)
    convo_data["messages"] = serialize_messages_with_enriched_links(convo_data.get("messages", []))
    return jsonify(convo_data)


@convs_blueprint.route("/messages/<message_id>/feedback", methods=["GET"])
@log_http_request
def get_message_feedback(message_id):
    """
    Return the feedback JSON for one message in the caller's own conversation.
    """
    store = get_store()
    msg = store.get_message(message_id)
    if not msg:  # not found or not authorized
        logger.error(f"Message {message_id} not found")
        return jsonify({"error": "Message not found"}), 404
    return jsonify({"messageId": message_id, "feedback": msg.feedback or {}}), 200


@convs_blueprint.route("/messages/<message_id>/feedback", methods=["PUT"])
@log_http_request
def upsert_message_feedback(message_id):
    """Create/update feedback on one message (thumbs up/down).

    Body: { "rating": 1|0, "text": "<optional string>" }.
    """
    store = get_store()
    msg = store.get_message(message_id)
    if not msg:  # not found or not authorized
        logger.error(f"Message {message_id} not found")
        return jsonify({"error": "Message not found"}), 404

    payload = request.get_json(force=True) or {}
    rating = payload.get("rating")

    if isinstance(rating, bool):
        rating = 1 if rating else 0
    elif isinstance(rating, str):
        rating = 1 if rating == "1" else 0

    if rating not in {0, 1}:
        logger.error("Invalid 'rating'. Use 1 (up) or 0 (down).")
        return jsonify({"error": "Invalid 'rating'. Use 1 (up) or 0 (down)."}), 400

    text = payload.get("text")
    text = text.strip() if isinstance(text, str) else None
    store.update_message_feedback(message_id, rating=rating, text=text)
    # Round-trip the stored row so GET/PUT shapes stay identical
    refreshed = store.get_message(message_id)
    return jsonify({"messageId": message_id, "feedback": refreshed.feedback.model_dump(mode="json") or {}}), 200


@convs_blueprint.route("/messages/<message_id>/feedback", methods=["DELETE"])
@log_http_request
def clear_message_feedback(message_id):
    """
    Clear feedback JSON for the message (set to {}).
    """
    store = get_store()
    msg = store.get_message(message_id)
    if not msg:
        logger.error(f"Message {message_id} not found")
        return jsonify({"error": "Message not found"}), 404
    store.clear_message_feedback(message_id)
    return jsonify({"messageId": message_id, "cleared": True}), 200


@convs_blueprint.route("/messages/<message_id>/connection-credentials", methods=["PUT"])
@log_http_request
def resolve_connection_credentials_issue(message_id):
    """
    Persist the resolution state for a connection credentials request on a message action.
    """
    payload = request.get_json(force=True) or {}
    resolved = payload.get("resolved")
    if resolved is not True:
        logger.error("Invalid 'resolved'. Use true.")
        return jsonify({"error": "Invalid 'resolved'. Use true."}), 400
    connection_name = payload.get("connection_name")
    connection_name = connection_name.strip() if isinstance(connection_name, str) else ""
    if not connection_name:
        logger.error("Invalid 'connection_name'. Use a non-empty string.")
        return jsonify({"error": "Invalid 'connection_name'. Use a non-empty string."}), 400

    store = get_store()
    if request.args.get("draft") == "1":
        agent_id = request.args.get("agentId")
        if not agent_id:
            logger.error("agentId is required when draft=1 for connection credentials resolution")
            return jsonify({"error": "agentId required when draft=1"}), 400
        updated_issue, error = resolve_connection_credentials_issue_for_draft_message(
            store, agent_id, message_id, connection_name
        )
    else:
        updated_issue, error = resolve_connection_credentials_issue_for_message(store, message_id, connection_name)

    if error:
        logger.error(error)
        return jsonify({"error": error}), 404
    return jsonify({"messageId": message_id, "issue": updated_issue}), 200


@convs_blueprint.route(
    "/<conversation_id>/messages/<message_id>/artifacts/<artifacts_id>/<artifact_index>/revision-review",
    methods=["POST"],
)
@log_http_request
def review_generated_document_revision_route(conversation_id, message_id, artifacts_id, artifact_index):
    if request.args.get("draft") == "1":
        logger.error("Generated document revision review is not supported in draft mode")
        return jsonify({"error": "Generated document revision review is not supported in draft mode"}), 400

    try:
        parsed_artifact_index = int(artifact_index)
    except ValueError:
        logger.error("Artifact index should be a number")
        return jsonify({"error": "Artifact index should be a number"}), 400

    payload = request.get_json(force=True, silent=True) or {}
    review_status = payload.get("status")

    store = get_store()
    try:
        updated_messages = review_generated_document_revision(
            store=store,
            conversation_id=conversation_id,
            message_id=message_id,
            artifacts_id=artifacts_id,
            artifact_index=parsed_artifact_index,
            review_status=review_status,
        )
    except RevisionReviewError as err:
        logger.error(err.message)
        return jsonify({"error": err.message}), err.status_code

    return jsonify({"updatedMessages": updated_messages}), 200


@convs_blueprint.route("/<conversation_id>/messages/<message_id>", methods=["PUT"])
@log_http_request
def update_message(conversation_id, message_id):
    payload = request.get_json()
    new_content = payload.get("content", "").strip()

    if not new_content:
        logger.error("Content cannot be empty")
        return jsonify({"error": "Content cannot be empty"}), 400

    store = get_store()

    try:
        # Get current conversation
        conversation = store.get_conversation(conversation_id)
        if not conversation:
            logger.error(f"Conversation {conversation_id} not found")
            return jsonify({"error": "Conversation not found"}), 404

        # Find the message to delete
        messages = conversation.messages or []
        message_index = None

        for i, msg in enumerate(messages):
            if msg.id == message_id:
                if msg.role != "user":
                    return jsonify({"error": "Can only edit user messages"}), 400
                message_index = i
                break

        if message_index is None:
            logger.error(f"Message {message_id} not found")
            return jsonify({"error": "Message not found"}), 404

        # Get all message IDs that need to be deleted (from edited message onwards)
        messages_to_delete = [msg.id for msg in messages[message_index:] if msg.id]

        # Delete the specific messages using store methods
        for msg_id in messages_to_delete:
            store.delete_message(msg_id)

        return jsonify(
            {
                "message": "Message deleted and subsequent messages removed",
                "messageId": message_id,
                "content": new_content,
                "messagesRemoved": len(messages_to_delete),
            }
        )

    except Exception:
        logger.exception("Failed to update message")
        return jsonify({"error": "Failed to update message"}), 500
