#!/usr/bin/env python3
"""
Verizon MiFi DNS Block Detector Agent  v2
Runs as a Windows Scheduled Task — reports to tools.vibelogicllc.com

DNS checks:  always available (runs as SYSTEM, no display needed)
Chrome check: runs when triggered, uses the system Chrome + Selenium
"""

import time, json, socket, os, subprocess, logging, configparser
from urllib.parse import urlparse
from datetime import datetime, timezone

try:
    import dns.resolver
    import requests as _requests
except ImportError:
    raise SystemExit('Run: pip install dnspython requests selenium webdriver-manager')

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
cfg = configparser.ConfigParser()
cfg.read(os.path.join(BASE_DIR, 'config.ini'))

SERVER_URL    = cfg.get('agent', 'server_url',   fallback='https://tools.vibelogicllc.com').rstrip('/')
TOKEN         = cfg.get('agent', 'token',         fallback='')
POLL_INTERVAL = cfg.getint('agent', 'poll_interval', fallback=10)

logging.basicConfig(
    filename=os.path.join(BASE_DIR, 'agent.log'),
    level=logging.INFO,
    format='%(asctime)s %(levelname)s %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S',
)
logging.getLogger('urllib3').setLevel(logging.WARNING)
logging.getLogger('selenium').setLevel(logging.WARNING)
logging.getLogger('WDM').setLevel(logging.WARNING)

def now_iso():
    return datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')

# ── DNS helpers ───────────────────────────────────────────────────────────────
def get_system_dns():
    try:
        out = subprocess.check_output(['ipconfig', '/all'], text=True, timeout=10)
        servers, capture = [], False
        for line in out.splitlines():
            if 'DNS Servers' in line:
                capture = True
                part = line.split(':', 1)[-1].strip()
                if part: servers.append(part)
            elif capture and line.startswith(' ' * 30):
                ip = line.strip()
                if ip: servers.append(ip)
            elif capture:
                capture = False
        return servers or ['(unknown)']
    except Exception as e:
        return [f'error: {e}']

def resolve_ns(domain, nameservers, label):
    r = dns.resolver.Resolver(configure=False)
    r.nameservers = nameservers
    r.timeout = 5; r.lifetime = 8
    try:
        return {'ips': sorted([str(a) for a in r.resolve(domain, 'A')]), 'error': None, 'via': label}
    except dns.resolver.NXDOMAIN:
        return {'ips': [], 'error': 'NXDOMAIN', 'via': label}
    except dns.resolver.NoAnswer:
        return {'ips': [], 'error': 'No answer', 'via': label}
    except Exception as e:
        return {'ips': [], 'error': str(e)[:100], 'via': label}

def resolve_system(domain):
    try:
        ips = sorted(set(i[4][0] for i in socket.getaddrinfo(domain, None, socket.AF_INET)))
        return {'ips': ips, 'error': None, 'via': 'Verizon (system)'}
    except Exception as e:
        return {'ips': [], 'error': str(e)[:100], 'via': 'Verizon (system)'}

def compare_dns(domain):
    sys_dns = get_system_dns()
    vz   = resolve_system(domain)
    goog = resolve_ns(domain, ['8.8.8.8', '8.8.4.4'], 'Google 8.8.8.8')
    cf   = resolve_ns(domain, ['1.1.1.1', '1.0.0.1'], 'Cloudflare 1.1.1.1')
    blocked, reason = False, None
    vz_s, pub_s = set(vz['ips']), set(goog['ips']) | set(cf['ips'])
    if vz['error'] and not (goog['error'] and cf['error']):
        blocked, reason = True, f"Verizon DNS error: {vz['error']}"
    elif vz_s and pub_s and not vz_s.intersection(pub_s):
        blocked, reason = True, f"Verizon returns {sorted(vz_s)} vs public {sorted(pub_s)}"
    return {
        'domain': domain, 'system_dns_servers': sys_dns,
        'verizon': vz, 'google': goog, 'cloudflare': cf,
        'verizon_blocked': blocked, 'block_reason': reason, 'checked_at': now_iso(),
    }

# ── Redirect chain trace (residential IP = real content served) ───────────────
UA = ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
      'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36')

def trace_redirects(start_url):
    chain, current = [], start_url
    session = _requests.Session()
    for hop_n in range(1, 21):
        parsed = urlparse(current)
        domain = parsed.netloc.split(':')[0]
        dns_r  = compare_dns(domain)
        hop    = {'hop': hop_n, 'url': current, 'domain': domain, 'dns': dns_r,
                  'status_code': None, 'redirect_to': None, 'error': None, 'final': False}
        try:
            resp = session.get(current, allow_redirects=False, timeout=15,
                               headers={'User-Agent': UA})
            hop['status_code'] = resp.status_code
            if resp.is_redirect or resp.status_code in (301, 302, 303, 307, 308):
                loc = resp.headers.get('Location', '')
                if loc.startswith('/'): loc = f"{parsed.scheme}://{parsed.netloc}{loc}"
                hop['redirect_to'] = loc; chain.append(hop); current = loc
            else:
                hop['final'] = True; chain.append(hop); break
        except _requests.exceptions.ConnectionError as e:
            hop['error'] = f'Connection failed: {str(e)[:150]}'; hop['final'] = True
            chain.append(hop); break
        except _requests.exceptions.Timeout:
            hop['error'] = 'Timeout (15s)'; hop['final'] = True; chain.append(hop); break
        except Exception as e:
            hop['error'] = str(e)[:150]; hop['final'] = True; chain.append(hop); break
    return chain

# ── Server comms ──────────────────────────────────────────────────────────────
def _hdrs():
    return {'Authorization': f'Bearer {TOKEN}', 'Content-Type': 'application/json'}

def poll():
    try:
        r = _requests.get(f'{SERVER_URL}/api/verizon-agent/poll', headers=_hdrs(), timeout=15)
        if r.status_code == 200: return r.json()
        logging.warning(f'Poll HTTP {r.status_code}')
    except Exception as e:
        logging.debug(f'Poll: {e}')
    return None


def post_result(payload):
    try:
        r = _requests.post(f'{SERVER_URL}/api/verizon-agent/result',
                           json=payload, headers=_hdrs(), timeout=30)
        return r.status_code == 200
    except Exception as e:
        logging.error(f'Post result: {e}'); return False

# ── Main ──────────────────────────────────────────────────────────────────────
def main():
    if not TOKEN:
        logging.error('No token in config.ini')
        print('Edit config.ini and set your token.')
        return

    logging.info(f'Verizon Agent v2 started — server={SERVER_URL} poll={POLL_INTERVAL}s')
    print(f'Verizon Agent v2 running. Log: {os.path.join(BASE_DIR, "agent.log")}')

    while True:
        try:
            data = poll()
            if data and data.get('command'):
                cmd = data['command']
                logging.info(f'Command: {cmd["type"]}')

                if cmd['type'] == 'check':
                    # DNS + redirect chain only (Verizon section)
                    result = run_check(cmd.get('url', ''))
                    post_result(result)


                elif cmd['type'] == 'reboot':
                    logging.info('Reboot command received')
                    post_result({'type': 'reboot_acknowledged', 'at': now_iso()})
                    time.sleep(3)
                    os.system('shutdown /r /t 10 /c "Verizon Agent: remote reboot"')

        except Exception as e:
            logging.error(f'Loop error: {e}')

        time.sleep(POLL_INTERVAL)

if __name__ == '__main__':
    main()
