"""The Markdown report, the JSON output and their inventories and caveats."""
from collections import defaultdict
from datetime import timedelta
from .common import (
    NOISY, QUOTA, RETENTION, SAMPLING_LIMIT, SHARING_LABELS, ads_id, get, lookback_days, month_back, n0, pct,
    rid_of, thresholded, todate, up)
from .rules_definitions import custom_defs, custom_metric_count
from .scoring import trust

def caveats(c, fs):
    out, dr, att = [], get(c.snap, 'dataRetention'), get(c.snap, 'attributionSettings')
    m = RETENTION.get(up(get(dr, 'eventDataRetention')))
    if m:
        out.append(f'Retention: explorations and funnels reach back about {m} months (to about {month_back(c.asof, m)}); standard reports are not limited by this setting.')
    days = c.daily(None, c.start, c.end)
    if days:
        avg, lim = sum(days.values()) / len(days), SAMPLING_LIMIT[c.is360]
        out.append(f'Sampling: explorations that read more than about {int(lim // avg) if avg else "many"} days sample on this property '
                   f'(average {n0(avg)} events a day, default exploration limit {n0(lim)} events per query'
                   + ('; selecting more detailed results raises it to 1,000,000,000' if c.is360 else '') + ').')
    thr = [n for k, n in (('R13A', 'demographic probe'), ('R13B', 'key event probe')) if thresholded(c.reports.get(k))]
    others = sorted(k for k, v in c.reports.items() if k not in ('R13A', 'R13B') and isinstance(v, dict) and thresholded(v))
    if thr or others:
        out.append(f'Thresholding: rows can be withheld in {", ".join(thr + others)}; a withheld row is not a zero.')
    if att:
        out.append(f'Attribution: {up(get(att, "reportingAttributionModel"))}, lookback {lookback_days(get(att, "acquisitionConversionEventLookbackWindow"))} days for acquisition key events '
                   f'and {lookback_days(get(att, "otherConversionEventLookbackWindow"))} days for other key events. Model changes rewrite history; lookback changes apply forward.')
    ri, sg = get(c.snap, 'reportingIdentity'), get(c.snap, 'googleSignals')
    if ri or sg:
        out.append(f'Identity: reporting identity {up(get(ri, "reportingIdentity")) or "unknown"}, Google signals {up(get(sg, "state")) or "unknown"}; user counts differ between identities.')
    mod = c.uival('identity.modeling')
    if mod or (str(c.cfg.get('consent_mode') or '').lower() == 'advanced' and 'BLENDED' in up(get(ri, 'reportingIdentity'))):
        out.append('Modelled data: ' + (f'Blended reports show "{mod}"' if mod else 'not recorded; read the data quality icon on a Blended report')
                   + '. Modelled users appear only under Blended and are never in the BigQuery export, which explains part of any gap between the two (answer 11161109).')
    tz = get(c.prop, 'timeZone')
    if tz:
        ads_tz = (c.ads or {}).get('time_zone')
        out.append(f'Time zone: GA4 reports in {tz}' + (f', Google Ads in {ads_tz}' if ads_tz else '') + '; BigQuery event_timestamp is UTC, event_date uses the property time zone.')
    noisy = sorted({t.split(' ')[0] for f in fs if f.id == 'G11' for st, t in f.subs if st in ('FAIL', 'WARN') and t.split(' ')[0] in NOISY | {'form_submit'}})
    if noisy:
        out.append(f'Key events: totals of all key events include {", ".join(noisy)}; read per-event key event metrics until that is fixed.')
    out.append(f'Window: {c.start} to {c.end}; the last two days are left out because processing takes 24 to 48 hours. Google Ads data in GA4 covers a rolling 36 months.')
    return out

def inventories(c):
    inv, counts = [], c.agg('R01', 'eventName', 'eventCount')
    sev = defaultdict(float)
    for x in c.rows('R02') or []:
        if c.start <= (todate(x.get('date')) or c.start - timedelta(days=1)) <= c.end:
            sev[str(x.get('streamId'))] += x.get('eventCount', 0)
    inv.append(('Streams', [f'"{get(s, "displayName")}": {get(s, "name")}, {get(s, "type")}, {get(get(s, "webStreamData", {}), "measurementId", "")}, '
                            f'{get(get(s, "webStreamData", {}), "defaultUri", "")}, {n0(sev.get(rid_of(get(s, "name")), 0))} events in the window'
                            for s in c.streams or []]))
    inv.append(('Key events', [f'{get(k, "eventName")}: {get(k, "countingMethod")}, default value '
                               f'{(str(get(get(k, "defaultValue"), "numericValue")) + " " + str(get(get(k, "defaultValue"), "currencyCode"))) if get(k, "defaultValue") else "none"}, '
                               f'deletable {str(get(k, "deletable", "unknown")).lower()}, custom {str(get(k, "custom", "unknown")).lower()}, created {str(get(k, "createTime", ""))[:10]}, '
                               f'{n0(counts.get(get(k, "eventName"), 0))} events in the window' for k in c.kes or []]))
    links = [f'Google Ads {ads_id(get(x, "customerId"))}: personalization {"on" if get(x, "adsPersonalizationEnabled") else "off"}, manager {"yes" if get(x, "canManageClients") else "no"}'
             for x in c.ads_links or []]
    links += [f'BigQuery {get(x, "project")} ({get(x, "datasetLocation")}): daily {"on" if get(x, "dailyExportEnabled") else "off"}, streaming {"on" if get(x, "streamingExportEnabled") else "off"}, '
              f'fresh daily {"on" if get(x, "freshDailyExportEnabled") else "off"}, streams {", ".join(rid_of(s) for s in get(x, "exportStreams", [])) or "all"}, '
              f'excluded {", ".join(get(x, "excludedEvents", [])) or "none"}' for x in c.bq_links or []]
    links += [f'Firebase {get(x, "project")}' for x in get(c.snap, 'firebaseLinks') or []]
    if c.uival('links.not_in_api'): links.append(f'Other product links (intake): {c.uival("links.not_in_api")}')
    links += [f'GA4 conversion "{k.split(".", 1)[1]}" (intake): {v}' for k, v in c.uirows('conversions.') if v.lower() != 'none']
    inv.append(('Product links', links))
    ds = get(c.snap, 'dataSharing')
    if ds:
        inv.append(('Account data sharing', [', '.join(f'{label} {"on" if get(ds, key) else "off"}' for key, label in SHARING_LABELS)]))
    defs, q = custom_defs(c), QUOTA[c.is360]
    if defs is not None:
        used = defaultdict(int)
        for scope, _, _ in defs: used[scope] += 1
        inv.append(('Custom definitions', [f'{s.lower()}-scoped dimensions: {used[s]} of {q[s]}' for s in ('EVENT', 'USER', 'ITEM')]
                    + [f'custom metrics: {custom_metric_count(c)} of {q["metrics"]}, calculated metrics: {len(get(c.snap, "calculatedMetrics") or [])} of {q["calculated"]}']))
    inv.append(('Data filters (intake)', [', '.join(f'{k} {v}' for k, v in x.items()) for x in c.filters] or ['none supplied']))
    inv.append(('Channel groups', [f'"{get(g, "displayName")}": primary {"yes" if get(g, "primary") else "no"}, system defined {"yes" if get(g, "systemDefined") else "no"}, '
                                   f'{len(get(g, "groupingRule", []))} channels' for g in get(c.snap, 'channelGroups') or []]))
    aud = get(c.snap, 'audiencesCount')
    if aud is not None:
        inv.append(('Audiences and annotations', [f'audiences: {aud}, annotations: {len(get(c.snap, "annotations") or [])}']))
    return [(t, rows) for t, rows in inv if rows]

def summary_line(sc):
    k = sc['counts']
    grade = sc['grade'] or 'grade withheld, partial audit'
    shown = f'{sc["score"]}/100' if sc['score'] is not None else 'not computed'
    return (f'Score {shown} ({grade}), coverage {sc["checked"]}/{sc["applicable"]} rules. '
            f'{k["FAIL"]} FAIL, {k["WARN"]} WARN, {k["PASS"]} PASS. Nothing in GA4 was changed.')

def render(c, fs, sc, inputs):
    p, L = c.prop, []
    pid = rid_of(get(p, 'name')) or c.cfg.get('property_id', 'unknown')
    L += [f'# GA4 audit: {get(p, "displayName", "property")} (properties/{pid})', '']
    if sc['banner']: L += ['> Data loss in progress: ' + ' | '.join(sc['banner']), '']
    if sc['risk']: L += ['> Data loss risk: ' + ' | '.join(sc['risk']), '']
    nr, dec = get(c.snap, 'notReadable'), c.cfg.get('decisions_fed') or []
    L += ['## Baseline', '',
          f'- Property: properties/{pid}, {get(p, "serviceLevel", "service level unknown")}, {get(p, "propertyType", "type unknown")}, time zone {get(p, "timeZone", "unknown")}, currency {get(p, "currencyCode", "unknown")}, created {str(get(p, "createTime", "unknown"))[:10]}',
          f'- Read route: {get(c.snap, "route", "unknown")}; scopes granted: {", ".join(get(c.snap, "scopesGranted", [])) or "not recorded"}; scope check: {get(c.snap, "scopeCheck", "not recorded")}',
          f'- Read time: {get(c.snap, "readTimeUtc", "unknown")}; as of {c.asof}; data window {c.start} to {c.end}',
          f'- Inputs: {inputs}',
          f'- Script: ran scripts/audit_ga4.py, exit code {1 if any(f.status == "FAIL" for f in fs) else 0}']
    L += [f'- Not readable: {get(x, "endpoint")} ({get(x, "status")}): {get(x, "reason", "")}' for x in nr or []] or \
        [f'- Not readable: {"none" if nr is not None else "not recorded in the snapshot"}']
    L.append(f'- Decisions this property feeds: {", ".join(dec) if dec else "not recorded (config decisions_fed)"}')
    grade = sc['grade'] or 'grade withheld: partial audit' + (f' (core rules not checked: {", ".join(sc["core_not_checked"])})' if sc['core_not_checked'] else ' (coverage below 70%)')
    L += ['', '## Score', '', f'- Score {str(sc["score"]) + "/100" if sc["score"] is not None else "not computed"}, {grade}, coverage {sc["checked"]} of {sc["applicable"]} applicable rules ({pct(sc["coverage"])}).']
    if sc['grade_limit']:
        why = ' and '.join(([' and '.join(sc['caps']) + ' FAIL'] if sc['caps'] else []) + (['data loss in progress'] if sc['banner'] else [])) or 'a FAIL stands'
        L.append(f'- Cap: {why}, so the grade can be no better than {sc["grade_limit"]}.')
    L += [f'- {a} {n}: ' + (f'{round(v * 100)}' if v is not None else 'not scored') + f' (weight {w})' for a, (n, w, v) in sc['areas'].items()]
    L.append(f'- Previous audit: {c.cfg.get("previous_audit") or "not recorded (config previous_audit)"}')
    L += ['', '## Trust summary', '']
    for t in trust(c, fs):
        L.append(f'- {t["family"]}: {t["label"]}' + (f' ({", ".join(t["because"])})' if t['because'] else '')
                 + (f'; not checked: {", ".join(t["not_checked"])}' if t['not_checked'] else ''))
    L += ['', '## Analyst caveats', ''] + [f'- {x}' for x in caveats(c, fs)]
    for st in ('FAIL', 'WARN', 'PASS'):
        L += ['', f'## {st}', '']
        for f in [f for f in fs if f.status == st]:
            if st == 'PASS':
                L.append(f'- {f.id} {f.slug}: {f.ok_text}' + (f' (partial: {"; ".join(f.partial)})' if f.partial else ''))
                continue
            L.append(f'- {f.id} {f.slug} (priority {f.prio}):')
            L += [f'  - {s} {t}' for s, t in sorted(f.subs, key=lambda x: x[0] != 'FAIL')] + [f'  - partial: {x}' for x in f.partial]
    L += ['', '## Not checked', ''] + ([f'- {f.id} {f.slug}: {f.reason}' for f in fs if f.status == 'NOT CHECKED'] or ['- none'])
    L += ['', '## Not applicable', ''] + ([f'- {f.id} {f.slug}: {f.reason}' for f in fs if f.status == 'N/A'] or ['- none'])
    order = sorted([f for f in fs if f.status in ('FAIL', 'WARN')], key=lambda f: (f.prio, f.status != 'FAIL', f.id))
    L += ['', '## Change list order', ''] + [f'- {f.prio} {f.id} {f.status}' for f in order]
    L += ['', '## Inventories']
    for title, rows in inventories(c): L += ['', f'### {title}', ''] + [f'- {r}' for r in rows]
    L += ['', '## Proposed annotations', ''] + ([f'- {a["date"]}: "{a["title"]}". {a["description"]}' for a in c.proposed] or ['- none'])
    L += ['', '## Thresholds used', '', '```text'] + [f'{k} = {v}' for k, v in c.th.items()] + ['```']
    L += ['', '## Next step', '', '- Write the change list from assets/change-list-template.md, ordered as above. Confirm each FAIL in G06 to G13, G16, G18, G23 and G24 before calling it confirmed.',
          '- Proposed context updates: the agent lists the stack.md and decisions.md changes here; nothing is written until the user approves.',
          '', summary_line(sc)]
    return '\n'.join(L) + '\n'

def as_json(c, fs, sc):
    return {'property': rid_of(get(c.prop, 'name')), 'as_of': c.asof.isoformat(), 'window': [c.start.isoformat(), c.end.isoformat()],
            'score': {k: v for k, v in sc.items() if k != 'areas'} | {'areas': {a: {'name': n, 'weight': w, 'score': v} for a, (n, w, v) in sc['areas'].items()}},
            'findings': [{'id': f.id, 'slug': f.slug, 'area': f.area, 'status': f.status, 'priority': f.prio, 'reason': f.reason,
                          'subs': [{'status': s, 'text': t} for s, t in f.subs], 'partial': f.partial, 'pass': f.ok_text} for f in fs],
            'trust': trust(c, fs), 'caveats': caveats(c, fs), 'inventories': dict(inventories(c)), 'annotations': c.proposed,
            'thresholds': c.th, 'summary': summary_line(sc)}
