"""Pure-function quality scoring helpers.

These are stateless and called by both the build phase
(`db.build_db()` populating the `quality` column) and the runtime
filter (when scoring an institution-only fallback).

Kept in a small module of their own so the build phase doesn't have
to import the read-only DB accessor just to get at the score
thresholds.
"""

from __future__ import annotations

import unicodedata
from typing import Optional

from ..constants import (
    CONFERENCE_QUALITY_DEFAULT,
    DOAJ_QUALITY_LISTED,
    INSTITUTION_HINDEX_HIGH,
    INSTITUTION_HINDEX_TOP,
    INSTITUTION_QUALITY_DEFAULT,
    INSTITUTION_QUALITY_HIGH,
    INSTITUTION_QUALITY_TOP,
    JOURNAL_HINDEX_ACCEPTABLE,
    JOURNAL_HINDEX_ELITE,
    JOURNAL_HINDEX_GOOD,
    JOURNAL_HINDEX_STRONG,
    JOURNAL_HINDEX_VERY_GOOD,
    JOURNAL_QUALITY_ACCEPTABLE,
    JOURNAL_QUALITY_DEFAULT,
    JOURNAL_QUALITY_ELITE,
    JOURNAL_QUALITY_GOOD,
    JOURNAL_QUALITY_PREDATORY,
    JOURNAL_QUALITY_STRONG,
    REPOSITORY_QUALITY_DEFAULT,
    JOURNAL_QUALITY_VERY_GOOD,
)


def normalize_name(name: str) -> str:
    """NFKC + lowercase + strip — used for consistent name matching.

    Mirrors the previous `_normalize` helper that lived in both
    `journal_reference_db.py` and `journal_data_manager.py`. Single
    home now so the build phase and the runtime accessor agree.
    """
    return unicodedata.normalize("NFKC", name).lower().strip()


def derive_quality_score(
    *,
    h_index: Optional[int] = None,
    quartile: Optional[str] = None,
    is_in_doaj: bool = False,
    is_predatory: bool = False,
    source_type: Optional[str] = None,
) -> Optional[int]:
    """Derive a 1–10 quality score from bibliometric data.

    Inputs (in order of preference):
    - ``quartile``: SJR-style Q1/Q2/Q3/Q4. Strongest single signal — this
      is what librarians and reviewers use when evaluating journals, so
      we honour it directly and only use h-index as a tiebreaker.
    - ``h_index``: used standalone when no quartile is available.
    - ``is_in_doaj``: weakest fall-through signal.

    There is deliberately no DOAJ Seal input anymore: DOAJ retired the
    Seal in April 2025 and removed it from their metadata, so the old
    ``has_doaj_seal`` tier (score 8) could never be earned again and
    only ever fired on stale pre-2025 data.

    H-index thresholds calibrated from real data:
    - Nature h-index: 1,442
    - PLOS ONE h-index: 467
    - Only 3 journals globally have h-index > 1,000

    Note: h-index has field-dependent bias (mathematics journals have
    naturally lower h-index than biomedical journals). These thresholds
    are general-purpose; field-specific normalization is not yet
    implemented.

    Returns:
        Score 1–10, or `None` if there is not enough signal.
    """
    if is_predatory and not is_in_doaj:
        return JOURNAL_QUALITY_PREDATORY  # Auto-remove threshold

    # Preprint repositories (arXiv, bioRxiv, SSRN, PsyArXiv, ...) are
    # not peer-reviewed. Their h-index reflects citation accumulation
    # across the thousands of papers they aggregate — not venue rigor.
    # Cap them at the ACCEPTABLE tier so Q-tier semantics remain
    # meaningful. The filter's Tier 3.5 institution-salvage path can
    # lift this via author affiliations when appropriate.
    #
    # NOTE: only ``"repository"`` is capped here. ``"conference"`` gets
    # its own flat score below via the ``source_type == "conference"``
    # branch. Other OpenAlex source types — ``"book series"`` (Springer
    # Lecture Notes etc.) and ``"ebook platform"`` (Elsevier
    # ScienceDirect, Springer Link) — CAN be peer-reviewed, so we
    # intentionally let h-index scoring apply for them. Reviewed in
    # the PR #3081 audit; not a gap.
    if source_type == "repository":
        return REPOSITORY_QUALITY_DEFAULT

    # Quartile takes precedence — it is the canonical librarian signal.
    # We still let a high h-index bump a Q1 to "elite" so Nature stays
    # distinguishable from a typical Q1. DOAJ listing applies
    # orthogonally via max() so it cannot be silently discarded.
    if quartile:
        q = quartile.upper().strip()
        q_score: Optional[int] = None
        if q == "Q1":
            if h_index and h_index > JOURNAL_HINDEX_ELITE:
                q_score = JOURNAL_QUALITY_ELITE
            else:
                q_score = JOURNAL_QUALITY_STRONG
        elif q == "Q2":
            q_score = JOURNAL_QUALITY_VERY_GOOD
        elif q == "Q3":
            q_score = JOURNAL_QUALITY_GOOD
        elif q == "Q4":
            q_score = JOURNAL_QUALITY_ACCEPTABLE

        if q_score is not None:
            if is_in_doaj:
                return max(q_score, DOAJ_QUALITY_LISTED)
            return q_score

    # h_index=0 means newly indexed, not meaningful. Negative values
    # would be a data error — treat as no signal rather than returning
    # DEFAULT which is ambiguous.
    if h_index and h_index > 0:
        if h_index > JOURNAL_HINDEX_ELITE:
            h_score = JOURNAL_QUALITY_ELITE  # Nature/Science/NEJM
        elif h_index > JOURNAL_HINDEX_STRONG:
            h_score = JOURNAL_QUALITY_STRONG
        elif h_index > JOURNAL_HINDEX_VERY_GOOD:
            h_score = JOURNAL_QUALITY_VERY_GOOD
        elif h_index > JOURNAL_HINDEX_GOOD:
            h_score = JOURNAL_QUALITY_GOOD
        elif h_index > JOURNAL_HINDEX_ACCEPTABLE:
            h_score = JOURNAL_QUALITY_ACCEPTABLE
        else:
            h_score = JOURNAL_QUALITY_DEFAULT

        # DOAJ listing is an orthogonal quality signal (verified open
        # access). Use max() so the signals reinforce rather than
        # conflict.
        if is_in_doaj:
            return max(h_score, DOAJ_QUALITY_LISTED)
        return h_score

    if is_in_doaj:
        return DOAJ_QUALITY_LISTED

    if source_type == "conference":
        return CONFERENCE_QUALITY_DEFAULT  # Neutral — in CS, top conferences are Q1-equivalent

    return None  # Insufficient data


def institution_score_from_h_index(h_index: Optional[int]) -> Optional[int]:
    """Derive a quality score from an institution's h-index.

    Capped at 6 — institution alone never beats a real venue match.
    Used by the Tier 3.5 affiliation salvage path in the filter.
    """
    if h_index is None:
        return None
    if h_index > INSTITUTION_HINDEX_TOP:
        return INSTITUTION_QUALITY_TOP  # Top-tier research universities
    if h_index > INSTITUTION_HINDEX_HIGH:
        return INSTITUTION_QUALITY_HIGH
    return INSTITUTION_QUALITY_DEFAULT
