<?php
// lookup.php — single-file domain inspector for pre-transfer discovery.
// Vanilla PHP API + Vue 2 (CDN) UI. Herd-compatible, PHP 8+ recommended.
//
// Discovers where a customer's domain "lives" before migrating it in:
//   - Ownership (RDAP with WHOIS fallback)
//   - Web hosting (A/AAAA -> PTR -> ASN/provider, Cloudflare/proxy detection, HTTP fingerprint)
//   - Mail (MX -> provider detection, SPF/DMARC/DKIM/MTA-STS/TLS-RPT, autodiscover/autoconfig)
//   - Subdomains (Certificate Transparency logs + zone-transfer attempt + common-name probing)
//   - Full DNS record set incl. SRV / HTTPS / DNSSEC signals

set_time_limit(120);

// ======================================================================
//  Utilities
// ======================================================================
function json_response($data, $code = 200)
{
    http_response_code($code);
    header('Content-Type: application/json');
    header('Cache-Control: no-store');
    echo json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    exit;
}

function is_valid_domain($d)
{
    return (bool) preg_match('/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i', $d);
}

function q($name, $type)
{
    try {
        return dns_get_record($name, $type) ?: [];
    } catch (Throwable $e) {
        return [];
    }
}

function qtxt($name)
{
    $rows = q($name, DNS_TXT);
    $vals = [];
    foreach ($rows as $r) {
        if (isset($r['txt'])) {
            $vals[] = $r['txt'];
        }
    }
    return $vals;
}

function http_get($url, $timeout = 12)
{
    if (function_exists('curl_init')) {
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_MAXREDIRS => 5,
            CURLOPT_TIMEOUT => $timeout,
            CURLOPT_CONNECTTIMEOUT => 6,
            CURLOPT_USERAGENT => 'DomainInspector/2.0',
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
        ]);
        $body = curl_exec($ch);
        curl_close($ch);
        return $body === false ? null : $body;
    }
    $ctx = stream_context_create(['http' => ['timeout' => $timeout, 'user_agent' => 'DomainInspector/2.0']]);
    $body = @file_get_contents($url, false, $ctx);
    return $body === false ? null : $body;
}

// ======================================================================
//  DNS: reverse / PTR
// ======================================================================
function qptr($ip)
{
    try {
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
            $reversed = implode('.', array_reverse(explode('.', $ip)));
            $result = @dns_get_record($reversed . '.in-addr.arpa', DNS_PTR);
            if ($result) {
                return $result[0]['target'] ?? null;
            }
        }
        $host = @gethostbyaddr($ip);
        return ($host && $host !== $ip) ? $host : null;
    } catch (Throwable $e) {
        return null;
    }
}

// ======================================================================
//  DNS: raw query (for record types dns_get_record can't do: HTTPS, DNSKEY)
// ======================================================================
function dns_parse_name($msg, $off)
{
    $labels = [];
    $jumped = false;
    $next = $off;
    $guard = 0;
    while (true) {
        if ($guard++ > 128 || $off >= strlen($msg)) {
            break;
        }
        $len = ord($msg[$off]);
        if ($len === 0) {
            $off++;
            if (!$jumped) {
                $next = $off;
            }
            break;
        }
        if (($len & 0xC0) === 0xC0) { // compression pointer
            $ptr = (($len & 0x3F) << 8) | ord($msg[$off + 1]);
            if (!$jumped) {
                $next = $off + 2;
            }
            $off = $ptr;
            $jumped = true;
            continue;
        }
        $labels[] = substr($msg, $off + 1, $len);
        $off += $len + 1;
    }
    return [implode('.', $labels), $next];
}

function dns_build_query($id, $name, $type)
{
    $header = pack('nnnnnn', $id, 0x0100, 1, 0, 0, 0);
    $qname = '';
    foreach (explode('.', $name) as $label) {
        if ($label === '') continue;
        $qname .= chr(strlen($label)) . $label;
    }
    $qname .= "\0";
    return $header . $qname . pack('nn', $type, 1);
}

// Extract A / CNAME from a raw DNS response (used by the parallel resolver).
function dns_parse_a_cname($resp)
{
    if ($resp === false || strlen($resp) < 12) return null;
    $qd = unpack('n', substr($resp, 4, 2))[1];
    $an = unpack('n', substr($resp, 6, 2))[1];
    $off = 12;
    for ($i = 0; $i < $qd; $i++) {
        [, $off] = dns_parse_name($resp, $off);
        $off += 4;
    }
    $ips = [];
    $cname = null;
    for ($i = 0; $i < $an && $off + 10 <= strlen($resp); $i++) {
        [, $off] = dns_parse_name($resp, $off);
        $type = unpack('n', substr($resp, $off, 2))[1];
        $rdlen = unpack('n', substr($resp, $off + 8, 2))[1];
        $rd = $off + 10;
        if ($type === 1 && $rdlen === 4) {
            $ips[] = inet_ntop(substr($resp, $rd, 4));
        } elseif ($type === 5) {
            [$cname] = dns_parse_name($resp, $rd);
        }
        $off = $rd + $rdlen;
    }
    if (!$ips && !$cname) return null;
    return ['type' => $cname ? 'CNAME' : 'A', 'target' => $cname, 'ip' => implode(', ', array_unique($ips))];
}

// Resolve many hostnames in parallel over UDP — one batch instead of N blocking
// dns_get_record calls. Returns [host => ['type','target','ip']] for names that resolve.
function batch_resolve($hosts, $server = '1.1.1.1', $timeout = 4)
{
    $sockets = [];  // id => socket resource
    $hostOf = [];   // id => hostname
    foreach ($hosts as $host) {
        do { $id = random_int(0, 0xffff); } while (isset($sockets[$id]));
        $sock = @stream_socket_client("udp://$server:53", $e, $es, $timeout);
        if (!$sock) continue;
        stream_set_blocking($sock, false);
        @fwrite($sock, dns_build_query($id, $host, 1)); // A (follows CNAME)
        $sockets[$id] = $sock;
        $hostOf[$id] = $host;
    }

    $results = [];
    $deadline = microtime(true) + $timeout;
    while ($sockets && microtime(true) < $deadline) {
        $read = array_values($sockets);
        $w = $e = null;
        $remain = $deadline - microtime(true);
        $sec = (int) $remain;
        $usec = (int) (($remain - $sec) * 1e6);
        if (@stream_select($read, $w, $e, max(0, $sec), max(0, $usec)) < 1) break;
        foreach ($read as $sock) {
            $id = array_search($sock, $sockets, true);
            if ($id === false) continue;
            $resp = @fread($sock, 4096);
            $parsed = dns_parse_a_cname($resp);
            if ($parsed) $results[$hostOf[$id]] = $parsed;
            @fclose($sock);
            unset($sockets[$id]);
        }
    }
    foreach ($sockets as $s) @fclose($s);
    return $results;
}

function dns_raw_query($name, $type, $server = '1.1.1.1', $timeout = 3)
{
    try {
        $id = random_int(0, 0xffff);
        $packet = dns_build_query($id, $name, $type);

        $fp = @fsockopen('udp://' . $server, 53, $errno, $errstr, $timeout);
        if (!$fp) return null;
        stream_set_timeout($fp, $timeout);
        fwrite($fp, $packet);
        $resp = fread($fp, 4096);
        fclose($fp);
        if ($resp === false || strlen($resp) < 12) return null;

        $an = unpack('n', substr($resp, 6, 2))[1];
        $qd = unpack('n', substr($resp, 4, 2))[1];
        $off = 12;
        for ($i = 0; $i < $qd; $i++) {
            [, $off] = dns_parse_name($resp, $off);
            $off += 4;
        }
        $records = [];
        for ($i = 0; $i < $an; $i++) {
            [$rname, $off] = dns_parse_name($resp, $off);
            if ($off + 10 > strlen($resp)) break;
            $rtype = unpack('n', substr($resp, $off, 2))[1];
            $rdlen = unpack('n', substr($resp, $off + 8, 2))[1];
            $rdstart = $off + 10;
            $rec = ['name' => $rname, 'type' => $rtype];
            if ($rtype === 65 || $rtype === 64) { // HTTPS / SVCB
                $rec['priority'] = unpack('n', substr($resp, $rdstart, 2))[1];
                [$rec['target']] = dns_parse_name($resp, $rdstart + 2);
            }
            $records[] = $rec;
            $off = $rdstart + $rdlen;
        }
        return $records;
    } catch (Throwable $e) {
        return null;
    }
}

// ======================================================================
//  Mail / SPF / DMARC parsing
// ======================================================================
function parse_spf($txtRows)
{
    $spfRaw = null;
    foreach (($txtRows ?? []) as $t) {
        $val = is_array($t) && isset($t['txt']) ? $t['txt'] : (is_string($t) ? $t : null);
        if ($val && stripos($val, 'v=spf1') === 0) {
            $spfRaw = $val;
            break;
        }
    }
    if (!$spfRaw) return null;

    $lookup_est = preg_match_all('/\b(include:|a[:\s]|mx[:\s]|ptr|exists:|redirect=)/i', $spfRaw);
    $permissive = (strpos($spfRaw, '+all') !== false)
        || ((strpos($spfRaw, '-all') === false) && (strpos($spfRaw, '~all') === false));
    return ['raw' => $spfRaw, 'lookup_estimate' => $lookup_est, 'permissive' => $permissive];
}

function parse_dmarc($dmarcTxt)
{
    $rec = null;
    foreach (($dmarcTxt ?? []) as $v) {
        if (is_string($v) && stripos($v, 'v=DMARC1') === 0) {
            $rec = $v;
            break;
        }
    }
    if (!$rec) return null;

    $parts = [];
    foreach (explode(';', $rec) as $p) {
        $p = trim($p);
        if ($p && strpos($p, '=') !== false) {
            [$k, $v] = array_map('trim', explode('=', $p, 2));
            $parts[strtolower($k)] = $v;
        }
    }
    $p = strtolower($parts['p'] ?? 'none');
    $grade = $p === 'reject' ? 'enforced' : ($p === 'quarantine' ? 'partial' : 'none');
    return [
        'raw' => $rec,
        'policy' => $p,
        'sp' => $parts['sp'] ?? null,
        'aspf' => $parts['aspf'] ?? null,
        'adkim' => $parts['adkim'] ?? null,
        'rua' => $parts['rua'] ?? null,
        'ruf' => $parts['ruf'] ?? null,
        'pct' => $parts['pct'] ?? '100',
        'grade' => $grade,
    ];
}

// Detect the mail provider from MX hostnames.
function detect_mail_provider($mxHosts)
{
    $map = [
        'google.com' => 'Google Workspace',
        'googlemail.com' => 'Google Workspace',
        'aspmx.l.google' => 'Google Workspace',
        'outlook.com' => 'Microsoft 365',
        'protection.outlook.com' => 'Microsoft 365',
        'mail.protection.outlook' => 'Microsoft 365',
        'zoho.com' => 'Zoho Mail',
        'zoho.eu' => 'Zoho Mail',
        'zohomail' => 'Zoho Mail',
        'pphosted.com' => 'Proofpoint',
        'ppe-hosted.com' => 'Proofpoint',
        'mimecast.com' => 'Mimecast',
        'barracudanetworks.com' => 'Barracuda',
        'messagingengine.com' => 'Fastmail',
        'protonmail.ch' => 'Proton Mail',
        'proton.me' => 'Proton Mail',
        'mail.me.com' => 'iCloud Mail',
        'icloud.com' => 'iCloud Mail',
        'amazonaws.com' => 'Amazon SES/WorkMail',
        'awsapps.com' => 'Amazon WorkMail',
        'mailgun.org' => 'Mailgun',
        'sendgrid.net' => 'SendGrid',
        'secureserver.net' => 'GoDaddy Email',
        'emailsrvr.com' => 'Rackspace Email',
        'yandex' => 'Yandex Mail',
        'titan.email' => 'Titan Email',
        'improvmx.com' => 'ImprovMX (forwarding)',
        'forwardemail.net' => 'ForwardEmail',
    ];
    foreach ($mxHosts as $h) {
        $h = strtolower(rtrim($h, '.'));
        foreach ($map as $needle => $name) {
            if (strpos($h, $needle) !== false) {
                return $name;
            }
        }
    }
    return $mxHosts ? 'Self-hosted / other' : null;
}

// ======================================================================
//  ASN / hosting provider (Team Cymru DNS — no external HTTP needed)
// ======================================================================
function asn_lookup($ip)
{
    if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) return null;
    $rev = implode('.', array_reverse(explode('.', $ip)));
    $rec = @dns_get_record($rev . '.origin.asn.cymru.com', DNS_TXT);
    if (!$rec || !isset($rec[0]['txt'])) return null;

    $parts = array_map('trim', explode('|', $rec[0]['txt']));
    $asn = explode(' ', trim($parts[0] ?? ''))[0] ?: null;
    $prefix = $parts[1] ?? null;
    $cc = $parts[2] ?? null;

    $asname = null;
    if ($asn) {
        $arec = @dns_get_record('AS' . $asn . '.asn.cymru.com', DNS_TXT);
        if ($arec && isset($arec[0]['txt'])) {
            $ap = array_map('trim', explode('|', $arec[0]['txt']));
            $asname = $ap[4] ?? null;
        }
    }
    return ['asn' => $asn, 'prefix' => $prefix, 'cc' => $cc, 'name' => $asname];
}

// Friendly provider name from ASN name / PTR string.
function friendly_provider($asName, $ptr)
{
    $hay = strtolower(($asName ?? '') . ' ' . ($ptr ?? ''));
    $map = [
        'cloudflare' => 'Cloudflare',
        'amazon' => 'Amazon AWS',
        'aws' => 'Amazon AWS',
        'google' => 'Google Cloud',
        'microsoft' => 'Microsoft Azure',
        'azure' => 'Microsoft Azure',
        'digitalocean' => 'DigitalOcean',
        'ovh' => 'OVH',
        'hetzner' => 'Hetzner',
        'linode' => 'Akamai/Linode',
        'akamai' => 'Akamai',
        'fastly' => 'Fastly',
        'godaddy' => 'GoDaddy',
        'squarespace' => 'Squarespace',
        'shopify' => 'Shopify',
        'wpengine' => 'WP Engine',
        'automattic' => 'WordPress.com',
        'vultr' => 'Vultr',
        'rackspace' => 'Rackspace',
        'namecheap' => 'Namecheap',
        'netlify' => 'Netlify',
        'vercel' => 'Vercel',
        'github' => 'GitHub Pages',
    ];
    foreach ($map as $needle => $name) {
        if (strpos($hay, $needle) !== false) return $name;
    }
    return $asName ?: null;
}

// ======================================================================
//  HTTP fingerprint (what is the website running?)
// ======================================================================
function http_probe($domain)
{
    if (!function_exists('curl_init')) {
        return ['reachable' => false, 'note' => 'curl not available'];
    }
    foreach (['https', 'http'] as $scheme) {
        $ch = curl_init($scheme . '://' . $domain);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_MAXREDIRS => 8,
            CURLOPT_TIMEOUT => 12,
            CURLOPT_CONNECTTIMEOUT => 6,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_SSL_VERIFYHOST => false,
            CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; DomainInspector/2.0)',
            CURLOPT_HEADER => true,
        ]);
        $resp = curl_exec($ch);
        if ($resp === false) {
            curl_close($ch);
            continue;
        }
        $info = curl_getinfo($ch);
        curl_close($ch);

        $headerText = substr($resp, 0, $info['header_size']);
        $body = substr($resp, $info['header_size'], 80000);

        $headers = [];
        foreach (explode("\n", $headerText) as $line) {
            if (strpos($line, ':') !== false) {
                [$k, $v] = explode(':', $line, 2);
                $headers[strtolower(trim($k))] = trim($v);
            }
        }

        return [
            'reachable' => true,
            'scheme' => $scheme,
            'final_url' => $info['url'] ?? null,
            'status' => $info['http_code'] ?? null,
            'redirected' => ($info['redirect_count'] ?? 0) > 0,
            'server' => $headers['server'] ?? null,
            'powered_by' => $headers['x-powered-by'] ?? null,
            'cms' => detect_cms($headers, $body),
        ];
    }
    return ['reachable' => false, 'note' => 'No HTTP/HTTPS response'];
}

function detect_cms($headers, $body)
{
    $server = strtolower($headers['server'] ?? '');
    $powered = strtolower($headers['x-powered-by'] ?? '');
    $b = strtolower($body);

    if (strpos($b, '/wp-content/') !== false || strpos($b, 'wp-json') !== false) return 'WordPress';
    if (strpos($b, 'cdn.shopify.com') !== false || isset($headers['x-shopid'])) return 'Shopify';
    if (strpos($b, 'static.wixstatic.com') !== false || strpos($server, 'wix') !== false) return 'Wix';
    if (strpos($server, 'squarespace') !== false || strpos($b, 'squarespace.com') !== false) return 'Squarespace';
    if (strpos($b, 'content="joomla') !== false) return 'Joomla';
    if (strpos($b, 'content="drupal') !== false || isset($headers['x-drupal-cache'])) return 'Drupal';
    if (strpos($b, 'webflow.io') !== false || strpos($server, 'webflow') !== false) return 'Webflow';
    if (strpos($b, 'ghost.org') !== false || strpos($powered, 'ghost') !== false) return 'Ghost';
    if (preg_match('/<meta[^>]+generator[^>]+content="([^"]+)"/i', $body, $m)) return trim($m[1]);
    return null;
}

// ======================================================================
//  Subdomain discovery
// ======================================================================
function ct_subdomains($domain)
{
    $raw = http_get('https://crt.sh/?q=' . urlencode('%.' . $domain) . '&output=json', 15);
    if (!$raw) return [];
    $json = json_decode($raw, true);
    if (!is_array($json)) return [];

    $names = [];
    foreach ($json as $entry) {
        $val = $entry['name_value'] ?? '';
        foreach (preg_split('/\s+/', $val) as $n) {
            $n = strtolower(trim($n));
            $n = ltrim($n, '*.');
            if ($n && $n !== $domain && substr($n, -strlen('.' . $domain)) === '.' . $domain) {
                $names[$n] = true;
            }
        }
    }
    return array_keys($names);
}

function common_subdomain_names($domain)
{
    $prefixes = [
        'www', 'mail', 'webmail', 'smtp', 'imap', 'pop', 'mx', 'email', 'autodiscover',
        'autoconfig', 'cpanel', 'whm', 'webdisk', 'ftp', 'sftp', 'ns1', 'ns2', 'remote',
        'vpn', 'portal', 'intranet', 'dev', 'staging', 'test', 'uat', 'demo', 'api',
        'app', 'admin', 'secure', 'shop', 'store', 'blog', 'news', 'support', 'help',
        'docs', 'wiki', 'status', 'cdn', 'static', 'assets', 'img', 'media', 'files',
        'git', 'gitlab', 'jenkins', 'db', 'sql', 'mysql', 'phpmyadmin', 'crm', 'erp',
        'm', 'mobile', 'login', 'sso', 'auth', 'cloud', 'backup', 'monitor', 'grafana',
    ];
    return array_map(fn($p) => $p . '.' . $domain, $prefixes);
}

// Attempt an AXFR zone transfer against a nameserver IP over TCP/53.
function try_axfr($domain, $nsIp, $timeout = 4)
{
    try {
        $fp = @fsockopen('tcp://' . $nsIp, 53, $errno, $errstr, $timeout);
        if (!$fp) return null;
        stream_set_timeout($fp, $timeout);

        $header = pack('nnnnnn', random_int(0, 0xffff), 0x0000, 1, 0, 0, 0);
        $qname = '';
        foreach (explode('.', $domain) as $label) {
            $qname .= chr(strlen($label)) . $label;
        }
        $qname .= "\0";
        $query = $header . $qname . pack('nn', 252, 1); // 252 = AXFR
        fwrite($fp, pack('n', strlen($query)) . $query);

        $buf = '';
        $start = time();
        while (!feof($fp) && (time() - $start) < $timeout) {
            $chunk = fread($fp, 4096);
            if ($chunk === '' || $chunk === false) break;
            $buf .= $chunk;
            if (strlen($buf) > 262144) break; // 256KB cap
        }
        fclose($fp);
        if (strlen($buf) < 4) return null;

        // Concatenated length-prefixed messages; harvest owner names from each.
        $names = [];
        $pos = 0;
        while ($pos + 2 <= strlen($buf)) {
            $msgLen = unpack('n', substr($buf, $pos, 2))[1];
            $msg = substr($buf, $pos + 2, $msgLen);
            $pos += 2 + $msgLen;
            if (strlen($msg) < 12) continue;

            $an = unpack('n', substr($msg, 6, 2))[1];
            $qd = unpack('n', substr($msg, 4, 2))[1];
            $off = 12;
            for ($i = 0; $i < $qd; $i++) {
                [, $off] = dns_parse_name($msg, $off);
                $off += 4;
            }
            for ($i = 0; $i < $an && $off + 10 <= strlen($msg); $i++) {
                [$owner, $off] = dns_parse_name($msg, $off);
                $rdlen = unpack('n', substr($msg, $off + 8, 2))[1];
                $off += 10 + $rdlen;
                $owner = strtolower(rtrim($owner, '.'));
                if ($owner && substr($owner, -strlen($domain)) === $domain) {
                    $names[$owner] = true;
                }
            }
        }
        return array_keys($names);
    } catch (Throwable $e) {
        return null;
    }
}

// Resolve a host to A/CNAME so we can show where it points.
function resolve_host($host)
{
    $type = null;
    $target = null;
    $ips = [];

    $cname = @dns_get_record($host, DNS_CNAME);
    if ($cname && isset($cname[0]['target'])) {
        $type = 'CNAME';
        $target = $cname[0]['target'];
    }
    $a = @dns_get_record($host, DNS_A);
    if ($a) {
        foreach ($a as $r) {
            if (isset($r['ip'])) $ips[] = $r['ip'];
        }
        if ($ips && !$type) $type = 'A';
    }
    if (!$type) return null;
    return ['type' => $type, 'target' => $target, 'ip' => implode(', ', array_unique($ips))];
}

function discover_subdomains($domain, $nsHosts)
{
    $sources = []; // name => [source => true]

    foreach (ct_subdomains($domain) as $n) {
        $sources[$n]['ct'] = true;
    }
    foreach (common_subdomain_names($domain) as $n) {
        $sources[$n]['probe'] = true;
    }
    // AXFR: try the first nameserver only. Almost always refused; a short timeout
    // keeps a firewalled TCP/53 from stalling the whole scan.
    foreach (array_slice($nsHosts, 0, 1) as $ns) {
        $nsA = @dns_get_record($ns, DNS_A);
        $nsIp = $nsA[0]['ip'] ?? null;
        if (!$nsIp) continue;
        $axfr = try_axfr($domain, $nsIp, 3);
        if ($axfr) {
            foreach ($axfr as $n) {
                if ($n !== $domain) $sources[$n]['axfr'] = true;
            }
        }
    }

    // Cap total hosts we resolve to keep runtime sane, then resolve them all in
    // one parallel batch (not sequentially — that was the main source of latency).
    $names = array_keys($sources);
    sort($names);
    $names = array_slice($names, 0, 200);

    // Fast path: resolve everything in parallel over UDP/53. If that yields nothing
    // (some hosts firewall outbound UDP/53 to public resolvers), fall back to the
    // system resolver sequentially so discovery still works.
    $resolved = batch_resolve($names, '1.1.1.1', 5);
    if (!$resolved && $names) {
        // Budget the sequential fallback: on a UDP-blocked network each NXDOMAIN
        // costs a full resolver timeout, so cap the total wall-clock spend.
        $deadline = microtime(true) + 15;
        foreach ($names as $n) {
            if (microtime(true) > $deadline) break;
            $r = resolve_host($n);
            if ($r) $resolved[$n] = $r;
        }
    }

    $out = [];
    foreach ($resolved as $name => $r) {
        $out[] = [
            'name' => $name,
            'type' => $r['type'],
            'target' => $r['target'],
            'ip' => $r['ip'],
            'sources' => array_keys($sources[$name]),
        ];
    }
    usort($out, fn($a, $b) => strcmp($a['name'], $b['name']));
    return $out;
}

// ======================================================================
//  RDAP / WHOIS ownership
// ======================================================================
function rdap_lookup($domain)
{
    $ctx = stream_context_create([
        'http' => ['timeout' => 10, 'user_agent' => 'Domain Inspector/2.0'],
        'https' => ['timeout' => 10, 'user_agent' => 'Domain Inspector/2.0'],
    ]);

    $urls = ['https://rdap.org/domain/' . $domain];
    $tldServers = [
        'com' => 'https://rdap.verisign.com/com/v1/domain/',
        'net' => 'https://rdap.verisign.com/net/v1/domain/',
        'org' => 'https://rdap.publicinterestregistry.org/rdap/domain/',
        'nz' => 'https://rdap.srs.net.nz/rdap/domain/',
        'co.nz' => 'https://rdap.srs.net.nz/rdap/domain/',
        'org.nz' => 'https://rdap.srs.net.nz/rdap/domain/',
        'net.nz' => 'https://rdap.srs.net.nz/rdap/domain/',
        'govt.nz' => 'https://rdap.srs.net.nz/rdap/domain/',
        'ac.nz' => 'https://rdap.srs.net.nz/rdap/domain/',
        'au' => 'https://rdap.auda.org.au/domain/',
        'com.au' => 'https://rdap.auda.org.au/domain/',
        'net.au' => 'https://rdap.auda.org.au/domain/',
        'org.au' => 'https://rdap.auda.org.au/domain/',
        'edu.au' => 'https://rdap.auda.org.au/domain/',
        'gov.au' => 'https://rdap.auda.org.au/domain/',
        'id.au' => 'https://rdap.auda.org.au/domain/',
    ];

    $labels = explode('.', $domain);
    $n = count($labels);
    $candidates = [];
    if ($n >= 3) $candidates[] = strtolower(implode('.', array_slice($labels, -3)));
    if ($n >= 2) $candidates[] = strtolower(implode('.', array_slice($labels, -2)));
    if ($n >= 1) $candidates[] = strtolower($labels[$n - 1]);

    foreach ($candidates as $cand) {
        if (isset($tldServers[$cand])) {
            array_unshift($urls, $tldServers[$cand] . $domain);
            break;
        }
    }

    $lastError = null;
    foreach ($urls as $url) {
        $raw = @file_get_contents($url, false, $ctx);
        if ($raw !== false) {
            $json = @json_decode($raw, true);
            if ($json && !isset($json['errorCode'])) {
                return $json;
            }
            $lastError = $json['title'] ?? 'Invalid JSON response';
        } else {
            $lastError = error_get_last()['message'] ?? 'Connection failed';
        }
    }

    $whois = whois_lookup($domain);
    if (!isset($whois['error'])) {
        return [
            'source' => 'whois',
            'ldhName' => $domain,
            'nameservers' => array_map(fn($ns) => ['ldhName' => $ns], $whois['nameservers']),
            'entities' => $whois['registrar'] ? [['roles' => ['registrar'], 'handle' => $whois['registrar']]] : [],
            'events' => array_filter([
                $whois['creation_date'] ? ['eventAction' => 'registration', 'eventDate' => $whois['creation_date']] : null,
                $whois['expiry_date'] ? ['eventAction' => 'expiration', 'eventDate' => $whois['expiry_date']] : null,
            ]),
            'status' => $whois['status'],
            'whois_raw' => $whois['raw'],
        ];
    }

    return ['error' => 'unavailable', 'rdap_details' => $lastError, 'whois_details' => $whois['error'] ?? null];
}

function whois_lookup($domain)
{
    $domainParts = explode('.', $domain);
    $n = count($domainParts);
    $tld = strtolower($domainParts[$n - 1]);
    $effectiveTld = $tld;
    if ($n >= 2) {
        $second = strtolower($domainParts[$n - 2] . '.' . $domainParts[$n - 1]);
        if (in_array($second, ['co.nz', 'org.nz', 'net.nz', 'com.au', 'net.au', 'org.au'])) {
            $effectiveTld = $second;
        }
    }

    $whoisServers = [
        'com' => 'whois.verisign-grs.com',
        'net' => 'whois.verisign-grs.com',
        'org' => 'whois.pir.org',
        'nz' => 'whois.srs.net.nz',
        'co.nz' => 'whois.srs.net.nz',
        'org.nz' => 'whois.srs.net.nz',
        'net.nz' => 'whois.srs.net.nz',
        'au' => 'whois.auda.org.au',
        'com.au' => 'whois.auda.org.au',
        'net.au' => 'whois.auda.org.au',
        'org.au' => 'whois.auda.org.au',
    ];
    $server = $whoisServers[$effectiveTld] ?? null;
    if (!$server) return ['error' => 'No WHOIS server available for TLD'];

    $fp = @fsockopen($server, 43, $errno, $errstr, 10);
    if (!$fp) return ['error' => "Connection failed: $errstr ($errno)"];
    fputs($fp, $domain . "\r\n");
    $response = '';
    while (!feof($fp)) {
        $response .= fgets($fp, 1024);
    }
    fclose($fp);
    if (empty($response)) return ['error' => 'Empty response from WHOIS server'];

    $parsed = parse_whois_response($response);
    $parsed['raw'] = $response;
    $parsed['server'] = $server;
    return $parsed;
}

function parse_whois_response($response)
{
    $data = [
        'registrar' => null, 'creation_date' => null, 'expiry_date' => null,
        'nameservers' => [], 'status' => [], 'registrant' => null,
    ];
    foreach (explode("\n", $response) as $line) {
        $line = trim($line);
        if ($line === '' || $line[0] === '%' || $line[0] === '#' || strpos($line, ':') === false) continue;
        [$key, $value] = array_map('trim', explode(':', $line, 2));
        $key = strtolower($key);

        if (in_array($key, ['registrar', 'sponsoring registrar'])) {
            $data['registrar'] = $value;
        } elseif (in_array($key, ['creation date', 'created', 'domain_dateregistered', 'registered'])) {
            $data['creation_date'] = $value;
        } elseif (in_array($key, ['expiry date', 'expires', 'domain_datebilleduntil', 'expiration date', 'registry expiry date'])) {
            $data['expiry_date'] = $value;
        } elseif (in_array($key, ['name server', 'nserver', 'nameserver', 'ns'])) {
            if ($value && !in_array($value, $data['nameservers'])) $data['nameservers'][] = $value;
        } elseif (in_array($key, ['status', 'domain status'])) {
            if ($value && !in_array($value, $data['status'])) $data['status'][] = $value;
        } elseif (in_array($key, ['registrant', 'registrant name', 'registrant_contact_name'])) {
            $data['registrant'] = $value;
        }
    }
    return $data;
}

// Flatten RDAP into a simple ownership summary the UI can render directly.
function summarize_ownership($rdap)
{
    if (isset($rdap['error'])) {
        return ['available' => false, 'error' => $rdap['rdap_details'] ?? $rdap['error']];
    }
    $registrar = null;
    foreach (($rdap['entities'] ?? []) as $e) {
        if (in_array('registrar', $e['roles'] ?? [])) {
            if (isset($e['vcardArray'][1])) {
                foreach ($e['vcardArray'][1] as $field) {
                    if (($field[0] ?? '') === 'fn') {
                        $registrar = $field[3] ?? null;
                        break;
                    }
                }
            }
            $registrar = $registrar ?: ($e['handle'] ?? null);
        }
    }
    $events = [];
    foreach (($rdap['events'] ?? []) as $ev) {
        $events[$ev['eventAction'] ?? '?'] = $ev['eventDate'] ?? null;
    }
    $ns = [];
    foreach (($rdap['nameservers'] ?? []) as $n) {
        $ns[] = is_string($n) ? $n : ($n['ldhName'] ?? $n['name'] ?? null);
    }
    return [
        'available' => true,
        'source' => $rdap['source'] ?? 'rdap',
        'registrar' => $registrar,
        'created' => $events['registration'] ?? null,
        'expires' => $events['expiration'] ?? null,
        'updated' => $events['last changed'] ?? ($events['last update of RDAP database'] ?? null),
        'status' => $rdap['status'] ?? [],
        'nameservers' => array_values(array_filter($ns)),
    ];
}

// ======================================================================
//  API endpoints
// ======================================================================
if (isset($_GET['api'])) {
    $api = $_GET['api'];

    if ($api === 'lookup') {
        $domain = strtolower(trim($_GET['domain'] ?? ''));
        if (!is_valid_domain($domain)) {
            json_response(['error' => 'Invalid domain'], 422);
        }

        // --- Core DNS ---
        $dns = [
            'A' => q($domain, DNS_A),
            'AAAA' => q($domain, DNS_AAAA),
            'MX' => q($domain, DNS_MX),
            'NS' => q($domain, DNS_NS),
            'SOA' => q($domain, DNS_SOA),
            'CAA' => defined('DNS_CAA') ? q($domain, DNS_CAA) : [],
            'TXT' => q($domain, DNS_TXT),
            'CNAME_www' => q('www.' . $domain, DNS_CNAME),
            'DMARC' => qtxt('_dmarc.' . $domain),
        ];

        // --- Web hosting (A/AAAA -> PTR -> ASN/provider) ---
        $webHosts = [];
        foreach (['A' => 'ip', 'AAAA' => 'ipv6'] as $rtype => $field) {
            foreach ($dns[$rtype] as $rec) {
                if (!isset($rec[$field])) continue;
                $ip = $rec[$field];
                $ptr = qptr($ip);
                $asn = asn_lookup($ip);
                $provider = friendly_provider($asn['name'] ?? null, $ptr);
                $webHosts[] = [
                    'ip' => $ip,
                    'kind' => $rtype,
                    'ptr' => $ptr,
                    'asn' => $asn['asn'] ?? null,
                    'as_name' => $asn['name'] ?? null,
                    'country' => $asn['cc'] ?? null,
                    'provider' => $provider,
                    'cloudflare' => stripos(($asn['name'] ?? '') . ($ptr ?? ''), 'cloudflare') !== false,
                ];
            }
        }

        // --- HTTP fingerprint ---
        $http = http_probe($domain);

        // --- Mail ---
        $mxHosts = array_map(fn($r) => $r['target'] ?? '', $dns['MX']);
        $spf = parse_spf($dns['TXT']);
        $dmarc = parse_dmarc($dns['DMARC']);

        // DKIM: auto-probe common selectors.
        $dkimSelectors = ['google', 'default', 'selector1', 'selector2', 'k1', 'k2', 'dkim',
            'mail', 's1', 's2', 'mandrill', 'zoho', 'zohomail', 'fm1', 'fm2', 'fm3', 'mxvault'];
        $dkim = [];
        foreach ($dkimSelectors as $s) {
            $found = qtxt($s . '._domainkey.' . $domain);
            if ($found) $dkim[$s] = $found;
        }

        $srvNames = [
            'autodiscover' => '_autodiscover._tcp.',
            'sip' => '_sip._tls.',
            'sipfederation' => '_sipfederationtls._tcp.',
            'submission' => '_submission._tcp.',
        ];
        $srv = [];
        foreach ($srvNames as $key => $prefix) {
            $rows = q($prefix . $domain, DNS_SRV);
            if ($rows) {
                $srv[$key] = array_map(fn($r) => [
                    'target' => $r['target'] ?? null,
                    'port' => $r['port'] ?? null,
                    'priority' => $r['pri'] ?? null,
                    'weight' => $r['weight'] ?? null,
                ], $rows);
            }
        }

        $mail = [
            'mx' => array_map(fn($r) => ['priority' => $r['pri'] ?? null, 'host' => $r['target'] ?? null], $dns['MX']),
            'provider' => detect_mail_provider($mxHosts),
            'spf' => $spf,
            'dmarc' => $dmarc,
            'dkim' => $dkim,
            'mta_sts' => qtxt('_mta-sts.' . $domain) ?: null,
            'tls_rpt' => qtxt('_smtp._tls.' . $domain) ?: null,
            'autodiscover' => resolve_host('autodiscover.' . $domain),
            'autoconfig' => resolve_host('autoconfig.' . $domain),
            'srv' => $srv,
        ];

        // --- Modern records / DNSSEC (raw queries) ---
        $https = dns_raw_query($domain, 65) ?: [];
        $dnskey = dns_raw_query($domain, 48);
        $dnssec = [
            'enabled' => is_array($dnskey) && count($dnskey) > 0,
            'dnskey_count' => is_array($dnskey) ? count($dnskey) : 0,
        ];

        // --- Subdomain discovery ---
        $nsHosts = array_values(array_filter(array_map(fn($r) => $r['target'] ?? null, $dns['NS'])));
        $subdomains = discover_subdomains($domain, $nsHosts);

        // --- Ownership summary ---
        $rdap = rdap_lookup($domain);
        $ownership = summarize_ownership($rdap);

        // --- Issues / health ---
        $issues = [];
        if (empty($dns['MX'])) $issues[] = ['sev' => 'high', 'msg' => 'No MX records — inbound mail will fail'];
        if (!$dmarc) $issues[] = ['sev' => 'med', 'msg' => 'No DMARC record'];
        elseif ($dmarc['grade'] === 'none') $issues[] = ['sev' => 'low', 'msg' => 'DMARC policy is p=none (monitoring only)'];
        if (!$spf) $issues[] = ['sev' => 'med', 'msg' => 'No SPF record'];
        elseif ($spf['permissive']) $issues[] = ['sev' => 'med', 'msg' => 'SPF is too permissive (+all or missing -all/~all)'];
        if ($spf && $spf['lookup_estimate'] > 10) $issues[] = ['sev' => 'low', 'msg' => 'SPF may exceed the 10 DNS-lookup limit'];
        if (!$dnssec['enabled']) $issues[] = ['sev' => 'info', 'msg' => 'DNSSEC not enabled'];
        if (isset($http['reachable']) && !$http['reachable']) $issues[] = ['sev' => 'low', 'msg' => 'Website did not respond over HTTP/HTTPS'];
        if (empty($ownership['available'])) $issues[] = ['sev' => 'low', 'msg' => 'Could not retrieve registration/ownership data'];

        json_response([
            'domain' => $domain,
            'generated_at' => date('c'),
            'ownership' => $ownership,
            'web' => ['hosts' => $webHosts, 'http' => $http],
            'mail' => $mail,
            'subdomains' => $subdomains,
            'dns' => $dns,
            'https_records' => $https,
            'dnssec' => $dnssec,
            'issues' => $issues,
            'rdap' => $rdap,
        ]);
    }

    if ($api === 'dkim' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
        $body = json_decode(file_get_contents('php://input'), true) ?: [];
        $domain = strtolower(trim($body['domain'] ?? ''));
        $selectors = array_values(array_filter(array_map('trim', $body['selectors'] ?? [])));
        if (!is_valid_domain($domain)) {
            json_response(['error' => 'Invalid domain'], 422);
        }
        $out = [];
        foreach ($selectors as $s) {
            $out[$s] = qtxt("{$s}._domainkey." . $domain);
        }
        json_response(['domain' => $domain, 'dkim' => $out]);
    }

    json_response(['error' => 'Unknown endpoint'], 404);
}

// ======================================================================
//  UI
// ======================================================================
?><!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Domain Inspector</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link rel="preconnect" href="https://unpkg.com">
</head>

<body class="bg-slate-100 font-sans text-slate-800">
    <div id="app" class="max-w-6xl mx-auto p-4 sm:p-6">
        <header class="mb-6">
            <h1 class="text-2xl sm:text-3xl font-bold text-slate-900">Domain Inspector</h1>
            <p class="text-slate-500 text-sm mt-1">Find where a domain's web, mail and DNS live before transferring it in.</p>
        </header>

        <div class="flex gap-2 mb-6">
            <input v-model="domain" @keyup.enter="run" placeholder="example.com"
                class="flex-1 px-4 py-2.5 border border-slate-300 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
            <button @click="run" :disabled="loading"
                class="px-6 py-2.5 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed">
                {{ loading ? 'Scanning…' : 'Inspect' }}
            </button>
        </div>

        <div v-if="error" class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6">{{ error }}</div>
        <p v-if="loading" class="text-slate-500 mb-6">Running discovery — CT logs, DNS, mail, hosting… this can take up to a minute.</p>

        <div v-if="data" class="space-y-6">
            <!-- Verdict -->
            <section class="bg-white border border-slate-200 rounded-xl p-5 shadow-sm">
                <div class="flex items-center justify-between flex-wrap gap-3">
                    <h2 class="text-lg font-semibold text-slate-900">{{ data.domain }}</h2>
                    <div class="flex gap-2">
                        <button @click="exportJson" class="text-sm px-3 py-1.5 border border-slate-300 rounded-md hover:bg-slate-50">Export JSON</button>
                    </div>
                </div>
                <div class="mt-4 flex flex-wrap gap-2">
                    <span v-if="!data.issues.length" class="px-3 py-1 rounded-full text-sm bg-green-100 text-green-700">No critical issues detected</span>
                    <span v-for="(it,i) in data.issues" :key="i" :class="sevClass(it.sev)" class="px-3 py-1 rounded-full text-sm">{{ it.msg }}</span>
                </div>
            </section>

            <div class="grid lg:grid-cols-2 gap-6">
                <!-- Ownership -->
                <section class="bg-white border border-slate-200 rounded-xl p-5 shadow-sm">
                    <h3 class="text-base font-semibold text-slate-900 mb-3 flex items-center gap-2">Ownership</h3>
                    <template v-if="data.ownership.available">
                        <dl class="text-sm divide-y divide-slate-100">
                            <div class="flex justify-between py-1.5"><dt class="text-slate-500">Registrar</dt><dd class="font-medium text-right">{{ data.ownership.registrar || '—' }}</dd></div>
                            <div class="flex justify-between py-1.5"><dt class="text-slate-500">Created</dt><dd class="text-right">{{ fmtDate(data.ownership.created) }}</dd></div>
                            <div class="flex justify-between py-1.5"><dt class="text-slate-500">Expires</dt><dd class="text-right">{{ fmtDate(data.ownership.expires) }}</dd></div>
                            <div class="flex justify-between py-1.5"><dt class="text-slate-500">Source</dt><dd class="text-right uppercase text-xs tracking-wide text-slate-400">{{ data.ownership.source }}</dd></div>
                        </dl>
                        <div class="mt-3">
                            <p class="text-slate-500 text-xs mb-1">Nameservers</p>
                            <ul class="text-sm space-y-0.5">
                                <li v-for="(ns,i) in data.ownership.nameservers" :key="i" class="font-mono text-slate-700">{{ ns }}</li>
                            </ul>
                        </div>
                    </template>
                    <p v-else class="text-slate-500 text-sm">Registration data unavailable ({{ data.ownership.error }}).</p>
                </section>

                <!-- Web hosting -->
                <section class="bg-white border border-slate-200 rounded-xl p-5 shadow-sm">
                    <h3 class="text-base font-semibold text-slate-900 mb-3">Web Hosting</h3>
                    <div v-if="data.web.hosts.length" class="space-y-3">
                        <div v-for="(h,i) in data.web.hosts" :key="i" class="border border-slate-100 rounded-lg p-3 text-sm">
                            <div class="flex items-center justify-between">
                                <span class="font-mono">{{ h.ip }}</span>
                                <span class="text-xs px-2 py-0.5 rounded-full" :class="h.cloudflare ? 'bg-orange-100 text-orange-700' : 'bg-slate-100 text-slate-600'">
                                    {{ h.provider || h.as_name || 'Unknown' }}
                                </span>
                            </div>
                            <div class="text-slate-500 text-xs mt-1">
                                <span v-if="h.ptr" class="font-mono">{{ h.ptr }}</span>
                                <span v-if="h.asn"> · AS{{ h.asn }}</span>
                                <span v-if="h.country"> · {{ h.country }}</span>
                            </div>
                        </div>
                    </div>
                    <p v-else class="text-slate-500 text-sm">No A/AAAA records.</p>

                    <div v-if="data.web.http && data.web.http.reachable" class="mt-4 pt-3 border-t border-slate-100 text-sm space-y-1">
                        <div class="flex justify-between"><span class="text-slate-500">Site</span><span class="font-mono text-xs">{{ data.web.http.final_url }}</span></div>
                        <div class="flex justify-between"><span class="text-slate-500">Status</span><span>{{ data.web.http.status }}<span v-if="data.web.http.redirected" class="text-slate-400"> (redirected)</span></span></div>
                        <div v-if="data.web.http.server" class="flex justify-between"><span class="text-slate-500">Server</span><span>{{ data.web.http.server }}</span></div>
                        <div v-if="data.web.http.cms" class="flex justify-between"><span class="text-slate-500">Platform</span><span class="font-medium text-blue-600">{{ data.web.http.cms }}</span></div>
                    </div>
                    <p v-else-if="data.web.http" class="mt-4 pt-3 border-t border-slate-100 text-sm text-slate-400">Website did not respond.</p>
                </section>
            </div>

            <!-- Mail -->
            <section class="bg-white border border-slate-200 rounded-xl p-5 shadow-sm">
                <div class="flex items-center justify-between mb-3">
                    <h3 class="text-base font-semibold text-slate-900">Mail</h3>
                    <span v-if="data.mail.provider" class="text-xs px-2.5 py-1 rounded-full bg-blue-100 text-blue-700 font-medium">{{ data.mail.provider }}</span>
                </div>
                <div class="grid md:grid-cols-2 gap-4 text-sm">
                    <div>
                        <p class="text-slate-500 text-xs mb-1">MX Records</p>
                        <ul v-if="data.mail.mx.length" class="space-y-0.5">
                            <li v-for="(mx,i) in data.mail.mx" :key="i" class="font-mono text-slate-700"><span class="text-slate-400">{{ mx.priority }}</span> {{ mx.host }}</li>
                        </ul>
                        <span v-else class="text-red-600">None</span>
                    </div>
                    <div class="space-y-2">
                        <div class="flex items-start gap-2"><span class="w-16 text-slate-500 shrink-0">SPF</span><span :class="data.mail.spf ? 'text-slate-700' : 'text-red-600'" class="font-mono text-xs break-all">{{ data.mail.spf ? data.mail.spf.raw : 'missing' }}</span></div>
                        <div class="flex items-start gap-2"><span class="w-16 text-slate-500 shrink-0">DMARC</span><span :class="dmarcClass" class="font-mono text-xs break-all">{{ data.mail.dmarc ? data.mail.dmarc.raw : 'missing' }}</span></div>
                        <div class="flex items-start gap-2"><span class="w-16 text-slate-500 shrink-0">DKIM</span>
                            <span v-if="dkimKeys.length" class="text-green-700">Found: {{ dkimKeys.join(', ') }}</span>
                            <span v-else class="text-slate-400">No common selectors found</span>
                        </div>
                        <div v-if="data.mail.mta_sts" class="flex items-start gap-2"><span class="w-16 text-slate-500 shrink-0">MTA-STS</span><span class="text-green-700 text-xs">enabled</span></div>
                        <div v-if="data.mail.autodiscover" class="flex items-start gap-2"><span class="w-16 text-slate-500 shrink-0">Autodisc.</span><span class="font-mono text-xs">{{ data.mail.autodiscover.target || data.mail.autodiscover.ip }}</span></div>
                    </div>
                </div>
            </section>

            <!-- Subdomains -->
            <section class="bg-white border border-slate-200 rounded-xl p-5 shadow-sm">
                <div class="flex items-center justify-between mb-3">
                    <h3 class="text-base font-semibold text-slate-900">Subdomains</h3>
                    <span class="text-xs text-slate-400">{{ data.subdomains.length }} live · CT logs + probes + AXFR</span>
                </div>
                <div v-if="data.subdomains.length" class="overflow-x-auto">
                    <table class="w-full text-sm">
                        <thead>
                            <tr class="text-left text-slate-400 text-xs border-b border-slate-100">
                                <th class="py-2 pr-3 font-medium">Host</th>
                                <th class="py-2 pr-3 font-medium">Type</th>
                                <th class="py-2 pr-3 font-medium">Points to</th>
                                <th class="py-2 font-medium">Found via</th>
                            </tr>
                        </thead>
                        <tbody>
                            <tr v-for="(s,i) in data.subdomains" :key="i" class="border-b border-slate-50 hover:bg-slate-50">
                                <td class="py-1.5 pr-3 font-mono">{{ s.name }}</td>
                                <td class="py-1.5 pr-3 text-slate-500">{{ s.type }}</td>
                                <td class="py-1.5 pr-3 font-mono text-xs text-slate-600">{{ s.target || s.ip }}</td>
                                <td class="py-1.5">
                                    <span v-for="src in s.sources" :key="src" class="inline-block text-[10px] uppercase tracking-wide px-1.5 py-0.5 mr-1 rounded" :class="srcClass(src)">{{ src }}</span>
                                </td>
                            </tr>
                        </tbody>
                    </table>
                </div>
                <p v-else class="text-slate-500 text-sm">No subdomains discovered.</p>
            </section>

            <!-- DNS records -->
            <section class="bg-white border border-slate-200 rounded-xl p-5 shadow-sm">
                <h3 class="text-base font-semibold text-slate-900 mb-3">DNS Records</h3>
                <div class="grid md:grid-cols-2 gap-x-6 gap-y-4 text-sm">
                    <div v-for="(rows,type) in flatDns" :key="type">
                        <p class="text-slate-500 text-xs font-semibold uppercase tracking-wide mb-1">{{ type }}</p>
                        <ul class="space-y-0.5">
                            <li v-for="(r,i) in rows" :key="i" class="font-mono text-xs text-slate-700 break-all">{{ r }}</li>
                        </ul>
                    </div>
                </div>
                <p class="mt-4 text-xs text-slate-400">DNSSEC: <span :class="data.dnssec.enabled ? 'text-green-600' : 'text-slate-500'">{{ data.dnssec.enabled ? 'enabled ('+data.dnssec.dnskey_count+' DNSKEY)' : 'not enabled' }}</span></p>
            </section>

            <!-- DKIM probe -->
            <section class="bg-white border border-slate-200 rounded-xl p-5 shadow-sm">
                <h3 class="text-base font-semibold text-slate-900 mb-3">Probe extra DKIM selectors</h3>
                <div class="flex gap-2 mb-3">
                    <input v-model="dkimInput" placeholder="s1, s2, custom-selector"
                        class="flex-1 px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" />
                    <button @click="probeDkim" :disabled="probing" class="px-5 py-2 bg-slate-800 text-white rounded-lg hover:bg-slate-900 disabled:opacity-50">Probe</button>
                </div>
                <div v-if="probeError" class="bg-red-50 border border-red-200 text-red-700 px-3 py-2 rounded-md mb-3 text-sm">{{ probeError }}</div>
                <pre v-if="dkim" class="bg-slate-50 p-3 rounded-md overflow-auto text-xs">{{ dkim }}</pre>
            </section>

            <!-- Raw -->
            <details class="bg-white border border-slate-200 rounded-xl p-5 shadow-sm">
                <summary class="cursor-pointer text-sm font-medium text-slate-600 hover:text-slate-900">Raw JSON response</summary>
                <pre class="bg-slate-50 p-4 rounded-md overflow-auto text-xs mt-3">{{ data }}</pre>
            </details>
        </div>
    </div>

    <script src="https://unpkg.com/vue@2.7.16/dist/vue.js"></script>
    <script>
        new Vue({
            el: '#app',
            data() {
                return {
                    domain: '', loading: false, error: '', data: null,
                    dkimInput: 's1, s2, k3, mxvault', dkim: null, probing: false, probeError: ''
                }
            },
            computed: {
                dkimKeys() { return this.data && this.data.mail.dkim ? Object.keys(this.data.mail.dkim) : [] },
                dmarcClass() {
                    if (!this.data || !this.data.mail.dmarc) return 'text-red-600';
                    const g = this.data.mail.dmarc.grade;
                    return g === 'enforced' ? 'text-green-700' : (g === 'partial' ? 'text-amber-600' : 'text-slate-600');
                },
                flatDns() {
                    if (!this.data) return {};
                    const d = this.data.dns, out = {};
                    const push = (label, rows, fn) => { const v = (rows || []).map(fn).filter(Boolean); if (v.length) out[label] = v; };
                    push('A', d.A, r => r.ip);
                    push('AAAA', d.AAAA, r => r.ipv6);
                    push('MX', d.MX, r => `${r.pri} ${r.target}`);
                    push('NS', d.NS, r => r.target);
                    push('CNAME (www)', d.CNAME_www, r => r.target);
                    push('TXT', d.TXT, r => r.txt);
                    push('CAA', d.CAA, r => `${r.flags || 0} ${r.tag || ''} ${r.value || ''}`);
                    push('SOA', d.SOA, r => `${r.mname} ${r.rname}`);
                    return out;
                }
            },
            methods: {
                sevClass(sev) {
                    return {
                        high: 'bg-red-100 text-red-700',
                        med: 'bg-amber-100 text-amber-700',
                        low: 'bg-slate-100 text-slate-600',
                        info: 'bg-blue-50 text-blue-600'
                    }[sev] || 'bg-slate-100 text-slate-600';
                },
                srcClass(src) {
                    return {
                        ct: 'bg-blue-50 text-blue-600',
                        probe: 'bg-slate-100 text-slate-500',
                        axfr: 'bg-red-100 text-red-700'
                    }[src] || 'bg-slate-100 text-slate-500';
                },
                fmtDate(d) {
                    if (!d) return '—';
                    const t = new Date(d);
                    return isNaN(t) ? d : t.toISOString().slice(0, 10);
                },
                async run() {
                    this.error = ''; this.loading = true; this.data = null; this.dkim = null;
                    try {
                        const r = await fetch(`?api=lookup&domain=${encodeURIComponent(this.domain.trim())}`);
                        const text = await r.text();
                        let body; try { body = JSON.parse(text); } catch (e) { body = text; }
                        if (!r.ok) throw new Error((body && body.error) || `HTTP ${r.status}`);
                        this.data = body;
                    } catch (e) { this.error = e.message }
                    finally { this.loading = false }
                },
                async probeDkim() {
                    this.probing = true; this.probeError = ''; this.dkim = null;
                    try {
                        const selectors = this.dkimInput.split(',').map(s => s.trim()).filter(Boolean);
                        const r = await fetch(`?api=dkim`, {
                            method: 'POST', headers: { 'Content-Type': 'application/json' },
                            body: JSON.stringify({ domain: this.data ? this.data.domain : this.domain, selectors })
                        });
                        this.dkim = await r.json();
                    } catch (e) { this.probeError = e.message }
                    finally { this.probing = false }
                },
                exportJson() {
                    const blob = new Blob([JSON.stringify(this.data, null, 2)], { type: 'application/json' });
                    const url = URL.createObjectURL(blob);
                    const a = Object.assign(document.createElement('a'), { href: url, download: `${this.data.domain}-lookup.json` });
                    a.click(); URL.revokeObjectURL(url);
                }
            }
        })
    </script>
</body>

</html>
