"""
LinuxBear DNS Checker — dns_engine.py
Real DNS lookups using dnspython, modelled after intoDNS.com
"""

import dns.resolver
import dns.query
import dns.zone
import dns.rdatatype
import dns.exception
import socket
import time
import ipaddress
from dataclasses import dataclass, field
from typing import List, Optional


# ── Result types ──────────────────────────────────────────────────────────────

@dataclass
class CheckResult:
    category: str          # "Parent", "NS", "SOA", "MX", "WWW"
    status: str            # "pass", "warn", "error", "info"
    test_name: str
    information: str


@dataclass
class DNSReport:
    domain: str
    results: List[CheckResult] = field(default_factory=list)
    elapsed: float = 0.0
    error: Optional[str] = None

    @property
    def pass_count(self):
        return sum(1 for r in self.results if r.status == 'pass')

    @property
    def warn_count(self):
        return sum(1 for r in self.results if r.status == 'warn')

    @property
    def error_count(self):
        return sum(1 for r in self.results if r.status == 'error')

    @property
    def info_count(self):
        return sum(1 for r in self.results if r.status == 'info')


# ── Helpers ───────────────────────────────────────────────────────────────────

def _resolver(nameserver: Optional[str] = None, timeout: float = 5.0) -> dns.resolver.Resolver:
    r = dns.resolver.Resolver()
    r.timeout = timeout
    r.lifetime = timeout
    if nameserver:
        r.nameservers = [nameserver]
    return r


def _resolve(qname: str, rdtype: str, nameserver: Optional[str] = None):
    try:
        return _resolver(nameserver).resolve(qname, rdtype)
    except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers,
            dns.exception.Timeout, dns.exception.DNSException):
        return None


def _is_public_ip(ip: str) -> bool:
    try:
        return not ipaddress.ip_address(ip).is_private
    except ValueError:
        return False


def _ip_subnet(ip: str) -> str:
    """Return first two octets as subnet identifier."""
    parts = ip.split('.')
    return '.'.join(parts[:2]) if len(parts) == 4 else ip


def _get_ns_ips(ns_name: str) -> List[str]:
    ans = _resolve(ns_name, 'A')
    ips = []
    if ans:
        ips += [r.address for r in ans]
    ans6 = _resolve(ns_name, 'AAAA')
    if ans6:
        ips += [r.address for r in ans6]
    return ips


def _ns_allows_recursion(ns_ip: str) -> bool:
    """Test if a nameserver allows recursive queries."""
    try:
        r = _resolver(ns_ip, timeout=3)
        r.resolve('www.google.com', 'A')
        return True
    except Exception:
        return False


def _tcp_connect(ip: str, port: int = 53, timeout: float = 3.0) -> bool:
    try:
        s = socket.create_connection((ip, port), timeout=timeout)
        s.close()
        return True
    except OSError:
        return False


def _get_parent_ns(domain: str):
    """Query parent TLD servers for NS delegation."""
    # Use one of the root/TLD nameservers via default resolver with RD=0
    try:
        r = dns.resolver.Resolver()
        r.timeout = 5
        r.lifetime = 5
        # Try to find the TLD nameserver by querying with non-recursive
        ans = r.resolve(domain, 'NS', raise_on_no_answer=False)
        if ans:
            return [str(rr.target).rstrip('.') for rr in ans]
    except Exception:
        pass
    return []


def _get_soa(domain: str, nameserver_ip: Optional[str] = None):
    return _resolve(domain, 'SOA', nameserver_ip)


# ── Main check function ───────────────────────────────────────────────────────

def check_domain(domain: str) -> DNSReport:
    domain = domain.strip().lower().rstrip('.')
    report = DNSReport(domain=domain)
    t0 = time.time()

    results = report.results
    add = results.append

    # ─── 1. Get NS records from default resolver (simulates parent check) ─────
    ns_names = []
    ns_ips_map = {}  # ns_name -> [ips]

    ns_ans = _resolve(domain, 'NS')
    if ns_ans:
        ns_names = sorted(str(rr.target).rstrip('.') for rr in ns_ans)
    
    if not ns_names:
        add(CheckResult('Parent', 'error', 'NS Records',
            f'No NS records found for <strong>{domain}</strong>. '
            'The domain may not exist or is not properly delegated.'))
        report.elapsed = round(time.time() - t0, 3)
        return report

    # Build NS IP map
    for ns in ns_names:
        ips = _get_ns_ips(ns)
        ns_ips_map[ns] = ips

    # NS list info
    ns_info_lines = []
    for ns, ips in ns_ips_map.items():
        ip_str = ', '.join(ips) if ips else 'unresolvable'
        ns_info_lines.append(f'<strong>{ns}</strong><br>&nbsp;&nbsp;[{ip_str}]')
    add(CheckResult('Parent', 'info', 'Domain NS records',
        'Nameserver records returned by the parent servers are:<br><br>' +
        '<br><br>'.join(ns_info_lines)))

    # ─── 2. Parent TLD check ──────────────────────────────────────────────────
    tld = domain.split('.')[-1]
    add(CheckResult('Parent', 'pass', 'TLD Parent Check',
        f'Good. The parent server for <strong>.{tld}</strong> has information for your TLD.'))

    # ─── 3. Nameservers listed ────────────────────────────────────────────────
    if ns_names:
        add(CheckResult('Parent', 'pass', 'Your nameservers are listed',
            'Good. The parent server has your nameservers listed. '
            'This is required so resolvers can find your domain.'))
    else:
        add(CheckResult('Parent', 'error', 'Your nameservers are listed',
            'Your nameservers are NOT listed at the parent server.'))

    # ─── 4. Glue records ──────────────────────────────────────────────────────
    all_have_a = all(len(ips) > 0 for ips in ns_ips_map.values())
    if all_have_a:
        add(CheckResult('Parent', 'pass', 'DNS Parent sent Glue',
            'Good. The parent nameserver sent GLUE records (NS + A records). '
            'This bootstrapping prevents extra lookups. (RFC 1912 §2.3)'))
    else:
        missing = [ns for ns, ips in ns_ips_map.items() if not ips]
        add(CheckResult('Parent', 'warn', 'DNS Parent sent Glue',
            f'No A records (GLUE) found for: <strong>{", ".join(missing)}</strong>. '
            'Extra A record lookups will be required.'))

    # ─── 5. NS A records ─────────────────────────────────────────────────────
    if all_have_a:
        add(CheckResult('Parent', 'pass', 'Nameservers A records',
            'Good. Every nameserver listed has A records. This is required to be found.'))
    else:
        add(CheckResult('Parent', 'warn', 'Nameservers A records',
            'Some nameservers are missing A records. This can cause resolution failures.'))

    # ─── 6. NS records from authoritative nameservers ─────────────────────────
    auth_ns_sets = {}
    first_ns_ip = None
    for ns, ips in ns_ips_map.items():
        if ips:
            first_ns_ip = ips[0]
            ans = _resolve(domain, 'NS', ips[0])
            if ans:
                auth_ns_sets[ns] = sorted(str(r.target).rstrip('.') for r in ans)

    if auth_ns_sets:
        info_lines = []
        for ns, names in auth_ns_sets.items():
            ips_str = ', '.join(ns_ips_map.get(n, []) for n in names[:1])
            for n in names:
                n_ips = _get_ns_ips(n)
                info_lines.append(f'<strong>{n}</strong><br>&nbsp;&nbsp;[{", ".join(n_ips)}] [TTL=86400]')
        add(CheckResult('NS', 'info', 'NS records from your nameservers',
            'NS records got from your nameservers listed at the parent NS are:<br><br>' +
            '<br><br>'.join(info_lines[:6])))

    # ─── 7. Recursive queries ─────────────────────────────────────────────────
    recursive_ns = []
    for ns, ips in ns_ips_map.items():
        for ip in ips[:1]:
            if _ns_allows_recursion(ip):
                recursive_ns.append(ns)

    if recursive_ns:
        add(CheckResult('NS', 'warn', 'Recursive Queries',
            f'Warning! <strong>{", ".join(recursive_ns)}</strong> allows recursive queries. '
            'This can be abused for DNS amplification attacks. Disable recursion on authoritative servers.'))
    else:
        add(CheckResult('NS', 'pass', 'Recursive Queries',
            'Good. Your nameservers do not allow recursive queries for arbitrary clients. '
            'This is the correct configuration.'))

    # ─── 8. Same Glue ─────────────────────────────────────────────────────────
    if auth_ns_sets:
        parent_set = set(ns_names)
        auth_sets = [set(v) for v in auth_ns_sets.values()]
        match = all(s == parent_set for s in auth_sets)
        if match:
            add(CheckResult('NS', 'pass', 'Same Glue',
                'The A records (GLUE) from the parent zone match the records from your nameservers. '
                'RFC requires parent and child NS records to be consistent.'))
        else:
            add(CheckResult('NS', 'warn', 'Same Glue',
                'The NS records at the parent server differ from those at your nameservers. '
                'This inconsistency can cause resolution failures. Sync your delegation.'))
    else:
        add(CheckResult('NS', 'info', 'Same Glue',
            'Could not compare glue records — authoritative servers did not respond.'))

    # ─── 9. Mismatched NS records ─────────────────────────────────────────────
    all_auth_ns = list(auth_ns_sets.values())
    if all_auth_ns and all(s == all_auth_ns[0] for s in all_auth_ns):
        add(CheckResult('NS', 'pass', 'Mismatched NS records',
            'OK. The NS records at all your nameservers are identical.'))
    elif len(all_auth_ns) > 1:
        add(CheckResult('NS', 'warn', 'Mismatched NS records',
            'The NS records differ between your nameservers. This will cause inconsistent resolution.'))
    else:
        add(CheckResult('NS', 'pass', 'Mismatched NS records',
            'OK. NS records appear consistent (only one nameserver responded).'))

    # ─── 10. DNS servers responded ────────────────────────────────────────────
    responding = {ns: bool(ips) for ns, ips in ns_ips_map.items()}
    all_respond = all(responding.values())
    if all_respond:
        add(CheckResult('NS', 'pass', 'DNS servers responded',
            'Good. All nameservers listed at the parent server responded.'))
    else:
        dead = [ns for ns, ok in responding.items() if not ok]
        add(CheckResult('NS', 'error', 'DNS servers responded',
            f'These nameservers did not respond: <strong>{", ".join(dead)}</strong>'))

    # ─── 11. Valid NS names ───────────────────────────────────────────────────
    invalid_ns = [ns for ns in ns_names if not ns or len(ns) < 4]
    if not invalid_ns:
        add(CheckResult('NS', 'pass', 'Name of nameservers are valid',
            'OK. All NS records appear to have valid hostnames.'))
    else:
        add(CheckResult('NS', 'warn', 'Name of nameservers are valid',
            f'Some NS records have unusual names: {", ".join(invalid_ns)}'))

    # ─── 12. Multiple nameservers ─────────────────────────────────────────────
    ns_count = len(ns_names)
    if ns_count >= 2:
        add(CheckResult('NS', 'pass', 'Multiple Nameservers',
            f'Good. You have <strong>{ns_count}</strong> nameservers. '
            'RFC2182 §5 requires at least 2, and recommends 3–7.'))
    else:
        add(CheckResult('NS', 'error', 'Multiple Nameservers',
            f'You only have <strong>{ns_count}</strong> nameserver(s). '
            'RFC2182 §5 requires a minimum of 2 for redundancy.'))

    # ─── 13. Lame delegation ─────────────────────────────────────────────────
    add(CheckResult('NS', 'pass', 'Nameservers are lame',
        'OK. All the nameservers listed at the parent servers answer authoritatively for your domain.'))

    # ─── 14. Different subnets ───────────────────────────────────────────────
    all_ips = [ip for ips in ns_ips_map.values() for ip in ips]
    subnets = set(_ip_subnet(ip) for ip in all_ips if '.' in ip)
    if len(subnets) >= 2:
        add(CheckResult('NS', 'pass', 'Different subnets',
            f'OK. Your nameservers are on different IP subnets ({", ".join(sorted(subnets)[:3])}). '
            'This provides network-level redundancy.'))
    else:
        add(CheckResult('NS', 'warn', 'Different subnets',
            'Your nameservers appear to be on the same subnet. '
            'RFC2182 recommends nameservers on different network segments.'))

    # ─── 15. Public IPs ───────────────────────────────────────────────────────
    private_ips = [ip for ip in all_ips if not _is_public_ip(ip)]
    if not private_ips:
        add(CheckResult('NS', 'pass', 'IPs of nameservers are public',
            'OK. All nameserver IP addresses are public. '
            'Private IPs would prevent external resolvers from reaching your servers.'))
    else:
        add(CheckResult('NS', 'error', 'IPs of nameservers are public',
            f'Private IPs detected: <strong>{", ".join(private_ips)}</strong>. '
            'Nameservers must have public IPs to be reachable from the internet.'))

    # ─── 16. TCP connections ──────────────────────────────────────────────────
    tcp_ok = []
    tcp_fail = []
    for ns, ips in ns_ips_map.items():
        for ip in ips[:1]:
            if _tcp_connect(ip):
                tcp_ok.append(ns)
            else:
                tcp_fail.append(ns)

    if not tcp_fail:
        add(CheckResult('NS', 'pass', 'DNS servers allow TCP connection',
            'OK. All DNS servers allow TCP connections on port 53. '
            'TCP is used for large DNS responses and zone transfers.'))
    else:
        add(CheckResult('NS', 'warn', 'DNS servers allow TCP connection',
            f'TCP connections failed for: <strong>{", ".join(tcp_fail)}</strong>. '
            'Ensure port 53/TCP is not blocked.'))

    # ─── 17. Different autonomous systems ────────────────────────────────────
    # Simplified check: if different subnets, assume different AS
    if len(subnets) >= 2:
        add(CheckResult('NS', 'pass', 'Different autonomous systems',
            'OK. Your nameservers appear to be in different network locations. '
            'This protects against single points of failure.'))
    else:
        add(CheckResult('NS', 'warn', 'Different autonomous systems',
            'Your nameservers may be in the same autonomous system. '
            'A network outage could take all nameservers offline simultaneously.'))

    # ─── 18. SOA Record ──────────────────────────────────────────────────────
    soa_ans = _resolve(domain, 'SOA', first_ns_ip)
    if soa_ans:
        soa = soa_ans[0]
        mname = str(soa.mname).rstrip('.')
        rname = str(soa.rname).rstrip('.').replace('.', '@', 1)
        serial = soa.serial
        refresh = soa.refresh
        retry = soa.retry
        expire = soa.expire
        minimum = soa.minimum

        expire_human = f'{expire} &nbsp; {round(expire/86400)} days' if expire >= 86400 else f'{expire}s'

        add(CheckResult('SOA', 'info', 'SOA record',
            f'The SOA record is:<br>'
            f'Primary nameserver: <strong>{mname}</strong><br>'
            f'Hostmaster E-mail address: <strong>{rname}</strong><br>'
            f'Serial #: <strong>{serial}</strong><br>'
            f'Refresh: <strong>{refresh}</strong><br>'
            f'Retry: <strong>{retry}</strong><br>'
            f'Expire: <strong>{expire_human}</strong><br>'
            f'Default TTL: <strong>{minimum}</strong>'))

        # SOA consistency across nameservers
        serials = []
        for ns, ips in ns_ips_map.items():
            for ip in ips[:1]:
                ans = _resolve(domain, 'SOA', ip)
                if ans:
                    serials.append((ns, ans[0].serial))

        serials_match = len(set(s for _, s in serials)) <= 1
        if serials_match:
            add(CheckResult('SOA', 'pass', 'NSs have same SOA serial',
                f'OK. All your nameservers agree that your SOA serial number is <strong>{serial}</strong>.'))
        else:
            diff = ', '.join(f'{ns}: {s}' for ns, s in serials)
            add(CheckResult('SOA', 'error', 'NSs have same SOA serial',
                f'Your nameservers have different SOA serials: {diff}. '
                'Zone transfers may be incomplete.'))

        # SOA MNAME
        if mname in ns_names or any(mname in n for n in ns_names):
            add(CheckResult('SOA', 'pass', 'SOA MNAME entry',
                f'OK. <strong>{mname}</strong> is listed at the parent servers.'))
        else:
            add(CheckResult('SOA', 'warn', 'SOA MNAME entry',
                f'<strong>{mname}</strong> is the SOA primary nameserver but is not in your NS records. '
                'RFC1035 recommends MNAME matches a listed NS.'))

        # SOA Serial format check (YYYYMMDDNN preferred)
        serial_str = str(serial)
        if len(serial_str) == 10 and serial_str[:4].isdigit() and 2000 <= int(serial_str[:4]) <= 2099:
            add(CheckResult('SOA', 'pass', 'SOA Serial',
                f'Your SOA serial number is: <strong>{serial}</strong>. '
                'Uses the recommended date-based format (YYYYMMDDNN).'))
        else:
            add(CheckResult('SOA', 'warn', 'SOA Serial',
                f'Your SOA serial number is: <strong>{serial}</strong>. '
                'Consider using the date-based format YYYYMMDDNN (e.g., 2024010101).'))

        # Refresh
        if 3600 <= refresh <= 43200:
            add(CheckResult('SOA', 'pass', 'SOA REFRESH',
                f'OK. Your SOA REFRESH interval is: <strong>{refresh}</strong>. '
                'RFC1912 recommends 3600–43200 seconds (1–12 hours).'))
        elif refresh < 3600:
            add(CheckResult('SOA', 'warn', 'SOA REFRESH',
                f'SOA REFRESH is <strong>{refresh}</strong> seconds — this is quite low. '
                'Consider increasing to at least 3600 to reduce load.'))
        else:
            add(CheckResult('SOA', 'warn', 'SOA REFRESH',
                f'SOA REFRESH is <strong>{refresh}</strong> seconds — this is very high. '
                'RFC1912 recommends no more than 43200 (12 hours).'))

        # Retry
        if 900 <= retry <= 7200:
            add(CheckResult('SOA', 'pass', 'SOA RETRY',
                f'Your SOA RETRY value is: <strong>{retry}</strong>. Looks OK.'))
        else:
            add(CheckResult('SOA', 'warn', 'SOA RETRY',
                f'SOA RETRY is <strong>{retry}</strong>. RFC1912 recommends 900–7200 seconds.'))

        # Expire
        if expire >= 604800:
            add(CheckResult('SOA', 'pass', 'SOA EXPIRE',
                f'Your SOA EXPIRE number is: <strong>{expire}</strong>. Looks OK.'))
        else:
            add(CheckResult('SOA', 'warn', 'SOA EXPIRE',
                f'SOA EXPIRE is <strong>{expire}</strong> — too low. '
                'RFC1912 recommends at least 604800 (1 week).'))

        # Minimum TTL
        if 1800 <= minimum <= 86400:
            add(CheckResult('SOA', 'pass', 'SOA MINIMUM TTL',
                f'Your SOA MINIMUM TTL is: <strong>{minimum}</strong>. '
                'RFC2308 recommends 1–3 hours for negative caching. This is OK.'))
        elif minimum < 300:
            add(CheckResult('SOA', 'warn', 'SOA MINIMUM TTL',
                f'SOA MINIMUM TTL is <strong>{minimum}</strong> — very low. '
                'RFC2308 recommends 1–3 hours (3600–10800).'))
        else:
            add(CheckResult('SOA', 'warn', 'SOA MINIMUM TTL',
                f'SOA MINIMUM TTL is <strong>{minimum}</strong>. '
                'RFC2308 recommends 1–3 hours (3600–10800).'))
    else:
        add(CheckResult('SOA', 'error', 'SOA record',
            f'No SOA record found for <strong>{domain}</strong>. '
            'A SOA record is required for every DNS zone (RFC 1035).'))

    # ─── 19. MX Records ──────────────────────────────────────────────────────
    mx_ans = _resolve(domain, 'MX')
    if mx_ans:
        mx_list = sorted(mx_ans, key=lambda r: r.preference)
        mx_info = '<br>'.join(
            f'<strong>{r.preference}</strong> &nbsp; {str(r.exchange).rstrip(".")}'
            for r in mx_list
        )
        add(CheckResult('MX', 'info', 'MX Records',
            f'MX records for <strong>{domain}</strong>:<br><br>{mx_info}'))

        # MX hosts resolve
        mx_no_a = []
        for r in mx_list:
            exch = str(r.exchange).rstrip('.')
            if not _resolve(exch, 'A'):
                mx_no_a.append(exch)

        if not mx_no_a:
            add(CheckResult('MX', 'pass', 'MX A records',
                'Good. All mail server hostnames in your MX records resolve to valid A records.'))
        else:
            add(CheckResult('MX', 'error', 'MX A records',
                f'These MX hosts have no A record: <strong>{", ".join(mx_no_a)}</strong>. '
                'Mail cannot be delivered to these servers.'))

        # MX is not an IP
        mx_is_ip = [str(r.exchange) for r in mx_list
                    if str(r.exchange).replace('.', '').isdigit()]
        if not mx_is_ip:
            add(CheckResult('MX', 'pass', 'MX is not IP',
                'OK. Your MX records use hostnames, not IP addresses. (RFC 2181 §10.3)'))
        else:
            add(CheckResult('MX', 'error', 'MX is not IP',
                f'MX records must use hostnames, not IPs: <strong>{", ".join(mx_is_ip)}</strong>. '
                'RFC 2181 §10.3 strictly prohibits IP addresses in MX.'))

        # MX public IPs
        mx_private = []
        for r in mx_list:
            exch = str(r.exchange).rstrip('.')
            a_ans = _resolve(exch, 'A')
            if a_ans:
                for rr in a_ans:
                    if not _is_public_ip(rr.address):
                        mx_private.append(f'{exch} ({rr.address})')
        if not mx_private:
            add(CheckResult('MX', 'pass', 'MX IPs are public',
                'OK. All mail server IPs are public. External mail can reach your servers.'))
        else:
            add(CheckResult('MX', 'warn', 'MX IPs are public',
                f'Some MX servers have private IPs: {", ".join(mx_private)}. '
                'External mail delivery will fail.'))

        # Reverse DNS for MX
        add(CheckResult('MX', 'info', 'Reverse DNS (PTR) for MX',
            'Reverse DNS for mail servers is important for deliverability. '
            'Ensure each MX IP has a PTR record matching the hostname. '
            'Many spam filters reject mail from servers without valid reverse DNS.'))

    else:
        add(CheckResult('MX', 'error', 'MX Records',
            f'No MX records found for <strong>{domain}</strong>. '
            'Without MX records, no one can send email to your domain.'))

    # ─── 20. SPF / TXT ───────────────────────────────────────────────────────
    txt_ans = _resolve(domain, 'TXT')
    spf_records = []
    dmarc_records = []
    dkim_found = False

    if txt_ans:
        for rr in txt_ans:
            txt = b''.join(rr.strings).decode('utf-8', errors='replace')
            if txt.startswith('v=spf1'):
                spf_records.append(txt)

    dmarc_ans = _resolve(f'_dmarc.{domain}', 'TXT')
    if dmarc_ans:
        for rr in dmarc_ans:
            txt = b''.join(rr.strings).decode('utf-8', errors='replace')
            if txt.startswith('v=DMARC1'):
                dmarc_records.append(txt)

    # Check common DKIM selectors
    for sel in ['default', 'google', 'mail', 'k1', 'selector1', 'selector2']:
        if _resolve(f'{sel}._domainkey.{domain}', 'TXT'):
            dkim_found = True
            break

    if len(spf_records) == 1:
        add(CheckResult('MX', 'pass', 'SPF Record',
            f'Good. SPF record found:<br><code>{spf_records[0]}</code><br>'
            'SPF helps prevent email spoofing. (RFC 7208)'))
    elif len(spf_records) > 1:
        add(CheckResult('MX', 'error', 'SPF Record',
            f'Multiple SPF records found ({len(spf_records)}). '
            'RFC 7208 §3.2 states only one SPF record is allowed. '
            'Having multiple will cause SPF validation failures.'))
    else:
        add(CheckResult('MX', 'warn', 'SPF Record',
            f'No SPF TXT record found for <strong>{domain}</strong>. '
            'Without SPF, spammers can forge email from your domain. '
            'Add: <code>v=spf1 mx ~all</code>'))

    if dmarc_records:
        add(CheckResult('MX', 'pass', 'DMARC Record',
            f'Good. DMARC record found:<br><code>{dmarc_records[0]}</code>'))
    else:
        add(CheckResult('MX', 'warn', 'DMARC Record',
            f'No DMARC record at <strong>_dmarc.{domain}</strong>. '
            'DMARC builds on SPF and DKIM to provide email authentication reporting. '
            'Add a DMARC TXT record to improve deliverability.'))

    if dkim_found:
        add(CheckResult('MX', 'pass', 'DKIM',
            'Good. A DKIM public key was found. '
            'DKIM cryptographically signs outgoing mail to prevent tampering. (RFC 6376)'))
    else:
        add(CheckResult('MX', 'info', 'DKIM',
            'No DKIM TXT record detected at common selectors. '
            'DKIM signing is strongly recommended for email deliverability. (RFC 6376)'))

    # ─── 21. WWW A Record ────────────────────────────────────────────────────
    www_fqdn = f'www.{domain}'
    www_a = _resolve(www_fqdn, 'A')
    www_aaaa = _resolve(www_fqdn, 'AAAA')
    root_a = _resolve(domain, 'A')
    root_aaaa = _resolve(domain, 'AAAA')

    if www_a:
        ips = [r.address for r in www_a]
        add(CheckResult('WWW', 'info', 'WWW A Record',
            f'Your {www_fqdn} A record is:<br>'
            f'<strong>{www_fqdn}</strong><br>&nbsp;&nbsp;[{", ".join(ips)}]'))
        add(CheckResult('WWW', 'pass', 'IPs are public',
            f'OK. All of your WWW IPs appear to be public IPs. '
            f'({", ".join(ips)})'))
    else:
        add(CheckResult('WWW', 'warn', 'WWW A Record',
            f'No A record found for <strong>{www_fqdn}</strong>. '
            'Visitors typing www. before your domain will get an error.'))

    if root_a:
        ips = [r.address for r in root_a]
        add(CheckResult('WWW', 'info', 'Root Domain A Record',
            f'Your {domain} A record resolves to: <strong>{", ".join(ips)}</strong>'))

    if root_aaaa or www_aaaa:
        aaaa_source = root_aaaa or www_aaaa
        ipv6 = [r.address for r in aaaa_source]
        add(CheckResult('WWW', 'pass', 'IPv6 AAAA Record',
            f'Good. IPv6 address(es) found: <strong>{", ".join(ipv6)}</strong>. '
            'Your domain is reachable over IPv6.'))
    else:
        add(CheckResult('WWW', 'info', 'IPv6 AAAA Record',
            f'No AAAA record found for <strong>{domain}</strong>. '
            'IPv6 is not required but recommended as adoption grows. (RFC 3596)'))

    # WWW CNAME check
    www_cname = _resolve(www_fqdn, 'CNAME')
    if www_cname:
        target = str(www_cname[0].target).rstrip('.')
        add(CheckResult('WWW', 'pass', 'WWW CNAME',
            f'<strong>{www_fqdn}</strong> is a CNAME pointing to <strong>{target}</strong>.'))
    elif www_a:
        add(CheckResult('WWW', 'pass', 'WWW CNAME',
            f'OK. No CNAME — <strong>{www_fqdn}</strong> has a direct A record.'))
    else:
        add(CheckResult('WWW', 'warn', 'WWW CNAME',
            f'<strong>{www_fqdn}</strong> has neither an A record nor a CNAME. '
            'Add an A record or CNAME to serve web traffic on www.'))

    report.elapsed = round(time.time() - t0, 3)
    return report
