"""Provide a data model for domains and some utility functions."""

# Standard Python Libraries
from collections import OrderedDict
from datetime import datetime, timedelta
from os import path, stat
from typing import Dict

# Third-Party Libraries
import publicsuffix

from . import PublicSuffixListFilename, PublicSuffixListReadOnly, trustymail


def get_psl():
    """
    Get the Public Suffix List - either new, or cached in the CWD for 24 hours.

    Returns
    -------
    PublicSuffixList: An instance of PublicSuffixList loaded with a cached or updated list
    """

    def download_psl():
        fresh_psl = publicsuffix.fetch()
        with open(PublicSuffixListFilename, "w", encoding="utf-8") as fresh_psl_file:
            fresh_psl_file.write(fresh_psl.read())

    # Download the psl if necessary
    if not PublicSuffixListReadOnly:
        if not path.exists(PublicSuffixListFilename):
            download_psl()
        else:
            psl_age = datetime.now() - datetime.fromtimestamp(
                stat(PublicSuffixListFilename).st_mtime
            )
            if psl_age > timedelta(hours=24):
                download_psl()

    with open(PublicSuffixListFilename, encoding="utf-8") as psl_file:
        psl = publicsuffix.PublicSuffixList(psl_file)

    return psl


def get_public_suffix(domain):
    """Return the public suffix of a given domain."""
    public_list = get_psl()

    return public_list.get_public_suffix(domain)


def format_list(record_list):
    """Format a list into a string to increase readability in CSV."""
    # record_list should only be a list, not an integer, None, or
    # anything else.  Thus this if clause handles only empty
    # lists.  This makes a "null" appear in the JSON output for
    # empty lists, as expected.
    if not record_list:
        return None

    return ", ".join(record_list)


class Domain:
    """Store information about a domain."""

    base_domains: Dict[str, "Domain"] = {}

    def __init__(
        self,
        domain_name,
        timeout,
        smtp_timeout,
        smtp_localhost,
        smtp_ports,
        smtp_cache,
        dns_hostnames,
    ):
        """Retrieve information about a given domain name."""
        self.domain_name = domain_name.lower()

        self.base_domain_name = get_public_suffix(self.domain_name)

        self.is_base_domain = True
        self.base_domain = None
        if self.base_domain_name != self.domain_name:
            self.is_base_domain = False
            if self.base_domain_name not in Domain.base_domains:
                # Populate DMARC for parent.
                domain = trustymail.scan(
                    self.base_domain_name,
                    timeout,
                    smtp_timeout,
                    smtp_localhost,
                    smtp_ports,
                    smtp_cache,
                    {"mx": False, "starttls": False, "spf": False, "dmarc": True},
                    dns_hostnames,
                )
                Domain.base_domains[self.base_domain_name] = domain
            self.base_domain = Domain.base_domains[self.base_domain_name]

        # Start off assuming the host is live unless an error tells us otherwise.
        self.is_live = True

        # Keep entire record for potential future use.
        self.mx_records = None
        self.mx_records_dnssec = None
        self.spf = None
        self.spf_dnssec = None
        self.dmarc = None
        self.dmarc_dnssec = False
        self.dmarc_policy = None
        self.dmarc_subdomain_policy = None
        self.dmarc_pct = None
        self.dmarc_aggregate_uris = []
        self.dmarc_forensic_uris = []
        self.dmarc_has_aggregate_uri = False
        self.dmarc_has_forensic_uri = False
        self.dmarc_reports_address_error = False

        # Syntax validity - default spf to false as the lack of an SPF is a bad thing.
        self.valid_spf = False
        self.valid_dmarc = True
        self.syntax_errors = []

        # Mail Info
        self.mail_servers = None

        # A dictionary for each port for each entry in mail_servers.
        # The dictionary's values indicate:
        # 1. Whether or not the server is listening on the port
        # 2. Whether or not the server supports SMTP
        # 3. Whether or not the server supports STARTTLS
        self.starttls_results = {}

        # A list of any debugging information collected while scanning records.
        self.debug_info = []

        # A list of the ports tested for SMTP
        self.ports_tested = set()

    def has_mail(self):
        """Check if there are any mail servers associated with this domain."""
        if self.mail_servers is not None:
            return len(self.mail_servers) > 0
        return None

    def has_supports_smtp(self):
        """Check if any of the mail servers associated with this domain are listening and support SMTP."""
        result = None
        if len(self.starttls_results) > 0:
            result = (
                len(
                    filter(
                        lambda x: self.starttls_results[x]["supports_smtp"],
                        self.starttls_results.keys(),
                    )
                )
                > 0
            )
        return result

    def has_starttls(self):
        """Check if any of the mail servers associated with this domain are listening and support STARTTLS."""
        result = None
        if len(self.starttls_results) > 0:
            result = (
                len(
                    filter(
                        lambda x: self.starttls_results[x]["starttls"],
                        self.starttls_results.keys(),
                    )
                )
                > 0
            )
        return result

    def has_spf(self):
        """Check if this domain has any Sender Policy Framework records."""
        if self.spf is not None:
            return len(self.spf) > 0
        return None

    def has_dmarc(self):
        """Check if this domain has a Domain-based Message Authentication, Reporting, and Conformance record."""
        if self.dmarc is not None:
            return len(self.dmarc) > 0
        return None

    def add_mx_record(self, record):
        """Add a mail server record for this domain."""
        if self.mx_records is None:
            self.mx_records = []
        self.mx_records.append(record)
        # The rstrip is because dnspython's string representation of
        # the record will contain a trailing period if it is a FQDN.
        if self.mail_servers is None:
            self.mail_servers = []
        self.mail_servers.append(record.exchange.to_text().rstrip(".").lower())

    def parent_has_dmarc(self):
        """Check if a domain or its parent has a Domain-based Message Authentication, Reporting, and Conformance record."""
        ans = self.has_dmarc()
        if self.base_domain:
            ans = self.base_domain.has_dmarc()
        return ans

    def parent_dmarc_dnssec(self):
        """Get this domain or its parent's DMARC DNSSEC information."""
        ans = self.dmarc_dnssec
        if self.base_domain:
            ans = self.base_domain.dmarc_dnssec
        return ans

    def parent_valid_dmarc(self):
        """Check if this domain or its parent have a valid DMARC record."""
        ans = self.valid_dmarc
        if self.base_domain:
            return self.base_domain.valid_dmarc
        return ans

    def parent_dmarc_results(self):
        """Get this domain or its parent's DMARC information."""
        ans = format_list(self.dmarc)
        if self.base_domain:
            ans = format_list(self.base_domain.dmarc)
        return ans

    def get_dmarc_policy(self):
        """Get this domain or its parent's DMARC policy."""
        ans = self.dmarc_policy
        # If the policy was never set, or isn't in the list of valid
        # policies, check the parents.
        if ans is None or ans.lower() not in ["quarantine", "reject", "none"]:
            if self.base_domain:
                # We check the *subdomain* policy in case one was
                # explicitly set.  If one was not explicitly set then
                # the subdomain policy is populated with the value for
                # the domain policy by trustymail.py anyway, in
                # accordance with the RFC
                # (https://tools.ietf.org/html/rfc7489#section-6.3).
                ans = self.base_domain.get_dmarc_subdomain_policy()
            else:
                ans = None
        return ans

    def get_dmarc_subdomain_policy(self):
        """Get this domain or its parent's DMARC subdomain policy."""
        ans = self.dmarc_subdomain_policy
        # If the policy was never set, or isn't in the list of valid
        # policies, check the parents.
        if ans is None or ans.lower() not in ["quarantine", "reject", "none"]:
            if self.base_domain:
                ans = self.base_domain.get_dmarc_subdomain_policy()
            else:
                ans = None
        return ans

    def get_dmarc_pct(self):
        """Get this domain or its parent's DMARC percentage information."""
        ans = self.dmarc_pct
        if not ans and self.base_domain:
            # Check the parents
            ans = self.base_domain.get_dmarc_pct()
        return ans

    def get_dmarc_has_aggregate_uri(self):
        """Get this domain or its parent's DMARC aggregate URI."""
        ans = self.dmarc_has_aggregate_uri
        # If there are no aggregate URIs then check the parents.
        if not ans and self.base_domain:
            ans = self.base_domain.get_dmarc_has_aggregate_uri()
        return ans

    def get_dmarc_has_forensic_uri(self):
        """Check if this domain or its parent have a DMARC forensic URI."""
        ans = self.dmarc_has_forensic_uri
        # If there are no forensic URIs then check the parents.
        if not ans and self.base_domain:
            ans = self.base_domain.get_dmarc_has_forensic_uri()
        return ans

    def get_dmarc_aggregate_uris(self):
        """Get this domain or its parent's DMARC aggregate URIs."""
        ans = self.dmarc_aggregate_uris
        # If there are no aggregate URIs then check the parents.
        if not ans and self.base_domain:
            ans = self.base_domain.get_dmarc_aggregate_uris()
        return ans

    def get_dmarc_forensic_uris(self):
        """Get this domain or its parent's DMARC forensic URIs."""
        ans = self.dmarc_forensic_uris
        # If there are no forensic URIs then check the parents.
        if not ans and self.base_domain:
            ans = self.base_domain.get_dmarc_forensic_uris()
        return ans

    def generate_results(self):
        """Generate the results for this domain."""
        if len(self.starttls_results.keys()) == 0:
            domain_supports_smtp = None
            domain_supports_starttls = None
            mail_servers_that_support_smtp = None
            mail_servers_that_support_starttls = None
        else:
            mail_servers_that_support_smtp = [
                x
                for x in self.starttls_results.keys()
                if self.starttls_results[x]["supports_smtp"]
            ]
            mail_servers_that_support_starttls = [
                x
                for x in self.starttls_results.keys()
                if self.starttls_results[x]["starttls"]
            ]
            domain_supports_smtp = bool(mail_servers_that_support_smtp)
            domain_supports_starttls = domain_supports_smtp and all(
                [
                    self.starttls_results[x]["starttls"]
                    for x in mail_servers_that_support_smtp
                ]
            )

        results = OrderedDict(
            [
                ("Domain", self.domain_name),
                ("Base Domain", self.base_domain_name),
                ("Live", self.is_live),
                ("MX Record", self.has_mail()),
                ("MX Record DNSSEC", self.mx_records_dnssec),
                ("Mail Servers", format_list(self.mail_servers)),
                (
                    "Mail Server Ports Tested",
                    format_list([str(port) for port in self.ports_tested]),
                ),
                (
                    "Domain Supports SMTP Results",
                    format_list(mail_servers_that_support_smtp),
                ),
                # True if and only if at least one mail server speaks SMTP
                ("Domain Supports SMTP", domain_supports_smtp),
                (
                    "Domain Supports STARTTLS Results",
                    format_list(mail_servers_that_support_starttls),
                ),
                # True if and only if all mail servers that speak SMTP
                # also support STARTTLS
                ("Domain Supports STARTTLS", domain_supports_starttls),
                ("SPF Record", self.has_spf()),
                ("SPF Record DNSSEC", self.spf_dnssec),
                ("Valid SPF", self.valid_spf),
                ("SPF Results", format_list(self.spf)),
                ("DMARC Record", self.has_dmarc()),
                ("DMARC Record DNSSEC", self.dmarc_dnssec),
                ("Valid DMARC", self.has_dmarc() and self.valid_dmarc),
                ("DMARC Results", format_list(self.dmarc)),
                ("DMARC Record on Base Domain", self.parent_has_dmarc()),
                ("DMARC Record on Base Domain DNSSEC", self.parent_dmarc_dnssec()),
                (
                    "Valid DMARC Record on Base Domain",
                    self.parent_has_dmarc() and self.parent_valid_dmarc(),
                ),
                ("DMARC Results on Base Domain", self.parent_dmarc_results()),
                ("DMARC Policy", self.get_dmarc_policy()),
                ("DMARC Subdomain Policy", self.get_dmarc_subdomain_policy()),
                ("DMARC Policy Percentage", self.get_dmarc_pct()),
                (
                    "DMARC Aggregate Report URIs",
                    format_list(self.get_dmarc_aggregate_uris()),
                ),
                (
                    "DMARC Forensic Report URIs",
                    format_list(self.get_dmarc_forensic_uris()),
                ),
                ("DMARC Has Aggregate Report URI", self.get_dmarc_has_aggregate_uri()),
                ("DMARC Has Forensic Report URI", self.get_dmarc_has_forensic_uri()),
                (
                    "DMARC Reporting Address Acceptance Error",
                    self.dmarc_reports_address_error,
                ),
                ("Syntax Errors", format_list(self.syntax_errors)),
                ("Debug Info", format_list(self.debug_info)),
            ]
        )

        return results
