#!/usr/bin/env python3
"""
intSignal Research - California Cybersecurity Risk Report 2027
Analysis of California Attorney General data breach register (list-export CSV capture).

Input:  raw-data/ca_ag_breach_register_capture.csv
        Verbatim capture of https://oag.ca.gov/privacy/databreach/list-export
        accessed 2026-08-25. Capture window: notices with Reported Date
        2024-01-02 through 2026-08-24 (all notices in that window).

Rules (documented, no silent cleaning):
- A row = one notification submitted to the CA AG (notices != unique incidents;
  the same incident can generate multiple notices).
- Annual counts = count of rows by calendar year of Reported Date.
- Lag analysis: lag_days = Reported Date - EARLIEST parseable breach date.
  Rows excluded from lag analysis (counted and reported, never deleted):
  (a) empty breach date; (b) no breach date <= reported date (source data
  errors, e.g., future-dated breach dates). All rows retained in counts.
- SB 446 comparison: notices whose earliest breach date is on/after 2026-01-01
  (post-SB 446 discovery era, effective 1/1/2026) vs earlier breach dates,
  among notices reported in 2026.
- Repeat notifiers: normalized org name (casefold, strip, collapse spaces)
  exact-match; conservative (does not merge name variants).
"""
import csv, statistics
from datetime import datetime, date
from collections import Counter, defaultdict

SRC = "/home/claude/ccrr2027/raw-data/ca_ag_breach_register_capture.csv"

def parse_d(s):
    try:
        return datetime.strptime(s.strip(), "%m/%d/%Y").date()
    except Exception:
        return None

rows = []
with open(SRC, newline='', encoding='utf-8') as f:
    r = csv.reader(f)
    header = next(r)
    for org, bdates, rep in r:
        rep_d = parse_d(rep)
        bs = [parse_d(x) for x in bdates.split(",")] if bdates.strip() else []
        bs = [b for b in bs if b]
        rows.append({"org": org.strip(), "breach_dates": bs, "rep": rep_d, "raw_b": bdates})

print(f"Total notices in capture: {len(rows)}")
by_year = Counter(x["rep"].year for x in rows)
print("Notices by reported year:", dict(sorted(by_year.items())))

# 2026 YTD pace vs 2025 same period (Jan 1 - Aug 24)
p2025 = sum(1 for x in rows if x["rep"].year==2025 and x["rep"] <= date(2025,8,24))
p2026 = sum(1 for x in rows if x["rep"].year==2026 and x["rep"] <= date(2026,8,24))
print(f"Same-period (Jan 1-Aug 24) notices: 2025={p2025}  2026={p2026}  change={100*(p2026-p2025)/p2025:.1f}%")

# Lag analysis
def lag_stats(sub, label):
    lags, no_bdate, bad = [], 0, 0
    for x in sub:
        if not x["breach_dates"]:
            no_bdate += 1; continue
        valid = [b for b in x["breach_dates"] if b <= x["rep"]]
        if not valid:
            bad += 1; continue
        lags.append((x["rep"] - min(valid)).days)
    lags.sort()
    if lags:
        med = statistics.median(lags)
        mean = statistics.mean(lags)
        p90 = lags[int(0.9*len(lags))-1] if len(lags)>=10 else max(lags)
        over180 = sum(1 for l in lags if l>180); over365 = sum(1 for l in lags if l>365)
        within30 = sum(1 for l in lags if l<=30); within60 = sum(1 for l in lags if l<=60)
        print(f"[{label}] n_lag={len(lags)} (excluded: {no_bdate} no breach date, {bad} unusable dates) | "
              f"median={med:.0f}d mean={mean:.0f}d p90={p90}d max={max(lags)}d | "
              f"<=30d: {100*within30/len(lags):.1f}%  <=60d: {100*within60/len(lags):.1f}%  "
              f">180d: {100*over180/len(lags):.1f}%  >365d: {100*over365/len(lags):.1f}%")
    return lags

for yr in (2024, 2025, 2026):
    lag_stats([x for x in rows if x["rep"].year==yr], f"reported {yr}")
lag_stats(rows, "all 2024-2026YTD")

# SB 446 era: notices reported in 2026 for breaches that BEGAN on/after 1/1/2026
sub26 = [x for x in rows if x["rep"].year==2026 and x["breach_dates"]]
post = [x for x in sub26 if min(x["breach_dates"]) >= date(2026,1,1)]
pre  = [x for x in sub26 if min(x["breach_dates"]) <  date(2026,1,1)]
lag_stats(post, "2026-reported, breach began on/after 2026-01-01 (SB446 era)")
lag_stats(pre,  "2026-reported, breach began before 2026-01-01 (legacy incidents)")

# Repeat notifiers
norm = lambda s: " ".join(s.casefold().split())
c = Counter(norm(x["org"]) for x in rows)
print("\nTop repeat notifiers (normalized exact name, 2024-2026YTD):")
for name, n in c.most_common(15):
    if n >= 4:
        print(f"  {n}x  {name}")

# Notable single incidents producing multi-org cascades (same breach-date signature)
sig = defaultdict(set)
for x in rows:
    if x["raw_b"].strip():
        sig[x["raw_b"].strip()].add(norm(x["org"]))
print("\nBreach-date signatures shared by 5+ distinct organizations (vendor-cascade indicator):")
for s, orgs in sorted(sig.items(), key=lambda kv: -len(kv[1])):
    if len(orgs) >= 5:
        print(f"  {len(orgs)} orgs | breach date(s): {s}")
