"""Rules C: traffic hygiene (G06 to G10)."""
from datetime import timedelta
from urllib.parse import urlsplit
from .common import PAY_HOSTS, cmatch, conds, get, host_in, n0, num, pct, regdom, todate

def filter_share(c, name):
    """(share of events a filter matches, its R11 rows by event, cap label). R11T gives exact totals;
    a truncated R11 without R11T gives approximate shares, so a FAIL built on them is capped at WARN."""
    src = 'R11T' if c.rows('R11T') else 'R11'
    rows = c.rows(src)
    if not rows: return None, None, ''
    tot = sum(r.get('eventCount', 0) for r in rows)
    matched = sum(r.get('eventCount', 0) for r in rows if r.get('testDataFilterName') == name)
    hit = [r for r in c.rows('R11') or [] if r.get('testDataFilterName') == name]
    return (matched / tot if tot else 0.0), hit, ('' if src == 'R11T' else c.trunc('R11'))

def testing_age(c, f, x):
    since = todate(x.get('since'))
    if since and (c.asof - since).days > c.th['testing_max_days']:
        f.warn(f'filter "{x.get("name")}" has been in Testing since {since} ({(c.asof - since).days} days), so nothing is excluded')

def g06(c, f):
    if c.app_only: return f.na('app-only property: internal traffic cannot be filtered for app users')
    rules_v = c.uival('tag.internal_traffic_rules')
    if rules_v is None and not c.filters_supplied:
        return f.nc('UI evidence not supplied (internal traffic rules and data filters)')
    rules = {k.strip(): v.strip() for k, v in (x.split('=', 1) for x in (rules_v or '').split(';') if '=' in x)}
    flt = [x for x in c.filters if x.get('type') == 'internal']
    live = [x for x in flt if x.get('state') in ('active', 'testing')]
    for name, val in rules.items():
        if val not in {x.get('value') for x in live}:
            f.fail(f'rule "{name}" sets traffic_type {val} but no Active or Testing internal traffic filter uses {val}' + c.ev('tag.internal_traffic_rules'))
    tagged, quiet = [], set()  # with no rule at all, tags may set traffic_type, which is allowed (rules.md G06)
    for x in live:
        if rules and x.get('value') not in rules.values():
            f.fail(f'filter "{x.get("name")}" ({x.get("state")}) excludes traffic_type {x.get("value")}, which no rule sets')
        elif rules_v is not None and not rules:
            if x.get('state') == 'testing' and filter_share(c, x.get('name'))[0]: tagged.append(x.get('name'))
            elif c.cfg.get('internal_locations_expected') or c.cfg.get('remote_staff') or x.get('state') == 'active':
                f.warn(f'no rule sets traffic_type {x.get("value")}; confirm whether tags set it')
            else: quiet.add(x.get('name'))
    if tagged: f.partial.append(f'filter {", ".join(tagged)} matches events with no rule, so traffic_type is set by tags')
    total_events, purchases = c.events_total(), c.agg('R01', 'eventName', 'eventCount').get('purchase', 0)
    for x in flt:
        if x.get('state') == 'testing':
            share, hit, cap = filter_share(c, x.get('name'))
            if share is None: f.partial.append(f'R11 not read: what "{x.get("name")}" would remove is unknown')
            else:
                if cap: f.partial.append(cap)
                matched = sum(r.get('eventCount', 0) for r in hit)
                mp = sum(r.get('eventCount', 0) for r in hit if r.get('eventName') == 'purchase')
                normal = purchases / total_events if total_events else 0
                if share >= c.th['internal_match_share']:
                    f.gated(f'filter "{x.get("name")}" in Testing matches {pct(share)} of events (VPN, carrier or shared range); do not activate', cap, prio='P1')
                elif mp >= 5 and matched and normal and mp / matched >= c.th['internal_purchase_ratio'] * normal:
                    f.gated(f'filter "{x.get("name")}" in Testing matches {n0(mp)} purchases, near the property\'s normal purchase rate; do not activate', cap, prio='P1')
            if x.get('name') not in quiet: testing_age(c, f, x)
        elif x.get('state') == 'inactive': f.warn(f'filter "{x.get("name")}" is Inactive')
    if not rules and not tagged and (rules_v is not None or not flt) and c.cfg.get('internal_locations_expected'):
        f.warn('no internal traffic definition while stack.md lists offices, agencies or call centres')
    if rules and c.cfg.get('remote_staff'):
        f.warn('internal traffic rules are IP based while staff work remotely; set traffic_type on staff events directly')
    f.ok('; '.join(x for x, ok in (('internal traffic rule values match the Active or Testing filters', bool(rules)),
                                    ('no internal traffic rule, and no filter or office depends on one', rules_v is not None and not rules),
                                    ('test data filter report read (R11)', bool(c.rows('R11') or c.rows('R11T')))) if ok)
         or 'internal traffic filters read')

def g07(c, f):
    if not c.filters_supplied: return f.nc('UI evidence not supplied (data filters)')
    devs = [x for x in c.filters if x.get('type') == 'developer']
    if not devs: f.warn('no Developer traffic filter: GTM Preview and DebugView traffic reaches reports and the export')
    for x in devs:
        st, since = x.get('state'), todate(x.get('since'))
        if st == 'inactive': f.warn(f'Developer traffic filter "{x.get("name")}" is Inactive')
        elif st == 'testing':
            share, _, cap = filter_share(c, x.get('name'))
            if share is None: f.partial.append('R11 not read: what activation would remove is unknown')
            else:
                if cap: f.partial.append(cap)
                if share >= c.th['developer_match_share']:
                    f.gated(f'Developer traffic filter in Testing matches {pct(share)} of events: debug_mode is on production events '
                            '(a debug_mode parameter set to false still enables debug mode); activating it would delete real data', cap)
            testing_age(c, f, x)
        elif st == 'active' and since:
            days = c.daily()
            before = [v for d, v in days.items() if since - timedelta(days=7) <= d < since]
            after = [v for d, v in days.items() if since < d <= since + timedelta(days=3)]
            if before and after:
                drop = 1 - (sum(after) / len(after)) / (sum(before) / len(before))
                if drop >= c.th['activation_drop']:
                    f.fail(f'daily events fell {pct(drop)} within 3 days after the filter was activated on {since}: real traffic is being excluded', loss=True)
            else: f.partial.append(f'activation date {since} is outside the R02 range: drop check not run')
    f.ok('Developer traffic filter Active with no sign of debug_mode on production events')

def g08(c, f):
    if c.app_only: return f.na('no web streams')
    r03, hf = c.rows('R03'), [x for x in c.filters if x.get('type') == 'hostname']
    if r03 is None and not hf: return f.nc('R03 hostnames and hostname filters not read')
    exp = c.cfg.get('expected_hostnames') or []
    if r03 is not None:
        if not exp: f.partial.append('expected_hostnames missing from the config: unexpected hostnames not judged')
        evs, kes = c.agg('R03', 'hostName', 'eventCount'), c.agg('R03', 'hostName', 'keyEvents')
        tot, low, trunc = sum(evs.values()), c.low_sessions(), c.trunc('R03')
        if trunc: f.partial.append(trunc)
        empty = sum(v for h, v in evs.items() if h in ('', '(not set)'))
        for h, v in sorted(evs.items(), key=lambda kv_: -kv_[1]):
            if h in ('', '(not set)') or not exp or host_in(h, exp): continue
            if kes.get(h, 0) > 0 or (tot and v / tot >= c.th['unexpected_host_share']):
                f.gated(f'unexpected hostname {h}: {pct(v / tot)} of events, {n0(kes.get(h, 0))} key events',
                        low or ('' if kes.get(h, 0) > 0 else trunc))
            else: f.warn(f'unexpected hostname {h}: {pct(v / tot)} of events, no key events')
        if tot and empty / tot >= c.th['empty_host_share']:
            f.warn(f'empty or (not set) hostname on {pct(empty / tot)} of events (Measurement Protocol, server or spam events)')
    for x in hf:
        if x.get('operation') != 'include': continue
        miss = [h for h in exp if not cmatch(conds(x.get('conditions')), h)]
        if x.get('state') == 'active' and miss:
            f.fail(f'Active Include hostname filter "{x.get("name")}" does not match {", ".join(miss)}: that data is dropped for good', prio='P1', loss=True)
        elif x.get('state') == 'testing':
            f.warn(f'Include hostname filter "{x.get("name")}" is in Testing' + (f'; activating it would drop {", ".join(miss)}' if miss else '; read R11 by hostname before activation'))
    f.ok('; '.join(x for x, ok in (('only expected hostnames in R03', r03 is not None and bool(exp)),
                                    ('hostname filters cover every expected hostname', bool(hf))) if ok) or 'hostname filters read')

def owned_domains(c):
    doms = {d.lower() for d in c.cfg.get('owned_registrable_domains') or []}
    return doms | {regdom(urlsplit(get(get(s, 'webStreamData', {}), 'defaultUri', '')).hostname or '') for s in c.web} - {''}

def referral_shares(c):
    out, tot = {}, c.sessions()
    for r in c.rows('R05') or []:
        d = regdom(r.get('sessionSource', ''))
        if d in owned_domains(c) and tot:
            out[r.get('sessionSource')] = out.get(r.get('sessionSource'), 0) + r.get('sessions', 0) / tot
    return out

def g09(c, f):
    if c.app_only: return f.na('no web streams')
    r05, r06 = c.rows('R05'), c.rows('R06')
    if r05 is None and r06 is None: return f.nc('R05 and R06 referral reports not read')
    uv = c.uival('tag.unwanted_referrals')
    unwanted, pay = conds(uv), PAY_HOSTS + list(c.cfg.get('payment_and_auth_hosts') or [])
    credit, scope = (c.agg('R06', 'sourceMedium', 'keyEvents'), 'key event credit') if r06 is not None else \
        ({f'{r.get("sessionSource")} / {r.get("sessionMedium")}': r.get('keyEvents', 0) for r in r05}, 'session-scoped key events')
    tke = c.key_events_total() or sum(credit.values())
    for sm, ke in sorted(credit.items(), key=lambda x: -x[1]):
        src, _, med = sm.partition(' / ')
        if med.strip() == 'referral' and host_in(src, pay) and tke:
            share = ke / tke
            if cmatch(unwanted, src):
                f.warn(f'{src} is in unwanted referrals but still holds {pct(share)} of {scope}: check when it was added (the lookback keeps earlier referrals)')
            elif share >= c.th['referral_share']:
                f.gated(f'{src} / referral holds {pct(share)} of {scope} and is not in unwanted referrals', c.low(tke, 'min_key_events'), c.ev('tag.unwanted_referrals'))
            elif ke: f.warn(f'{src} / referral holds {pct(share)} of {scope}')
    for src, share in referral_shares(c).items():
        if share >= c.th['referral_share']:
            f.gated(f'owned domain {src} is a referral source with {pct(share)} of sessions', c.low_sessions())
        else: f.warn(f'owned domain {src} is a referral source with {pct(share)} of sessions')
    if uv is None: f.partial.append('unwanted referral list not supplied')
    else:
        for t, v in unwanted:
            if regdom(v) in owned_domains(c):
                f.warn(f'{v} is an owned, tagged domain in unwanted referrals; it belongs in Configure your domains' + c.ev('tag.unwanted_referrals'))
        if len(unwanted) >= c.th['unwanted_referrals_warn']: f.warn(f'{len(unwanted)} of 50 unwanted referrals used')
    f.ok('no payment, sign-in or owned domain takes referral credit')

def g10(c, f):
    if c.app_only: return f.na('no web streams')
    keys = ('tag.domains', 'tag.session_timeout_minutes', 'tag.engaged_session_seconds', 'tag.cookie_override')
    if all(c.uival(k) is None for k in keys): return f.nc('UI evidence not supplied (Google tag settings)')
    owned = sorted({d.lower() for d in c.cfg.get('owned_registrable_domains') or []})
    dv = c.uival('tag.domains')
    if dv is not None and len(owned) > 1:
        cs, exp = conds(dv), c.cfg.get('expected_hostnames') or []
        clicks = c.agg('R07', 'linkDomain', 'eventCount')
        tclicks, refs = sum(clicks.values()), referral_shares(c)
        for d in owned:
            hosts = [h for h in exp if regdom(h) == d] or [d]
            if any(cmatch(cs, h) for h in hosts): continue
            evid = [f'{s} is a referral with {pct(v)} of sessions' for s, v in refs.items()
                    if regdom(s) == d and v >= c.th['referral_share']]
            out = sum(v for h, v in clicks.items() if regdom(h) == d) / tclicks if tclicks else 0
            if out >= c.th['outbound_click_share']: evid.append(f'{pct(out)} of click events are outbound clicks to it')
            if evid:
                f.fail(f'{d} uses the same measurement ID but is missing from Configure your domains ({"; ".join(evid)})' + c.ev('tag.domains'))
            else:
                f.warn(f'{d} is missing from Configure your domains (no referral or outbound click evidence yet)' + c.ev('tag.domains'))
    for key, default, dec, label in (('tag.session_timeout_minutes', 30, 'session_timeout', 'session timeout (minutes)'),
                                     ('tag.engaged_session_seconds', 10, 'engaged_session_timer', 'engaged-session timer (seconds)')):
        v = c.uival(key)
        if v and num(v) != default and not c.documented(dec):
            f.warn(f'{label} is {v}, not {default}, without a decision (engagement rate and benchmarks shift)' + c.ev(key))
    if (c.uival('tag.cookie_override') or '').lower().startswith('on') and not c.documented('cookie_override'):
        f.warn('cookie override is on without a decision' + c.ev('tag.cookie_override'))
    if (c.cfg.get('gtm_findings') or {}).get('manual_linker') is True:
        f.warn('a manual linker in GTM overrides the Admin domain list (gtm-container-audit finding)')
    f.ok('; '.join(x for x, ok in (('every owned registrable domain is in Configure your domains', dv is not None and len(owned) > 1),
                                    ('one owned registrable domain, so no cross-domain list is needed', dv is not None and len(owned) <= 1),
                                    ('session settings at defaults or documented', any(c.uival(k) for k in keys[1:]))) if ok)
         or 'Google tag settings read')
