from __future__ import annotations

from datetime import datetime

from sqlalchemy import Select, case, func, select
from sqlalchemy.ext.asyncio import AsyncSession

from app.db.models import (
    CrawlRun,
    Publication,
    PublicationPdfJob,
    RunStatus,
    ScholarProfile,
    ScholarPublication,
)
from app.services.publications.modes import MODE_LATEST, MODE_UNREAD
from app.services.publications.types import PublicationListItem, UnreadPublicationItem


def _normalized_citation_count(value: object) -> int:
    try:
        return int(value or 0)  # type: ignore[call-overload]  # intentionally accepts any object
    except (TypeError, ValueError):
        return 0


def _pdf_status_sort_rank():
    return case(
        (Publication.pdf_url.is_not(None), 4),
        (PublicationPdfJob.status == "resolved", 4),
        (PublicationPdfJob.status == "running", 3),
        (PublicationPdfJob.status == "queued", 2),
        (PublicationPdfJob.status == "failed", 0),
        else_=1,
    )


def _sort_column(sort_by: str):
    sort_columns = {
        "first_seen": ScholarPublication.created_at,
        "title": Publication.title_raw,
        "year": Publication.year,
        "citations": Publication.citation_count,
        "scholar": ScholarProfile.display_name,
        "pdf_status": _pdf_status_sort_rank(),
    }
    return sort_columns.get(sort_by, ScholarPublication.created_at)


async def get_latest_run_id_for_user(
    db_session: AsyncSession,
    *,
    user_id: int,
) -> int | None:
    # We include RUNNING and RESOLVING statuses so that the "New" tab shows
    # results in real-time as they are discovered.
    result = await db_session.execute(
        select(func.max(CrawlRun.id)).where(
            CrawlRun.user_id == user_id,
            CrawlRun.status.in_(
                [
                    RunStatus.RUNNING,
                    RunStatus.RESOLVING,
                    RunStatus.SUCCESS,
                    RunStatus.PARTIAL_FAILURE,
                ]
            ),
        )
    )
    latest_run_id = result.scalar_one_or_none()
    return int(latest_run_id) if latest_run_id is not None else None


def publications_query(
    *,
    user_id: int,
    mode: str,
    latest_run_id: int | None,
    scholar_profile_id: int | None,
    favorite_only: bool,
    limit: int,
    offset: int = 0,
    search: str | None = None,
    sort_by: str = "first_seen",
    sort_dir: str = "desc",
    snapshot_before: datetime | None = None,
) -> Select[tuple]:
    scholar_label = ScholarProfile.display_name
    stmt = (
        select(
            Publication.id,
            ScholarProfile.id,
            scholar_label,
            ScholarProfile.scholar_id,
            Publication.title_raw,
            Publication.year,
            Publication.citation_count,
            Publication.venue_text,
            Publication.pub_url,
            Publication.pdf_url,
            ScholarPublication.is_read,
            ScholarPublication.is_favorite,
            ScholarPublication.first_seen_run_id,
            ScholarPublication.created_at,
        )
        .join(ScholarPublication, ScholarPublication.publication_id == Publication.id)
        .join(ScholarProfile, ScholarProfile.id == ScholarPublication.scholar_profile_id)
        .outerjoin(PublicationPdfJob, PublicationPdfJob.publication_id == Publication.id)
        .where(ScholarProfile.user_id == user_id)
    )
    if search:
        safe_search = search.replace("%", r"\%").replace("_", r"\_")
        pat = f"%{safe_search}%"
        stmt = stmt.where(
            Publication.title_raw.ilike(pat)
            | ScholarProfile.display_name.ilike(pat)
            | Publication.venue_text.ilike(pat)
        )
    if scholar_profile_id is not None:
        stmt = stmt.where(ScholarProfile.id == scholar_profile_id)
    if favorite_only:
        stmt = stmt.where(ScholarPublication.is_favorite.is_(True))
    if mode == MODE_UNREAD:
        stmt = stmt.where(ScholarPublication.is_read.is_(False))
    if mode == MODE_LATEST:
        if latest_run_id is None:
            return stmt.where(False)
        stmt = stmt.where(ScholarPublication.first_seen_run_id == latest_run_id)
    if snapshot_before is not None:
        stmt = stmt.where(ScholarPublication.created_at <= snapshot_before)

    sort_col = _sort_column(sort_by)
    order = sort_col.desc() if sort_dir == "desc" else sort_col.asc()
    stmt = stmt.order_by(order, Publication.id.desc())

    if limit is not None:
        stmt = stmt.offset(max(int(offset), 0)).limit(limit)

    return stmt


def publication_query_for_user(
    *,
    user_id: int,
    scholar_profile_id: int,
    publication_id: int,
) -> Select[tuple]:
    return (
        select(
            Publication.id,
            ScholarProfile.id,
            ScholarProfile.display_name,
            ScholarProfile.scholar_id,
            Publication.title_raw,
            Publication.year,
            Publication.citation_count,
            Publication.venue_text,
            Publication.pub_url,
            Publication.pdf_url,
            ScholarPublication.is_read,
            ScholarPublication.is_favorite,
            ScholarPublication.first_seen_run_id,
            ScholarPublication.created_at,
        )
        .join(ScholarPublication, ScholarPublication.publication_id == Publication.id)
        .join(ScholarProfile, ScholarProfile.id == ScholarPublication.scholar_profile_id)
        .where(
            ScholarProfile.user_id == user_id,
            ScholarProfile.id == scholar_profile_id,
            Publication.id == publication_id,
        )
        .limit(1)
    )


async def get_publication_item_for_user(
    db_session: AsyncSession,
    *,
    user_id: int,
    scholar_profile_id: int,
    publication_id: int,
) -> PublicationListItem | None:
    latest_run_id = await get_latest_run_id_for_user(db_session, user_id=user_id)
    result = await db_session.execute(
        publication_query_for_user(
            user_id=user_id,
            scholar_profile_id=scholar_profile_id,
            publication_id=publication_id,
        )
    )
    row = result.one_or_none()
    if row is None:
        return None
    return publication_list_item_from_row(row, latest_run_id=latest_run_id)


def publication_list_item_from_row(
    row: tuple,
    *,
    latest_run_id: int | None,
) -> PublicationListItem:
    (
        publication_id,
        scholar_profile_id,
        display_name,
        scholar_id,
        title_raw,
        year,
        citation_count,
        venue_text,
        pub_url,
        pdf_url,
        is_read,
        is_favorite,
        first_seen_run_id,
        created_at,
    ) = row
    return PublicationListItem(
        publication_id=int(publication_id),
        scholar_profile_id=int(scholar_profile_id),
        scholar_label=(display_name or scholar_id),
        title=title_raw,
        year=year,
        citation_count=_normalized_citation_count(citation_count),
        venue_text=venue_text,
        pub_url=pub_url,
        pdf_url=pdf_url,
        is_read=bool(is_read),
        is_favorite=bool(is_favorite),
        first_seen_at=created_at,
        is_new_in_latest_run=(latest_run_id is not None and int(first_seen_run_id or 0) == latest_run_id),
    )


def unread_item_from_row(row: tuple) -> UnreadPublicationItem:
    (
        publication_id,
        scholar_profile_id,
        display_name,
        scholar_id,
        title_raw,
        year,
        citation_count,
        venue_text,
        pub_url,
        pdf_url,
        _is_read,
        _is_favorite,
        _first_seen_run_id,
        _created_at,
    ) = row
    return UnreadPublicationItem(
        publication_id=int(publication_id),
        scholar_profile_id=int(scholar_profile_id),
        scholar_label=(display_name or scholar_id),
        title=title_raw,
        year=year,
        citation_count=_normalized_citation_count(citation_count),
        venue_text=venue_text,
        pub_url=pub_url,
        pdf_url=pdf_url,
    )
