"""PII and secret detection shared by ga4_snapshot.py and audit_ga4.py. Values never leave these functions."""
import re
from urllib.parse import parse_qsl, unquote, unquote_plus, urlsplit

# Keys are compared after norm_key(): first-name, first_name and firstName are one key, and the exact
# match on the result keeps hotel= out of tel=.
PII_KEYS = {'email', 'emailaddress', 'mail', 'phone', 'phonenumber', 'tel', 'mobile', 'firstname', 'lastname', 'fullname',
            'address', 'dob', 'birthdate', 'dateofbirth', 'ssn'}
SECRET_KEYS = {'password', 'passwd', 'pwd', 'token', 'accesstoken', 'authtoken', 'idtoken', 'resettoken', 'refreshtoken',
               'apikey', 'secret', 'clientsecret', 'otp', 'auth', 'reset'}
SECRET_ENDINGS = ('token', 'password', 'secret')
AMBIGUOUS_KEYS = {'name', 'zip', 'zipcode', 'postcode', 'id'}
EMAIL_RE = re.compile(r'[A-Za-z0-9._%+-]+(@|%40)[A-Za-z0-9.-]+\.[A-Za-z]{2,}')
PHONE_RE = re.compile(r'^(\+\d[\d\s().-]{6,18}\d|\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4})$')
KEY_RE = re.compile(r'^[a-z0-9_.\[\]-]{1,40}$')

def spellings(*words):
    """A multi-word key the way a URL may write it: phonenumber, phone_number, phone-number."""
    return [sep.join(words) for sep in ('', '_', '-')]

# R15 CONTAINS terms. mail= also covers email= and e-mail=, address= covers email_address=, and token=,
# password= and secret= cover every key that ends in them. reduce_url() then drops what the filter over-catches.
R15_TERMS = ['@', '%40'] + [k + '=' for k in (
    'mail', 'address', 'phone', *spellings('phone', 'number'), 'tel', 'mobile', *spellings('first', 'name'),
    *spellings('last', 'name'), *spellings('full', 'name'), 'dob', *spellings('birth', 'date'),
    *spellings('date', 'of', 'birth'), 'ssn', 'password', 'passwd', 'pwd', 'token', *spellings('api', 'key'),
    'secret', 'otp', 'auth', 'reset')]

def norm_key(k): return re.sub(r'[^a-z0-9]', '', str(k).lower())

def key_kind(k):
    """'pii', 'secret' or 'ambiguous' for a query key name, else None."""
    nk = norm_key(k)
    if nk in PII_KEYS: return 'pii'
    if nk in SECRET_KEYS or nk.endswith(SECRET_ENDINGS): return 'secret'
    return 'ambiguous' if nk in AMBIGUOUS_KEYS else None

def safe_key(k):
    """A key name fit to store and print. An address, a long number or anything else that is
    not a plain parameter name becomes REDACTED."""
    if k == 'REDACTED': return k
    k = unquote(str(k)).strip().lower()
    return k if KEY_RE.match(k) and not EMAIL_RE.search(k) and not re.search(r'\d{5}', k) else 'REDACTED'

def reduce_url(u, field, n):
    """Host, path and query key names only; the values never leave this function."""
    try:
        sp = urlsplit(u if '//' in u else '//' + u)
    except ValueError:  # junk in page_location must not stop the audit, and an address in it still counts
        if not EMAIL_RE.search(unquote(u)): return None
        return {'field': field, 'host': '(not parsed)', 'path': '', 'keys': [], 'rule': 'email-like value', 'events': n, 'severity': 'FAIL'}
    pairs = parse_qsl(sp.query, keep_blank_values=True)
    bare = {unquote_plus(p) for p in sp.query.split('&') if p and '=' not in p}  # a value with no key
    hits = ['email-like value'] if EMAIL_RE.search(unquote(u)) else []
    hits += [f'{key_kind(k)} key {safe_key(k)}' for k, v in pairs if v and key_kind(k) in ('pii', 'secret')]
    amb = sorted({safe_key(k) for k, v in pairs if v and key_kind(k) == 'ambiguous'})
    if not hits and not amb: return None
    path = '/'.join('REDACTED' if EMAIL_RE.search(unquote(seg)) else seg for seg in sp.path.split('/'))
    return {'field': field, 'host': (sp.hostname or '').lower(), 'path': path or '/',
            'keys': sorted({'REDACTED' if k in bare else safe_key(k) for k, _ in pairs}),
            'rule': '; '.join(dict.fromkeys(hits)) or 'ambiguous key ' + ', '.join(amb),
            'events': n, 'severity': 'FAIL' if hits else 'REVIEW'}
