"""JabRef journal abbreviations data source.

Downloads ~14 small CSV files from the JabRef GitHub repo and merges
them into a single gzipped JSON mapping abbreviations → full names.
"""

from __future__ import annotations

import csv
import gzip
import io
import json
from pathlib import Path

from loguru import logger

from .base import DataSource

_JABREF_BASE = (
    "https://raw.githubusercontent.com/JabRef/abbrv.jabref.org/main/journals"
)
_JABREF_FILES = [
    "journal_abbreviations_entrez.csv",
    "journal_abbreviations_ubc.csv",
    "journal_abbreviations_lifescience.csv",
    "journal_abbreviations_mechanical.csv",
    "journal_abbreviations_mathematics.csv",
    "journal_abbreviations_medicus.csv",
    "journal_abbreviations_ams.csv",
    "journal_abbreviations_general.csv",
    "journal_abbreviations_acs.csv",
    "journal_abbreviations_geology_physics.csv",
    "journal_abbreviations_ieee.csv",
    "journal_abbreviations_meteorology.csv",
    "journal_abbreviations_astronomy.csv",
    "journal_abbreviations_sociology.csv",
]


class JabRefSource(DataSource):
    key = "jabref"  # gitleaks:allow
    name = "JabRef abbreviations"
    url = "https://github.com/JabRef/abbrv.jabref.org"
    dataset_url = (
        "https://github.com/JabRef/abbrv.jabref.org/tree/main/journals"
    )
    license = "CC0 1.0"
    license_url = "https://creativecommons.org/publicdomain/zero/1.0/"
    description = "~66K journal abbreviation → full name mappings"
    filename = "jabref_abbreviations.json.gz"
    count_label = "abbreviations"
    auto_download = True  # ~0.5 MB total across 14 files
    required = False
    approx_size_mb = 0.5

    def fetch(self, data_dir: Path, progress_cb=None) -> int:
        from ...security.safe_requests import (
            safe_get_with_retries as safe_get,
        )

        abbrev_to_full: dict[str, str] = {}

        for filename in _JABREF_FILES:
            url = f"{_JABREF_BASE}/{filename}"
            try:
                resp = safe_get(url, timeout=30, consume_body=True)
                resp.raise_for_status()
                reader = csv.reader(io.StringIO(resp.text))
                for row in reader:
                    if len(row) < 2:
                        continue
                    full_name = row[0].strip().strip('"')
                    abbreviation = row[1].strip().strip('"')
                    if full_name and abbreviation and full_name != abbreviation:
                        abbrev_lower = abbreviation.lower()
                        # Last-writer-wins across 14 source CSVs. Log the
                        # collision at debug level (one per file change,
                        # not per row) so operators can audit which
                        # source resolves a given abbreviation — not a
                        # warning, because collisions are expected.
                        if (
                            abbrev_lower in abbrev_to_full
                            and abbrev_to_full[abbrev_lower] != full_name
                        ):
                            logger.debug(
                                "jabref collision "
                                f"[{filename}] {abbrev_lower!r}: "
                                f"{abbrev_to_full[abbrev_lower]!r} → "
                                f"{full_name!r}"
                            )
                        abbrev_to_full[abbrev_lower] = full_name
                        # Also store without dots: "Phys Rev Lett" → same
                        no_dots = abbreviation.replace(".", "").strip().lower()
                        if no_dots != abbreviation.lower():
                            if (
                                no_dots in abbrev_to_full
                                and abbrev_to_full[no_dots] != full_name
                            ):
                                logger.debug(
                                    "jabref collision (no-dots) "
                                    f"[{filename}] {no_dots!r}: "
                                    f"{abbrev_to_full[no_dots]!r} → "
                                    f"{full_name!r}"
                                )
                            abbrev_to_full[no_dots] = full_name
            except Exception:
                # Preserve traceback — operators diagnosing partial
                # fetch failures need the exception type (timeout,
                # SSRF block, decode error, etc.), not just the
                # filename. The outer loop tolerates per-file
                # failures, so this is a non-fatal warning.
                logger.exception(f"Failed to fetch JabRef file: {filename}")
                continue

        # Sanity check: if every single file failed (e.g., GitHub raw
        # CDN unreachable), don't silently overwrite existing abbreviation
        # data with an empty mapping.
        _MIN_JABREF_ABBREVS = 100
        if len(abbrev_to_full) < _MIN_JABREF_ABBREVS:
            raise RuntimeError(
                f"JabRef: suspiciously few abbreviations "
                f"({len(abbrev_to_full)} < {_MIN_JABREF_ABBREVS}); "
                "refusing to overwrite existing data"
            )

        output = data_dir / self.filename
        tmp = data_dir / f"{self.filename}.tmp"
        with gzip.open(tmp, "wt", encoding="utf-8") as f:
            json.dump({"abbrev_to_full": abbrev_to_full}, f)
        tmp.rename(output)

        logger.info(
            f"JabRef: saved {len(abbrev_to_full)} abbreviation mappings"
        )
        return len(abbrev_to_full)
