"""Original offline lab heuristic. Counts observed LDAP search records, not connections."""
import argparse
import csv
import ipaddress
import json
import sys
from collections import defaultdict, deque
from datetime import datetime, timezone


def detect(rows, threshold=100, window_seconds=60):
    if threshold < 1 or window_seconds < 1:
        raise ValueError('threshold and window must be positive')
    parsed, seen = [], {}
    for line, row in enumerate(rows, 2):
        try:
            record_id = row['record_id'].strip()
            if not record_id:
                raise ValueError('empty record_id')
            stamp = datetime.fromisoformat(row['timestamp'].replace('Z', '+00:00'))
            if stamp.tzinfo is None:
                raise ValueError('timestamp needs a UTC offset')
            stamp = stamp.astimezone(timezone.utc)
            source = str(ipaddress.ip_address(row['source_ip']))
            dc = str(ipaddress.ip_address(row['dc_ip']))
            if row['event_type'] != 'ldap_search':
                raise ValueError('event_type must be ldap_search; do not use connection events')
            # record_id must be unique within a DC's collection epoch/log.
            identity, value = (dc, record_id), (stamp, source)
            if identity in seen:
                if seen[identity] != value:
                    raise ValueError('conflicting duplicate record_id')
                continue
            seen[identity] = value
            parsed.append((stamp, source, dc, record_id))
        except (KeyError, ValueError, AttributeError) as exc:
            raise ValueError(f'CSV row {line}: {exc}') from exc
    windows, active, alerts = defaultdict(deque), set(), []
    for stamp, source, dc, record_id in sorted(parsed):
        key = (source, dc)
        queue = windows[key]
        # Inclusive window: events exactly window_seconds apart count together.
        while queue and (stamp - queue[0][0]).total_seconds() > window_seconds:
            queue.popleft()
        if len(queue) < threshold:
            active.discard(key)
        queue.append((stamp, record_id))
        if len(queue) >= threshold and key not in active:
            alerts.append({
                'rule': 'observed_ldap_search_burst', 'source_ip': source,
                'dc_ip': dc, 'window_start': queue[0][0].isoformat(),
                'window_end': stamp.isoformat(), 'observed_count': len(queue),
                'threshold': threshold, 'window_seconds': window_seconds,
                'record_ids': [r for _, r in queue],
                'assessment': 'Review required; volume alone does not establish reconnaissance.'
            })
            active.add(key)
    return alerts


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('csv_file')
    parser.add_argument('--threshold', type=int, default=100)
    parser.add_argument('--window-seconds', type=int, default=60)
    args = parser.parse_args()
    try:
        with open(args.csv_file, encoding='utf-8-sig', newline='') as stream:
            alerts = detect(csv.DictReader(stream), args.threshold, args.window_seconds)
        print(json.dumps({'alerts': alerts}, indent=2))
    except (OSError, ValueError, csv.Error) as exc:
        print(f'Error: {exc}', file=sys.stderr)
        return 2
    return 0


if __name__ == '__main__':
    sys.exit(main())
