# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
DNS Manager for email server setup.

Generates required DNS records and verifies configuration.
"""

import logging
import socket
from typing import Any, Dict

logger = logging.getLogger(__name__)

# Optional dnspython for DNS lookups
try:
    import dns.exception
    import dns.resolver

    HAS_DNSPYTHON = True
except ImportError:
    HAS_DNSPYTHON = False
    logger.debug("dnspython not installed - DNS verification limited")


class DNSManager:
    """
    DNS configuration manager for email.

    Handles:
    - MX record configuration
    - SPF record generation
    - DKIM record generation
    - DMARC record generation
    - PTR (reverse DNS) verification
    - DNS record verification
    """

    def __init__(self, domain: str, hostname: str, dkim_manager=None):
        self.domain = domain
        self.hostname = hostname
        self.dkim_manager = dkim_manager
        self._public_ip = None

    async def get_public_ip(self) -> str:
        """Get the public IP address of this server."""
        if self._public_ip:
            return self._public_ip

        # Try multiple IP detection services
        services = [
            "https://api.ipify.org",
            "https://ifconfig.me/ip",
            "https://icanhazip.com",
        ]

        import aiohttp

        for service in services:
            try:
                async with aiohttp.ClientSession() as session:
                    async with session.get(service, timeout=5) as resp:
                        if resp.status == 200:
                            self._public_ip = (await resp.text()).strip()
                            return self._public_ip
            except Exception:
                continue

        # Fallback to socket
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            s.connect(("8.8.8.8", 80))
            self._public_ip = s.getsockname()[0]
            s.close()
            return self._public_ip
        except Exception:
            return "YOUR_PUBLIC_IP"

    async def get_required_records(self) -> Dict[str, Any]:
        """
        Get all required DNS records for email.

        Returns dict with record configurations.
        """
        ip = await self.get_public_ip()

        records = {
            "mx": {
                "type": "MX",
                "host": "@",
                "value": self.hostname,
                "priority": 10,
                "description": "Mail exchanger - tells other servers where to send mail",
            },
            "a": {
                "type": "A",
                "host": "mail"
                if self.hostname.startswith("mail.")
                else self.hostname.split(".")[0],
                "value": ip,
                "description": "IP address of mail server",
            },
            "spf": {
                "type": "TXT",
                "host": "@",
                "value": f"v=spf1 ip4:{ip} a:{self.hostname} ~all",
                "description": "Specifies which servers can send mail for your domain",
            },
            "dmarc": {
                "type": "TXT",
                "host": "_dmarc",
                "value": f"v=DMARC1; p=quarantine; rua=mailto:dmarc@{self.domain}; pct=100",
                "description": "Policy for handling email authentication failures",
            },
        }

        # Add DKIM if available
        if self.dkim_manager:
            dkim_public = await self.dkim_manager.get_public_key_dns()
            if dkim_public:
                records["dkim"] = {
                    "type": "TXT",
                    "host": f"{self.dkim_manager.selector}._domainkey",
                    "value": dkim_public,
                    "description": "DKIM public key for message signing verification",
                }

        # PTR record (informational - must be set by ISP/hosting provider)
        records["ptr"] = {
            "type": "PTR",
            "host": f"{self._reverse_ip(ip)}.in-addr.arpa" if ip != "YOUR_PUBLIC_IP" else "N/A",
            "value": self.hostname,
            "description": "Reverse DNS - must be set by your hosting provider",
            "note": "Contact your ISP or hosting provider to set this",
        }

        return records

    async def verify_records(self) -> Dict[str, Any]:
        """
        Verify that DNS records are properly configured.

        Returns status of each required record.
        """
        results = {
            "overall": True,
            "records": {},
        }

        if not HAS_DNSPYTHON:
            results["warning"] = "dnspython not installed — DNS verification unavailable."
            return results

        ip = await self.get_public_ip()

        # Check MX
        mx_result = await self._check_mx()
        results["records"]["mx"] = mx_result
        if not mx_result["valid"]:
            results["overall"] = False

        # Check A record
        a_result = await self._check_a(ip)
        results["records"]["a"] = a_result
        if not a_result["valid"]:
            results["overall"] = False

        # Check SPF
        spf_result = await self._check_spf()
        results["records"]["spf"] = spf_result
        if not spf_result["valid"]:
            results["overall"] = False

        # Check DKIM
        if self.dkim_manager:
            dkim_result = await self._check_dkim()
            results["records"]["dkim"] = dkim_result
            if not dkim_result["valid"]:
                results["overall"] = False

        # Check DMARC
        dmarc_result = await self._check_dmarc()
        results["records"]["dmarc"] = dmarc_result
        if not dmarc_result["valid"]:
            results["overall"] = False

        # Check PTR (reverse DNS)
        ptr_result = await self._check_ptr(ip)
        results["records"]["ptr"] = ptr_result
        # PTR is often not controllable, so don't fail overall

        return results

    async def _check_mx(self) -> Dict[str, Any]:
        """Check MX record."""
        try:
            answers = dns.resolver.resolve(self.domain, "MX")
            mx_hosts = [str(r.exchange).rstrip(".") for r in answers]

            if self.hostname in mx_hosts or any(
                self.hostname.lower() in h.lower() for h in mx_hosts
            ):
                return {
                    "valid": True,
                    "found": mx_hosts,
                    "message": "MX record correctly configured",
                }
            else:
                return {
                    "valid": False,
                    "found": mx_hosts,
                    "expected": self.hostname,
                    "message": f"MX record points to {mx_hosts}, expected {self.hostname}",
                }
        except dns.exception.DNSException as e:
            return {
                "valid": False,
                "error": str(e),
                "message": "MX record not found",
            }

    async def _check_a(self, expected_ip: str) -> Dict[str, Any]:
        """Check A record for mail hostname."""
        try:
            answers = dns.resolver.resolve(self.hostname, "A")
            ips = [str(r) for r in answers]

            if expected_ip in ips:
                return {
                    "valid": True,
                    "found": ips,
                    "message": "A record correctly configured",
                }
            else:
                return {
                    "valid": False,
                    "found": ips,
                    "expected": expected_ip,
                    "message": f"A record points to {ips}, expected {expected_ip}",
                }
        except dns.exception.DNSException as e:
            return {
                "valid": False,
                "error": str(e),
                "message": f"A record not found for {self.hostname}",
            }

    async def _check_spf(self) -> Dict[str, Any]:
        """Check SPF record."""
        try:
            answers = dns.resolver.resolve(self.domain, "TXT")

            for r in answers:
                txt = str(r).strip('"')
                if txt.startswith("v=spf1"):
                    return {
                        "valid": True,
                        "found": txt,
                        "message": "SPF record found",
                    }

            return {
                "valid": False,
                "message": "SPF record not found",
            }
        except dns.exception.DNSException as e:
            return {
                "valid": False,
                "error": str(e),
                "message": "SPF record not found",
            }

    async def _check_dkim(self) -> Dict[str, Any]:
        """Check DKIM record."""
        selector = self.dkim_manager.selector if self.dkim_manager else "default"
        dkim_domain = f"{selector}._domainkey.{self.domain}"

        try:
            answers = dns.resolver.resolve(dkim_domain, "TXT")

            for r in answers:
                txt = str(r).strip('"')
                if "v=DKIM1" in txt or "k=rsa" in txt:
                    return {
                        "valid": True,
                        "found": txt[:100] + "..." if len(txt) > 100 else txt,
                        "message": "DKIM record found",
                    }

            return {
                "valid": False,
                "message": f"DKIM record not found at {dkim_domain}",
            }
        except dns.exception.DNSException as e:
            return {
                "valid": False,
                "error": str(e),
                "message": f"DKIM record not found at {dkim_domain}",
            }

    async def _check_dmarc(self) -> Dict[str, Any]:
        """Check DMARC record."""
        dmarc_domain = f"_dmarc.{self.domain}"

        try:
            answers = dns.resolver.resolve(dmarc_domain, "TXT")

            for r in answers:
                txt = str(r).strip('"')
                if txt.startswith("v=DMARC1"):
                    return {
                        "valid": True,
                        "found": txt,
                        "message": "DMARC record found",
                    }

            return {
                "valid": False,
                "message": f"DMARC record not found at {dmarc_domain}",
            }
        except dns.exception.DNSException as e:
            return {
                "valid": False,
                "error": str(e),
                "message": f"DMARC record not found at {dmarc_domain}",
            }

    async def _check_ptr(self, ip: str) -> Dict[str, Any]:
        """Check PTR (reverse DNS) record."""
        if ip == "YOUR_PUBLIC_IP":
            return {
                "valid": False,
                "message": "Could not determine public IP",
            }

        try:
            # Reverse lookup
            hostname, _, _ = socket.gethostbyaddr(ip)

            if self.hostname.lower() in hostname.lower():
                return {
                    "valid": True,
                    "found": hostname,
                    "message": "PTR record correctly configured",
                }
            else:
                return {
                    "valid": False,
                    "found": hostname,
                    "expected": self.hostname,
                    "message": f"PTR record points to {hostname}, expected {self.hostname}",
                    "note": "Contact your ISP/hosting provider to fix this",
                }
        except socket.herror:
            return {
                "valid": False,
                "message": "PTR record not found - contact your ISP/hosting provider",
            }

    def _reverse_ip(self, ip: str) -> str:
        """Reverse IP address for PTR lookup."""
        parts = ip.split(".")
        return ".".join(reversed(parts))

    async def get_setup_guide(self) -> str:
        """
        Generate human-readable DNS setup guide.
        """
        records = await self.get_required_records()
        ip = await self.get_public_ip()

        guide = f"""
# DNS Setup Guide for {self.domain}

## Overview

To receive and send email, you need to configure the following DNS records
at your domain registrar or DNS provider.

## Your Server Information

- **Domain:** {self.domain}
- **Mail Hostname:** {self.hostname}
- **Server IP:** {ip}

---

## Required DNS Records

### 1. MX Record (Mail Exchanger)

Tells other mail servers where to deliver email for your domain.

```
Type:     MX
Host:     @  (or leave blank for root domain)
Value:    {self.hostname}
Priority: 10
TTL:      3600
```

### 2. A Record (Mail Server Address)

Points your mail hostname to your server's IP address.

```
Type:     A
Host:     mail  (or the subdomain part of {self.hostname})
Value:    {ip}
TTL:      3600
```

### 3. SPF Record (Sender Policy Framework)

Specifies which servers are allowed to send email for your domain.
Helps prevent email spoofing.

```
Type:     TXT
Host:     @  (or leave blank for root domain)
Value:    {records["spf"]["value"]}
TTL:      3600
```

### 4. DKIM Record (DomainKeys Identified Mail)

Contains your public key for cryptographic email signing.
Helps verify that emails actually came from your server.

```
Type:     TXT
Host:     {records.get("dkim", {}).get("host", "default._domainkey")}
Value:    {records.get("dkim", {}).get("value", "[Run email server to generate DKIM key]")}
TTL:      3600
```

**Note:** The DKIM value is a long string. Make sure to copy the entire value.
Some DNS providers require you to split it into multiple strings.

### 5. DMARC Record (Domain-based Message Authentication)

Tells receiving servers how to handle emails that fail authentication.

```
Type:     TXT
Host:     _dmarc
Value:    {records["dmarc"]["value"]}
TTL:      3600
```

### 6. PTR Record (Reverse DNS)

Maps your IP address back to your hostname. This is **critical** for
email deliverability.

```
IP:       {ip}
Hostname: {self.hostname}
```

**⚠️ Important:** You cannot set PTR records at your domain registrar.
You must contact your ISP or hosting provider (AWS, DigitalOcean, etc.)
to configure reverse DNS for your server's IP address.

---

## Verification

After adding all records, wait 5-30 minutes for DNS propagation, then run:

```bash
familiar email verify-dns
```

Or use online tools:
- https://mxtoolbox.com/SuperTool.aspx
- https://dmarcian.com/dmarc-inspector/

---

## Common Issues

### "MX record not found"
- Make sure you added the MX record at the root domain (@)
- Wait for DNS propagation (up to 48 hours)

### "SPF record invalid"
- Ensure the entire value is in one TXT record
- Don't add extra quotes

### "DKIM record not found"
- Check the host is exactly: `{records.get("dkim", {}).get("host", "default._domainkey")}`
- Some providers require you to omit the domain part

### "PTR record missing"
- This must be set by your hosting provider
- For AWS: Set in Route 53 or contact support
- For DigitalOcean: Set in networking settings
- For home ISP: Contact customer support

### Emails going to spam
- Ensure PTR record is correctly set
- Build sending reputation slowly
- Use a relay service (Mailgun, SendGrid) initially

---

## Need Help?

If you're stuck, the Familiar community can help:
- Documentation: https://familiar.ai/docs/email
- Discord: https://discord.gg/familiar
"""

        return guide.strip()
