# Copyright (c) 2021 Felix Fontein
# Copyright (c) 2020 Markus Bergholz <markuman+spambelongstogoogle@gmail.com>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

# Note that this plugin util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!

from __future__ import annotations

import abc
import typing as t
from collections.abc import Sequence

from ansible.errors import AnsibleError
from ansible.plugins.inventory import BaseInventoryPlugin
from ansible.template import Templar
from ansible.utils.display import Display
from ansible_collections.community.library_inventory_filtering_v1.plugins.plugin_utils.inventory_filter import (
    filter_host,
    parse_filters,
)

from ansible_collections.community.dns.plugins.module_utils._conversion.base import (
    DNSConversionError,
)
from ansible_collections.community.dns.plugins.module_utils._conversion.converter import (
    RecordConverter,
)
from ansible_collections.community.dns.plugins.module_utils._provider import (
    ProviderInformation,
    ensure_type,
)
from ansible_collections.community.dns.plugins.module_utils._zone_record_api import (
    DNSAPIAuthenticationError,
    DNSAPIError,
    ZoneRecordAPI,
)
from ansible_collections.community.dns.plugins.module_utils._zone_record_set_api import (
    ZoneRecordSetAPI,
)
from ansible_collections.community.dns.plugins.plugin_utils._unsafe import make_unsafe

display = Display()


class RecordsInventoryModule(BaseInventoryPlugin, metaclass=abc.ABCMeta):
    VALID_ENDINGS = ("dns.yaml", "dns.yml")
    NAME: str

    @abc.abstractmethod
    def setup_api(self) -> tuple[ProviderInformation, ZoneRecordAPI | ZoneRecordSetAPI]:
        """
        This function needs to create a provider information and an API object.
        It can indicate errors by raising DNSAPIError.
        """

    def verify_file(self, path: str) -> bool:
        if super().verify_file(path):
            if path.endswith(self.VALID_ENDINGS):
                return True
            endings = " or ".join([f"'{ending}'" for ending in self.VALID_ENDINGS])
            display.debug(f"{self.NAME} inventory filename must end with {endings}")
        return False

    def parse(
        self, inventory: t.Any, loader: t.Any, path: str, cache: bool = False
    ) -> None:
        super().parse(inventory, loader, path, cache)

        self._read_config_data(path)

        self.templar = Templar(loader=loader)

        try:
            provider_information, api = self.setup_api()

            record_converter = RecordConverter(provider_information, self)
            record_converter.emit_deprecations(display.deprecated)

            zone_name = self.get_option("zone_name")
            if self.templar.is_template(zone_name):
                zone_name = self.templar.template(variable=zone_name)
            zone_id = self.get_option("zone_id")
            if zone_id is not None:
                if self.templar.is_template(zone_id):
                    zone_id = self.templar.template(variable=zone_id)
                # For templating, we need to make the zone_id type 'string' or 'raw'.
                # This converts the value to its proper type expected by the API.
                zone_id_type = provider_information.get_record_id_type()
                try:
                    zone_id = ensure_type(zone_id, zone_id_type)
                except TypeError as exc:
                    raise AnsibleError(
                        f"Error while ensuring that zone_id is of type {zone_id_type}: {exc}"
                    ) from exc

            if isinstance(api, ZoneRecordAPI):
                if zone_name is not None:
                    zone_with_records = api.get_zone_with_records_by_name(zone_name)
                elif zone_id is not None:
                    zone_with_records = api.get_zone_with_records_by_id(zone_id)
                else:
                    raise AnsibleError(
                        "One of zone_name and zone_id must be specified!"
                    )

                if zone_with_records is None:
                    raise AnsibleError("Zone does not exist")

                zone = zone_with_records.zone
                records = zone_with_records.records
            else:
                if zone_name is not None:
                    zone_with_record_sets = api.get_zone_with_record_sets_by_name(
                        zone_name
                    )
                elif zone_id is not None:
                    zone_with_record_sets = api.get_zone_with_record_sets_by_id(zone_id)
                else:
                    raise AnsibleError(
                        "One of zone_name and zone_id must be specified!"
                    )

                if zone_with_record_sets is None:
                    raise AnsibleError("Zone does not exist")

                zone = zone_with_record_sets.zone
                records = []
                for record_set in zone_with_record_sets.record_sets:
                    records.extend(record_set.records)

            record_converter.process_multiple_from_api(records)
            record_converter.process_multiple_to_user(records)

        except DNSConversionError as e:
            raise AnsibleError(
                f"Error while converting DNS values: {e.error_message}"
            ) from e
        except DNSAPIAuthenticationError as e:
            raise AnsibleError(f"Cannot authenticate: {e}") from e
        except DNSAPIError as e:
            raise AnsibleError(f"Error: {e}") from e

        simple_filters = self.get_option("simple_filters")
        filters = parse_filters(self.get_option("filters"))

        filter_types = simple_filters.get("type") or ["A", "AAAA", "CNAME"]
        if not isinstance(filter_types, Sequence) or isinstance(
            filter_types, (str, bytes)
        ):
            filter_types = [filter_types]

        if self.inventory is None:  # pragma: no cover
            raise AssertionError(  # pragma: no cover
                "Inventory must not be None in parse()"
            )
        for record in records:
            if record.type in filter_types:
                name = zone.name
                if record.prefix:
                    name = f"{record.prefix}.{name}"
                facts = {
                    "ansible_host": make_unsafe(record.target),
                }
                if not filter_host(self, name, facts, filters):
                    continue

                self.inventory.add_host(name)
                for key, value in facts.items():
                    self.inventory.set_variable(name, key, value)
