"""Rules A and B: property and streams (G01 to G03), retention and identity (G04, G05)."""
import re
from collections import defaultdict
from urllib.parse import urlsplit
from .common import LEADS, RETENTION, get, items, kv, num, pct, regdom, rid_of, thresholded, up
from .rules_bigquery import bq_history_gap

def g01(c, f):
    p = c.prop
    if not p: return f.nc('property settings not read')
    if get(p, 'deleteTime') or get(p, 'expireTime'):
        f.fail(f'the property is in the trash (deleteTime {get(p, "deleteTime")}, expireTime {get(p, "expireTime")})')
    pt = up(get(p, 'propertyType'))
    if pt and 'ORDINARY' not in pt:
        f.warn(f'property type {pt}: confirm this is the property to audit before reading anything else')
    tz, cur = get(p, 'timeZone'), get(p, 'currencyCode')
    for key, val, label in (('expected_time_zone', tz, 'time zone'), ('expected_currency', cur, 'currency')):
        if c.cfg.get(key) and val and val != c.cfg[key]: f.warn(f'{label} {val} differs from stack.md ({c.cfg[key]})')
    if c.ads:
        for key, val, label in (('time_zone', tz, 'time zone'), ('currency_code', cur, 'currency')):
            if c.ads.get(key) and val and c.ads[key] != val:
                f.warn(f'Google Ads {label} {c.ads[key]} differs from GA4 {val}: day boundaries and values will not reconcile')
    f.ok(f'time zone {tz}, currency {cur}, {get(p, "serviceLevel")}, not in the trash')

def g02(c, f):
    r03 = c.rows('R03')
    if c.streams is None and r03 is None: return f.nc('data streams and R03 not read')
    if c.app_only: return f.na('no web streams')
    exp, doms = set(c.cfg.get('expected_measurement_ids') or []), defaultdict(list)
    for s in c.web:
        wd, name = get(s, 'webStreamData', {}), get(s, 'displayName')
        if exp and get(wd, 'measurementId') not in exp:
            f.warn(f'measurement ID {get(wd, "measurementId")} ("{name}") is not in stack.md')
        doms[regdom(urlsplit(get(wd, 'defaultUri', '')).hostname or '')].append(name)
    for d, names in doms.items():
        if len(names) > 1: f.warn(f'{len(names)} web streams for {d}: {", ".join(names)}')
    if r03 is None: f.partial.append('R03 hostnames by stream not read: double tagging not checked')
    else:
        per = defaultdict(lambda: defaultdict(float))
        for r in r03:
            per[r.get('hostName', '')][str(r.get('streamId') or r.get('streamName') or '?')] += r.get('eventCount', 0)
        names = {rid_of(get(s, 'name')): get(s, 'displayName') for s in c.web}
        for h, d in per.items():
            tot = sum(d.values())
            big = {k: v / tot for k, v in d.items() if tot and v / tot >= c.th['stream_share']}
            if h not in ('', '(not set)') and len(big) > 1:
                f.gated(f'{h} sends events to {len(big)} streams (' + ', '.join(f'"{names.get(k, k)}" {pct(v)}' for k, v in big.items())
                        + '): double tagging splits users and sessions', c.low_sessions())
        seen = set(k for d in per.values() for k in d) | {str(x.get('streamId')) for x in c.rows('R02') or []}
        for s in c.web:
            if rid_of(get(s, 'name')) not in seen and get(s, 'displayName') not in seen:
                f.warn(f'stream "{get(s, "displayName")}" has no events in the window')
        trunc = c.trunc('R03')
        if trunc: f.partial.append(trunc)
    tc = c.tagcov or (kv(c.uival('tag.coverage')) if c.uival('tag.coverage') else None)
    src = ' [Tag coverage CSV]' if c.tagcov else c.ev('tag.coverage')
    if tc is None: f.partial.append('Tag coverage not supplied')
    else:
        keys = c.cfg.get('key_paths') or []
        if 'pages' in tc:
            bad = [u for u, st in tc['pages'] if re.search(r'not tagged|no recent', st, re.I)]
            kb = [u for u in bad if any(k in u for k in keys)]
            other = len(bad) - len(kb)
        else:
            kb = items(tc.get('key_paths_not_tagged'))
            other = int(num(tc.get('not_tagged')) + num(tc.get('no_recent_activity')))
        if kb:
            f.fail('key path pages show Not tagged or No recent activity in Tag coverage: ' + ', '.join(kb[:5]) + src)
        if other:
            f.warn(f'{other} other pages show Not tagged or No recent activity in Tag coverage' + src)
    f.ok('; '.join(x for x, ok in (('each hostname feeds one stream (R03)', r03 is not None),
                                    ('key paths are tagged in Tag coverage', tc is not None),
                                    (f'{len(c.web)} web stream{"s" if len(c.web) != 1 else ""}, one per registrable domain', bool(c.web))) if ok))

def g03(c, f):
    if c.app_only: return f.na('no web streams')
    ems = [(get(s, 'displayName'), get(s, 'enhancedMeasurement')) for s in c.web if get(s, 'enhancedMeasurement')]
    if not ems: return f.nc('enhanced measurement settings not read')
    leads = (LEADS | {n for n, v in c.intended.items() if not v.get('transactional')}) & set(c.cfg.get('tag_sent_events') or [])
    site_param, vsr = c.cfg.get('site_search_param'), c.agg('R01', 'eventName', 'eventCount').get('view_search_results', 0)
    for name, em in ems:
        if not get(em, 'streamEnabled', True): continue
        if get(em, 'pageChangesEnabled') and c.cfg.get('spa_manual_page_views'):
            f.warn(f'"{name}": page changes on while the tracking plan sends route-change page_view manually (duplicate page views)')
        if get(em, 'formInteractionsEnabled') and leads:
            f.warn(f'"{name}": form interactions on while tags send {", ".join(sorted(leads))}; form_submit also fires on failed, search and newsletter forms')
        if get(em, 'siteSearchEnabled') and site_param:
            keys = [k.strip() for k in (get(em, 'searchQueryParameter') or 'q,s,search,query,keyword').split(',')]
            if site_param not in keys:
                f.warn(f'"{name}": site search keys {",".join(keys)} lack the site\'s key {site_param}')
            elif c.rows('R01') is not None and not vsr:
                f.warn(f'"{name}": site search on but view_search_results is zero in the window')
    f.ok('enhanced measurement matches the tracking plan')

def g04(c, f):
    dr = get(c.snap, 'dataRetention')
    if not dr: return f.nc('data retention not read')
    ev, need = up(get(dr, 'eventDataRetention')), int(c.cfg.get('needs_history_months') or 14)
    months = RETENTION.get(ev)
    if months is not None and months < need:
        text = f'event data retention {ev} ({months} months) is shorter than the {need} months needed; explorations and funnels lose older data for good'
        if c.cfg.get('long_range_history_source') != 'bigquery': f.fail(text)
        elif not any(get(x, 'dailyExportEnabled') for x in c.bq_links or []):
            f.fail(text + '; BigQuery cannot stand in: no daily export is linked')
        else:
            gap = bq_history_gap(c, need)
            if gap: f.fail(text + f'; BigQuery cannot stand in: {gap}')
            else: f.warn(text + ' (BigQuery is the documented long-range source, and its daily tables are fresh and kept long enough)')
    if up(get(dr, 'userDataRetention')) == 'TWO_MONTHS':
        f.warn('user data retention is TWO_MONTHS; check on the live retention page which reports it limits')
    if not c.documented('reset_user_data_on_new_activity'):
        f.warn(f'reset user data on new activity ({get(dr, "resetUserDataOnNewActivity", False)}) is not recorded in decisions.md')
    f.ok(f'event retention {ev} meets the {need} months needed')

def g05(c, f):
    ri, r12 = get(c.snap, 'reportingIdentity'), c.rows('R12')
    if not ri and r12 is None: return f.nc('reporting identity and User-ID coverage not read')
    ident = up(get(ri, 'reportingIdentity'))
    if ri and not c.documented('reporting_identity'):
        probes = [n for rid, n, need in (('R13B', 'key event probe', True), ('R13A', 'demographic probe', c.cfg.get('demographics_used')))
                  if need and thresholded(c.reports.get(rid))]
        if probes and ('BLENDED' in ident or 'OBSERVED' in ident):
            f.warn(f'identity {ident} while the {" and ".join(probes)} is thresholded, and no Device based comparison is recorded')
        else: f.warn(f'reporting identity {ident or "unknown"} is not recorded in decisions.md')
    if r12 is not None and c.cfg.get('user_id_expected'):
        users = c.agg('R12', 'signedInWithUserId', 'totalUsers')
        tot, yes = sum(users.values()), users.get('yes', 0)
        if tot and not yes: f.fail('User-ID is expected but no user is signed in with a User-ID in the window')
        elif tot and yes / tot < float(c.cfg.get('user_id_min_share') or 0):
            f.warn(f'User-ID share {pct(yes / tot)} is below the expected {pct(float(c.cfg["user_id_min_share"]))} (users summed over hostnames, approximate)')
    mod = c.uival('identity.modeling')
    if c.cfg.get('modelled_data_expected') and ident:
        if 'BLENDED' not in ident:
            f.warn(f'decisions.md expects modelled data, but identity is {ident}; modelled users appear only under Blended (answer 11161109)')
        elif mod and (c.cfg.get('consent_mode') or '').lower() == 'advanced' and re.search(r'unavailable|not eligible|excluding', mod, re.I):
            f.warn(f'decisions.md expects modelled data, but Blended reports show "{mod}": behavioural modelling is not active, '
                   'so consent-denied users are missing from user counts' + c.ev('identity.modeling'))
    f.ok('; '.join(x for x, ok in ((f'identity {ident}' + (' documented' if c.documented('reporting_identity') else ''), bool(ri)),
                                    ('User-ID present at the expected share (R12)', r12 is not None and bool(c.cfg.get('user_id_expected'))),
                                    (f'modelling: {mod}', bool(mod))) if ok))
