# Copyright (c) 2017-2021 Felix Fontein
# 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 module 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 traceback
import typing as t

from ansible.module_utils.common.text.converters import to_text

from ansible_collections.community.dns.plugins.module_utils._argspec import (
    ArgumentSpec,
    ModuleOptionProvider,
)
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._options import (
    create_record_transformation_argspec,
)
from ansible_collections.community.dns.plugins.module_utils._record import (
    format_records_for_output,
)
from ansible_collections.community.dns.plugins.module_utils._record_set import (
    format_record_set_for_output,
)
from ansible_collections.community.dns.plugins.module_utils._zone_record_api import (
    NOT_PROVIDED,
    DNSAPIAuthenticationError,
    DNSAPIError,
    ZoneRecordAPI,
)

from ._utils import (
    create_zone_name_id_argspec,
    get_prefix,
    get_zone_id_or_name_with_prefix_filter,
    normalize_dns_name,
)

if t.TYPE_CHECKING:  # pragma: no cover
    from ansible.module_utils.basic import AnsibleModule

    from .._provider import ProviderInformation
    from .._record import (
        DNSRecord,
        RecordIDT,
    )
    from .._record_set import RecordSetIDT
    from .._zone import ZoneIDT
    from .._zone_record_api import NotProvidedType
    from .._zone_record_set_api import ZoneRecordSetAPI


def create_module_argument_spec(
    provider_information: ProviderInformation,
) -> ArgumentSpec:
    return (
        ArgumentSpec(
            argument_spec={
                "what": {
                    "type": "str",
                    "choices": ["single_record", "all_types_for_record", "all_records"],
                    "default": "single_record",
                },
                "record": {"type": "str"},
                "prefix": {"type": "str"},
                "type": {
                    "type": "str",
                    "choices": provider_information.get_supported_record_types(),
                    "default": None,
                },
            },
            required_if=[
                ("what", "single_record", ["type"]),
                ("what", "single_record", ["record", "prefix"], True),
                ("what", "all_types_for_record", ["record", "prefix"], True),
            ],
            mutually_exclusive=[
                ("record", "prefix"),
            ],
        )
        .merge(create_record_transformation_argspec())
        .merge(create_zone_name_id_argspec(provider_information))
    )


def _run_module_record_api(
    module: AnsibleModule,
    provider_information: ProviderInformation,
    record_converter: RecordConverter,
    filter_record_type: str | NotProvidedType,
    api: ZoneRecordAPI[ZoneIDT, RecordIDT],
) -> t.NoReturn:
    # Get zone information
    zone_name_in, zone_id_in, filter_prefix = get_zone_id_or_name_with_prefix_filter(
        module.params,
        provider_information,
        use_prefix_or_record=module.params["what"] != "all_records",
    )
    if provider_information.is_zone_id_equal_to_zone_name():
        zone_records = api.get_zone_records(
            zone_id_in,  # type: ignore
            prefix=filter_prefix,
            record_type=filter_record_type,
        )
        if zone_records is None:
            module.fail_json(msg="Zone not found")
        assert zone_name_in is not None
        zone_in = zone_name_in
        zone_id: ZoneIDT = zone_name_in  # type: ignore
    elif zone_name_in is not None:
        zone_in = zone_name_in
        zone = api.get_zone_with_records_by_name(
            zone_in, prefix=filter_prefix, record_type=filter_record_type
        )
        if zone is None:
            module.fail_json(msg="Zone not found")
        zone_records = zone.records
        zone_id = zone.zone.id
    else:
        zone = api.get_zone_with_records_by_id(
            zone_id_in,  # type: ignore
            prefix=filter_prefix,
            record_type=filter_record_type,
        )
        if zone is None:
            module.fail_json(msg="Zone not found")
        zone_in = normalize_dns_name(zone.zone.name)
        zone_records = zone.records
        zone_id = zone.zone.id

    # Retrieve requested information
    if module.params["what"] == "single_record":
        # Extract prefix
        record_in = normalize_dns_name(module.params.get("record"))
        prefix_in = module.params.get("prefix")
        record_in, prefix = get_prefix(
            normalized_zone=zone_in,
            normalized_record=record_in,
            prefix=prefix_in,
            provider_information=provider_information,
        )

        # Find matching records
        records_list = []
        for record in zone_records:
            if record.prefix == prefix:
                records_list.append(record)

        # Convert records
        record_converter.process_multiple_from_api(records_list)
        record_converter.process_multiple_to_user(records_list)

        # Format output
        data = (
            format_records_for_output(records_list, record_in, prefix)
            if records_list
            else {}
        )
        module.exit_json(
            changed=False,
            set=data,
            zone_id=zone_id,
        )
    else:
        # Extract prefix if necessary
        if module.params["what"] == "all_types_for_record":
            check_prefix = True
            record_in = normalize_dns_name(module.params.get("record"))
            prefix_in = module.params.get("prefix")
            record_in, prefix = get_prefix(
                normalized_zone=zone_in,
                normalized_record=record_in,
                prefix=prefix_in,
                provider_information=provider_information,
            )
        else:
            check_prefix = False
            prefix = None

        # Find matching records
        records: dict[tuple[str, str], list[DNSRecord]] = {}
        for record in zone_records:
            if check_prefix and record.prefix != prefix:
                continue
            key = (
                (record.prefix + "." + zone_in) if record.prefix else zone_in,
                record.type,
            )
            record_list = records.get(key)
            if record_list is None:
                record_list = records[key] = []
            record_list.append(record)

        # Convert records
        for record_list in records.values():
            record_converter.process_multiple_from_api(record_list)
            record_converter.process_multiple_to_user(record_list)

        # Format output
        data2 = [
            format_records_for_output(record_list, record_name, record_list[0].prefix)
            for (record_name, dummy), record_list in sorted(records.items())
        ]
        module.exit_json(
            changed=False,
            sets=data2,
            zone_id=zone_id,
        )


def _run_module_record_set_api(
    module: AnsibleModule,
    provider_information: ProviderInformation,
    record_converter: RecordConverter,
    filter_record_type: str | NotProvidedType,
    api: ZoneRecordSetAPI[ZoneIDT, RecordSetIDT, RecordIDT],
) -> t.NoReturn:
    # Get zone information
    zone_name_in, zone_id_in, filter_prefix = get_zone_id_or_name_with_prefix_filter(
        module.params,
        provider_information,
        use_prefix_or_record=module.params["what"] != "all_records",
    )
    if provider_information.is_zone_id_equal_to_zone_name():
        zone_record_sets = api.get_zone_record_sets(
            zone_id_in,  # type: ignore
            prefix=filter_prefix,
            record_type=filter_record_type,
        )
        if zone_record_sets is None:
            module.fail_json(msg="Zone not found")
        assert zone_name_in is not None
        zone_in = zone_name_in
        zone_id: ZoneIDT = zone_name_in  # type: ignore
    elif zone_name_in is not None:
        zone_in = zone_name_in
        zone = api.get_zone_with_record_sets_by_name(
            zone_in, prefix=filter_prefix, record_type=filter_record_type
        )
        if zone is None:
            module.fail_json(msg="Zone not found")
        zone_record_sets = zone.record_sets
        zone_id = zone.zone.id
    else:
        zone = api.get_zone_with_record_sets_by_id(
            zone_id_in,  # type: ignore
            prefix=filter_prefix,
            record_type=filter_record_type,
        )
        if zone is None:
            module.fail_json(msg="Zone not found")
        zone_in = normalize_dns_name(zone.zone.name)
        zone_record_sets = zone.record_sets
        zone_id = zone.zone.id

    # Retrieve requested information
    if module.params["what"] == "single_record":
        # Extract prefix
        record_in = normalize_dns_name(module.params.get("record"))
        prefix_in = module.params.get("prefix")
        record_in, prefix = get_prefix(
            normalized_zone=zone_in,
            normalized_record=record_in,
            prefix=prefix_in,
            provider_information=provider_information,
        )

        # Find matching record set
        record_set = next(
            (
                candidate_record_set
                for candidate_record_set in zone_record_sets
                if candidate_record_set.prefix == prefix
            ),
            None,
        )

        # Convert record set
        if record_set:
            record_converter.process_set_from_api(record_set)
            record_converter.process_set_to_user(record_set)

        # Format output
        data = (
            format_record_set_for_output(record_set, record_in, prefix)
            if record_set
            else {}
        )
        module.exit_json(
            changed=False,
            set=data,
            zone_id=zone_id,
        )
    else:
        # Extract prefix if necessary
        if module.params["what"] == "all_types_for_record":
            check_prefix = True
            record_in = normalize_dns_name(module.params.get("record"))
            prefix_in = module.params.get("prefix")
            record_in, prefix = get_prefix(
                normalized_zone=zone_in,
                normalized_record=record_in,
                prefix=prefix_in,
                provider_information=provider_information,
            )
        else:
            check_prefix = False
            prefix = None

        def get_record_name(record_set):
            return record_set.prefix + "." + zone_in if record_set.prefix else zone_in

        # Find matching records
        record_sets = list(zone_record_sets)
        if check_prefix:
            record_sets = [
                record_set for record_set in record_sets if record_set.prefix == prefix
            ]
        record_sets.sort(
            key=lambda record_set: (get_record_name(record_set), record_set.type)
        )

        # Convert records
        for record_set in record_sets:
            record_converter.process_set_from_api(record_set)
            record_converter.process_set_to_user(record_set)

        # Format output
        data2 = [
            format_record_set_for_output(
                record_set, get_record_name(record_set), record_set.prefix
            )
            for record_set in record_sets
        ]
        module.exit_json(
            changed=False,
            sets=data2,
            zone_id=zone_id,
        )


def run_module(
    module: AnsibleModule,
    create_api: t.Callable[[], ZoneRecordAPI | ZoneRecordSetAPI],
    provider_information: ProviderInformation,
) -> t.NoReturn:
    option_provider = ModuleOptionProvider(module)
    record_converter = RecordConverter(provider_information, option_provider)
    record_converter.emit_deprecations(module.deprecate)

    filter_record_type: str | NotProvidedType = NOT_PROVIDED
    if module.params["what"] == "single_record":
        filter_record_type = module.params.get("type")
        if (
            filter_record_type
            and filter_record_type
            not in provider_information.get_supported_record_types()
        ):
            module.fail_json(msg=f"Invalid record type {filter_record_type}")

    try:
        # Create API
        api = create_api()

        if isinstance(api, ZoneRecordAPI):
            _run_module_record_api(
                module,
                provider_information,
                record_converter,
                filter_record_type,
                api,
            )
        else:
            _run_module_record_set_api(
                module,
                provider_information,
                record_converter,
                filter_record_type,
                api,
            )

    except DNSConversionError as e:
        module.fail_json(
            msg=f"Error while converting DNS values: {e.error_message}",
            error=e.error_message,
            exception=traceback.format_exc(),
        )
    except DNSAPIAuthenticationError as e:
        module.fail_json(
            msg=f"Cannot authenticate: {e}",
            error=to_text(e),
            exception=traceback.format_exc(),
        )
    except DNSAPIError as e:
        module.fail_json(
            msg=f"Error: {e}",
            error=to_text(e),
            exception=traceback.format_exc(),
        )
