"""Audit proxied Cloudflare hosts vs. current tunnel ingress.

For each proxied record in the zone:
  - classify origin (internal LAN, public IP owned by us, external)
  - test HTTPS through CF (currently 2xx/3xx/4xx/5xx?)
  - cross-check against ingress list in config.yml

Flags which proxied hosts would benefit from being added to the tunnel.
"""
import json, os, re, socket, subprocess, urllib.error, urllib.request
import paramiko, yaml

ZONE = '1beb9917c22b54be32e5215df2c227ce'
CF_TOKEN = os.environ.get('CF_API_TOKEN_FULL_DNS', '')
if not CF_TOKEN:
    raise SystemExit('set CF_API_TOKEN_FULL_DNS env var')

# Our public IPs (from pfSense WAN)
OUR_PUBLIC_IPS = {
    '72.194.62.' + str(n) for n in range(2, 11)
} | {
    '70.175.28.' + str(n) for n in list(range(51, 55)) + [56, 57]
} | {'98.181.90.163'}

# Known internal LAN reachability from Jupiter (where tunnel runs)
LAN_HOSTS = {
    '172.16.3.10': 'IX (cPanel/WHM)',
    '172.16.3.20': 'Jupiter (this tunnel host)',
    '172.16.3.22': 'gitea',
    '172.16.3.29': 'UniFi OS Server VM',
    '172.16.0.1':  'pfSense',
}

def cfapi(path):
    req = urllib.request.Request(
        f'https://api.cloudflare.com/client/v4{path}',
        headers={'Authorization': f'Bearer {CF_TOKEN}'},
    )
    with urllib.request.urlopen(req, timeout=30) as r:
        return json.load(r)

def probe(host):
    """HEAD https://host/ with a browser UA, return (status, cf_ray_or_server)."""
    try:
        req = urllib.request.Request(f'https://{host}/', method='HEAD',
            headers={'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0'})
        with urllib.request.urlopen(req, timeout=12) as r:
            return r.status, r.headers.get('Server', '-')
    except urllib.error.HTTPError as e:
        return e.code, e.headers.get('Server', '-') if hasattr(e,'headers') else '-'
    except Exception as e:
        return 'ERR', str(e)[:40]

def load_current_ingress():
    """Pull config.yml from Jupiter and return the set of hostnames already tunneled."""
    creds = yaml.safe_load(subprocess.run(
        ['sops','-d','D:/vault/infrastructure/jupiter-unraid-primary.sops.yaml'],
        capture_output=True, text=True, timeout=30, check=True,
    ).stdout)
    c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    c.connect('172.16.3.20', username='root', password=creds['credentials']['password'],
              timeout=30, look_for_keys=False, allow_agent=False)
    _, o, _ = c.exec_command('cat /mnt/cache/appdata/cloudflared/config.yml', timeout=30)
    cfg = yaml.safe_load(o.read().decode())
    c.close()
    return {i.get('hostname') for i in cfg.get('ingress', []) if i.get('hostname')}

def classify(content, ctype):
    """Bucket the origin."""
    if ctype == 'A':
        if content in OUR_PUBLIC_IPS:
            return 'OUR_PUBLIC_IP'
        if content in LAN_HOSTS:
            return 'LAN'
        return 'EXTERNAL_IP'
    if ctype == 'CNAME':
        low = content.lower()
        if low.endswith('cfargotunnel.com'):
            return 'TUNNEL_CNAME'
        if any(low.endswith(d) for d in [
            'outlook.com','msftonline.com','microsoft.com','office.com','microsoftonline.com',
            'sendgrid.net','unbouncepages.com','msp360.com','secureserver.net',
            'azurestaticapps.net','azurefd.net','aws.com','acm-validations.aws','ucaasnetwork.com',
            'itglue.com','manage.microsoft.com','windows.net','mtasv.net','onmicrosoft.com',
        ]):
            return 'EXTERNAL_SAAS'
        if low.endswith('azcomputerguru.com'):
            return 'SELF_CNAME'
        return 'EXTERNAL_CNAME'
    return 'OTHER'

def main():
    print('[INFO] fetching DNS records...')
    a_recs = cfapi(f'/zones/{ZONE}/dns_records?type=A&per_page=100')['result']
    cname_recs = cfapi(f'/zones/{ZONE}/dns_records?type=CNAME&per_page=100')['result']
    all_recs = [r for r in a_recs + cname_recs if r.get('proxied')]
    print(f'[INFO] {len(all_recs)} proxied records')

    print('[INFO] reading current tunnel ingress...')
    tunneled = load_current_ingress()
    print(f'[INFO] currently tunneled hostnames: {sorted(tunneled)}')

    print()
    print(f'{"HOSTNAME":42} {"TYPE":6} {"TARGET":35} {"CLASS":14} {"IN_TUNNEL":10} {"HTTPS":>5} {"SERVER":10}')
    print('-' * 130)

    candidates = []
    for r in sorted(all_recs, key=lambda x: x['name']):
        name = r['name']
        ctype = r['type']
        content = r['content']
        cls = classify(content, ctype)
        in_tunnel = 'YES' if name in tunneled else ''
        status, server = probe(name)
        line = f'{name:42} {ctype:6} {content[:35]:35} {cls:14} {in_tunnel:10} {status!s:>5} {server[:10]:10}'
        print(line)
        # Candidates for tunnel: our origin (LAN or OUR_PUBLIC_IP) + not already in tunnel
        if cls in ('LAN','OUR_PUBLIC_IP') and name not in tunneled:
            candidates.append((name, content, cls, status))

    print()
    print('=' * 60)
    print('CANDIDATES FOR TUNNEL INGRESS (own origin, not yet tunneled):')
    print('=' * 60)
    if not candidates:
        print('(none)')
    for name, content, cls, status in candidates:
        print(f'  {name:42} -> {content:20} ({cls}, currently HTTP {status})')

if __name__ == '__main__':
    main()
