"""Discordance bias direction diagnosis taxonomy + rule-based diagnosis.

When designs disagree, this module produces a ranked list of bias hypotheses.
"""
from __future__ import annotations

from typing import Any

from .grid import build_grid, direction_of

BIAS_TAXONOMY: dict[str, str] = {
    "confounding_by_indication": (
        "Treatment assignment in observational studies is driven by physician "
        "judgment of patient risk; differs systematically from RCT randomization."
    ),
    "healthy_user_bias": (
        "Patients who take/adhere to a drug differ systematically (lifestyle, "
        "adherence to other preventive care) from non-users."
    ),
    "survivor_bias": (
        "Long-term observational cohorts condition on survival; effects may be "
        "attenuated or inflated depending on differential mortality."
    ),
    "detection_bias": (
        "Increased clinical contact for some drug users leads to higher "
        "ascertainment of an outcome (e.g. pancreatitis on DPP-4i)."
    ),
    "protopathic_bias": (
        "Drug is prescribed for an early symptom of the outcome being studied "
        "(reverse-causation in observational data)."
    ),
    "canalization_compensation": (
        "Lifelong genetic variation triggers developmental compensation, so "
        "MR effect is smaller than pharmacological RCT effect."
    ),
    "pleiotropy": (
        "MR instrument acts on the outcome through pathways other than the "
        "intended drug target; biases MR effect away from null."
    ),
    "dose_translation": (
        "Ex vivo / animal model uses concentrations or exposures not achievable "
        "in humans; effect size does not translate."
    ),
    "external_validity": (
        "RCT enrolled a narrow population (e.g. CV-risk-enriched T2DM); effect "
        "may not generalize to broader real-world users."
    ),
    "off_target": (
        "Pharmacologic agent has biologic effects beyond the intended molecular "
        "target; MR using the canonical pQTL may miss those effects."
    ),
}


def _has(grid: dict, design: str) -> bool:
    return design in grid and len(grid[design]) > 0


def _design_direction(grid: dict, design: str) -> int:
    if not _has(grid, design):
        return 0
    # Sample-size-weighted majority among present effects of that design.
    # Ties → fall back to the direction of the largest-n study.
    counts = {-1: 0.0, 0: 0.0, 1: 0.0}
    rows = grid[design]
    for r in rows:
        w = float(r.get("sample_size") or 1)
        counts[direction_of(r)] += w
    # If non-null bucket beats null bucket, pick the non-null one for the rule
    # engine (preserves the spec example where SGLT2i RCT CV death is "benefit"
    # even though one trial CI crosses 1).
    best = max(counts, key=lambda k: counts[k])
    if best == 0 and (counts[-1] + counts[1]) >= counts[0]:
        return -1 if counts[-1] >= counts[1] else 1
    return best


def diagnose_discordance(pair_effects: list[dict[str, Any]]) -> list[dict[str, Any]]:
    """Return a ranked list of {bias_type, score, rationale}.

    Score is a 0-3 hint, higher = stronger suspicion. Rules below encode the
    classic patterns from causal-epi triangulation literature.
    """
    grid = build_grid(pair_effects)
    rct = _design_direction(grid, "RCT")
    obs = _design_direction(grid, "observational")
    mr = _design_direction(grid, "target-MR")
    exv = _design_direction(grid, "ex vivo")
    wsj = _design_direction(grid, "within-subject")

    hits: list[dict[str, Any]] = []

    def add(bias: str, score: int, rationale: str) -> None:
        hits.append({
            "bias_type": bias,
            "score": score,
            "rationale": rationale,
            "definition": BIAS_TAXONOMY.get(bias, ""),
        })

    # Rule 1: observational stronger benefit than RCT → confounding / healthy-user
    if _has(grid, "RCT") and _has(grid, "observational"):
        if rct == 0 and obs == -1:
            add(
                "confounding_by_indication",
                3,
                "Observational shows benefit (-) but RCT null — typical pattern for "
                "indication-driven prescribing creating apparent benefit.",
            )
            add(
                "healthy_user_bias",
                2,
                "Adherence-correlated lifestyle confounding can inflate observational benefit.",
            )
        if rct == -1 and obs == -1 and _has(grid, "target-MR") and mr == 0:
            add(
                "off_target",
                2,
                "RCT + observational both show benefit but MR null — drug effect may "
                "act via off-target pathways the pQTL instrument does not capture.",
            )

    # Rule 2: observational shows risk, RCT null → detection bias
    if _has(grid, "RCT") and _has(grid, "observational"):
        if rct == 0 and obs == 1:
            add(
                "detection_bias",
                3,
                "Observational signal of harm but RCT null — increased clinical "
                "ascertainment in drug users a leading explanation.",
            )
            add(
                "protopathic_bias",
                2,
                "Drug may be prescribed for prodromal symptoms of the outcome.",
            )

    # Rule 3: RCT + but MR null → canalization or off-target
    if _has(grid, "RCT") and _has(grid, "target-MR"):
        if rct != 0 and mr == 0:
            add(
                "canalization_compensation",
                2,
                "Lifelong genetic exposure may trigger compensatory adaptation that "
                "blunts MR effect relative to short-term pharmacological perturbation.",
            )
            add(
                "off_target",
                2,
                "MR via canonical pQTL captures only on-target effects; drug may "
                "act through additional off-target pathways.",
            )

    # Rule 4: MR + but RCT null → external validity / underpowered RCT
    if _has(grid, "RCT") and _has(grid, "target-MR"):
        if mr != 0 and rct == 0:
            add(
                "external_validity",
                2,
                "MR signal not replicated in RCT — RCT population/follow-up may "
                "differ from the lifelong-exposure setting MR captures.",
            )

    # Rule 5: ex vivo + but RCT null → dose translation
    if _has(grid, "ex vivo") and _has(grid, "RCT"):
        if exv != 0 and rct == 0:
            add(
                "dose_translation",
                3,
                "Ex vivo / animal effect not seen in human RCT — concentrations or "
                "exposures may not translate to clinical pharmacokinetics.",
            )

    # Rule 6: within-subject discordant with population RCT → external validity
    if _has(grid, "within-subject") and _has(grid, "RCT"):
        if wsj != rct and wsj != 0:
            add(
                "external_validity",
                1,
                "Within-subject (N-of-1 / crossover) signal differs from population "
                "RCT — individual responsiveness heterogeneity.",
            )

    # Rule 7: observational benefit, MR null → confounding (no genetic support)
    if _has(grid, "observational") and _has(grid, "target-MR"):
        if obs == -1 and mr == 0:
            add(
                "healthy_user_bias",
                2,
                "Observational benefit not corroborated by MR — non-causal "
                "association via healthy-user / lifestyle confounding.",
            )
            add(
                "survivor_bias",
                1,
                "Differential mortality in long-term cohorts can spuriously favor "
                "the drug arm.",
            )

    # Deduplicate by (bias_type) keeping highest score, then sort.
    by_bias: dict[str, dict[str, Any]] = {}
    for h in hits:
        prev = by_bias.get(h["bias_type"])
        if prev is None or h["score"] > prev["score"]:
            by_bias[h["bias_type"]] = h
    ranked = sorted(by_bias.values(), key=lambda x: -x["score"])
    return ranked
