"""Runs every rule, then scores the findings and sets the trust labels."""
import sys
from decimal import ROUND_HALF_UP, Decimal
from . import rules_ads, rules_bigquery, rules_definitions, rules_events, rules_privacy, rules_property, rules_traffic
from .common import AREAS, CORE, GRADES, INPUTS, POINTS, RULES, TRUST, get, kv, num
from .context import Finding

CHECKS = {rid: getattr(mod, rid.lower()) for mod in (rules_property, rules_traffic, rules_events, rules_ads,
                                                     rules_bigquery, rules_definitions, rules_privacy)
          for rid in RULES if hasattr(mod, rid.lower())}

def primary_missing(c, rid):
    """Primary inputs a rule needs before it may PASS (references/rules.md). A would-be PASS without
    them becomes NOT CHECKED; a WARN or FAIL stays and lists them as partial."""
    miss = []
    if rid == 'G05':
        if not get(c.snap, 'reportingIdentity'): miss.append('reporting identity settings')
        if c.cfg.get('user_id_expected') and c.rows('R12') is None: miss.append('R12 User-ID coverage')
    elif rid == 'G06' and c.uival('tag.internal_traffic_rules') is None: miss.append('intake tag.internal_traffic_rules')
    elif rid == 'G08':
        if c.rows('R03') is None: miss.append('R03 hostnames')
        if not c.cfg.get('expected_hostnames'): miss.append('config expected_hostnames')
    elif rid == 'G10' and len({d.lower() for d in c.cfg.get('owned_registrable_domains') or []}) >= 2 \
            and c.uival('tag.domains') is None:
        miss.append('intake tag.domains')
    elif rid == 'G13' and c.reports.get('R08') is None and c.rows('R09') is None: miss.append('R08 transaction IDs or R09 currency')
    elif rid == 'G23' and not c.uirows('consent.stream.'):
        eea = c.uival('consent.eea_share')
        if not (eea and num(kv(eea).get('traffic', eea)) == 0): miss.append('intake consent.stream rows')
    elif rid == 'G24':
        if c.reports.get('R15') is None: miss.append('R15 PII probe')
        if c.streams is None or any(get(s, 'dataRedaction') is None for s in c.web): miss.append('data redaction settings')
    return miss

def run_rules(c):
    out = []
    for rid in RULES:
        f = Finding(rid)
        try:
            CHECKS[rid](c, f)
        except Exception as e:  # a broken input must not hide the other rules
            print(f'rule {rid} error: {type(e).__name__}: {e}', file=sys.stderr)
            f.subs, f.partial, f.loss, f.risk = [], [], [], []  # a rule that errors leaves no banner or sub-finding
            f.nc(f'rule error ({type(e).__name__}); check the input shape')
        miss = primary_missing(c, rid) if f.status in POINTS else []
        if miss and f.status == 'PASS': f.nc('primary evidence missing: ' + ', '.join(miss))
        elif miss: f.partial.append('primary evidence missing: ' + ', '.join(miss))
        for x in INPUTS.get(rid, '').split() if f.status in POINTS else []:
            kind, _, key = x.rpartition(':')
            have = c.reports.get(key) is not None if not kind else bool(c.uirows(key)) if kind == 'ui' else get(c.snap, key) is not None
            if not have: f.partial.append(f'{key.rstrip(".")} not read' if kind != 'ui' else f'intake {key.rstrip(".")} not supplied')
        exc = (c.cfg.get('accepted_exceptions') or {}).get(rid)
        if exc and f.status == 'FAIL':
            f.subs = [('WARN', t + f' (exception cited: {exc})') if st == 'FAIL' else (st, t) for st, t in f.subs]
            f.loss, f.risk = [], []
        out.append(f)
    return out

def score(fs):
    by, areas = {f.id: f for f in fs}, {}
    for a, (name, w) in AREAS.items():
        pts = [POINTS[f.status] for f in fs if f.area == a and f.status in POINTS]
        areas[a] = (name, w, sum(pts) / len(pts) if pts else None)
    wsum = sum(w for _, w, v in areas.values() if v is not None)
    raw = sum(w * v for _, w, v in areas.values() if v is not None) / wsum * 100 if wsum else None
    sc = None if raw is None else int(Decimal(str(round(raw, 6))).quantize(Decimal('1'), rounding=ROUND_HALF_UP))
    appl = [f for f in fs if f.status != 'N/A']
    checked = [f for f in appl if f.status in POINTS]
    cov = len(checked) / len(appl) if appl else 0
    core_nc = [r for r in CORE if by[r].status == 'NOT CHECKED']
    grade = None if sc is None or cov < 0.7 or core_nc else next(g for lim, g in GRADES if sc >= lim)
    counts = {k: sum(f.status == k for f in fs) for k in ('FAIL', 'WARN', 'PASS', 'NOT CHECKED', 'N/A')}
    banner, risk = [f'{f.id} {t}' for f in fs for t in f.loss], [f'{f.id} {t}' for f in fs for t in f.risk]
    caps = [r for r in ('G23', 'G24') if by[r].status == 'FAIL']
    limit = 'material issues' if caps or banner else 'usable with caveats' if counts['FAIL'] else None
    ladder = [g for _, g in GRADES]
    if grade and limit and ladder.index(grade) < ladder.index(limit): grade = limit
    return {'score': sc, 'grade': grade, 'coverage': cov, 'checked': len(checked), 'applicable': len(appl),
            'core_not_checked': core_nc, 'caps': caps, 'grade_limit': limit, 'areas': areas, 'counts': counts,
            'banner': banner, 'risk': risk}

def trust(c, fs):
    by, out = {f.id: f.status for f in fs}, []
    for fam, rules in TRUST:
        st = [(r, by[r]) for r in rules.split()]
        vals = [v for _, v in st]
        lab = 'do not use until fixed' if 'FAIL' in vals else 'use with caveat' if 'WARN' in vals else \
            'trust' if 'PASS' in vals else 'not assessed'
        because = [f'{r} {v}' for r, v in st if v in ('FAIL', 'WARN')]
        if fam == 'Revenue and ecommerce' and by['G13'] in ('PASS', 'WARN') and not num((c.cfg.get('backend_orders') or {}).get('count')):
            because.append('no order system comparison (backend_orders)')
            if lab == 'trust': lab = 'use with caveat'
        out.append({'family': fam, 'label': lab, 'because': because,
                    'not_checked': [r for r, v in st if v == 'NOT CHECKED']})
    return out
