from __future__ import annotations

from typing import Any

from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.db.models import Publication, ScholarProfile, ScholarPublication
from app.services.doi.normalize import normalize_doi
from app.services.ingestion.application import build_publication_url, normalize_title
from app.services.portability.normalize import (
    _normalize_citation_count,
    _normalize_optional_text,
    _normalize_optional_year,
    _resolve_fingerprint,
)
from app.services.portability.types import ImportedPublicationInput


async def _find_publication_by_cluster(
    db_session: AsyncSession,
    *,
    cluster_id: str,
) -> Publication | None:
    result = await db_session.execute(select(Publication).where(Publication.cluster_id == cluster_id))
    return result.scalar_one_or_none()


async def _find_publication_by_fingerprint(
    db_session: AsyncSession,
    *,
    fingerprint_sha256: str,
) -> Publication | None:
    result = await db_session.execute(select(Publication).where(Publication.fingerprint_sha256 == fingerprint_sha256))
    return result.scalar_one_or_none()


async def _find_linked_publication_by_title(
    db_session: AsyncSession,
    *,
    scholar_profile_id: int,
    title: str,
) -> Publication | None:
    normalized_title = normalize_title(title)
    result = await db_session.execute(
        select(Publication)
        .join(
            ScholarPublication,
            ScholarPublication.publication_id == Publication.id,
        )
        .where(
            ScholarPublication.scholar_profile_id == scholar_profile_id,
            Publication.title_normalized == normalized_title,
        )
        .order_by(Publication.id.asc())
        .limit(1)
    )
    return result.scalar_one_or_none()


def _apply_imported_publication_values(
    *,
    publication: Publication,
    title: str,
    year: int | None,
    citation_count: int,
    author_text: str | None,
    venue_text: str | None,
    pub_url: str | None,
    pdf_url: str | None,
    cluster_id: str | None,
) -> bool:
    updated = False
    if cluster_id and publication.cluster_id != cluster_id:
        publication.cluster_id = cluster_id
        updated = True
    if publication.title_raw != title:
        publication.title_raw = title
        publication.title_normalized = normalize_title(title)
        updated = True
    if publication.year != year:
        publication.year = year
        updated = True
    if int(publication.citation_count or 0) != citation_count:
        publication.citation_count = citation_count
        updated = True
    if publication.author_text != author_text:
        publication.author_text = author_text
        updated = True
    if publication.venue_text != venue_text:
        publication.venue_text = venue_text
        updated = True
    if pub_url and publication.pub_url != pub_url:
        publication.pub_url = pub_url
        updated = True
    if pdf_url and publication.pdf_url != pdf_url:
        publication.pdf_url = pdf_url
        updated = True
    return updated


def _new_publication(
    *,
    cluster_id: str | None,
    fingerprint_sha256: str,
    title: str,
    year: int | None,
    citation_count: int,
    author_text: str | None,
    venue_text: str | None,
    pub_url: str | None,
    pdf_url: str | None,
) -> Publication:
    return Publication(
        cluster_id=cluster_id,
        fingerprint_sha256=fingerprint_sha256,
        title_raw=title,
        title_normalized=normalize_title(title),
        year=year,
        citation_count=citation_count,
        author_text=author_text,
        venue_text=venue_text,
        pub_url=pub_url,
        pdf_url=pdf_url,
    )


async def _resolve_publication_for_import(
    db_session: AsyncSession,
    *,
    scholar_profile_id: int,
    title: str,
    cluster_id: str | None,
    fingerprint_sha256: str,
    cluster_cache: dict[str, Publication | None],
    fingerprint_cache: dict[str, Publication | None],
) -> Publication | None:
    if cluster_id:
        if cluster_id not in cluster_cache:
            cluster_cache[cluster_id] = await _find_publication_by_cluster(
                db_session,
                cluster_id=cluster_id,
            )
        if cluster_cache[cluster_id] is not None:
            return cluster_cache[cluster_id]
    if fingerprint_sha256 not in fingerprint_cache:
        fingerprint_cache[fingerprint_sha256] = await _find_publication_by_fingerprint(
            db_session,
            fingerprint_sha256=fingerprint_sha256,
        )
    if fingerprint_cache[fingerprint_sha256] is not None:
        return fingerprint_cache[fingerprint_sha256]
    return await _find_linked_publication_by_title(
        db_session,
        scholar_profile_id=scholar_profile_id,
        title=title,
    )


async def _upsert_scholar_publication_link(
    db_session: AsyncSession,
    *,
    scholar_profile_id: int,
    publication_id: int,
    is_read: bool,
) -> tuple[bool, bool]:
    result = await db_session.execute(
        select(ScholarPublication).where(
            ScholarPublication.scholar_profile_id == scholar_profile_id,
            ScholarPublication.publication_id == publication_id,
        )
    )
    link = result.scalar_one_or_none()
    if link is None:
        db_session.add(
            ScholarPublication(
                scholar_profile_id=scholar_profile_id,
                publication_id=publication_id,
                is_read=bool(is_read),
            )
        )
        return True, False
    if bool(link.is_read) == bool(is_read):
        return False, False
    link.is_read = bool(is_read)
    return False, True


def _initialize_import_counters(counters: dict[str, int]) -> None:
    counters.update(
        {
            "publications_created": 0,
            "publications_updated": 0,
            "links_created": 0,
            "links_updated": 0,
        }
    )


def _build_imported_publication_input(
    *,
    item: dict[str, Any],
    scholar_map: dict[str, ScholarProfile],
) -> ImportedPublicationInput | None:
    scholar_id = _normalize_optional_text(item.get("scholar_id"))
    title = _normalize_optional_text(item.get("title"))
    if not scholar_id or not title:
        return None

    profile = scholar_map.get(scholar_id)
    if profile is None:
        return None

    year = _normalize_optional_year(item.get("year"))
    author_text = _normalize_optional_text(item.get("author_text"))
    venue_text = _normalize_optional_text(item.get("venue_text"))
    return ImportedPublicationInput(
        profile=profile,
        title=title,
        year=year,
        citation_count=_normalize_citation_count(item.get("citation_count")),
        author_text=author_text,
        venue_text=venue_text,
        cluster_id=_normalize_optional_text(item.get("cluster_id")),
        pub_url=build_publication_url(_normalize_optional_text(item.get("pub_url"))),
        doi=normalize_doi(_normalize_optional_text(item.get("doi"))),
        pdf_url=build_publication_url(_normalize_optional_text(item.get("pdf_url"))),
        fingerprint=_resolve_fingerprint(
            title=title,
            year=year,
            author_text=author_text,
            venue_text=venue_text,
            provided_fingerprint=item.get("fingerprint_sha256"),
        ),
        is_read=bool(item.get("is_read", False)),
    )


def _update_link_counters(
    *,
    counters: dict[str, int],
    link_created: bool,
    link_updated: bool,
) -> None:
    if link_created:
        counters["links_created"] += 1
    if link_updated:
        counters["links_updated"] += 1


def _cache_resolved_publication(
    *,
    publication: Publication,
    cluster_id: str | None,
    fingerprint_sha256: str,
    cluster_cache: dict[str, Publication | None],
    fingerprint_cache: dict[str, Publication | None],
) -> None:
    if cluster_id:
        cluster_cache[cluster_id] = publication
    fingerprint_cache[fingerprint_sha256] = publication


async def _create_import_publication(
    db_session: AsyncSession,
    *,
    payload: ImportedPublicationInput,
) -> Publication:
    publication = _new_publication(
        cluster_id=payload.cluster_id,
        fingerprint_sha256=payload.fingerprint,
        title=payload.title,
        year=payload.year,
        citation_count=payload.citation_count,
        author_text=payload.author_text,
        venue_text=payload.venue_text,
        pub_url=payload.pub_url,
        pdf_url=payload.pdf_url,
    )
    db_session.add(publication)
    await db_session.flush()
    return publication


def _update_import_publication(
    *,
    publication: Publication,
    payload: ImportedPublicationInput,
) -> bool:
    return _apply_imported_publication_values(
        publication=publication,
        title=payload.title,
        year=payload.year,
        citation_count=payload.citation_count,
        author_text=payload.author_text,
        venue_text=payload.venue_text,
        pub_url=payload.pub_url,
        pdf_url=payload.pdf_url,
        cluster_id=payload.cluster_id,
    )


async def _upsert_publication_entity(
    db_session: AsyncSession,
    *,
    payload: ImportedPublicationInput,
    cluster_cache: dict[str, Publication | None],
    fingerprint_cache: dict[str, Publication | None],
) -> tuple[Publication, bool, bool]:
    publication = await _resolve_publication_for_import(
        db_session,
        scholar_profile_id=int(payload.profile.id),
        title=payload.title,
        cluster_id=payload.cluster_id,
        fingerprint_sha256=payload.fingerprint,
        cluster_cache=cluster_cache,
        fingerprint_cache=fingerprint_cache,
    )
    created = False
    updated = False
    if publication is None:
        publication = await _create_import_publication(db_session, payload=payload)
        created = True
    else:
        updated = _update_import_publication(publication=publication, payload=payload)

    _cache_resolved_publication(
        publication=publication,
        cluster_id=payload.cluster_id,
        fingerprint_sha256=payload.fingerprint,
        cluster_cache=cluster_cache,
        fingerprint_cache=fingerprint_cache,
    )
    return publication, created, updated


async def _upsert_imported_publication(
    db_session: AsyncSession,
    *,
    payload: ImportedPublicationInput,
    cluster_cache: dict[str, Publication | None],
    fingerprint_cache: dict[str, Publication | None],
    counters: dict[str, int],
) -> None:
    publication, created, updated = await _upsert_publication_entity(
        db_session,
        payload=payload,
        cluster_cache=cluster_cache,
        fingerprint_cache=fingerprint_cache,
    )
    if created:
        counters["publications_created"] += 1
    if updated:
        counters["publications_updated"] += 1

    link_created, link_updated = await _upsert_scholar_publication_link(
        db_session,
        scholar_profile_id=int(payload.profile.id),
        publication_id=int(publication.id),
        is_read=payload.is_read,
    )
    _update_link_counters(
        counters=counters,
        link_created=link_created,
        link_updated=link_updated,
    )
