"""Rules D: key events and values (G11 to G14)."""
import re
from collections import defaultdict
from .common import ECOM, NOISY, get, items, n0, num, pct, todate, up

def g11(c, f):
    if c.kes is None: return f.nc('key events not read')
    form_auto = any(get(get(s, 'enhancedMeasurement'), 'formInteractionsEnabled') for s in c.web)
    link_days = {todate(get(x, 'createTime')) for x in c.ads_links or []} - {None}
    steps, other, autos = [], [], 0
    for k in c.kes:
        n = get(k, 'eventName')
        auto = todate(get(k, 'createTime')) in link_days and not get(k, 'custom')
        hint = ' (marked the day the Google Ads link was created)' if auto else ''
        if n in NOISY or (n == 'form_submit' and form_auto):
            why = 'enhanced measurement sends it on every submit attempt, including search and newsletter forms' if n == 'form_submit' else 'it fires in nearly every session'
            if get(k, 'deletable', True) is False:
                f.warn(f'{n} is a key event and cannot be removed (deletable false); read per-event key event metrics, not totals{hint}')
            else: f.fail(f'{n} is a key event and {why}{hint}')
        elif n in ECOM and n not in c.intended:
            steps.append(n)
            autos += auto
        elif n != 'purchase' and n not in c.intended and n not in ECOM: other.append(n)
    if steps:
        f.warn(f'{len(steps)} ecommerce steps are key events without a documented purpose: {", ".join(steps)}'
               + (' (all marked the day the Google Ads link with ads personalization was created)' if autos == len(steps) else ''))
    if other and c.intended: f.warn(f'key events not in the tracking plan: {", ".join(other)}')
    limit = 50 if c.is360 else 30
    if len(c.kes) > c.th['key_events_warn']:
        f.warn(f'{len(c.kes)} key events, so totals of all key events mix unlike outcomes')
    if len(c.kes) >= c.th['quota_share'] * limit: f.warn(f'{len(c.kes)} of {limit} key event slots used')
    missing = [n for n in c.intended if n not in c.ke_names]
    if missing: f.warn(f'intended key events not marked: {", ".join(missing)}')
    if c.rows('R01') is not None:
        counts = c.agg('R01', 'eventName', 'eventCount')
        zero = sorted(n for n in c.ke_names if not counts.get(n))
        if zero: f.warn(f'key events with no events in the window: {", ".join(zero)}')
    f.ok(f'{len(c.kes)} key events, all documented business outcomes')

def g12(c, f):
    if c.kes is None: return f.nc('key events not read')
    trans = {'purchase'} | {n for n, v in c.intended.items() if v.get('transactional')}
    for k in c.kes:
        n = get(k, 'eventName')
        if 'ONCE_PER_SESSION' in up(get(k, 'countingMethod')):
            if n in trans:
                f.fail(f'{n} is counted Once per session, so repeat purchases in a session are dropped '
                       '(the legacy default for key events migrated from Universal Analytics goals)')
            elif up((c.intended.get(n) or {}).get('counting')) != 'ONCE_PER_SESSION' and not c.documented(f'counting:{n}'):
                f.warn(f'{n} is counted Once per session without a decision')
    r01 = c.rows('R01')
    if r01 and any('sessions' in r for r in r01):
        evs, ses = c.agg('R01', 'eventName', 'eventCount'), c.agg('R01', 'eventName', 'sessions')
        for n in sorted((c.ke_names or set()) & (set(c.intended) | {'purchase'})):
            lim = c.th['purchase_per_session'] if n in trans else c.th['lead_per_session']
            if ses.get(n) and evs.get(n, 0) >= c.th['min_key_events'] and evs[n] / ses[n] > lim:
                f.warn(f'{n}: {evs[n] / ses[n]:.2f} events per session in which it occurs (limit {lim}); fix the duplicate trigger, not the counting method')
    else: f.partial.append('R01 sessions per event not read: duplicate pattern not checked')
    f.ok('Once per event where outcomes can repeat; no duplicate pattern')

def first_row(c, rid):
    rows = c.rows(rid)
    return rows[0] if rows else ({} if rows is not None else None)

def g13(c, f):
    r08, r09 = c.reports.get('R08'), c.rows('R09')
    evs, vals = c.agg('R01', 'eventName', 'eventCount'), c.agg('R01', 'eventName', 'eventValue')
    needs = {n for n, v in c.intended.items() if v.get('needs_value')}
    r17a, r17b, r17c = first_row(c, 'R17A'), first_row(c, 'R17B'), c.rows('R17C')
    purchases = num((r08 or {}).get('purchases')) or num((r17a or {}).get('ecommercePurchases')) or evs.get('purchase', 0)
    if r08 is None and r09 is None and r17a is None and c.rows('R01') is None: return f.nc('R01, R08, R09 and R17A not read')
    if not purchases and not needs: return f.na('no purchase events and no value-bearing key events')
    low, read = purchases < c.th['min_purchases'], []
    if r08:
        read.append('transaction IDs (R08)')
        p, miss = num(r08.get('purchases')), num(r08.get('missing_ids'))
        cap = 'low volume' if low else ('R08 truncated, so counts cover part of the window' if r08.get('truncated') else '')
        if r08.get('truncated'): f.partial.append(f'R08 truncated after {n0(num(r08.get("rows_read")))} rows: transaction ID shares approximate')
        if p and miss / p >= c.th['missing_id_share']:
            f.gated(f'{pct(miss / p)} of {n0(p)} purchase events have no transaction_id (counts only, IDs not shown)', cap)
        elif p and miss:  # under the line: the PASS says so, and never claims every purchase carries an ID
            read[-1] = f'transaction IDs (R08: {n0(miss)} of {n0(p)} missing, under the {pct(c.th["missing_id_share"])} line)'
        dist, dup = r08.get('distinct_ids'), r08.get('ids_on_more_than_one_event')
        if dist is None or dup is None: f.partial.append('duplicate transaction IDs not checked (count-only R08)')
        elif num(dist) and num(dup) / num(dist) >= c.th['duplicate_id_share']:
            f.warn(f'{pct(num(dup) / num(dist))} of transaction IDs appear on more than one purchase event (refreshes or two senders)')
    else: f.partial.append('R08 transaction IDs not read')
    if r09:
        read.append('currency (R09)')
        tot = sum(x.get('eventCount', 0) for x in r09)
        nocur = sum(x.get('eventCount', 0) for x in r09 if str(x.get('currencyCode', '')).strip() in ('', '(not set)'))
        if tot and nocur / tot >= c.th['missing_currency_share']:
            f.gated(f'{pct(nocur / tot)} of purchase events carry no currency, so revenue is not computed', low)
        elif tot and nocur:
            read[-1] = f'currency (R09: {n0(nocur)} of {n0(tot)} missing, under the {pct(c.th["missing_currency_share"])} line)'
    for k in c.kes or []:
        n, dv = get(k, 'eventName'), get(k, 'defaultValue')
        if n == 'purchase' and dv:
            f.warn(f'purchase has a default value ({get(dv, "numericValue")} {get(dv, "currencyCode")}), which hides missing values')
        if n in needs and not dv and c.rows('R01') is not None and not vals.get(n):
            f.warn(f'{n} needs a value but has zero event value and no default value')
    bo = c.cfg.get('backend_orders') or {}
    if num(bo.get('count')) and purchases:
        read.append('the order system count')
        ratio, src = purchases / num(bo['count']), bo.get('source') or 'the order system'
        if not c.th['backend_low'] <= ratio <= c.th['backend_high']:
            text = f'GA4 records {n0(purchases)} purchases against {n0(num(bo["count"]))} orders in {src} for the window ({pct(ratio)})'
            if ratio > c.th['backend_high']:
                text += ': duplicates, test orders or a second sender; order-level causes go to purchase-reconciliation'
            else:
                text += ': purchases are not captured (tags that fail, payment returns, blockers)'
                if (c.cfg.get('consent_mode') or '').lower() in ('basic', 'advanced'):
                    text += '; consent-denied purchases are left out of reports unless modelled, so compare orders from regions without a consent requirement first'
            f.warn(text)
    elif purchases: f.partial.append('backend_orders not supplied: GA4 purchases not compared with the order system')
    gross, items = num((r17a or {}).get('grossPurchaseRevenue')), num((r17b or {}).get('itemsPurchased'))
    if purchases and r17b is not None and gross:
        read.append('item revenue (R17B)')
        item_rev = num(r17b.get('grossItemRevenue'))
        gap = abs(item_rev - gross) / gross
        if gap > c.th['item_revenue_gap']:
            f.warn(f'gross item revenue ({n0(item_rev)}) and gross purchase revenue ({n0(gross)}) differ by {pct(gap)}: '
                   'value includes shipping or tax, or items are missing or priced wrongly, so item reports disagree with revenue')
    if purchases and items and r17c is not None:
        read.append('item names (R17C)')
        ns = sum(x.get('itemsPurchased', 0) for x in r17c)
        if ns / items >= c.th['item_not_set_share']:
            f.warn(f'{pct(ns / items)} of items purchased have item name (not set): item reports and item-based audiences miss them')
    if purchases and (r17b is None or r17c is None): f.partial.append('R17B or R17C not read: item checks not run')
    if purchases and low: f.warn(f'only {n0(purchases)} purchases in the window, so percentages are provisional')
    f.ok(f'{n0(purchases)} purchases; ' + (', '.join(read) + ' checked with no issue' if read else 'event values read from R01'))

def g14(c, f):
    if c.app_only: return f.na('no web streams; event create and edit rules are set per web stream')
    rules = [(get(s, 'displayName'), get(s, 'eventCreateRules'), get(s, 'eventEditRules')) for s in c.web]
    if not any(cr is not None or er is not None for _, cr, er in rules):
        return f.nc('event create and edit rules not read')
    ke, sent = c.ke_names or set(), set(c.cfg.get('tag_sent_events') or [])
    for name, creates, edits in rules:
        for r in creates or []:
            dest, cs = get(r, 'destinationEvent'), get(r, 'eventConditions', [])
            from_pv = any(get(x, 'field') == 'event_name' and get(x, 'value') == 'page_view' for x in cs)
            on_url = [f'{get(x, "field")} {get(x, "comparisonType", "")} {get(x, "value")}'.lower() for x in cs
                      if get(x, 'field') in ('page_location', 'page_path', 'page_referrer')]
            if dest in ke and dest in sent:
                f.fail(f'create rule in "{name}" makes key event {dest} ({"; ".join(on_url) or "conditions"}) while tags already send {dest}: double count')
            elif dest in ke and from_pv and on_url:
                f.warn(f'create rule in "{name}" makes key event {dest} from page_view on a URL condition: reloads and back-button visits count')
        for r in edits or []:
            touched = {get(m, 'parameter') for m in get(r, 'parameterMutations', [])}
            hit = touched & {'event_name', 'value', 'currency', 'transaction_id', 'items'}
            if hit:
                f.warn(f'edit rule "{get(r, "displayName")}" in "{name}" sets {", ".join(sorted(hit))}: check every consumer, including BigQuery')
    names = list(c.agg('R01', 'eventName', 'eventCount'))
    clash = defaultdict(set)
    for n in names: clash[n.lower()].add(n)
    odd = sorted({n for n in names if re.search(r'[A-Z\s]', n)} | {n for v in clash.values() if len(v) > 1 for n in v})
    if odd:
        f.warn(f'event names with capitals, spaces or case-only differences: {", ".join(odd[:8])} (see event-naming-convention)')
    f.ok('no create or edit rule changes key events or values')
