"""
Practice: the launch checklist as code, the way Lab 17's script 03 does it. Each item is a check
that returns its evidence (a file, or the line it matched), not a tick, so a person can read it.
You'll write a keyword check and the stricter CI-file check, count what 16 systems have, and
measure how far two raters agree on which items would have caught 12 real failures.

Solution.
"""
import re


def find_line(files: dict[str, str], pattern: str) -> str | None:
    """files: {path: text}, searched in the order given. Return "path: the whole line" for the first
    line that matches the pattern (case-insensitive, ^ and $ at line ends), stripped, or None."""
    rx = re.compile(pattern, re.I | re.M)
    for path, text in files.items():
        m = rx.search(text)
        if m:
            start = text.rfind("\n", 0, m.start()) + 1
            end = text.find("\n", m.end())
            line = text[start:end if end != -1 else len(text)].strip()
            return f"{path}: {line}"
    return None


def ci_runs_on_change(workflows: dict[str, str], folder: str) -> str | None:
    """workflows: {path: YAML text} under .github/workflows. Evidence that a system is re-tested on
    every change: a workflow with a `pull_request:` or `push:` trigger line that names the system's
    folder as "labs/<folder>". Return "<path>: runs on every change" for the first, or None."""
    for path, text in workflows.items():
        if re.search(r"^\s*(pull_request|push)\s*:", text, re.M) and f"labs/{folder}" in text:
            return f"{path}: runs on every change"
    return None


def evidence_counts(systems: dict[str, dict[str, str | None]]) -> dict[str, int]:
    """systems: {system: {item: evidence or None}}. How many systems have evidence for each item?"""
    items = next(iter(systems.values())).keys()
    return {k: sum(bool(r[k]) for r in systems.values()) for k in items}


def best_system(systems: dict[str, dict[str, str | None]], items: list[str]) -> tuple[str, int]:
    """The system with evidence for the most of these items, and how many (the first, on a tie)."""
    best = max(systems, key=lambda s: sum(bool(systems[s][k]) for k in items))
    return best, sum(bool(systems[best][k]) for k in items)


def agreement(a: list[bool], b: list[bool]) -> dict[str, float]:
    """Two raters' yes/no labels for the same judgments. Return
      raw     the share where they agree
      chance  the agreement two raters with these yes rates would reach by luck:
              pa*pb + (1-pa)*(1-pb), where pa, pb are each rater's share of yes
      kappa   (raw - chance) / (1 - chance); 1.0 if chance is 1"""
    n = len(a)
    raw = sum(x == y for x, y in zip(a, b)) / n
    pa, pb = sum(a) / n, sum(b) / n
    chance = pa * pb + (1 - pa) * (1 - pb)
    kappa = 1.0 if chance == 1 else (raw - chance) / (1 - chance)
    return {"raw": raw, "chance": chance, "kappa": kappa}


def cases_with(cases: list[dict], items: list[str]) -> int:
    """cases: [{"caught_by": [item ids]}]. How many cases list at least one of these items?"""
    return sum(bool(set(c["caught_by"]) & set(items)) for c in cases)
