import argparse
import logging
from typing import Dict

from install_party.dns import dns_provider
from install_party.dns.dns_provider_client import DNSRecord, DNSProviderClient
from install_party.instances import instances_provider
from install_party.instances.instances_provider_client import (
    Instance,
    InstancesProviderClient,
)
from install_party.lister.list import get_list
from install_party.util.entry import Entry

logger = logging.getLogger(__name__)


def filter_entries_dict(entries_dict, args) -> Dict[str, Entry]:
    """Filter the entries dict according to the command-line arguments.

    Args:
        entries_dict (dict): Dict of entries generated by list.get_list
        args (Namespace): The parsed command-line arguments.

    Returns:
        A dict following the same schema and data as the provided one but filtered minus
        the entries that the command-line arguments mandate to remove from it.
    """

    if args.server:
        # Only keep the entries that are in args.server.
        filtered_dict = {k: entries_dict[k] for k in args.server}
    elif args.exclude:
        # Only keep the entries that are in the dict but not in args.exclude. We don't
        # care about args.all because args.exclude can only be provided if args.all was
        # provided (therefore the -a/--all argument is more here to help the user
        # understand how the mode is made to work than to serve an actual function).
        filtered_dict = {
            k: entries_dict[k] for k in set(entries_dict.keys()).difference(args.exclude)
        }
    else:
        filtered_dict = entries_dict

    return filtered_dict


def delete_instance(
        entry_id: str,
        instance: Instance,
        client: InstancesProviderClient,
        dry_run: bool
):
    """Delete the provided instance using the provided API client for the instances
    provider, or only print a message if the dry-run mode is on.

    If the deletion failed, just print a line about it.

    Args:
        entry_id (str): The ID of the entry we're deleting the instance of.
        instance (Instance): The instance to delete.
        client (InstancesProviderClient): A client to the instances provider's API to
            use to perform the deletion.
        dry_run (bool): Whether we're running in dry-run mode.
    """
    logger.info("Deleting instance for name %s..." % entry_id)

    # Only delete if the dry-run mode is off.
    if not dry_run:
        client.delete_instance(instance)


def delete_record(
        entry_id: str,
        record: DNSRecord,
        client: DNSProviderClient,
        dry_run: bool
):
    """Delete the DNS record associated with the provided entry.

    Args:
        entry_id (str): The ID of the entry we're deleting the DNS record of.
        record (DNSRecord): The DNS record to delete.
        client (DNSProviderClient): A client to the DNS provider's API to use to perform
            the deletion.
        dry_run (bool): Whether we're running in dry-run mode.
    """
    logger.info("Deleting domain name for name %s..." % entry_id)

    # Only delete if the dry-run mode is off.
    if not dry_run:
        client.delete_sub_domain(record)


def delete(config):
    """Delete one or several (or all) entries, as defined by the command-line arguments.

    Args:
        config (dict): The parsed configuration.
    """
    args = parse_args()

    # Warn that we're running in dry-run mode if it's the case.
    if args.dry_run:
        logger.info("Running in dry-run mode.")

    # Populate the dict of entries.
    entries_dict = get_list(config)

    # Instantiate the clients for the instances and the DNS providers.
    instances_client = instances_provider.get_instances_provider_client(config)
    dns_client = dns_provider.get_dns_provider_client(config)

    # Filter the entries_dict accordingly with the arguments.
    entries_to_delete = filter_entries_dict(entries_dict, args)

    # Loop over the entries to delete and delete them.
    instances_refresh_needed = False
    dns_refresh_needed = False

    for entry_id, entry in entries_to_delete.items():
        instance = entry.instance
        record = entry.record

        # If we know about an instance for this entry, delete it.
        if instance:
            try:
                delete_instance(entry_id, instance, instances_client, args.dry_run)
                instances_refresh_needed = True
            except Exception as e:
                logger.error("Failed to delete instance for %s:" % entry_id, e)

        # If we know about a DNS record for this entry, delete it.
        if record:
            try:
                delete_record(entry_id, record, dns_client, args.dry_run)
                dns_refresh_needed = True
            except Exception as e:
                logger.error("Failed to delete domain name for %s:" % entry_id, e)

    if instances_refresh_needed:
        logger.info("Applying the instances deletion...")

        if not args.dry_run:
            # Commit the deletion to make it effective.
            instances_client.commit()

    if dns_refresh_needed:
        logger.info("Applying the DNS changes...")

        if not args.dry_run:
            # Refresh the DNS server's configuration to make it aware of the changes.
            dns_client.commit(config["dns"]["zone"])

    logger.info("Done!")


def parse_args():
    parser = argparse.ArgumentParser(
        prog="install_party delete",
        description="Delete existing servers.",
    )
    parser.add_argument(
        "-v", "--verbose",
        action="store_true",
        help="Increases the verbosity."
    )
    parser.add_argument(
        "-d", "--dry-run",
        action="store_true",
        help="List the deletions that would normally happen but don't actually perform"
             " them.",
    )
    parser.add_argument(
        "-e", "--exclude",
        action="append",
        metavar="NAME",
        help="Delete all servers which name(s) belong to the configured namespace, except"
             " for the provided name(s) (use it once per name). Can only be used with the"
             " -a/--all argument.",
    )

    group = parser.add_mutually_exclusive_group(required=True)

    group.add_argument(
        "-s", "--server",
        action="append",
        metavar="NAME",
        help="Only delete the servers for the provided name(s) instead of all of the"
             " existing ones in the configured namespace (use it once per name)."
    )
    group.add_argument(
        "-a", "--all",
        action="store_true",
        help="Delete all of the servers (except the ones provided with --exclude, if"
             " any)."
    )

    args = parser.parse_args()

    if args.exclude and not args.all:
        parser.error("argument -e/--exclude can only be used with argument -a/--all")

    if args.verbose:
        logging.getLogger("install_party").setLevel(logging.DEBUG)

    return args
