#!/usr/bin/env python3
"""Bounded IP echo sampler. Python 3 + curl. No automatic modem rotation."""
import argparse
import csv
from datetime import datetime, timezone
from getpass import getpass
import ipaddress
import json
from pathlib import Path
import subprocess
import sys
import tempfile
import time
from urllib.parse import urlsplit


def quoted(value):
    if any(ord(c) < 32 or ord(c) == 127 for c in value):
        raise ValueError('Control characters are not allowed in configuration.')
    return '"' + value.replace('\\', '\\\\').replace('"', '\\"') + '"'


def sample(server, username, password, endpoint):
    """Return only sanitized fields. The CLI restricts destinations to HTTPS."""
    at = datetime.now(timezone.utc).isoformat()
    with tempfile.TemporaryDirectory() as directory:
        body = Path(directory) / 'body.json'
        config = '\n'.join([
            'proxy = ' + quoted(server),
            'proxy-user = ' + quoted(username + ':' + password),
            'url = ' + quoted(endpoint),
            'noproxy = ""', 'silent', 'connect-timeout = 5', 'max-time = 15',
            'max-filesize = 4096', 'output = ' + quoted(str(body)),
            'write-out = "%{http_code}"',
        ])
        try:
            result = subprocess.run(['curl', '--disable', '--config', '-'], input=config,
                                    text=True, capture_output=True, timeout=20, check=False)
        except (OSError, subprocess.TimeoutExpired):
            return [at, '', '', 'curl_unavailable_or_timeout']
        status = result.stdout.strip()
        status = status if status.isdigit() and len(status) == 3 else ''
        if result.returncode:
            return [at, status, '', 'transfer_failed']
        if status != '200':
            return [at, status, '', 'http_not_200']
        try:
            if body.stat().st_size > 4096:
                raise ValueError('Response too large')
            value = json.loads(body.read_text())['ip']
            if not isinstance(value, str):
                raise ValueError('Expected one IP string')
            address = str(ipaddress.ip_address(value))
        except (OSError, ValueError, KeyError, TypeError):
            return [at, status, '', 'invalid_ip_response']
        return [at, status, address, '']


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--samples', type=int, default=6)
    parser.add_argument('--interval', type=int, default=10, help='Seconds after each completed sample')
    args = parser.parse_args()
    if not 1 <= args.samples <= 60 or not 1 <= args.interval <= 300 or args.samples * (args.interval + 20) > 3600:
        parser.error('Use 1–60 samples and 1–300 seconds; total worst-case window must be at most one hour.')
    # Prompts go to stderr: redirected stdout remains valid CSV.
    def prompt(label):
        print(label, file=sys.stderr, end='', flush=True)
        return sys.stdin.readline().strip()
    server = prompt('Proxy server (http://host:port or socks5h://host:port): ')
    endpoint = prompt('Approved HTTPS JSON IP endpoint: ')
    for value, schemes in [(server, ('http', 'https', 'socks5h')), (endpoint, ('https',))]:
        parsed = urlsplit(value)
        if parsed.scheme not in schemes or not parsed.hostname or parsed.username or parsed.password or parsed.fragment:
            parser.error('Use a valid URL without embedded credentials or a fragment.')
        quoted(value)
    username = prompt('Proxy username: ')
    password = getpass('Proxy password: ')
    if not username or ':' in username or not password:
        parser.error('Username and password are required; username cannot contain a colon.')
    quoted(username); quoted(password)
    writer = csv.writer(sys.stdout)
    writer.writerow(['observed_at_utc', 'http_status', 'observed_exit_ip', 'error'])
    for index in range(args.samples):
        writer.writerow(sample(server, username, password, endpoint))
        sys.stdout.flush()
        if index + 1 < args.samples:
            time.sleep(args.interval)


if __name__ == '__main__':
    try:
        main()
    except (ValueError, KeyboardInterrupt):
        print('Stopped: invalid configuration or interruption.', file=sys.stderr)
        sys.exit(1)
