"""
  Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.

  Permission is hereby granted, free of charge, to any person obtaining a copy of this
  software and associated documentation files (the "Software"), to deal in the Software
  without restriction, including without limitation the rights to use, copy, modify,
  merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
  permit persons to whom the Software is furnished to do so.

  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
  INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
  PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
import re
from cfnlint import CloudFormationLintRule
from cfnlint import RuleMatch
from cfnlint.helpers import REGEX_IPV4, REGEX_IPV6, REGEX_ALPHANUMERIC


class RecordSet(CloudFormationLintRule):
    """Check Route53 Recordset Configuration"""
    id = 'E3020'
    shortdesc = 'Validate Route53 RecordSets'
    description = 'Check if all RecordSets are correctly configured'
    source_url = 'https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html'
    tags = ['resources', 'route53', 'record_set']

    # Regex generated from https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DomainNameFormat.html
    REGEX_DOMAINNAME = re.compile(r'^[a-zA-Z0-9\!\"\#\$\%\&\'\(\)\*\+\,-\/\:\;\<\=\>\?\@\[\\\]\^\_\`\{\|\}\~\.]+$')
    REGEX_TXT = re.compile(r'^("[^"]{1,255}" *)*"[^"]{1,255}"$')
    REGEX_CNAME_VALIDATIONS = re.compile(r'^.*\.acm-validations\.aws\.?$')

    def count_c_names(self, records, path, cfn):
        """ Count C Names """
        matches = []

        scenarios = cfn.get_object_without_nested_conditions(records, path)
        for scenario in scenarios:
            if len(scenario.get('Object')) > 1:
                scenario = scenario.get('Scenario')
                message = 'A CNAME recordset can only contain 1 value'
                if scenario is None:
                    message = 'A CNAME recordset can only contain 1 value'
                    matches.append(
                        RuleMatch(path, message.format('/'.join(map(str, message)))))
                else:
                    message = 'A CNAME recordset can only contain 1 value {0} at {1}'
                    scenario_text = ' and '.join(['when condition "%s" is %s' % (k, v) for (k, v) in scenario.items()])
                    matches.append(
                        RuleMatch(path, message.format(scenario_text, '/'.join(map(str, path)))))

        return matches

    def check_a_record(self, value, path):
        """Check A record Configuration"""
        matches = []

        # Check if a valid IPv4 address is specified
        if not re.match(REGEX_IPV4, value):
            message = 'A record ({}) is not a valid IPv4 address'
            matches.append(RuleMatch(path, message.format(value)))

        return matches

    def check_aaaa_record(self, value, path):
        """Check AAAA record Configuration"""
        matches = []

        if not isinstance(value, dict):
            # Check if a valid IPv4 address is specified
            if not re.match(REGEX_IPV6, value):
                message = 'AAAA record ({}) is not a valid IPv6 address'
                matches.append(RuleMatch(path, message.format(value)))

        return matches

    def check_caa_record(self, value, path):
        """Check CAA record Configuration"""
        matches = []

        if not isinstance(value, dict):
            # Split the record up to the mandatory settings (flags tag "value")
            items = value.split(' ', 2)
            # Check if the 3 settings are given.
            if len(items) != 3:
                message = 'CAA record must contain 3 settings (flags tag "value"), record contains {} settings.'
                matches.append(RuleMatch(path, message.format(len(items))))
            else:
                # Check the flag value
                if not items[0].isdigit():
                    message = 'CAA record flag setting ({}) should be of type Integer.'
                    matches.append(RuleMatch(path, message.format(items[0])))
                else:
                    if int(items[0]) not in [0, 128]:
                        message = 'Invalid CAA record flag setting ({}) given, must be 0 or 128.'
                        matches.append(RuleMatch(path, message.format(items[0])))

                # Check the tag value
                if not re.match(REGEX_ALPHANUMERIC, items[1]):
                    message = 'Invalid CAA record tag setting {}. Value has to be alphanumeric.'
                    matches.append(RuleMatch(path, message.format(items[1])))

                # Check the value
                if not items[2].startswith('"') or not items[2].endswith('"'):
                    message = 'CAA record value setting has to be enclosed in double quotation marks (").'
                    matches.append(RuleMatch(path, message))

        return matches

    def check_cname_record(self, value, path):
        """Check CNAME record Configuration"""
        matches = []

        if not isinstance(value, dict):
            if (not re.match(self.REGEX_DOMAINNAME, value) and
                    not re.match(self.REGEX_CNAME_VALIDATIONS, value)):
                # ACM Route 53 validation uses invalid CNAMEs starting with `_`,
                # special-case them rather than complicate the regex.
                message = 'CNAME record ({}) does not contain a valid domain name'
                matches.append(RuleMatch(path, message.format(value)))

        return matches

    def check_mx_record(self, value, path):
        """Check MX record Configuration"""
        matches = []

        if not isinstance(value, dict):
            # Split the record up to the mandatory settings (priority domainname)
            items = value.split(' ')

            # Check if the 3 settings are given.
            if len(items) != 2:
                message = 'MX record must contain 2 settings (priority domainname), record contains {} settings.'
                matches.append(RuleMatch(path, message.format(len(items), value)))
            else:
                # Check the priority value
                if not items[0].isdigit():
                    message = 'MX record priority setting ({}) should be of type Integer.'
                    matches.append(RuleMatch(path, message.format(items[0], value)))
                else:
                    if not 0 <= int(items[0]) <= 65535:
                        message = 'Invalid MX record priority setting ({}) given, must be between 0 and 65535.'
                        matches.append(RuleMatch(path, message.format(items[0], value)))

                # Check the domainname value
                if not re.match(self.REGEX_DOMAINNAME, items[1]):
                    matches.append(RuleMatch(path, message.format(items[1])))

        return matches

    def check_ns_record(self, value, path):
        """Check NS record Configuration"""
        matches = []

        if not isinstance(value, dict):
            if not re.match(self.REGEX_DOMAINNAME, value):
                message = 'NS record ({}) does not contain a valid domain name'
                matches.append(RuleMatch(path, message.format(value)))

        return matches

    def check_ptr_record(self, value, path):
        """Check PTR record Configuration"""
        matches = []

        if not isinstance(value, dict):
            if not re.match(self.REGEX_DOMAINNAME, value):
                message = 'PTR record ({}) does not contain a valid domain name'
                matches.append(RuleMatch(path, message.format(value)))
        return matches

    def check_txt_record(self, value, path):
        """Check TXT record Configuration"""
        matches = []

        if not isinstance(value, dict) and not re.match(self.REGEX_TXT, value):
            message = 'TXT record is not structured as one or more items up to 255 characters ' \
                      'enclosed in double quotation marks at {0}'
            matches.append(RuleMatch(
                path,
                (
                    message.format('/'.join(map(str, path)))
                ),
            ))

        return matches

    def check_recordset(self, path, recordset, cfn):
        """Check record configuration"""

        matches = []
        recordset_type = recordset.get('Type')

        # Skip Intrinsic functions
        if not isinstance(recordset_type, dict):
            if not recordset.get('AliasTarget'):
                # If no Alias is specified, ResourceRecords has to be specified
                if not recordset.get('ResourceRecords'):
                    return matches
                # Record type specific checks
                if recordset_type == 'A':
                    matches.extend(
                        cfn.check_value(
                            recordset, 'ResourceRecords', path[:],
                            check_value=self.check_a_record,
                        )
                    )
                elif recordset_type == 'AAAA':
                    matches.extend(
                        cfn.check_value(
                            recordset, 'ResourceRecords', path[:],
                            check_value=self.check_aaaa_record,
                        )
                    )
                elif recordset_type == 'CAA':
                    matches.extend(
                        cfn.check_value(
                            recordset, 'ResourceRecords', path[:],
                            check_value=self.check_caa_record,
                        )
                    )
                elif recordset_type == 'CNAME':
                    matches.extend(
                        self.count_c_names(
                            recordset.get('ResourceRecords'), path[:] + ['ResourceRecords'], cfn
                        )
                    )
                    matches.extend(
                        cfn.check_value(
                            recordset, 'ResourceRecords', path[:],
                            check_value=self.check_cname_record,
                        )
                    )
                elif recordset_type == 'MX':
                    matches.extend(
                        cfn.check_value(
                            recordset, 'ResourceRecords', path[:],
                            check_value=self.check_mx_record,
                        )
                    )
                elif recordset_type == 'NS':
                    matches.extend(
                        cfn.check_value(
                            recordset, 'ResourceRecords', path[:],
                            check_value=self.check_ns_record,
                        )
                    )
                elif recordset_type == 'PTR':
                    matches.extend(
                        cfn.check_value(
                            recordset, 'ResourceRecords', path[:],
                            check_value=self.check_ptr_record,
                        )
                    )
                elif recordset_type == 'TXT':
                    matches.extend(
                        cfn.check_value(
                            recordset, 'ResourceRecords', path[:],
                            check_value=self.check_txt_record,
                        )
                    )

        return matches

    def match(self, cfn):
        """Check RecordSets and RecordSetGroups Properties"""

        matches = []

        recordsets = cfn.get_resources(['AWS::Route53::RecordSet'])

        for name, recordset in recordsets.items():
            path = ['Resources', name, 'Properties']

            if isinstance(recordset, dict):
                props = recordset.get('Properties')
                if props:
                    matches.extend(self.check_recordset(path, props, cfn))

        recordsetgroups = cfn.get_resource_properties(['AWS::Route53::RecordSetGroup', 'RecordSets'])

        for recordsetgroup in recordsetgroups:
            path = recordsetgroup['Path']
            value = recordsetgroup['Value']
            if isinstance(value, list):
                for index, recordset in enumerate(value):
                    tree = path[:] + [index]
                    matches.extend(self.check_recordset(tree, recordset, cfn))

        return matches
