"""Constants and small helpers shared by the rules, the context and the report."""
import re
from datetime import date, datetime, timedelta

RULES = {
    'G01': ('property-basics', 'A'), 'G02': ('streams-and-tag-coverage', 'A'), 'G03': ('enhanced-measurement', 'A'),
    'G04': ('data-retention', 'B'), 'G05': ('reporting-identity-and-user-id', 'B'),
    'G06': ('internal-traffic', 'C'), 'G07': ('developer-traffic', 'C'),
    'G08': ('hostnames-and-hostname-filters', 'C'), 'G09': ('referrals', 'C'),
    'G10': ('cross-domain-and-session-settings', 'C'), 'G11': ('key-event-inventory', 'D'),
    'G12': ('key-event-counting-and-duplicates', 'D'), 'G13': ('purchase-and-value-integrity', 'D'),
    'G14': ('event-rules-and-names', 'D'), 'G15': ('google-ads-links', 'E'), 'G16': ('ads-conversion-sharing', 'E'),
    'G17': ('bigquery-link-config', 'F'), 'G18': ('bigquery-volume-and-health', 'F'),
    'G19': ('custom-definition-design', 'G'), 'G20': ('custom-definition-data', 'G'),
    'G21': ('attribution-settings', 'H'), 'G22': ('channel-groups-and-unassigned', 'H'),
    'G23': ('consent-signals', 'I'), 'G24': ('pii-and-redaction', 'I'),
    'G25': ('thresholding-sampling-cardinality', 'J'), 'G26': ('diagnostics-and-change-records', 'J'),
}
AREAS = {'A': ('Property and streams', 8), 'B': ('Retention and identity', 6), 'C': ('Traffic hygiene', 14),
         'D': ('Key events and values', 16), 'E': ('Google Ads integration', 10), 'F': ('BigQuery export', 8),
         'G': ('Custom definitions', 6), 'H': ('Attribution and channels', 10), 'I': ('Consent and privacy', 16),
         'J': ('Reporting integrity', 6)}
CORE = ('G04', 'G06', 'G07', 'G08', 'G11', 'G12', 'G23', 'G24')
TRUST = [('Acquisition by session', 'G06 G07 G08 G09 G10 G15 G22'),
         ('Key events and conversion rates', 'G06 G07 G11 G12 G14'), ('Revenue and ecommerce', 'G08 G12 G13'),
         ('Explorations and long date ranges', 'G04 G25'),
         ('Google Ads imports and bidding signals', 'G11 G15 G16 G23'),
         ('BigQuery and warehouse models', 'G17 G18 G20'), ('EEA audiences and remarketing', 'G15 G23')]
PRIORITY = {r: p for p, rs in (('P1', 'G07 G18 G23 G24'), ('P2', 'G02 G06 G08 G11 G12 G13 G14 G16'),
                               ('P3', 'G04 G05 G09 G10 G15 G17 G21 G22'), ('P4', 'G01 G03 G19 G20 G25 G26'))
            for r in rs.split()}
POINTS = {'PASS': 1.0, 'WARN': 0.5, 'FAIL': 0.0}
GRADES = ((90, 'decision-grade'), (75, 'usable with caveats'), (50, 'material issues'), (0, 'not decision-grade'))
DEFAULTS = {
    'min_sessions': 1000, 'min_key_events': 100, 'min_purchases': 100, 'stream_share': 0.01,
    'unexpected_host_share': 0.01, 'empty_host_share': 0.05, 'internal_match_share': 0.20,
    'internal_purchase_ratio': 0.5, 'developer_match_share': 0.05, 'activation_drop': 0.50, 'testing_max_days': 30,
    'referral_share': 0.01, 'unwanted_referrals_warn': 40, 'outbound_click_share': 0.01, 'key_events_warn': 10,
    'quota_share': 0.80, 'lead_per_session': 1.2, 'purchase_per_session': 1.1, 'missing_id_share': 0.01,
    'missing_currency_share': 0.01, 'duplicate_id_share': 0.01, 'ads_no_customer_share': 0.10,
    'export_limit': 1000000, 'export_warn': 800000, 'table_max_age_days': 3, 'table_update_days': 3, 'export_tolerance': 0.02,
    'cardinality_per_day': 500, 'unassigned_warn': 0.02, 'unassigned_fail': 0.10, 'unassigned_key_event_fail': 0.05,
    'direct_rise': 0.10, 'eea_unset_share': 0.05, 'sampling_horizon_days': 90, 'diagnostic_max_days': 30,
    'change_window_days': 365, 'primary_group_days': 90, 'annotation_days': 3, 'backend_low': 0.85,
    'backend_high': 1.02, 'item_revenue_gap': 0.02, 'item_not_set_share': 0.01,
}
DAYS_PER_MONTH = 30.4
SAMPLING_LIMIT = {False: 1e7, True: 1e8}  # default events per exploration query; 360 reaches 1e9 with more detailed results
PAY_HOSTS = ['paypal.com', 'checkout.stripe.com', 'accounts.google.com', 'login.microsoftonline.com',
             'appleid.apple.com']
NOISY = {'session_start', 'first_visit', 'page_view', 'user_engagement', 'form_start'}
ECOM = {'view_item', 'view_item_list', 'add_to_cart', 'add_to_wishlist', 'begin_checkout', 'view_search_results'}
LEADS = {'generate_lead', 'sign_up', 'submit_lead_form', 'qualify_lead', 'contact'}
ID_NAMES = {'client_id', 'session_id', 'ga_session_id', 'user_id', 'event_id', 'order_id', 'transaction_id',
            'timestamp', 'gclid', 'uuid'}
PREDEFINED = {'page_location', 'page_title', 'page_referrer', 'transaction_id', 'currency', 'item_id', 'item_name',
              'search_term', 'link_url'}
QUOTA = {False: {'EVENT': 50, 'USER': 25, 'ITEM': 10, 'metrics': 50, 'calculated': 5},
         True: {'EVENT': 125, 'USER': 100, 'ITEM': 25, 'metrics': 125, 'calculated': 50}}
RETENTION = {'TWO_MONTHS': 2, 'FOURTEEN_MONTHS': 14, 'TWENTY_SIX_MONTHS': 26, 'THIRTY_EIGHT_MONTHS': 38,
             'FIFTY_MONTHS': 50}
GA4_PREFIX = 'GOOGLE_ANALYTICS_4_'
NATIVE_TYPES = {'WEBPAGE', 'WEBPAGE_CODELESS', 'UPLOAD_CLICKS'}
LEAD_CATEGORIES = {'SUBMIT_LEAD_FORM', 'SIGNUP', 'CONTACT', 'QUALIFIED_LEAD', 'REQUEST_QUOTE', 'BOOK_APPOINTMENT'}
SHARING_LABELS = (('sharingWithGoogleProductsEnabled', 'Google products & services'),
                  ('sharingWithOthersEnabled', 'Modeling contributions & business insights'),
                  ('sharingWithGoogleSupportEnabled', 'Technical support'),
                  ('sharingWithGoogleAssignedSalesEnabled', 'Recommendations for your business'))
MULTI_SUFFIX = {'co.uk', 'org.uk', 'ac.uk', 'gov.uk', 'com.au', 'net.au', 'org.au', 'co.nz', 'co.jp', 'co.in',
                'com.br', 'com.mx', 'co.za', 'com.sg', 'com.tr', 'co.kr'}
MATCH = ('exact', 'contains', 'begins_with', 'ends_with', 'regex')
EXPORTS = [('R11', ('test data filter name', 'event name', 'event count')), ('R08', ('transaction id', 'event count')),
           ('R15', ('page location', 'event count')), ('R03', ('hostname', 'event count')),
           ('R05', ('session source', 'session medium', 'sessions')),
           ('R04', ('session default channel group', 'sessions')), ('R01', ('event name', 'event count', 'key events')),
           ('R02', ('date', 'event count'))]
LABELS = {'event name': 'eventName', 'event count': 'eventCount', 'key events': 'keyEvents', 'event value': 'eventValue',
          'sessions': 'sessions', 'hostname': 'hostName', 'stream name': 'streamName', 'stream id': 'streamId',
          'date': 'date', 'session default channel group': 'sessionDefaultChannelGroup',
          'session primary channel group': 'sessionPrimaryChannelGroup', 'session source': 'sessionSource',
          'session medium': 'sessionMedium', 'transaction id': 'transactionId',
          'test data filter name': 'testDataFilterName', 'page location': 'pageLocation'}
METRICS = {'eventCount', 'keyEvents', 'eventValue', 'sessions'}
# Secondary inputs: a missing one is listed as partial. Primary inputs are in primary_missing().
INPUTS = {'G05': 'R13B', 'G08': 'ui:filters.', 'G09': 'R05 R06', 'G10': 'R05 R07', 'G13': 'R09', 'G15': 'R10',
          'G18': 'R02', 'G21': 'ui:change_history.', 'G22': 'snap:channelGroups R06', 'G23': 'ui:tag.manage_default_consent',
          'G25': 'R02 R13A R13B R16'}


def snake(k): return re.sub(r'([A-Z])', lambda m: '_' + m.group(1).lower(), k)

def get(d, key, default=None):
    """Reads a REST camelCase key or its MCP snake_case twin."""
    if isinstance(d, dict):
        for k in (key, snake(key)):
            if d.get(k) is not None: return d[k]
    return default

def up(v): return str(v or '').upper()

def num(v):
    try:
        return float(str(v).replace(',', '').replace('%', '').strip() or 0)
    except ValueError:
        return 0.0

def pct(x): return f'{100 * x:.1f}%'

def n0(x): return f'{int(round(x)):,}'

def todate(v):
    s = str(v or '').strip()[:10].replace('-', '')
    try:
        return datetime.strptime(s[:8], '%Y%m%d').date()
    except ValueError:
        return None

def rid_of(name): return str(name or '').rstrip('/').split('/')[-1]

def ads_id(v):
    d = re.sub(r'\D', '', str(v or ''))
    return f'{d[:3]}-{d[3:6]}-{d[6:]}' if len(d) == 10 else str(v)

def digits(v): return re.sub(r'\D', '', str(v or ''))

def regdom(h):
    parts = (h or '').lower().strip().strip('.').split(':')[0].split('.')
    n = 3 if len(parts) >= 3 and '.'.join(parts[-2:]) in MULTI_SUFFIX else 2
    return '.'.join(parts[-n:])

def host_in(h, pats):
    h = (h or '').lower()
    return any(h == p.lower().lstrip('*.') or h.endswith('.' + p.lower().lstrip('*.')) for p in pats)

def kv(s):
    out = {}
    for part in (s or '').split(';'):
        if '=' in part:
            k, v = part.split('=', 1)
            out[k.strip().lower()] = v.strip()
    return out

def items(s):
    s = (s or '').strip()
    return [] if s.lower() in ('', 'none', 'empty') else [x.strip() for x in re.split(r'[;|]', s) if x.strip()]

def conds(s):
    out = []
    for x in items(s):
        t, sep, v = x.partition(':')
        out.append((t.strip().lower(), v.strip().lower()) if sep and t.strip().lower() in MATCH else ('contains', x.lower()))
    return out

def cmatch(cs, h):
    h = (h or '').lower()
    return any((t == 'exact' and h == v) or (t == 'contains' and v in h) or (t == 'begins_with' and h.startswith(v))
               or (t == 'ends_with' and h.endswith(v)) or (t == 'regex' and re.search(v, h)) for t, v in cs)

def missing_signal(v): return bool(re.search(r'not|missing|no\b|absent', (v or '').lower()))

def lookback_days(v):
    m = re.search(r'(\d+)_DAYS', up(v))
    return int(m.group(1)) if m else None

def month_back(d, months):
    y, m = divmod(d.year * 12 + d.month - 1 - months, 12)
    return date(y, m + 1, min(d.day, 28))

def table(rep):
    """A runReport response (REST or MCP) or an export table as a list of dicts."""
    if rep is None: return None
    if '_table' in rep: return rep['_table']
    dims = [get(h, 'name') for h in get(rep, 'dimensionHeaders', [])]
    mets = [get(h, 'name') for h in get(rep, 'metricHeaders', [])]
    out = []
    for r in get(rep, 'rows', []):
        row = {k: get(v, 'value', '') for k, v in zip(dims, get(r, 'dimensionValues', []))}
        row.update({k: num(get(v, 'value', 0)) for k, v in zip(mets, get(r, 'metricValues', []))})
        out.append(row)
    return out

def row_count(rep):
    """rowCount of a runReport response; None for exports and reduced shapes."""
    if not isinstance(rep, dict) or '_table' in rep: return None
    v = get(rep, 'rowCount')
    return None if v is None else int(num(v))

def daterange(lo, hi): return [lo + timedelta(days=i) for i in range((hi - lo).days + 1)]

def thresholded(rep): return bool(get(get(rep, 'metadata', {}), 'subjectToThresholding', False))
