"""Rules I and J: consent and privacy (G23, G24), reporting integrity (G25, G26)."""
import re
from collections import defaultdict
from .common import SAMPLING_LIMIT, get, items, kv, missing_signal, n0, num, pct, thresholded, todate
from .pii import key_kind, safe_key

def g23(c, f):
    eea, srows = c.uival('consent.eea_share'), c.uirows('consent.stream.')
    mdc, ovr = c.uival('tag.manage_default_consent'), c.uival('tag.consent_mode_override')
    if eea is None and not srows and mdc is None:
        return f.nc('UI evidence not supplied (Consent settings and Google tag settings)')
    traffic = num(kv(eea).get('traffic', eea)) if eea else None
    regions = c.cfg.get('consent_regions') or []
    if traffic == 0 and not regions: return f.na('no traffic from regions where consent is required')
    shares = any(get(x, 'adsPersonalizationEnabled') for x in c.ads_links or []) or \
        c.cfg.get('ga4_key_events_imported_to_ads') or c.cfg.get('ga4_audiences_used_in_ads')
    for key, v in srows:
        sig, name = kv(v), key.split('.', 2)[2]
        miss = [s for s in ('ad_user_data', 'ad_personalization') if missing_signal(sig.get(s))]
        if miss and traffic != 0:
            share = f'with {traffic:g}% of traffic from the EEA' if traffic is not None else 'EEA traffic share not recorded (intake consent.eea_share empty)'
            text = f'stream "{name}": {" and ".join(miss)} not received, {share}'
            if shares: f.fail(text + ', while the property shares data with Google Ads' + c.ev(key))
            else: f.warn(text + c.ev(key))
        if re.search(r'flag|issue|warn', sig.get('behavior_analytics', ''), re.I):
            f.warn(f'stream "{name}": Behavior analytics consent signals flagged' + c.ev(key))
    mode = str(c.cfg.get('consent_mode') or '').strip().lower()
    if mdc and mdc.lower().startswith('y'):
        ev = c.ev('tag.manage_default_consent')
        if mode in ('basic', 'advanced'):
            f.warn('Manage default consent settings is Yes: Google does not apply this setting while consent mode is used (answer 12131703). '
                   'Yes still blocks the consent mode override and would label data as consented if consent mode were removed. Set it to No.' + ev)
        elif mode in ('', 'none') and c.cfg.get('tags_wait_for_consent') is False:
            f.fail('Manage default consent settings is Yes while consent mode is not implemented and tags load before consent: '
                   'all data sent to Google is labelled consented' + ev)
        else:
            f.warn('Manage default consent settings is Yes: confirm tags load only after consent' + ev)
    if ovr and ovr.lower() not in ('off', 'none', 'no') and regions and set(items(ovr.replace(',', ';'))) != set(regions):
        f.warn(f'consent mode override regions ({ovr}) differ from decisions.md ({", ".join(regions)})' + c.ev('tag.consent_mode_override'))
    e = (c.bq or {}).get('eea_consent') or {}
    if isinstance(e, list): e = {str(r.get('state', '')).lower(): r.get('events') for r in e}  # the BQ4 rows as returned
    tot = sum(num(e.get(k)) for k in ('yes', 'no', 'unset'))
    if tot and num(e.get('unset')) / tot >= c.th['eea_unset_share']:
        f.warn(f'privacy_info.analytics_storage is unset on {pct(num(e.get("unset")) / tot)} of EEA events in BigQuery')
    f.ok('; '.join(x for x, ok in (('advertising consent signals received for every stream with EEA traffic', bool(srows)),
                                    ('Manage default consent settings is No', bool(mdc) and not mdc.lower().startswith('y')),
                                    ('EEA consent state set in BigQuery', bool(tot))) if ok) or 'consent settings read')

def g24(c, f):
    r15, r14 = c.reports.get('R15'), c.reports.get('R14')
    red = [(get(s, 'displayName'), get(s, 'dataRedaction')) for s in c.web if get(s, 'dataRedaction') is not None]
    if r15 is None and not r14 and not red:
        return f.nc('PII probe, custom dimension probes and redaction settings not read')
    found_keys = set()
    if r15 is None: f.partial.append('R15 PII probe not read')
    else:
        groups = defaultdict(float)
        for r in r15.get('rows') or []:
            shown = sorted({safe_key(k) for k in r.get('keys') or []})  # a snapshot from another route may hold raw keys
            groups[(r.get('severity'), r.get('field'), r.get('host'), r.get('path'), r.get('rule'), ','.join(shown))] += num(r.get('events'))
            found_keys |= {k for k in shown if key_kind(k) in ('pii', 'secret')}
        for (sev, field, h, path, rule, keys), n in sorted(groups.items(), key=lambda x: -x[1])[:6]:
            where = f'{h}{path}' if path or str(h).startswith('(') else f'{h} (count-only probe, path not read)'
            text = f'{n0(n)} events with {field} on {where} match {rule}' + (f' (query keys {keys}; values not shown)' if keys else ' (values not shown)')
            (f.fail if sev == 'FAIL' else f.warn)(text if sev == 'FAIL' else text + ', human review')
    for k, v in sorted((r14 or {}).items()):
        if num(v.get('email_like')) or num(v.get('phone_like')):
            unit = 'events in the count-only probe' if v.get('count_only') else 'values in the 28-day probe'
            f.fail(f'custom dimension {k} holds email-like or phone-like values ({n0(num(v.get("email_like")))} email-like, '
                   f'{n0(num(v.get("phone_like")))} phone-like {unit})')
    off_email = [f'"{n}"' for n, d in red if not get(d, 'emailRedactionEnabled')]
    off_query = [f'"{n}"' for n, d in red if not get(d, 'queryParameterRedactionEnabled')]
    if off_email: f.warn(f'email redaction is off on {", ".join(off_email)}')
    if found_keys and off_query:
        f.warn(f'query parameter redaction is off on {", ".join(off_query)} while PII keys were found ({", ".join(sorted(found_keys))})')
    upd = get(c.snap, 'userProvidedData') or {}
    if (get(upd, 'userProvidedDataCollectionEnabled') or get(upd, 'automaticallyDetectedDataCollectionEnabled')) and not c.documented('user_provided_data'):
        f.warn('user-provided data collection or automatic detection is on without a recorded decision')
    for key, v in c.uirows('mp_secrets.'):
        n = int(num(kv(v).get('count', v)))
        if n:
            f.warn(f'{n} Measurement Protocol secret(s) on {key.split(".", 1)[1]}: redaction does not cover Measurement Protocol or data import' + c.ev(key))
    ds = get(c.snap, 'dataSharing')
    if ds and get(ds, 'sharingWithGoogleProductsEnabled') and not c.documented('data_sharing'):
        f.warn('account data sharing with Google products & services is on without a recorded decision (account level; the privacy owner decides)')
    f.ok('; '.join(x for x, ok in (('no PII or secret rows in the R15 probe', r15 is not None),
                                    ('no email-like or phone-like custom dimension values (R14)', bool(r14)),
                                    ('email redaction on for every web stream', bool(red))) if ok))

def g25(c, f):
    probes = {k: c.reports.get(k) for k in ('R13A', 'R13B', 'R16')}
    days = c.daily(None, c.start, c.end)
    if not days and not any(probes.values()): return f.nc('R02 daily volume and probe metadata not read')
    if days:
        avg, lim = sum(days.values()) / len(days), SAMPLING_LIMIT[c.is360]
        horizon = int(lim // avg) if avg else 10 ** 6
        if horizon < c.th['sampling_horizon_days']:
            f.warn(f'sampling horizon {horizon} days at the default exploration limit: explorations over more than {horizon} days sample '
                   f'(average {n0(avg)} events a day' + ('; more detailed results raise the 360 limit to 1,000,000,000 events' if c.is360 else '') + ')')
    if thresholded(probes['R13B']): f.warn('the key event probe, which has no demographics, is subject to thresholding')
    if thresholded(probes['R13A']) and c.cfg.get('demographics_used'):
        f.warn('the demographic probe is thresholded and the team uses demographic reports')
    if get(get(probes['R16'], 'metadata', {}), 'dataLossFromOtherRow'):
        f.warn('the 28-day page report has an (other) row (high cardinality)')
    f.ok('; '.join(x for x, ok in (('sampling horizon 90 days or more at the default exploration limit', bool(days)),
                                    ('probes not thresholded where it matters', any(probes.values()))) if ok))

def g26(c, f):
    diags, chg = c.uirows('diagnostics.'), c.uirows('change_history.')
    if not diags and not chg: return f.nc('UI evidence not supplied (diagnostics and change history)')
    for key, v in diags:
        d = kv(v)
        seen = todate(d.get('first_seen'))
        if v.lower() != 'none' and seen and (c.asof - seen).days > c.th['diagnostic_max_days'] and d.get('owner', '').lower() in ('', 'none', 'no'):
            f.warn(f'diagnostic "{d.get("summary")}" open since {seen} ({(c.asof - seen).days} days) with no owner' + c.ev(key))
    for key, d, text, area in c.changes({'filters', 'key_events', 'retention', 'consent'}):
        if not c.annotated(d):
            f.warn(f'{text} on {d} ({area}) has no annotation' + c.ev(key))
            c.propose(d, text, f'{area.replace("_", " ").capitalize()} changed in Admin; reported numbers may shift from this date.')
    if not diags: f.partial.append('diagnostics not supplied')
    if not chg: f.partial.append('change history not supplied')
    f.ok('diagnostics owned; changes annotated')
