"""Expand cloudflared ingress to cover the 9 additional proxied hostnames.

Mapping (per pfSense NAT discovery):
  ix.              .5  -> 172.16.3.10:443   (IX direct, like the existing 4)
  git./plex./plexrequest./rmm./rmm-api./sync./rustdesk.  -> 172.16.3.20:18443 via NPM
  secure.          .2  -> 172.16.1.16:443   (unknown host, try with SNI)

NPM routes on SNI, so every ingress gets originServerName = <hostname>.

Then flips their DNS (A 72.194.62.* proxied) -> CNAME tunnel proxied.
"""
import json, os, subprocess, time, urllib.request, urllib.error
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')

APPDATA = '/mnt/cache/appdata/cloudflared'

# (hostname, service-url)
IX = 'https://172.16.3.10:443'
JNPM = 'https://172.16.3.20:18443'
FULL_INGRESS = [
    # Existing 4 (IX cPanel)
    ('azcomputerguru.com', IX),
    ('analytics.azcomputerguru.com', IX),
    ('community.azcomputerguru.com', IX),
    ('radio.azcomputerguru.com', IX),
    # New IX-origin
    ('ix.azcomputerguru.com', IX),
    # Jupiter NPM-served
    ('git.azcomputerguru.com', JNPM),
    ('plex.azcomputerguru.com', JNPM),
    ('plexrequest.azcomputerguru.com', JNPM),
    ('rmm.azcomputerguru.com', JNPM),
    ('rmm-api.azcomputerguru.com', JNPM),
    ('sync.azcomputerguru.com', JNPM),
    ('rustdesk.azcomputerguru.com', JNPM),
    # Different subnet, likely pfSense-routable
    ('secure.azcomputerguru.com', 'https://172.16.1.16:443'),
]

NEW_HOSTS = [h for h,_ in FULL_INGRESS if h not in {
    'azcomputerguru.com','analytics.azcomputerguru.com',
    'community.azcomputerguru.com','radio.azcomputerguru.com'
}]

def cfapi(method, path, body=None):
    req = urllib.request.Request(
        f'https://api.cloudflare.com/client/v4{path}',
        data=json.dumps(body).encode() if body else None,
        method=method,
        headers={'Authorization': f'Bearer {CF_TOKEN}', 'Content-Type':'application/json'},
    )
    try:
        with urllib.request.urlopen(req, timeout=30) as r:
            return json.loads(r.read())
    except urllib.error.HTTPError as e:
        try: return json.loads(e.read())
        except: return {'success':False,'errors':[{'message':str(e)}]}

# -- Jupiter SSH --
def _pwd(v): return yaml.safe_load(subprocess.run(['sops','-d',v],capture_output=True,text=True,timeout=30,check=True).stdout)['credentials']['password']
j = paramiko.SSHClient(); j.set_missing_host_key_policy(paramiko.AutoAddPolicy())
j.connect('172.16.3.20', username='root', password=_pwd('D:/vault/infrastructure/jupiter-unraid-primary.sops.yaml'),
          timeout=30, look_for_keys=False, allow_agent=False)

def jrun(cmd, to=60):
    _, o, _ = j.exec_command(cmd, timeout=to)
    return o.read().decode('utf-8','replace')

try:
    # Read current tunnel UUID
    out = jrun(f'grep "^tunnel:" {APPDATA}/config.yml')
    UUID = out.split(':',1)[1].strip()
    print(f'[INFO] tunnel UUID: {UUID}')

    # Build new config.yml
    config = f'tunnel: {UUID}\n'
    config += f'credentials-file: /home/nonroot/.cloudflared/{UUID}.json\n'
    config += 'ingress:\n'
    for h, svc in FULL_INGRESS:
        config += f'  - hostname: {h}\n'
        config += f'    service: {svc}\n'
        config += f'    originRequest:\n'
        config += f'      originServerName: {h}\n'
        config += f'      noTLSVerify: true\n'
    config += '  - service: http_status:404\n'

    print('\n=== [1] write new config.yml ===')
    print(config)

    # Backup then write
    jrun(f'cp {APPDATA}/config.yml {APPDATA}/config.yml.bak-$(date +%Y%m%d-%H%M%S)')
    HEREDOC = "'EOF_CFG'"
    jrun(f"cat > {APPDATA}/config.yml <<{HEREDOC}\n{config}\nEOF_CFG")
    jrun(f'chown 65532:65532 {APPDATA}/config.yml')
    print('\n[OK] config.yml written')

    print('\n=== [2] DNS cutover for new hostnames ===')
    tunnel_target = f'{UUID}.cfargotunnel.com'
    for h in NEW_HOSTS:
        r = cfapi('GET', f'/zones/{ZONE}/dns_records?name={h}')
        if not r.get('success') or not r['result']:
            print(f'  [SKIP] {h}: no record found')
            continue
        rec = r['result'][0]
        print(f'  [{h}] current: type={rec["type"]} content={rec["content"]} proxied={rec["proxied"]}')
        if rec['type']=='CNAME' and rec['content']==tunnel_target:
            print(f'    already tunneled, skipping')
            continue
        d = cfapi('DELETE', f'/zones/{ZONE}/dns_records/{rec["id"]}')
        if not d.get('success'):
            print(f'    [FAIL delete] {d.get("errors")}')
            continue
        body = {'type':'CNAME','name':h,'content':tunnel_target,'proxied':True,'ttl':1}
        cr = cfapi('POST', f'/zones/{ZONE}/dns_records', body)
        if cr.get('success'):
            print(f'    [OK] -> CNAME tunnel proxied')
        else:
            print(f'    [FAIL create] {cr.get("errors")}')

    print('\n=== [3] restart cloudflared ===')
    print(jrun('docker restart cloudflared').rstrip())

    print('\n=== [4] wait for reconnect ===')
    for i in range(25):
        time.sleep(3)
        logs = jrun('docker logs cloudflared 2>&1 | tail -40')
        conns = logs.count('Registered tunnel connection')
        if conns >= 4 and ('INF Starting metrics' in logs or 'initiating connection' in logs or 'Registered tunnel connection connIndex=3' in logs):
            print(f'  [try {i+1}] {conns} connections registered')
            break
        print(f'  [try {i+1}] connections: {conns}')
finally:
    j.close()

# External verification
print('\n=== [5] external probe all 13 hostnames ===')
for h, _ in FULL_INGRESS:
    try:
        req = urllib.request.Request(f'https://{h}/', method='HEAD',
            headers={'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0'})
        with urllib.request.urlopen(req, timeout=15) as r:
            print(f'  {h:42} HTTP {r.status} {r.headers.get("Server","-")}')
    except urllib.error.HTTPError as e:
        print(f'  {h:42} HTTP {e.code}')
    except Exception as e:
        print(f'  {h:42} ERR {str(e)[:40]}')
