import json

from flask import Blueprint, jsonify, request, session
from loguru import logger
from sqlalchemy import func

from ...constants import (
    HISTORY_LOGS_DEFAULT_LIMIT,
    HISTORY_LOGS_HARD_CAP,
    ResearchStatus,
)
from ...database.models import ResearchHistory
from ...database.models.library import Document as Document
from ...database.session_context import get_user_db_session
from ..auth.decorators import login_required
from ..models.database import (
    get_logs_for_research,
    get_total_logs_for_research,
)
from ..routes.globals import get_active_research_snapshot
from ..routes.research_routes import _research_not_found
from ..services.research_service import get_research_strategy
from ...security.rate_limiter import limiter
from ...security import filter_research_metadata
from ..utils.templates import render_template_with_defaults

# Create a Blueprint for the history routes
history_bp = Blueprint("history", __name__, url_prefix="/history")

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


# resolve_report_path removed - reports are now stored in database


@history_bp.route("/")
@login_required
def history_page():
    """Render the history page"""
    return render_template_with_defaults("pages/history.html")


@history_bp.route("/api", methods=["GET"])
@login_required
def get_history():
    """Get the research history JSON data"""
    username = session["username"]

    try:
        limit = request.args.get("limit", 200, type=int)
        limit = max(1, min(limit, 500))
        offset = request.args.get("offset", 0, type=int)
        offset = max(0, offset)

        with get_user_db_session(username) as db_session:
            # Single query with JOIN to get history + document counts.
            # Select only the columns the response needs — never the
            # large ``report_content`` Text body — so listing history
            # doesn't pull every report into memory (#4560).
            results = (
                db_session.query(
                    ResearchHistory.id,
                    ResearchHistory.title,
                    ResearchHistory.query,
                    ResearchHistory.mode,
                    ResearchHistory.status,
                    ResearchHistory.created_at,
                    ResearchHistory.completed_at,
                    ResearchHistory.duration_seconds,
                    ResearchHistory.research_meta,
                    ResearchHistory.chat_session_id,
                    func.count(Document.id).label("document_count"),
                )
                .outerjoin(Document, Document.research_id == ResearchHistory.id)
                .group_by(ResearchHistory.id)
                .order_by(ResearchHistory.created_at.desc())
                .limit(limit)
                .offset(offset)
                .all()
            )

            logger.debug(f"All research count: {len(results)}")

            # Convert to list of dicts
            history = []
            for research in results:
                item = {
                    "id": research.id,
                    "title": research.title,
                    "query": research.query,
                    "mode": research.mode,
                    "status": research.status,
                    "created_at": research.created_at,
                    "completed_at": research.completed_at,
                    "duration_seconds": research.duration_seconds,
                    "document_count": research.document_count,
                }

                item["metadata"] = filter_research_metadata(
                    research.research_meta
                )
                if research.chat_session_id is not None:
                    item["metadata"]["chat_session_id"] = (
                        research.chat_session_id
                    )

                # Recalculate duration if null but both timestamps exist
                if (
                    item["duration_seconds"] is None
                    and item["created_at"]
                    and item["completed_at"]
                ):
                    try:
                        from dateutil import parser  # type: ignore[import-untyped]

                        start_time = parser.parse(item["created_at"])
                        end_time = parser.parse(item["completed_at"])
                        item["duration_seconds"] = int(
                            (end_time - start_time).total_seconds()
                        )
                    except Exception:
                        logger.warning("Error recalculating duration")
                        logger.debug("Duration error details", exc_info=True)

                history.append(item)

        # Format response to match what client expects
        response_data = {
            "status": "success",
            "items": history,  # Use 'items' key as expected by client
        }

        # CORS headers are handled by SecurityHeaders middleware
        return jsonify(response_data)
    except Exception:
        logger.exception("Error getting history")
        return jsonify(
            {
                "status": "error",
                "items": [],
                "message": "Failed to retrieve history",
            }
        ), 500


@history_bp.route("/status/<string:research_id>")
@limiter.exempt
@login_required
def get_research_status(research_id):
    username = session["username"]

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

        if not research:
            return _research_not_found(research_id)

        # Extract attributes while session is active
        # to avoid DetachedInstanceError after the with block exits
        result = {
            "id": research.id,
            "query": research.query,
            "mode": research.mode,
            "status": research.status,
            "created_at": research.created_at,
            "completed_at": research.completed_at,
            "progress_log": research.progress_log,
            "report_path": research.report_path,
        }

    # Add progress information from active research (atomic snapshot)
    snapshot = get_active_research_snapshot(research_id)
    if snapshot is not None:
        result["progress"] = snapshot["progress"]
        result["log"] = snapshot["log"]
    elif result.get("status") == ResearchStatus.COMPLETED:
        result["progress"] = 100
        try:
            result["log"] = json.loads(result.get("progress_log", "[]"))
        except Exception:
            logger.warning(
                "Error parsing progress_log for research {}", research_id
            )
            result["log"] = []
    else:
        result["progress"] = 0
        try:
            result["log"] = json.loads(result.get("progress_log", "[]"))
        except Exception:
            logger.warning(
                "Error parsing progress_log for research {}", research_id
            )
            result["log"] = []

    return jsonify(result)


@history_bp.route("/details/<string:research_id>")
@login_required
def get_research_details(research_id):
    """Get detailed progress log for a specific research"""

    logger.debug(f"Details route accessed for research_id: {research_id}")

    username = session["username"]

    try:
        with get_user_db_session(username) as db_session:
            research = (
                db_session.query(ResearchHistory)
                .filter_by(id=research_id)
                .first()
            )
            logger.debug(f"Research found: {research.id if research else None}")

            if not research:
                logger.error(f"Research not found for id: {research_id}")
                return _research_not_found(research_id)

            # Extract all needed attributes while session is active
            # to avoid DetachedInstanceError after the with block exits
            research_data = {
                "query": research.query,
                "mode": research.mode,
                "status": research.status,
                "created_at": research.created_at,
                "completed_at": research.completed_at,
            }
    except Exception:
        logger.exception("Database error")
        return jsonify(
            {
                "status": "error",
                "message": "An internal database error occurred.",
            }
        ), 500

    # Get logs from the dedicated log database
    logs = get_logs_for_research(research_id)

    # Get strategy information
    strategy_name = get_research_strategy(research_id, username=username)

    # Get an atomic snapshot of active research state
    snapshot = get_active_research_snapshot(research_id)

    # If this is an active research, merge with any in-memory logs
    if snapshot is not None:
        # Use the logs from memory temporarily until they're saved to the database
        memory_logs = snapshot["log"]

        # Filter out logs that are already in the database by timestamp
        db_timestamps = {log["time"] for log in logs}
        unique_memory_logs = [
            log for log in memory_logs if log["time"] not in db_timestamps
        ]

        # Add unique memory logs to our return list
        logs.extend(unique_memory_logs)

        # Sort logs by timestamp
        logs.sort(key=lambda x: x["time"])

    progress = (
        snapshot["progress"]
        if snapshot is not None
        else (100 if research_data["status"] == ResearchStatus.COMPLETED else 0)
    )

    return jsonify(
        {
            "research_id": research_id,
            "query": research_data["query"],
            "mode": research_data["mode"],
            "status": research_data["status"],
            "strategy": strategy_name,
            "progress": progress,
            "created_at": research_data["created_at"],
            "completed_at": research_data["completed_at"],
            "log": logs,
        }
    )


@history_bp.route("/report/<string:research_id>")
@login_required
def get_report(research_id):
    from ..auth.decorators import current_user

    username = current_user()

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

        if not research:
            return _research_not_found(research_id, message="Report not found")

        try:
            # research.report_content holds the answer-only string;
            # the legacy display shape is reconstructed on demand by
            # appending Sources (from research_resources) and Metrics
            # (from research_meta).
            from ..services.report_assembly_service import (
                assemble_full_report,
            )

            content = assemble_full_report(research, db_session)
            # Only None means "research not found" — the existence check
            # above already returns 404 for that. An empty-but-found row
            # (no body, no sources, no metrics) returns "" and is valid.
            if content is None:
                return _research_not_found(
                    research_id, message="Report content not found"
                )

            stored_metadata = research.research_meta or {}

            # Create an enhanced metadata dictionary with database fields
            enhanced_metadata = {
                "query": research.query,
                "mode": research.mode,
                "created_at": research.created_at,
                "completed_at": research.completed_at,
                "duration": research.duration_seconds,
            }

            # Merge with stored metadata
            enhanced_metadata.update(stored_metadata)

            return jsonify(
                {
                    "status": "success",
                    "content": content,
                    "query": research.query,
                    "mode": research.mode,
                    "created_at": research.created_at,
                    "completed_at": research.completed_at,
                    "metadata": enhanced_metadata,
                }
            )
        except Exception:
            logger.exception(
                "Failed to retrieve report for research {}", research_id
            )
            return jsonify(
                {"status": "error", "message": "Failed to retrieve report"}
            ), 500


@history_bp.route("/markdown/<string:research_id>")
@login_required
def get_markdown(research_id):
    """Get markdown export for a specific research"""
    from ..auth.decorators import current_user

    username = current_user()

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

        if not research:
            return _research_not_found(research_id, message="Report not found")

        try:
            from ..services.report_assembly_service import (
                assemble_full_report,
            )

            content = assemble_full_report(research, db_session)
            if content is None:
                return _research_not_found(
                    research_id, message="Report content not found"
                )

            return jsonify({"status": "success", "content": content})
        except Exception:
            logger.exception(
                "Failed to retrieve markdown report for research {}",
                research_id,
            )
            return jsonify(
                {"status": "error", "message": "Failed to retrieve report"}
            ), 500


@history_bp.route("/logs/<string:research_id>")
@login_required
def get_research_logs(research_id):
    """Get logs for a specific research ID.

    Accepts ``?limit=N`` to bound the response size; the default matches
    the frontend's ``MAX_LOG_ENTRIES`` DOM cap. Clamped to
    ``[1, HISTORY_LOGS_HARD_CAP]`` so a client cannot force an unbounded
    load (a long langgraph run can persist thousands of rows; pre-cap
    the route allocated ~150 MB transient on the server and Firefox
    parsed a ~50 MB JSON response).
    """
    username = session["username"]

    limit = request.args.get(
        "limit", default=HISTORY_LOGS_DEFAULT_LIMIT, type=int
    )
    limit = max(1, min(limit, HISTORY_LOGS_HARD_CAP))

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

        if not research:
            return _research_not_found(research_id)

    logs = get_logs_for_research(research_id, limit=limit)

    # Defensive backfill for any row missing the three frontend-required
    # fields. `get_logs_for_research` always sets these from ResearchLog
    # columns, but the defensive layer is covered by
    # test_logs_with_missing_fields_get_defaults (extra keys must be
    # preserved, missing keys must take a default). In-place mutation is
    # safe — the formatter returned a fresh list of fresh dicts.
    for log in logs:
        log.setdefault("time", "")
        log.setdefault("message", "No message")
        log.setdefault("type", "info")

    return jsonify({"status": "success", "logs": logs})


@history_bp.route("/log_count/<string:research_id>")
@login_required
def get_log_count(research_id):
    """Get the total number of logs for a specific research ID"""
    # Get the total number of logs for this research ID
    total_logs = get_total_logs_for_research(research_id)

    return jsonify({"status": "success", "total_logs": total_logs})
