"""Issue bookkeeping and the health score.

Each issue costs a penalty: its severity weight (high 15, medium 7, low 2, info 0) scaled by how
much of the data it touches (half weight at 0% affected, full weight from 10%). Leaks, train/test
overlap and conflicting labels always cost full weight. The overall score compounds the penalties:
100 x product of (1 - penalty/100), so a few problems lower it gently and many lower it a lot.
"""
from .jscompat import js_round

SEVERITY_WEIGHTS = {"high": 15, "medium": 7, "low": 2, "info": 0}
CATEGORY = {
    "missing_values": "Completeness", "disguised_missing": "Completeness", "missing_together": "Completeness",
    "target_missing": "Completeness",
    "numeric_text": "Validity", "mixed_types": "Validity", "invalid_dates": "Validity", "outliers": "Validity",
    "future_dates": "Validity",
    "duplicate_rows": "Uniqueness", "near_duplicate_rows": "Uniqueness", "conflicting_labels": "Uniqueness",
    "category_variants": "Consistency", "mixed_date_formats": "Consistency", "ambiguous_dates": "Consistency",
    "ragged_rows": "Consistency",
    "constant_column": "Model readiness", "near_constant": "Model readiness", "id_column": "Model readiness",
    "redundant_columns": "Model readiness", "leakage": "Model readiness", "order_leakage": "Model readiness",
    "class_imbalance": "Model readiness", "missing_informative": "Model readiness",
    "pii": "Privacy",
    "train_test_overlap": "Train vs test", "distribution_shift": "Train vs test",
    "unseen_categories": "Train vs test", "column_mismatch": "Train vs test",
}
CATEGORY_ORDER = ["Model readiness", "Train vs test", "Uniqueness", "Validity", "Consistency", "Completeness", "Privacy"]
SEVERITY_RANK = {"high": 0, "medium": 1, "low": 2, "info": 3}
FULL_WEIGHT = ("leakage", "train_test_overlap", "conflicting_labels", "order_leakage")


class IssueList(list):
    """List of issue dicts; add() fills in id, category and a clamped `affected` share."""

    def add(self, **o):
        o["id"] = "i" + str(len(self) + 1)
        o["category"] = CATEGORY.get(o["type"], "Other")
        a = o.get("affected") or 0
        if a != a:
            a = 0
        o["affected"] = max(0, min(1, a))
        o.setdefault("column", None)
        o["message"] = o.get("title", "")          # the prototype's field name, kept for callers
        self.append(o)
        return o


def _category_index(c):
    return CATEGORY_ORDER.index(c) if c in CATEGORY_ORDER else -1


def score_issues(issues):
    """Set each issue's penalty, sort issues (severity, penalty, category) and return the score."""
    penalties = {c: 0 for c in CATEGORY_ORDER}
    for issue in issues:
        f = 0.5 + 0.5 * min(1, 10 * issue["affected"])
        if issue["type"] in FULL_WEIGHT:
            f = 1
        issue["penalty"] = js_round(SEVERITY_WEIGHTS[issue["severity"]] * f * 10) / 10
        penalties[issue["category"]] = penalties.get(issue["category"], 0) + issue["penalty"]
    issues.sort(key=lambda i: (SEVERITY_RANK[i["severity"]], -i["penalty"], _category_index(i["category"])))
    s = 1
    for issue in issues:
        s = s * (1 - issue["penalty"] / 100)
    return {"overall": js_round(100 * s), "penalties": penalties, "order": list(CATEGORY_ORDER)}


def calculate_score(issues):
    """Overall 0-100 score for a list of issues (kept from the first prototype's API)."""
    return score_issues(issues)["overall"]
