"""
Research History Indexer Service

Enables semantic search over research history by:
- Converting ResearchHistory reports into indexable Documents
- Linking documents to the Research History collection
- Triggering RAG indexing via LibraryRAGService
"""

import hashlib
import uuid
from datetime import datetime, UTC
from typing import Any, Dict, Optional

from loguru import logger
from sqlalchemy.exc import IntegrityError

from ....constants import ResearchStatus
from ....database.library_init import ensure_research_history_collection
from ....database.models.library import (
    Document,
    DocumentStatus,
    SourceType,
)
from ....database.models.research import ResearchHistory
from ....database.session_context import get_user_db_session
from ...utils import ensure_in_collection


class ResearchHistoryIndexer:
    """
    Service for indexing research history into a searchable collection.

    Converts research reports into Documents that can be indexed for
    semantic search using the existing RAG infrastructure.
    """

    # Source type names used in the database
    SOURCE_TYPE_REPORT = "research_report"
    COLLECTION_TYPE = "research_history"

    # convert_all_research pages through candidates this many rows at a time.
    # report_content is a large Text column, so loading every completed
    # report body at once can exhaust memory on a big history (#4560). This
    # caps how many bodies are resident at any moment.
    CONVERT_BATCH_SIZE = 50

    def __init__(self, username: str, db_password: Optional[str] = None):
        """
        Initialize the indexer for a user.

        Args:
            username: Username for database access
            db_password: Optional database password for encrypted DB access
        """
        self.username = username
        self.db_password = db_password

    def get_or_create_collection(self) -> str:
        """
        Get or create the Research History collection for this user.

        Returns:
            UUID of the Research History collection
        """
        return ensure_research_history_collection(
            self.username, self.db_password
        )

    def index_research(
        self,
        research_id: str,
        collection_id: Optional[str] = None,
    ) -> Dict[str, Any]:
        """
        Convert a single research entry into a Document and add it to a
        collection.  Idempotent — safe to call multiple times.

        Args:
            research_id: UUID of the research to index
            collection_id: Target collection UUID (defaults to Research History)

        Returns:
            Dict with status and document count
        """
        if collection_id is None:
            collection_id = self.get_or_create_collection()

        with get_user_db_session(self.username, self.db_password) as session:
            research = (
                session.query(ResearchHistory)
                .filter(ResearchHistory.id == research_id)
                .first()
            )

            if not research:
                return {"status": "error", "error": "Research not found"}

            if research.status != ResearchStatus.COMPLETED:
                return {
                    "status": "error",
                    "error": "Research is not yet completed",
                }

            if not research.report_content:
                return {
                    "status": "error",
                    "error": "Research has no report content",
                }

            try:
                report_doc = self._create_document_from_report(
                    research, collection_id, session
                )
                if report_doc is None:
                    return {
                        "status": "error",
                        "error": "SourceType 'research_report' not found. "
                        "Run library initialization.",
                    }
                logger.info(
                    f"Created/found document for research: {research_id[:8]}"
                )
            except Exception:
                logger.exception("Error creating report document")
                return {
                    "status": "error",
                    "error": "Failed to create report document",
                }

            try:
                session.commit()
            except IntegrityError:
                session.rollback()
                logger.info(
                    f"DocumentCollection already exists for research "
                    f"{research_id[:8]} (concurrent insert)"
                )

            return {
                "status": "success",
                "research_id": research_id,
                "collection_id": collection_id,
                "documents_added": 1,
            }

    def convert_all_research(self, force: bool = False) -> Dict[str, Any]:
        """
        Convert all completed research entries into Documents (without RAG indexing).

        Single-session implementation that calls private helpers directly to
        avoid the nested-session issues that arise on SQLite when
        index_research opens its own session inside a loop.

        Args:
            force: If True, process all entries even if already converted.
                   If False (default), skip entries that already have a report
                   Document.

        Returns:
            Dict with:
                - converted: Number of research entries successfully converted
                - skipped:   Number of entries skipped (already converted)
                - failed:    Number of entries that raised an exception
                - collection_id: UUID of the Research History collection

        Note: the "already converted" filter checks ``Document.research_id``.
        When two research entries produce identical ``report_content``,
        ``_create_document_from_report`` reuses the existing Document (its
        ``research_id`` stays as the first creator's), so the duplicate
        research keeps appearing in the candidate set on every call. Calling
        this from a hot path (request handler, polling loop) will repeatedly
        re-attempt those entries. Call only from explicit user actions
        (e.g. the manual ``/convert-all`` endpoint or ``auto_convert_research``
        on research completion).

        Only report Documents are created; source documents are not indexed.
        """
        collection_id = self.get_or_create_collection()

        with get_user_db_session(self.username, self.db_password) as session:
            # Resolve the report SourceType — required to create report Documents
            report_type = (
                session.query(SourceType)
                .filter_by(name=self.SOURCE_TYPE_REPORT)
                .first()
            )
            if report_type is None:
                logger.warning(
                    f"SourceType '{self.SOURCE_TYPE_REPORT}' not found. "
                    "Run library initialization to seed source types before "
                    "converting research history."
                )
                return {
                    "converted": 0,
                    "skipped": 0,
                    "failed": 0,
                    "collection_id": collection_id,
                }

            # Build subquery of research IDs that already have a report Document
            already_converted_subquery = (
                session.query(Document.research_id)
                .filter(Document.source_type_id == report_type.id)
                .filter(Document.research_id.isnot(None))
                .distinct()
                .subquery()
            )

            # Count total eligible research entries (before filtering)
            total_eligible = (
                session.query(ResearchHistory)
                .filter(ResearchHistory.status == ResearchStatus.COMPLETED)
                .filter(ResearchHistory.report_content.isnot(None))
                .filter(ResearchHistory.report_content != "")
                .count()
            )

            # Fetch candidate IDs only — optionally excluding already-converted
            # entries. We must NOT materialize every full ResearchHistory row
            # here: report_content is a large Text column, and loading every
            # completed report body at once can exhaust memory on a big history
            # (#4560). IDs are tiny, so the full candidate list is cheap; we
            # then load the full rows one bounded batch at a time below.
            id_query = (
                session.query(ResearchHistory.id)
                .filter(ResearchHistory.status == ResearchStatus.COMPLETED)
                .filter(ResearchHistory.report_content.isnot(None))
                .filter(ResearchHistory.report_content != "")
                .order_by(ResearchHistory.created_at.desc())
            )
            if not force:
                id_query = id_query.filter(
                    ResearchHistory.id.notin_(
                        already_converted_subquery.select()
                    )
                )

            research_ids = [row.id for row in id_query.all()]

            converted = 0
            skipped = total_eligible - len(research_ids) if not force else 0
            failed = 0

            for start in range(0, len(research_ids), self.CONVERT_BATCH_SIZE):
                batch_ids = research_ids[
                    start : start + self.CONVERT_BATCH_SIZE
                ]
                # Load one batch of full rows (report bodies) at a time so peak
                # memory stays bounded regardless of total history size.
                batch = (
                    session.query(ResearchHistory)
                    .filter(ResearchHistory.id.in_(batch_ids))
                    .order_by(ResearchHistory.created_at.desc())
                    .all()
                )

                for research in batch:
                    try:
                        # Create (or reuse) report Document
                        report_doc = self._create_document_from_report(
                            research,
                            collection_id,
                            session,
                            report_type_id=report_type.id,
                        )
                        if report_doc is None:
                            # SourceType missing inside helper (already warned)
                            failed += 1
                            continue

                        # Commit each entry individually so a rollback on
                        # failure only loses the failing entry, not the batch.
                        session.commit()
                        converted += 1

                    except Exception:
                        logger.exception(
                            f"Error converting research {research.id}"
                        )
                        session.rollback()
                        failed += 1

        logger.info(
            f"convert_all_research complete — converted={converted}, "
            f"skipped={skipped}, failed={failed}"
        )
        return {
            "converted": converted,
            "skipped": skipped,
            "failed": failed,
            "collection_id": collection_id,
        }

    def _create_document_from_report(
        self,
        research: ResearchHistory,
        collection_id: str,
        session,
        report_type_id: Optional[str] = None,
    ) -> Optional[Document]:
        """
        Create a Document from a research report.

        Args:
            research: ResearchHistory entry
            collection_id: Target collection UUID
            session: Database session
            report_type_id: Pre-resolved SourceType ID (avoids N+1 queries
                when called in a loop from convert_all_research)

        Returns:
            Created Document or None if skipped
        """
        # Resolve report SourceType (cached ID avoids per-entry query)
        if report_type_id is None:
            report_type = (
                session.query(SourceType)
                .filter_by(name=self.SOURCE_TYPE_REPORT)
                .first()
            )
            if report_type is None:
                logger.warning(
                    f"SourceType '{self.SOURCE_TYPE_REPORT}' not found for research "
                    f"{research.id}. Cannot create document — run library initialization "
                    "to seed source types."
                )
                return None
            report_type_id = report_type.id
        existing_doc = (
            session.query(Document)
            .filter(Document.research_id == research.id)
            .filter(Document.source_type_id == report_type_id)
            .first()
        )

        if existing_doc:
            # Ensure it's in the collection
            ensure_in_collection(session, existing_doc.id, collection_id)
            return existing_doc

        # Create document or reuse existing one with same content hash
        # (document_hash has a unique constraint, so identical content
        # must share a Document row — research_id points to the first creator)
        content = research.report_content
        doc_hash = hashlib.sha256(content.encode("utf-8")).hexdigest()

        document = (
            session.query(Document)
            .filter(Document.document_hash == doc_hash)
            .first()
        )

        if document is None:
            doc_id = str(uuid.uuid4())
            document = Document(
                id=doc_id,
                source_type_id=report_type_id,
                research_id=research.id,
                document_hash=doc_hash,
                file_size=len(content.encode("utf-8")),
                file_type="markdown",
                mime_type="text/markdown",
                title=research.title
                or (research.query[:100] if research.query else "Untitled"),
                text_content=content,
                status=DocumentStatus.COMPLETED,
                processed_at=datetime.now(UTC),
                character_count=len(content),
                word_count=len(content.split()),
            )
            session.add(document)
            session.flush()

        ensure_in_collection(session, document.id, collection_id)
        return document


def auto_convert_research(
    username: str, research_id: str, db_password: str | None = None
):
    """Auto-convert a completed research entry to a document in the History collection.

    Safe to call from any context — exceptions are caught and logged.
    """
    try:
        indexer = ResearchHistoryIndexer(username, db_password=db_password)
        result = indexer.index_research(research_id)
        logger.info(
            f"Auto-converted research {research_id} for user {username}: "
            f"{result.get('status')}"
        )
    except Exception:
        logger.warning(
            f"Failed to auto-convert research {research_id} for user {username}"
        )
