"""Post-research quality score and upgrade nudge.

Computes a quality score based on 5 core sources and builds
a nudge message describing what the user missed and how to fix it.

Fix text comes from ``lib.prescriptions`` (the single remediation
vocabulary shared with the doctor command, KTD 7); only the trigger
logic and the message framing live here.
"""

from typing import List

from . import prescriptions


# The 5 core sources
CORE_SOURCES = ["hn", "polymarket", "x", "youtube", "reddit"]

# Labels for display
SOURCE_LABELS = {
    "hn": "Hacker News",
    "polymarket": "Polymarket",
    "x": "X/Twitter",
    "youtube": "YouTube",
    "reddit": "Reddit",
}


def _is_x_active(config: dict, research_results: dict) -> bool:
    """Check if X source is active (has credentials AND didn't error)."""
    has_creds = _has_x_credentials(config)
    if not has_creds:
        return False
    # If X errored this run, it's configured but broken
    if research_results.get("x_error"):
        return False
    return True


def _has_x_credentials(config: dict) -> bool:
    """Return True when any X/Twitter source credential is configured."""
    return bool(
        config.get("AUTH_TOKEN")
        or config.get("XAI_API_KEY")
        or config.get("XQUIK_API_KEY")
    )


def _has_ytdlp() -> bool:
    """Return True when the local/free YouTube lane is available."""
    try:
        from . import youtube_yt
        return bool(youtube_yt.is_ytdlp_installed())
    except Exception:
        return False


def _youtube_returned_data(research_results: dict) -> bool:
    """Return True when YouTube produced usable items through any provider."""
    videos = int(research_results.get("youtube_videos_count") or 0)
    transcripts = int(research_results.get("youtube_transcripts_count") or 0)
    return videos > 0 or transcripts > 0


def _is_youtube_active(config: dict, research_results: dict, *, has_ytdlp: bool) -> bool:
    """Check if YouTube source is active (yt-dlp installed)."""
    if not has_ytdlp:
        return False
    if research_results.get("youtube_error"):
        return False
    return True


# Below this transcript-fetch ratio, YouTube is considered "degraded" rather
# than active. Picked at 50% so a single legitimate caption-disabled video in a
# multi-video result does not trip the nudge, but a stale-yt-dlp run that fails
# every transcript does. Tunable via DEGRADED_TRANSCRIPT_THRESHOLD env var if
# operators need to adjust without code changes.
DEFAULT_DEGRADED_TRANSCRIPT_THRESHOLD = 0.5


def _is_youtube_degraded(research_results: dict, threshold: float) -> bool:
    """YouTube is degraded when videos were returned but the transcript-fetch
    ratio is below threshold. The canonical cause is a stale yt-dlp binary -
    YouTube's caption format changes frequently and old binaries silently fail
    every transcript while the search itself still succeeds.

    Captions-disabled videos are subtracted from the denominator: an uploader
    who turned off captions can never produce a transcript, so counting that
    video toward "fetch failures" produces false positives. A single
    captions-disabled video in a small result set was tripping the nudge.

    When actual fetch outcomes are available, they take precedence over the
    post-pruning ratio: the report counts only see items that survived
    freshness/relevance pruning, so a run where every transcript fetch
    succeeded but the fetched videos were later pruned looks identical to a
    stale-binary run (#531). Zero failures across attempted fetches proves
    the binary works - don't flag.
    """
    videos = int(research_results.get("youtube_videos_count") or 0)
    transcripts = int(research_results.get("youtube_transcripts_count") or 0)
    captions_disabled = int(research_results.get("youtube_captions_disabled_count") or 0)
    if videos <= 0:
        return False
    fetch_attempts = int(research_results.get("youtube_transcript_fetch_attempts") or 0)
    fetch_failures = int(research_results.get("youtube_transcript_fetch_failures") or 0)
    if fetch_attempts > 0 and fetch_failures == 0:
        return False
    eligible = videos - captions_disabled
    if eligible <= 0:
        # Every returned video had captions disabled - upstream content fact,
        # not a yt-dlp problem. Don't flag.
        return False
    return (transcripts / eligible) < threshold


def _is_instagram_silent_failure(config: dict, research_results: dict) -> bool:
    """Instagram is silently failing when SC is configured but the source
    returned zero items. The canonical cause is SC's v2 reels endpoint
    500'ing on multi-token queries (it wraps Google Search and is documented
    to be flaky there). Pre-fix the user got no signal at all - no Instagram
    section in the brief, no error in the footer, just unexplained absence.
    """
    if not config.get("SCRAPECREATORS_API_KEY"):
        return False  # not configured — not a silent failure
    # Honor EXCLUDE_SOURCES: a user who set EXCLUDE_SOURCES=instagram
    # intentionally turned the source off, so a zero-item count is
    # expected, not a silent failure. Mirror the canonical parsing
    # pattern from pipeline.available_sources().
    excluded = {
        s.strip().lower()
        for s in (config.get("EXCLUDE_SOURCES") or "").split(",")
        if s.strip()
    }
    # Symmetric case: INCLUDE_SOURCES is an opt-in allowlist. If it is
    # non-empty and does not name instagram, the source was intentionally
    # filtered out, so a zero-item count is expected — not a silent failure.
    included = {
        s.strip().lower()
        for s in (config.get("INCLUDE_SOURCES") or "").split(",")
        if s.strip()
    }
    if "instagram" in excluded or (included and "instagram" not in included):
        return False
    count = research_results.get("instagram_items_count")
    if count is None:
        return False  # source not run this invocation
    return int(count) == 0


def compute_quality_score(config: dict, research_results: dict) -> dict:
    """Compute research quality score based on 5 core sources.

    Args:
        config: Configuration dict from env.get_config()
        research_results: Dict with keys like x_error, youtube_error,
            reddit_error reflecting what happened this run. Optional keys
            ``youtube_videos_count`` and ``youtube_transcripts_count`` enable
            degraded-YouTube detection (transcript-fetch ratio below threshold,
            or fallback/provider data returned without local yt-dlp).
            Optional key ``instagram_items_count`` enables silent-failure
            detection for the bonus Instagram source.

    Returns:
        {
            "score_pct": 40-100,
            "core_active": ["hn", "polymarket", ...],
            "core_missing": ["x", "youtube"],
            "core_errored": [],          # configured but errored at top level
            "core_degraded": [],         # configured and returned items but quality below threshold
            "bonus_errored": [],         # bonus sources (Instagram, etc.) configured but silent
            "nudge_text": "..." or None if all sources healthy
        }
    """
    core_active: List[str] = []
    core_missing: List[str] = []
    core_errored: List[str] = []
    core_degraded: List[str] = []
    bonus_errored: List[str] = []

    # HN, Polymarket, and Reddit are always active
    core_active.append("hn")
    core_active.append("polymarket")
    core_active.append("reddit")

    # X
    has_x_creds = _has_x_credentials(config)
    if _is_x_active(config, research_results):
        core_active.append("x")
    else:
        core_missing.append("x")
        if has_x_creds and research_results.get("x_error"):
            core_errored.append("x")

    # YouTube
    has_ytdlp = _has_ytdlp()
    yt_active = _is_youtube_active(config, research_results, has_ytdlp=has_ytdlp)
    youtube_returned_data = _youtube_returned_data(research_results)
    if yt_active:
        core_active.append("youtube")
        # Active means yt-dlp is installed and search did not error at the top
        # level. But search-success + transcript-failure is the canonical
        # stale-binary failure mode that the footer used to hide. Flag as
        # degraded so the user gets an actionable nudge to update the binary.
        threshold = float(config.get("DEGRADED_TRANSCRIPT_THRESHOLD") or DEFAULT_DEGRADED_TRANSCRIPT_THRESHOLD)
        if _is_youtube_degraded(research_results, threshold):
            core_degraded.append("youtube")
    elif youtube_returned_data and not research_results.get("youtube_error"):
        # YouTube produced data through a fallback/provider lane even though the
        # local free yt-dlp lane is unavailable. Count the source as present,
        # but surface it as degraded so users do not see the contradictory
        # "Missing: YouTube" ending after a report with YouTube evidence.
        # has_ytdlp is provably False here: yt_active is False and youtube_error
        # is excluded by this guard, leaving unavailable yt-dlp as the cause.
        core_active.append("youtube")
        core_degraded.append("youtube")
    else:
        core_missing.append("youtube")
        # Check if configured but errored (yt-dlp installed but failed this run)
        if has_ytdlp and research_results.get("youtube_error"):
            core_errored.append("youtube")

    # Bonus sources (Instagram, etc.): SC-key holders expect content from
    # these but until now the pipeline fell silent on configured-but-zero.
    if _is_instagram_silent_failure(config, research_results):
        bonus_errored.append("instagram")

    score_pct = int(len(core_active) / 5 * 100)

    has_sc = bool(config.get("SCRAPECREATORS_API_KEY"))
    active_sources = research_results.get("active_sources") or []
    nudge_text = _build_nudge_text(
        core_missing,
        core_errored,
        core_degraded,
        research_results,
        has_sc=has_sc,
        active_sources=active_sources,
        bonus_errored=bonus_errored,
        has_ytdlp=has_ytdlp,
    ) if (core_missing or core_degraded or bonus_errored) else None

    return {
        "score_pct": score_pct,
        "core_active": core_active,
        "core_missing": core_missing,
        "core_errored": core_errored,
        "core_degraded": core_degraded,
        "bonus_errored": bonus_errored,
        "nudge_text": nudge_text,
    }


def _build_nudge_text(
    core_missing: List[str],
    core_errored: List[str],
    core_degraded: List[str] = None,
    research_results: dict = None,
    has_sc: bool = False,
    active_sources: list = None,
    bonus_errored: List[str] = None,
    has_ytdlp: bool = False,
) -> str:
    """Build human-readable nudge text describing what was missed or degraded.

    Prioritizes free suggestions. Optionally mentions bonus sources
    (TikTok, Instagram, Threads, Pinterest) if ScrapeCreators key is configured.
    """
    lines: List[str] = []
    core_degraded = core_degraded or []
    bonus_errored = bonus_errored or []
    research_results = research_results or {}

    # Describe what was missed
    missed_parts: List[str] = []
    for src in core_missing:
        label = SOURCE_LABELS[src]
        if src in core_errored:
            missed_parts.append(f"{label} (errored this run)")
        else:
            missed_parts.append(label)

    active_count = 5 - len(core_missing)
    lines.append(f"Research quality: {active_count}/5 core sources.")
    if missed_parts:
        lines.append(f"Missing: {', '.join(missed_parts)}.")
    if core_degraded:
        degraded_labels = ", ".join(SOURCE_LABELS[s] for s in core_degraded)
        lines.append(f"Degraded: {degraded_labels}.")
    if bonus_errored:
        bonus_labels = ", ".join(s.capitalize() for s in bonus_errored)
        lines.append(f"Bonus source silent: {bonus_labels}.")
    lines.append("")

    # Free suggestions
    free_suggestions: List[str] = []

    if "x" in core_missing:
        if "x" in core_errored:
            x_fix = prescriptions.get("x", "cookies_expired")
            free_suggestions.append(f"X/Twitter errored - {x_fix.fix_nl}.")
        else:
            x_fix = prescriptions.get("x", "cookies_missing")
            # Pick by state: telling a user who already installed grok to
            # install it again is the stale-shim reading the health layer
            # exists to avoid. Mirrors _probe_grok's three-way split.
            from . import grok_x as _grok_x
            grok_key = (
                "grok_not_authenticated"
                if _grok_x.binary_path() and not _grok_x.has_stored_auth()
                else "grok_cli_missing"
            )
            grok_fix = prescriptions.get("x", grok_key)
            # Deliberately not described as free: grok needs no X credential,
            # but it does need an installed, signed-in grok CLI drawing on a
            # Grok plan. This block is headed "Free suggestions", so the
            # precondition has to be stated inline rather than inherited.
            free_suggestions.append(
                "X/Twitter: real-time posts with likes and reposts - the fastest "
                "signal for breaking topics. Easiest path if you have a Grok "
                f"account: {grok_fix.fix_nl} (no X credential at all). "
                f"Otherwise: {x_fix.fix_nl}."
            )

    if "youtube" in core_missing:
        if "youtube" in core_errored:
            yt_fix = prescriptions.get("youtube", "ytdlp_stale")
            free_suggestions.append(
                f"YouTube errored - update yt-dlp: {yt_fix.fix_cli}"
            )
        else:
            yt_fix = prescriptions.get("youtube", "ytdlp_missing")
            free_suggestions.append(
                "YouTube: video transcripts with key moments - often the deepest "
                f"explanations on any topic. Install yt-dlp: {yt_fix.fix_cli} (free)"
            )

    if "youtube" in core_degraded:
        videos = int(research_results.get("youtube_videos_count") or 0)
        transcripts = int(research_results.get("youtube_transcripts_count") or 0)
        captions_disabled = int(research_results.get("youtube_captions_disabled_count") or 0)
        if not has_ytdlp and _youtube_returned_data(research_results):
            install = prescriptions.get("youtube", "ytdlp_missing")
            # Tolerant lookup: alt_cli makes no arity promise, so an entry
            # gaining/losing a platform alternate must degrade the wording,
            # never crash the nudge path.
            scoop_install = install.alt_cli[0] if len(install.alt_cli) > 0 else install.fix_cli
            pip_install = install.alt_cli[1] if len(install.alt_cli) > 1 else scoop_install
            free_suggestions.append(
                f"YouTube returned {videos} videos and {transcripts} transcripts "
                "through a fallback/provider path, but local yt-dlp is not "
                "installed. Install yt-dlp to enable the free local YouTube lane "
                f"and reduce reliance on fallback providers: {install.fix_cli} "
                f"(macOS), {scoop_install} (Windows), or {pip_install}."
            )
        else:
            captions_note = ""
            if captions_disabled > 0:
                captions_note = (
                    f" ({captions_disabled} of those had captions disabled by the "
                    "uploader, which is a separate cause and not fixable on your end)"
                )
            update = prescriptions.get("youtube", "ytdlp_stale")
            # Same tolerant lookup as the install branch above.
            scoop_update = update.alt_cli[0] if len(update.alt_cli) > 0 else update.fix_cli
            pip_update = update.alt_cli[1] if len(update.alt_cli) > 1 else scoop_update
            free_suggestions.append(
                f"YouTube returned {videos} videos but only {transcripts} transcripts "
                f"captured{captions_note}. The most common remaining cause is a stale "
                "yt-dlp binary - YouTube's caption format changes frequently and old "
                "binaries silently fail every transcript. Update via your package "
                f"manager: {scoop_update} (Windows), {update.fix_cli} (macOS), "
                f"or {pip_update}."
            )

    if "instagram" in bonus_errored:
        free_suggestions.append(
            "Instagram returned 0 reels despite SC being configured. SC's "
            "v2 reels endpoint wraps Google Search and 500's frequently on "
            "multi-token queries. The skill now retries with hashtag-form "
            "automatically; if zero items still appear, the topic may have "
            "no reel coverage on Instagram. Try a single-word topic like "
            "the most distinctive noun in your query."
        )

    # Mention bonus opt-in sources when SC key is present
    if has_sc:
        bonus_hints = []
        if "threads" not in (active_sources or []):
            bonus_hints.append("Threads")
        if "pinterest" not in (active_sources or []):
            bonus_hints.append("Pinterest")
        if bonus_hints:
            free_suggestions.append(
                f"Your SC key also powers {', '.join(bonus_hints)} and YouTube comments. "
                "Add them to INCLUDE_SOURCES in your .env to enable."
            )

    if free_suggestions:
        lines.append("Free fixes:")
        for s in free_suggestions:
            lines.append(f"  - {s}")
        lines.append("")

    # Bonus sources mention (non-blocking)
    if not has_sc:
        lines.append(
            "Bonus: TikTok and Instagram are available with a free "
            "ScrapeCreators key at scrapecreators.com (no affiliation)."
        )
    else:
        lines.append("last30days has no affiliation with any API provider.")

    return "\n".join(lines)
