"""Generate synthetic TLS flow test data matching TlsDB.csv column spec.

Outputs 1998 CSV files (0.csv .. 1997.csv) with realistic TLS flow data.
Columns match TlsDB.csv exactly; per-file column order is randomized.
Columns that are all-blank in a given file are auto-dropped from that file's header.

Usage:
    runtime\\python\\python.exe scripts\\gen_test_data.py [--files N] [--rows N] [--output-dir DIR]
"""
import random
import csv
import argparse
from pathlib import Path
from datetime import datetime, timedelta

random.seed(42)

# ── Column spec (exactly matches TlsDB.csv — 'row' removed because not in TlsDB) ──

COLUMNS = [
    ':ips', ':ipd', ':prs', ':prd', 'scnt', 'dcnt',
    ':ips.latd', ':ips.lond', ':ipd.latd', ':ipd.lond',
    ':ips.ispn', ':ipd.ispn', ':ips.orgn', ':ipd.orgn',
    ':ips.city', ':ipd.city', ':ips.anon', ':ipd.anon',
    ':ips.doma', ':ipd.doma',
    'server-ip', 'client-ip', 'time', 'timestamp',
    '1ipp', '4dbn', 'tabl', '4ksz', 'cnrs', 'isrs',
    'cnam', '0ver', 'snam', '4dur', '8seq', '2tmo', 'name',
    'source-node', 'cipher-suite', 'ecdhe-named-curve',
    '0cph', '0crv', '0rnd', '0rnt',
    '8ack', '8pak', '8did', '4srs', '8ppk', '8ses', '8byt', '8dbd',
    'crcc', 'orga', 'orgu', 'eiph', '@iph',
]

# ── Data pools ───────────────────────────────────────────────────────────

TLS_VERSIONS = ['03 03', '03 04', '02 00', '03 01', '03 02']
TLS_WEIGHTS = [0.55, 0.30, 0.05, 0.05, 0.05]

CIPHER_HEX = ['c0 2b', 'c0 2f', '13 01', '13 02', 'c0 2c', 'cc a9', 'c0 23',
              'c0 27', 'c0 13', 'c0 14', '00 9e', '00 9f', '00 35']
CIPHER_NAMES = [
    'TLS_ECDHE_ECDSA_AES128_GCM_SHA256', 'TLS_ECDHE_RSA_AES128_GCM_SHA256',
    'TLS_AES_128_GCM_SHA256', 'TLS_AES_256_GCM_SHA384',
    'TLS_ECDHE_ECDSA_AES256_GCM_SHA384', 'TLS_ECDHE_ECDSA_CHACHA20_POLY1305',
    'TLS_ECDHE_RSA_AES256_GCM_SHA384', 'TLS_ECDHE_RSA_AES128_SHA256',
    'TLS_ECDHE_ECDSA_AES128_SHA', 'TLS_ECDHE_RSA_AES128_SHA',
    'TLS_RSA_WITH_AES_128_GCM_SHA256', 'TLS_RSA_WITH_AES_256_GCM_SHA384',
    'TLS_RSA_WITH_AES_128_CBC_SHA',
]

NAMED_CURVES_HEX = ['00 1d', '00 17', '00 18', '00 19', '00 1e']
NAMED_CURVES_TEXT = ['secp256r1', 'secp384r1', 'secp521r1', 'x25519', 'x448']

SRC_IPS = [f'{random.randint(1,223)}.{random.randint(0,255)}.{random.randint(0,255)}.{random.randint(1,254)}' for _ in range(200)]
DST_IPS = [f'{random.randint(1,223)}.{random.randint(0,255)}.{random.randint(0,255)}.{random.randint(1,254)}' for _ in range(500)]

PORTS = [443, 80, 8080, 8443, 465, 993, 995, 53, 22, 3389, 25, 110, 143, 21, 12345, 50000, 443, 443, 443, 443]
COUNTRIES = ['US','CN','KR','JP','GB','DE','FR','RU','BR','IN','SG','NL','CA','AU','HK','UA','IL','SE','NO','FI']
ISPS = ['China Telecom','China Mobile','BT Group','Orange','Deutsche Telekom','AT&T','Verizon','Comcast','NTT','KDDI','SK Telecom','Singtel','Telstra']
ORGS = ['Baidu Inc.','Alibaba Inc.','Amazon.com Inc.','Google LLC','Microsoft Corp.','Meta Platforms','Tencent','Samsung','Apple Inc.','Netflix Inc.']
CITIES = ['Beijing','Shanghai','Seoul','Berlin','Paris','London','New York','San Francisco','Tokyo','Singapore','Sydney','Moscow','Seattle','Dublin','Mumbai']
SERVICES = ['mail.example.com','api.example.com','cdn.example.com','auth.example.net','stream.example.org','login.live.com','*.cloudfront.net','*.s3.amazonaws.com','graph.facebook.com','www.google.com']
SRC_NODES = ['packet_capture','ssl_logs','netflow','zeek','suricata']
NODE_NAMES = ['wan-link','core-02','gw-09','edge-01','backbone-03']


def load_coverage(tlsdb_path=None):
    """Parse TlsDB.csv and return {column_name: coverage_float}.

    Locates TlsDB.csv relative to the scripts/ directory (project root).
    """
    if tlsdb_path is None:
        tlsdb_path = Path(__file__).parent.parent / 'TlsDB.csv'
    coverage = {}
    with open(tlsdb_path, 'r', encoding='utf-8') as f:
        reader = csv.DictReader(f)
        for row in reader:
            name = row['标题'].strip()
            pct_str = row.get('覆盖率', '100%').strip().rstrip('%')
            try:
                coverage[name] = float(pct_str) / 100.0
            except ValueError:
                coverage[name] = 1.0
    return coverage


COVERAGE = load_coverage()


def _rand_ip(pool):
    return random.choice(pool)


def _rand_hex(n):
    return ' '.join(f'{random.randint(0,255):02x}' for _ in range(n))


def _maybe(rate):
    """Return True with given probability (0.0-1.0)."""
    return random.random() < rate


def _blank_or(rate, value, blank_marker=''):
    """Return value with rate probability, else blank_marker."""
    return value if _maybe(rate) else blank_marker


def _blank_or_plus(rate, value):
    """Return value with rate probability, else '+' (null marker)."""
    return value if _maybe(rate) else '+'


def generate_row(row_id, base_time):
    """Generate a single TLS flow row matching all TlsDB.csv columns.

    row_id is a global sequential counter used to derive:
      - time: base_time.strftime('%Y-%m-%d %H:%M:%S.') + f'{row_id % 1000000:06d}'
      - timestamp: round(base_time.timestamp() + row_id * 0.000001, 6)
    """
    src_ip = _rand_ip(SRC_IPS)
    dst_ip = _rand_ip(DST_IPS)
    src_port = random.choice(PORTS)
    dst_port = random.choice(PORTS)
    src_country = random.choice(COUNTRIES)
    dst_country = random.choice(COUNTRIES)
    server_ip = _rand_ip(DST_IPS + SRC_IPS)
    client_ip = src_ip if _maybe(0.85) else _rand_ip(SRC_IPS)
    tls_ver = random.choices(TLS_VERSIONS, weights=TLS_WEIGHTS, k=1)[0]
    cipher_hex = random.choice(CIPHER_HEX)
    cipher_name = random.choice(CIPHER_NAMES)
    curve_hex = random.choice(NAMED_CURVES_HEX)
    curve_name = random.choice(NAMED_CURVES_TEXT)
    service = random.choice(SERVICES)
    cert_cn = service if _maybe(0.7) else f'*.{random.choice(SERVICES).split(".")[-2:]}'

    # (a) time: YYYY-MM-DD HH:MM:SS.XXXXXX
    time_str = base_time.strftime('%Y-%m-%d %H:%M:%S.') + f'{row_id % 1000000:06d}'
    # (b) timestamp: float with 6 decimals
    ts = round(base_time.timestamp() + row_id * 0.000001, 6)

    # Row-local lat/lon with realistic blank rates
    src_lat = round(random.uniform(-60, 60), 6)
    src_lon = round(random.uniform(-180, 180), 6)
    dst_lat = round(random.uniform(-60, 60), 6)
    dst_lon = round(random.uniform(-180, 180), 6)

    return {
        # ── Core (always present) ──
        ':ips': src_ip,
        ':ipd': dst_ip,
        ':prs': src_port,
        ':prd': dst_port,
        'time': time_str,
        'timestamp': ts,

        # ── TLS fingerprint (very high coverage) ──
        '0ver': tls_ver,
        '0cph': cipher_hex,
        'cipher-suite': _blank_or(COVERAGE.get('cipher-suite', 1.0), cipher_name),
        'ecdhe-named-curve': _blank_or(COVERAGE.get('ecdhe-named-curve', 1.0), curve_name),
        '0crv': _blank_or(COVERAGE.get('0crv', 1.0), curve_hex),
        '0rnd': _blank_or(COVERAGE.get('0rnd', 1.0), _rand_hex(28)),
        '0rnt': _blank_or(COVERAGE.get('0rnt', 1.0), _rand_hex(4)),
        '4ksz': _blank_or(COVERAGE.get('4ksz', 1.0), str(random.choice([128, 256, 384, 512]))),

        # ── Session / connection info (medium-high coverage) ──
        'snam': _blank_or(COVERAGE.get('snam', 1.0), service),
        'cnam': _blank_or(COVERAGE.get('cnam', 1.0), cert_cn),
        'server-ip': _blank_or(COVERAGE.get('server-ip', 1.0), server_ip),
        'client-ip': _blank_or(COVERAGE.get('client-ip', 1.0), client_ip),
        '4dur': _blank_or(COVERAGE.get('4dur', 1.0), str(round(random.uniform(0.5, 120.0), 2))),
        '8ack': _blank_or(COVERAGE.get('8ack', 1.0), str(random.randint(100, 200000))),
        '8pak': _blank_or(COVERAGE.get('8pak', 1.0), str(random.randint(1, 2000))),
        '8ppk': _blank_or(COVERAGE.get('8ppk', 1.0), str(random.randint(1, 500))),
        '8seq': _blank_or(COVERAGE.get('8seq', 1.0), str(round(random.uniform(1, 2000), 4))),
        '8ses': _blank_or(COVERAGE.get('8ses', 1.0), str(round(random.uniform(1, 2000), 4))),
        '8byt': _blank_or(COVERAGE.get('8byt', 1.0), str(random.randint(200, 500000))),
        '2tmo': _blank_or(COVERAGE.get('2tmo', 1.0), str(round(random.uniform(0, 1000), 4))),

        # ── TCP / network info (medium coverage) ──
        '1ipp': str(random.randint(1, 50)),
        'tabl': random.choice(['TlsC', 'TlsS']),
        'name': _blank_or(COVERAGE.get('name', 1.0), random.choice(NODE_NAMES)),
        'source-node': _blank_or(COVERAGE.get('source-node', 1.0), random.choice(SRC_NODES)),

        # ── Session resumption (boolean) ──
        'cnrs': _blank_or(COVERAGE.get('cnrs', 1.0), '+'),
        'isrs': _blank_or(COVERAGE.get('isrs', 1.0), '+'),

        # ── Database metadata (low-medium coverage) ──
        '4dbn': _blank_or(COVERAGE.get('4dbn', 1.0), str(random.randint(1, 100))),
        '8dbd': _blank_or(COVERAGE.get('8dbd', 1.0), str(random.randint(1, 200))),
        '8did': _blank_or(COVERAGE.get('8did', 1.0), str(random.randint(1000, 9999))),
        '4srs': _blank_or(COVERAGE.get('4srs', 1.0), str(random.randint(10000, 99999))),

        # ── GeoIP location ──
        ':ips.latd': _blank_or_plus(COVERAGE.get(':ips.latd', 1.0), str(src_lat)),
        ':ips.lond': _blank_or_plus(COVERAGE.get(':ips.lond', 1.0), str(src_lon)),
        ':ipd.latd': _blank_or_plus(COVERAGE.get(':ipd.latd', 1.0), str(dst_lat)),
        ':ipd.lond': _blank_or_plus(COVERAGE.get(':ipd.lond', 1.0), str(dst_lon)),
        ':ips.ispn': _blank_or(COVERAGE.get(':ips.ispn', 1.0), random.choice(ISPS)),
        ':ipd.ispn': _blank_or(COVERAGE.get(':ipd.ispn', 1.0), random.choice(ISPS)),
        ':ips.orgn': _blank_or(COVERAGE.get(':ips.orgn', 1.0), random.choice(ORGS)),
        ':ipd.orgn': _blank_or(COVERAGE.get(':ipd.orgn', 1.0), random.choice(ORGS)),
        ':ips.city': _blank_or(COVERAGE.get(':ips.city', 1.0), random.choice(CITIES)),
        ':ipd.city': _blank_or(COVERAGE.get(':ipd.city', 1.0), random.choice(CITIES)),
        ':ips.anon': _blank_or(COVERAGE.get(':ips.anon', 1.0), 'anon, hosting'),
        ':ipd.anon': _blank_or(COVERAGE.get(':ipd.anon', 1.0), 'anon, hosting'),
        ':ips.doma': _blank_or(COVERAGE.get(':ips.doma', 1.0), random.choice(SERVICES)),
        ':ipd.doma': _blank_or(COVERAGE.get(':ipd.doma', 1.0), random.choice(SERVICES)),

        # ── Two-letter / abbreviation columns ──
        # (c) scnt/dcnt: .lower() → e.g. 'us', 'cn'
        'scnt': _blank_or(COVERAGE.get('scnt', 1.0), src_country.lower()),
        'dcnt': _blank_or(COVERAGE.get('dcnt', 1.0), dst_country.lower()),
        'crcc': _blank_or(COVERAGE.get('crcc', 1.0), random.choice(['OK', 'ER', '--'])),
        '@iph': _blank_or(COVERAGE.get('@iph', 1.0), random.choice(['A', 'B', 'C', 'D'])),
        'eiph': _blank_or_plus(COVERAGE.get('eiph', 1.0), '+'),

        # ── Obscure / rarely populated fields ──
        'orga': _blank_or(COVERAGE.get('orga', 1.0), random.choice(ORGS)),
        'orgu': _blank_or(COVERAGE.get('orgu', 1.0), str(random.randint(1000, 99999))),
    }


def main():
    parser = argparse.ArgumentParser(
        description='Generate TLS flow test data matching TlsDB.csv — '
                    '1998 files (0.csv..1997.csv) with randomized column order per file'
    )
    parser.add_argument('--files', type=int, default=1998,
                        help='Number of CSV files (default: 1998)')
    parser.add_argument('--rows', type=int, default=100,
                        help='Rows per file (default: 100)')
    parser.add_argument('--output-dir', type=str, default='data/test_csvs',
                        help='Output directory (default: data/test_csvs)')
    parser.add_argument('--seed', type=int, default=42,
                        help='Random seed for data generation (default: 42)')
    args = parser.parse_args()

    random.seed(args.seed)
    out_dir = Path(args.output_dir)
    out_dir.mkdir(parents=True, exist_ok=True)
    base_time = datetime(2026, 6, 1, 0, 0, 0)

    total_rows = 0

    for file_idx in range(args.files):
        # File-specific RNG for column-order randomization (g)
        file_seed = args.seed + file_idx
        file_rng = random.Random(file_seed)

        # Generate all rows for this file
        rows = []
        for row_i in range(args.rows):
            row = generate_row(total_rows + row_i, base_time)
            rows.append(row)

        # (f) Auto-drop columns that are ALL blank ('' or '+') across every row
        non_blank_cols = set()
        for row in rows:
            for col in COLUMNS:
                val = row.get(col, '')
                if val != '' and val != '+':
                    non_blank_cols.add(col)
        file_columns = [c for c in COLUMNS if c in non_blank_cols]

        # (g) Randomize column order per file — high+low coverage interleaved
        shuffled = file_columns[:]
        file_rng.shuffle(shuffled)

        # (e) Write: 0.csv, 1.csv, ... 1997.csv
        out_path = out_dir / f'{file_idx}.csv'
        with open(out_path, 'w', newline='', encoding='utf-8') as f:
            writer = csv.DictWriter(f, fieldnames=shuffled, extrasaction='ignore')
            writer.writeheader()
            for row in rows:
                writer.writerow(row)

        total_rows += args.rows

        if (file_idx + 1) % 100 == 0 or file_idx == 0 or file_idx == args.files - 1:
            print(f'  [{file_idx + 1}/{args.files}] {total_rows} rows, '
                  f'{len(shuffled)} cols, seed={file_seed}')

    print(f'\nDone: {args.files} files × {args.rows} rows = {total_rows} total rows')
    print(f'Output: {out_dir.resolve()}')


if __name__ == '__main__':
    main()
