#!/usr/bin/env python3
"""Compare AdsBot web labels with a local special-crawler IP range snapshot."""

import argparse
import csv
import io
import ipaddress
import json
import re
from pathlib import Path

FIELDS = ['timestamp', 'client_ip', 'user_agent', 'path', 'status', 'edge_action', 'request_id']
ADS_BOT_WEB = re.compile(r'(?<![\w-])AdsBot-Google(?:-Mobile)?(?![\w-])', re.IGNORECASE)
DEMO_RANGES = {
    'creationTime': 'synthetic',
    'prefixes': [{'ipv4Prefix': '192.0.2.0/24'}, {'ipv6Prefix': '2001:db8::/32'}],
}
DEMO_LOG = """timestamp,client_ip,user_agent,path,status,edge_action,request_id
2026-11-03T08:00:00Z,192.0.2.5,AdsBot-Google,/offer,200,none,example-a
2026-11-03T08:01:00Z,2001:db8::5,Mozilla (compatible; AdsBot-Google-Mobile),/offer,200,challenge,example-b
2026-11-03T08:02:00Z,192.0.2.6,AdsBot-Google,/offer,503,none,example-c
2026-11-03T08:03:00Z,198.51.100.7,AdsBot-Google,/offer,200,none,example-d
2026-11-03T08:04:00Z,192.0.2.8,AdsBot-Google-Mobile-Apps,/offer,403,block,example-e
2026-11-03T08:05:00Z,192.0.2.9,AdsBot-Google,/other,429,rate_limit,example-f
"""


def review(log, snapshot, path):
    networks = [
        ipaddress.ip_network(record[key])
        for record in snapshot['prefixes']
        for key in ('ipv4Prefix', 'ipv6Prefix')
        if key in record
    ]
    if not networks:
        raise ValueError('The range snapshot contains no IPv4 or IPv6 prefixes.')

    rows = csv.DictReader(log, strict=True)
    if rows.fieldnames != FIELDS:
        raise ValueError('Expected these CSV columns in order: ' + ','.join(FIELDS))

    observations = []
    skipped = 0
    for row in rows:
        if None in row or any(value is None for value in row.values()):
            raise ValueError(f'CSV record ending at line {rows.line_num} has the wrong number of fields.')
        label = ADS_BOT_WEB.search(row['user_agent'])
        if row['path'] != path or label is None:
            skipped += 1
            continue
        address = ipaddress.ip_address(row['client_ip'])
        if isinstance(address, ipaddress.IPv6Address) and address.ipv4_mapped:
            address = address.ipv4_mapped
        status = int(row['status'])
        if not 100 <= status <= 599:
            raise ValueError(f'CSV record ending at line {rows.line_num} needs a valid HTTP status.')
        observations.append({
            'request_id': row['request_id'],
            'timestamp': row['timestamp'],
            'label': label.group(0),
            'range_match': any(address in network for network in networks),
            'status': status,
            'edge_action': row['edge_action'],
        })
    return {'snapshot': snapshot.get('creationTime'), 'skipped': skipped, 'observations': observations}


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--demo', action='store_true', help='Use only synthetic local records and ranges.')
    parser.add_argument('--log', type=Path, help='CSV from a single destination hostname.')
    parser.add_argument('--prefixes', type=Path, help='Local special-crawlers.json snapshot from Google.')
    parser.add_argument('--path', help='Exact logged path to compare, such as /offer.')
    args = parser.parse_args()
    try:
        if args.demo:
            if args.log or args.prefixes or args.path:
                parser.error('--demo runs by itself.')
            report = review(io.StringIO(DEMO_LOG), DEMO_RANGES, '/offer')
        else:
            if not (args.log and args.prefixes and args.path):
                parser.error('Provide --log, --prefixes and --path, or use --demo.')
            with args.log.open(encoding='utf-8-sig', newline='') as log:
                report = review(log, json.loads(args.prefixes.read_text(encoding='utf-8')), args.path)
    except (OSError, ValueError, TypeError, KeyError, csv.Error) as error:
        parser.exit(1, f'{error}\n')
    print(json.dumps(report, indent=2))


if __name__ == '__main__':
    main()
