"""
Functions for interacting with an instance based on its ID
"""
import copy
import itertools
from typing import Dict
from typing import List

__func_alias__ = {"list_": "list"}


DEFAULT_SSH_PORT = 22
DEFAULT_TUNNEL_PLUGIN = "asyncssh"


def __init__(hub):
    hub.pop.sub.add(dyne_name="heist")


async def bootstrap(
    hub,
    ctx,
    instance_id,
    *,
    username: str = None,
    host: str = None,
    port: int = DEFAULT_SSH_PORT,
    ssh_public_key: str = None,
    ssh_private_key: str = None,
    availability_zone: str = None,
    heist_manager: str,
    artifact_version: str = None,
    tunnel_plugin: str = DEFAULT_TUNNEL_PLUGIN,
    auto_configure: bool = False,
    verify: bool = True,
    **kwargs,
):
    """Connect to the instance with an ssh keypair and call heist to bootstrap it.

    If an ssh keypair is not provided one will be created.

    Args:
        instance_id:
            An AWS EC2 Instance ID.

        username(str, Optional):
            The instance OS username to use in the connection, Defaults to ec2-user.

        host(str, Optional):
            The public ip address or dns name of the instance.  Defaults to autodetect.

        port(int, Optional):
            The port to connect to on the host.  Defaults to 22.

        ssh_public_key(str, Optional):
            A public ssh key or path to send to the instance.

        ssh_private_key(str, Optional):
            A private ssh key or path to send to the instance.

        availability_zone(str, Optional):
            The Availability Zone in which the EC2 instance was launched.

        heist_manager(str, Required):
            The heist manager to use to bootstrap the instance (I.E. salt.minion).

        artifact_version(str, Optional):
            The version of the heist_manager's artifact to upload to the instance, Defaults to latest.

        tunnel_plugin(str, Optional):
            The heist tunnel plugin to use to ssh into the instance. Defaults to asyncssh.

        auto_configure(Bool, Optional):
            automatically apply internet_gateway/subnet/vpc/route_table changes needed to SSH into an instance.

        verify(bool, Optional):
            If set to True, then verify that the instance is prepared for ssh connections.

        kwargs:
            All extra kwargs are used in asyncssh.SSHClientConnectionOptions.

    Returns:
        {"result": True|False, "comment": A message Tuple, "ret": Dict}

    Examples:
        .. code-block:: bash

            idem exec aws.ec2.instance.bootstrap <instance_id> heist_manager="salt.minion"
    """
    result = dict(comment=[], result=True, ret=None)

    if verify or auto_configure:
        verify_ssh_ret = await hub.exec.aws.ec2.instance.verify_ssh(
            ctx, instance_id=instance_id
        )
        result["result"] &= verify_ssh_ret.result
        result["comment"] += verify_ssh_ret.comment
        if not result["result"]:
            return result

    with hub.tool.aws.ec2.instance.key_pair.verify(
        ssh_public_key=ssh_public_key,
        ssh_private_key=ssh_private_key,
        auto_configure=auto_configure,
    ) as key_pair:
        verified_public_key, private_key_file = key_pair

        connection_ret = await hub.exec.aws.ec2.instance.connect(
            ctx,
            instance_id=instance_id,
            username=username,
            host=host,
            port=port,
            tunnel_plugin=tunnel_plugin,
            ssh_public_key=verified_public_key,
            private_key_file=private_key_file,
            availability_zone=availability_zone,
            **kwargs,
        )
        result["result"] &= connection_ret.result
        result["comment"] += connection_ret.comment
        if not result["result"]:
            return result

        # Bootstrap the instance
        await hub.heist[heist_manager].run(
            remotes={instance_id: connection_ret.ret}, artifact_version=artifact_version
        )
        # TODO Check for a successful bootstrapping

    result["comment"] += [
        f"Successfully bootstrapped instance '{instance_id}' with '{heist_manager}'",
    ]

    return result


async def verify_ssh(hub, ctx, instance_id: str, auto_configure: bool = False):
    """Verify that an instance meets all the requirements for SSH connection

    Args:
        instance_id(str):
            An AWS EC2 Instance ID.

        auto_configure(bool, Optional):
            Automatically apply internet_gateway/subnet/vpc/route_table changes needed to SSH into an instance.

    Returns:
        {"result": True|False, "comment": A message Tuple, "ret": Dict}

    Examples:

        .. code-block:: bash

            idem exec aws.ec2.instance.verify_ssh <instance_id>
    """
    result = dict(result=True, comment=[], ret=None)

    # Create an instance resource object to use for defaults
    instance = hub.tool.boto3.resource.create(ctx, "ec2", "Instance", instance_id)

    # Verify that the instance is in a state with all the right permissions
    if not instance.state["Name"] == "running":
        result["comment"] += ["Instance is not running"]
        if not auto_configure:
            result["result"] = False
            return result
        # TODO start the instance

    if not instance.vpc.vpc_id:
        result["comment"] += [f"No vpc attached to instance"]
        if not auto_configure:
            result["result"] = False
            return result
        # TODO attach the default vpc

    if not instance.subnet.subnet_id:
        result["comment"] += [f"No subnet attached to instance"]
        if not auto_configure:
            result["result"] = False
            return result
        # TODO attach the default subnet for the instance's availability zone

    # Verify that an internet gateway is attached to the instance's vpc
    internet_gateway_ret = await hub.exec.boto3.client.ec2.describe_internet_gateways(
        ctx,
        Filters=[
            {"Name": "attachment.vpc-id", "Values": [instance.vpc.vpc_id]},
        ],
    )
    if not internet_gateway_ret.result and internet_gateway_ret.ret["InternetGateways"]:
        result["comment"] += [
            f"No internet gateway attached to instance's vpc: {instance.vpc.vpc_id}"
        ]
        if not auto_configure:
            result["result"] = False
        # TODO attach an internet gateway to the vpc

    internet_gateway_ids = [
        igw["InternetGatewayId"] for igw in internet_gateway_ret.ret["InternetGateways"]
    ]

    # Verify that there is a route with 0.0.0.0/0 as the destination and the internet gateway for your VPC as the target
    ipv4_route_table_ret = await hub.exec.boto3.client.ec2.describe_route_tables(
        ctx,
        Filters=[
            {"Name": "vpc-id", "Values": [instance.vpc.vpc_id]},
            {"Name": "route.destination-cidr-block", "Values": ["0.0.0.0/0"]},
            {"Name": "route.gateway-id", "Values": internet_gateway_ids},
        ],
    )
    has_route_table = False
    if not (ipv4_route_table_ret and ipv4_route_table_ret.ret["RouteTables"]):
        result["comment"] += [
            f"No route from vpc's internet gateway {internet_gateway_ids} to '0.0.0.0/0'"
        ]
    else:
        has_route_table = True

    if not has_route_table:
        # Verify that there is a route with ::/0 as the destination and the internet gateway for your VPC as the target
        ipv6_route_table_ret = await hub.exec.boto3.client.ec2.describe_route_tables(
            ctx,
            Filters=[
                {"Name": "vpc-id", "Values": [instance.vpc.vpc_id]},
                {"Name": "route.destination-ipv6-cidr-block", "Values": ["::/0"]},
                {"Name": "route.gateway-id", "Values": internet_gateway_ids},
            ],
        )
        if not (ipv6_route_table_ret and ipv6_route_table_ret.ret["RouteTables"]):
            result["comment"] += [
                f"No route from vpc's internet gateway {internet_gateway_ids} to '::/0'"
            ]
        else:
            has_route_table = True

    # If there is still no route table then add a command to the comments that would enable it
    if not has_route_table:
        if not auto_configure:
            # At least one ipv4 or ipv6 route table must be configured for SSH
            result["result"] = False
        # TODO configure route table

        # Get the first internet gateway to use in an example
        igw_id = next(iter(internet_gateway_ids))
        # Get the id of the main route table attached to the vpc
        try_route_table = await hub.exec.boto3.client.ec2.describe_route_tables(
            ctx,
            Filters=[
                {"Name": "vpc-id", "Values": [instance.vpc.vpc_id]},
                {"Name": "route.gateway-id", "Values": [igw_id]},
            ],
        )
        if try_route_table and try_route_table.ret:
            result["comment"] += [
                "Use the following command to create a valid SSH route for the instance"
            ]
            # Get the id of the main route table
            rt_id = next(iter(try_route_table.ret["RouteTables"]))["RouteTableId"]
            result["comment"] += [
                f"idem exec boto3.client.ec2.create_route RouteTableID={rt_id} VpcEndpointId={instance.vpc.vpc_id} DestinationCidrBlock='0.0.0.0/0' GatewayId={igw_id}"
            ]

    # Verify that the ACL for the subnet allows inbound traffic
    network_acl_ret = await hub.exec.boto3.client.ec2.describe_network_acls(
        ctx,
        Filters=[
            {"Name": "association.subnet-id", "Values": [instance.subnet.subnet_id]},
            {"Name": "entry.rule-action", "Values": ["allow"]},
            {"Name": "vpc-id", "Values": [instance.vpc.vpc_id]},
        ],
    )
    if not (network_acl_ret and network_acl_ret.ret["NetworkAcls"]):
        result["comment"] += [f"No valid ACL"]
        if not auto_configure:
            result["result"] = False
        # TODO configure network acl

    return result


async def connect(
    hub,
    ctx,
    instance_id: str,
    *,
    username: str = None,
    host: str = None,
    port: int = DEFAULT_SSH_PORT,
    tunnel_plugin: str = DEFAULT_TUNNEL_PLUGIN,
    ssh_public_key: str,
    private_key_file: str,
    availability_zone: str = None,
    **kwargs,
):
    """Attempt connection with SSH to the instance with the given parameters.

    If no host or username is specified, then make reasonable guesses and return the target configuration that was successful

    Args:
        instance_id(str):
            An AWS EC2 Instance ID.

        username(str, Optional):
            The instance OS username to use in the connection, Defaults to ec2-user.

        host(str, Optional):
            The public ip address or dns name of the instance.  Defaults to autodetect.

        port(int, Optional):
            The port to connect to on the host.  Defaults to 22.

        ssh_public_key(str, Optional):
            A public ssh key or path to send to the instance.

        private_key_file(str, Optional):
            The path to a private ssh key file.

        availability_zone(str, Optional):
            The Availability Zone in which the EC2 instance was launched.

        tunnel_plugin(str, Optional):
            The heist tunnel plugin to use to ssh into the instance. Defaults to asyncssh.

        kwargs:
            All extra kwargs are used in asyncssh.SSHClientConnectionOptions.

    Returns:
        {"result": True|False, "comment": A message Tuple, "ret": Dict}

    Examples:
        .. code-block:: bash

            idem exec aws.ec2.instance.connect <instance_id>
    """
    result = dict(result=True, comment=[], ret={})

    # Create an instance resource object to use for defaults
    instance = hub.tool.boto3.resource.create(ctx, "ec2", "Instance", instance_id)

    # Use the availability zone defined in the instance if none was provided
    if availability_zone is None:
        availability_zone = instance.placement["AvailabilityZone"]

    # If no host was provided then try all the instance's dns names and ip addresses
    if host:
        hosts_to_try = (host,)
    else:
        hosts_to_try = (
            instance.public_dns_name,
            instance.private_dns_name,
            instance.private_ip_address,
            instance.ipv6_address,
            instance.public_ip_address,
        )

    # If no username was provided then try all common known usernames for ec2 images
    if username:
        # Only use the given username
        user_names_to_try = (username,)
    else:
        user_names_to_try = (
            "ec2-user",
            "root",
            "admin",
            "ubuntu",
            "bitnami",
            "fedora",
            "centos",
        )

    # If no username or host was specified, then try multiple combinations until one works
    for try_user, try_host in itertools.product(user_names_to_try, hosts_to_try):
        if not try_host:
            # the ipv6_address or other attempted host name might not be set
            continue
        target = dict(
            host=try_host,
            port=port,
            bootstrap=True,
            username=try_user,
            client_keys=[private_key_file],
            IdentitiesOnly="yes",
            **kwargs,
        )

        # Connect to the ec2 instance with the public key, connection will be available for 60 seconds
        connection_ret = await hub.exec.boto3.client[
            "ec2-instance-connect"
        ].send_ssh_public_key(
            ctx,
            InstanceId=instance_id,
            InstanceOSUser=try_user,
            SSHPublicKey=ssh_public_key,
            AvailabilityZone=availability_zone,
        )
        if connection_ret.comment:
            hub.log.debug(
                f"Sending public key to {instance_id}: {connection_ret.comment}"
            )

        if not connection_ret.result:
            hub.log.debug(f"Connection error sending public key to {instance_id}")
            continue
        if not connection_ret.ret.get("Success"):
            hub.log.debug(f"Unable to send public key to {instance_id}")
            continue

        hub.log.debug(
            f"Connecting to instance '{instance_id}' with ssh: {try_user}@{try_host}"
        )

        # Create a connection verify the ability to SSH to the instance
        ret = await hub.tunnel[tunnel_plugin].create(
            name=instance_id, target=copy.deepcopy(target)
        )
        if not ret:
            hub.log.debug(
                f"Unable to connect to the instance with SSH: {try_user}@{try_host}"
            )
            continue

        # Success!
        result["comment"] += [
            f"Successfully connected to instance with SSH: {try_user}@{try_host}"
        ]
        result["ret"] = target
        return result

    # There were no successful connections in the loop
    result["comment"] += [
        f"Unable to connect to instance with SSH",
        "Try troubleshooting: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/TroubleshootingInstancesConnecting.html",
    ]
    result["result"] = False

    return result


async def start(hub, ctx, instance_id: str):
    """
    Start the instance and wait for it to be running
    """
    result = dict(result=True, comment=[], ret={})

    ret = await hub.exec.boto3.client.ec2.start_instances(
        ctx, InstanceIds=[instance_id]
    )
    if ret.comment:
        result["comments"].append(ret.comment)
    if not ret.result:
        return False

    resource = hub.tool.boto3.resource.create(ctx, "ec2", "Instance", instance_id)
    await hub.tool.boto3.resource.exec(ctx, resource, "wait_until_running")

    return result


async def stop(hub, ctx, instance_id: str):
    """
    Stop the instance and wait for it to be stopped.
    """
    result = dict(result=True, comment=[], ret={})

    ret = await hub.exec.boto3.client.ec2.stop_instances(
        ctx, InstanceIds=[instance_id], Hibernate=False, Force=False
    )
    if ret.comment:
        result["comments"].append(ret.comment)
    if not ret.result:
        return False

    resource = hub.tool.boto3.resource.create(ctx, "ec2", "Instance", instance_id)
    await hub.tool.boto3.resource.exec(ctx, resource, "wait_until_stopped")

    return result


async def get(
    hub,
    ctx,
    *,
    name=None,
    resource_id: str = None,
    filters: List = None,
) -> Dict:
    """
    Args:
        name(str):
            The name of the Idem state.

        resource_id(str, Optional):
            AWS VPC id to identify the resource.

        filters(list, Optional):
            One or more filters: for example, tag :<key>, tag-key. A complete list of filters can be found at
            https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#EC2.Client.describe_instances.
    """
    result = dict(comment=[], ret=None, result=True)
    if resource_id:
        ret = await hub.exec.boto3.client.ec2.describe_instances(
            ctx, InstanceIds=[resource_id], Filters=filters
        )
    else:
        ret = await hub.exec.boto3.client.ec2.describe_instances(
            ctx,
            Filters=filters,
        )

    # If there was an error in the call then report failure
    if not ret["result"]:
        result["comment"] += list(ret["comment"])
        result["result"] = False
        return result

    present_states = await hub.tool.aws.ec2.instance.state.convert_to_present(
        ctx, ret.ret
    )

    # If the resource can't be found but there were no results then "result" is True and "ret" is None
    if not present_states:
        result["comment"].append(
            hub.tool.aws.comment_utils.list_empty_comment(
                resource_type="aws.ec2.instance", name=name
            )
        )
        return result

    # return the first result as a plain dictionary
    result["ret"] = next(iter((present_states).values()))

    return result


async def list_(hub, ctx, *, name=None, filters: List = None) -> Dict:
    """
    Args:
        name(str):
            The name of the Idem state.

        filters(list, Optional):
            One or more filters: for example, tag :<key>, tag-key. A complete list of filters can be found at
            https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#EC2.Client.describe_instances.
    """
    result = dict(comment=[], ret=[], result=True)
    ret = await hub.exec.boto3.client.ec2.describe_instances(
        ctx,
        Filters=filters,
    )

    # If there was an error in the call then report failure
    if not ret["result"]:
        result["comment"] += list(ret["comment"])
        result["result"] = False
        return result

    present_states = await hub.tool.aws.ec2.instance.state.convert_to_present(
        ctx, ret.ret
    )
    if not present_states:
        result["comment"].append(
            hub.tool.aws.comment_utils.list_empty_comment(
                resource_type="aws.ec2.instance", name=name
            )
        )
        return result

    # Return a list of dictionaries with details about all the instances
    result["ret"] = list(present_states.values())

    return result
