#!/usr/bin/env python3
"""KVOCloud SEO-16-02: reproducible, low-rate TCP connect latency sampler.

Use only on endpoints you own or have express permission to test.
The measured quantity is TCP connection setup time, NOT ICMP ping or TTFB.
No connections are made without --targets. Stdlib-only, Windows/Linux/macOS.
"""
import argparse
import csv
import datetime as dt
import math
import pathlib
import socket
import statistics
import time


def parse_target(text):
    if '=' not in text:
        raise argparse.ArgumentTypeError('Use name=host:port (e.g. SG=YOUR_VPS_HOST:443)')
    name, hostport = text.split('=', 1)
    try:
        host, port_text = hostport.rsplit(':', 1)
        port = int(port_text)
    except ValueError as exc:
        raise argparse.ArgumentTypeError('Use name=host:port') from exc
    if not name.strip() or not host.strip() or not 1 <= port <= 65535:
        raise argparse.ArgumentTypeError('Invalid name/host/port')
    return (name.strip(), host.strip(), port)


def p95(values):
    # Nearest-rank percentile; document the method for replication.
    ordered = sorted(values)
    return ordered[max(0, math.ceil(.95 * len(ordered)) - 1)]


def main():
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument('--targets', nargs='+', type=parse_target, required=True,
                    help='name=host:port for YOUR authorized endpoints; 2+ targets for fair comparison')
    ap.add_argument('--client-label', required=True, help='Anonymous ID, e.g. VN-HN-ISP-A')
    ap.add_argument('--network-type', default='unknown', help='FTTH/4G/5G etc')
    ap.add_argument('--window', required=True, help='morning/afternoon/evening')
    ap.add_argument('--samples', type=int, default=20, help='Samples per endpoint, 10..50')
    ap.add_argument('--interval', type=float, default=0.3, help='Delay between each connect, seconds, 0.2..10')
    ap.add_argument('--timeout', type=float, default=3.0, help='Connection timeout in seconds, 1..10')
    ap.add_argument('--output-dir', default='results')
    args = ap.parse_args()
    if not (10 <= args.samples <= 50) or not (.2 <= args.interval <= 10) or not (1 <= args.timeout <= 10):
        ap.error('Samples must be 10..50, interval 0.2..10 sec, timeout 1..10 sec')
    names = [x[0] for x in args.targets]
    if len(names) != len(set(names)):
        ap.error('Target names must be unique')
    if len(args.targets) > 6:
        ap.error('At most 6 explicitly authorized endpoints')
    outdir = pathlib.Path(args.output_dir)
    outdir.mkdir(parents=True, exist_ok=True)
    stamp = dt.datetime.now(dt.timezone.utc).strftime('%Y%m%dT%H%M%SZ')
    raw_path = outdir / ('kvo_tcp_raw_' + stamp + '.csv')
    sum_path = outdir / ('kvo_tcp_summary_' + stamp + '.csv')
    raw_rows = []
    print('Method: client-side TCP connect setup time, not ICMP RTT or HTTP TTFB.')
    print('Permission required for ALL targets. Client:', args.client_label, 'window:', args.window)
    for sample in range(1, args.samples + 1):
        # Alternate targets per iteration to avoid systematic time-of-day bias.
        for index in range(len(args.targets)):
            name, host, port = args.targets[(sample - 1 + index) % len(args.targets)]
            utc = dt.datetime.now(dt.timezone.utc).isoformat(timespec='seconds')
            start = time.perf_counter()
            success, error = 1, ''
            try:
                with socket.create_connection((host, port), timeout=args.timeout):
                    pass
            except (OSError, TimeoutError) as exc:
                success, error = 0, type(exc).__name__
            elapsed_ms = round((time.perf_counter() - start) * 1000, 3)
            raw_rows.append([utc, args.client_label, args.network_type, args.window,
                             name, host, port, sample, success, elapsed_ms if success else '', error])
            time.sleep(args.interval)
    with raw_path.open('w', newline='', encoding='utf-8-sig') as f:
        w = csv.writer(f); w.writerow(['timestamp_utc', 'client_label', 'network_type', 'window',
            'target', 'host', 'port', 'sample_no', 'ok', 'tcp_connect_ms', 'error_type']); w.writerows(raw_rows)
    with sum_path.open('w', newline='', encoding='utf-8-sig') as f:
        w = csv.writer(f); w.writerow(['timestamp_utc', 'client_label', 'network_type', 'window',
            'target', 'samples', 'success', 'error_rate_pct', 'median_tcp_ms', 'p95_tcp_ms', 'min_tcp_ms', 'max_tcp_ms', 'method'])
        for name, host, port in args.targets:
            subset = [r for r in raw_rows if r[4] == name]
            valid = [float(r[9]) for r in subset if r[8] == 1]
            w.writerow([stamp, args.client_label, args.network_type, args.window,
                name, len(subset), len(valid), round(100*(1-len(valid)/len(subset)), 2),
                round(statistics.median(valid), 3) if valid else '',
                round(p95(valid), 3) if valid else '',
                round(min(valid), 3) if valid else '',
                round(max(valid), 3) if valid else '', 'tcp_connect'])
            print(name, 'ok', len(valid), '/', len(subset),
                  'median_ms', round(statistics.median(valid), 3) if valid else 'N/A')
    print('Saved:', raw_path, sum_path)
    print('Review host IPs in CSV before sharing externally; redact them if necessary.')


if __name__ == '__main__':
    main()
