"""DataSource base class for academic data downloads.

Each external dataset (OpenAlex sources, DOAJ journals, Stop Predatory
Journals, JabRef abbreviations, …) is a subclass of `DataSource` that
declares its metadata as class attributes and implements the `fetch()`
method. The `data_sources` package's `ALL_SOURCES` registry then drives
the bulk download flow, the dashboard banner status endpoint, and the
lazy-load auto-download path in `JournalDataManager`.
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from pathlib import Path
from typing import Any

from loguru import logger


class DataSource(ABC):
    """Abstract base class for one downloadable academic data source.

    Subclasses MUST override the metadata class attributes (key, name,
    url, license, license_url, description, filename, count_label) and
    implement `fetch()`. They MAY override `auto_download`, `required`,
    and `approx_size_mb`.
    """

    # ── metadata (subclasses MUST override) ────────────────────────
    key: str = ""
    name: str = ""
    url: str = ""
    # Direct link to the actual dataset/dump that fetch() downloads —
    # the bulk file or manifest, not the project homepage. Surfaced
    # on the dashboard so users can inspect the upstream artifact.
    dataset_url: str = ""
    license: str = ""
    license_url: str = ""
    description: str = ""
    filename: str = ""
    count_label: str = ""

    # ── policy (sensible defaults) ─────────────────────────────────
    # If True, lazy-load callers will fetch the file on demand. Set
    # only for small files (<1 MB) so first-use latency stays low.
    auto_download: bool = False
    # If True, a fetch() failure inside the bulk download loop is
    # fatal — the loop returns early with an error message. Other
    # sources are best-effort (logged and skipped).
    required: bool = False
    # Informational; surfaced to UIs that want to warn the user
    # about download size before they hit the button.
    approx_size_mb: float = 0.0

    # ── methods (subclasses override only fetch) ───────────────────

    @abstractmethod
    def fetch(self, data_dir: Path, progress_cb=None) -> int:
        """Download from upstream and write `self.filename` into data_dir.

        Subclasses are responsible for HTTP, parsing, and atomic write
        (download to a `.tmp` file then rename). Should raise on
        unrecoverable network/parse errors so the caller can decide
        whether to abort the bulk download or continue best-effort.

        Args:
            data_dir: target directory for the compact output file.
            progress_cb: optional `Callable[[int, int, str], None]`
                that chunk-processing sources call periodically as
                ``progress_cb(done, total, detail)``. Used by the
                dashboard to drive a live per-source progress bar.
                One-shot sources (no loops) can ignore this.

        Returns:
            Number of records fetched (used in success messages).
        """

    def is_present(self, data_dir: Path) -> bool:
        """True if the data file already exists in `data_dir`."""
        return (data_dir / self.filename).exists()

    def ensure(self, data_dir: Path) -> bool:
        """Ensure the data file is present, optionally fetching on demand.

        - If already present → return True (no work).
        - If missing AND `auto_download=True` → fetch and return whether
          the file is present after the fetch attempt.
        - If missing AND `auto_download=False` → return False without
          touching the network. Caller is expected to drive the bulk
          download flow (dashboard "Download Data" button) instead.

        Idempotent and safe to call repeatedly. Logs but does not
        re-raise exceptions from `fetch()`.
        """
        if self.is_present(data_dir):
            return True
        if not self.auto_download:
            return False
        logger.info(
            f"{self.name} data not found — fetching from upstream "
            f"(~{self.approx_size_mb:.1f} MB)..."
        )
        try:
            data_dir.mkdir(parents=True, exist_ok=True)
            self.fetch(data_dir)
        except Exception:
            logger.exception(f"Failed to fetch {self.name}")
            return False
        return self.is_present(data_dir)

    def status_dict(self, data_dir: Path) -> dict[str, Any]:
        """Return the dict shape consumed by the dashboard banner JS.

        Mirrors the entries the JS in `journal_quality.html` expects:
        `key`, `name`, `url`, `license`, `license_url`, `description`,
        `file`, `present`.
        """
        return {
            "key": self.key,
            "name": self.name,
            "url": self.url,
            "dataset_url": self.dataset_url,
            "license": self.license,
            "license_url": self.license_url,
            "description": self.description,
            "file": self.filename,
            "present": self.is_present(data_dir),
        }
