#!/usr/bin/env python3
# pylint: disable=invalid-name
# pylint: enable=invalid-name
"""
Tool used for creating CNaaS safespring machines the way we expect them to be created

It is formatted and linted in the following way:
===
black create-cnaas-instance
isort --profile black create-cnaas-instance
mypy --python-executable /opt/homebrew/bin/python3 --strict create-cnaas-instance
PYTHONPATH=/opt/homebrew/lib/python3.10/site-packages pylint create-cnaas-instance
===
"""

# Make code work on python 3.7
from __future__ import annotations

import argparse
import base64
import ipaddress
import re
import string
import sys
import textwrap
import typing
from typing import Any, Union

try:
    from typing import TypeGuard
except ImportError:
    from typing_extensions import (  # use `typing_extensions` for Python 3.9 and below
        TypeGuard,
    )

import openstack
from openstack.compute.v2.flavor import Flavor
from openstack.compute.v2.image import Image
from openstack.compute.v2.keypair import Keypair
from openstack.compute.v2.server import Server
from openstack.compute.v2.server_group import ServerGroup

# Make mypy type checking happy, also helps with go-to-definition support in editor
from openstack.connection import Connection
from openstack.exceptions import ConflictException
from openstack.network.v2.network import Network
from openstack.network.v2.port import Port
from openstack.network.v2.security_group import SecurityGroup


def get_keypair(conn: Connection, keypair_name: str) -> Keypair:
    """Lookup already existing keypair by name and return it"""
    keypair = conn.compute.find_keypair(keypair_name)

    if not keypair:
        raise RuntimeError(f"unable to find keypair with name '{keypair_name}'")

    if not isinstance(keypair, Keypair):
        raise TypeError(f"keypair should be {Keypair} but is {type(keypair)}")

    return keypair


def get_port(  # pylint:disable=too-many-arguments,too-many-branches
    args: argparse.Namespace,
    conn: Connection,
    network: Network,
    port_name: str,
) -> Port:
    """Either create a new named port or return already existing one"""

    for port in conn.network.ports():
        if not isinstance(port, Port):
            raise TypeError(f"port should be {Port} but is {type(port)}")
        if port.name == port_name:
            print(f"named port already exists, using it: {port.name} ({port.id})")
            return port

    print(f"creating port: {port_name}")
    port = conn.network.create_port(network_id=network.id, name=port_name)
    if not isinstance(port, Port):
        raise TypeError(f"port should be {Port} but is {type(port)}")

    return port


def is_int_list(test_list: list[Any]) -> TypeGuard[list[int]]:
    """Help with typechecking a list of ints"""
    return all(isinstance(element, int) for element in test_list)


def create_server(  # pylint: disable=too-many-arguments, too-many-locals
    conn: Connection,
    server_name: str,
    port: Port,
    image: Image,
    flavor: Flavor,
    keypair: Keypair,
    disk_size: int,
) -> Server:
    """Create a new server"""

    print(f"creating server: {server_name}")

    disk_sizes = [disk_size]

    block_mappings = []

    if flavor.disk == 0:
        print(
            f"flavor '{flavor.name}' includes no disk, using block storage for all disks"
        )
        volume_type_name = "fast"
        #volume_type = conn.volume.find_type(volume_type_name)
        #if volume_type is None:
            #raise RuntimeError(f"unable to find volume type '{volume_type_name}'")
        for i, size in enumerate(disk_sizes):
            block_mapping = {
                "delete_on_termination": True,
                "destination_type": "volume",
                "volume_size": size,
                #"volume_type": volume_type.name,
            }
            # The first disk is the OS disk, base it on the selected image and bootable
            if i == 0:
                block_mapping["boot_index"] = 0
                block_mapping["source_type"] = "image"
                block_mapping["uuid"] = image.id
            else:
                # Any aditional disk is a data disk, should not be booted from
                # and has no automatic contents
                block_mapping["source_type"] = "blank"

            block_mappings.append(block_mapping)
        server = conn.compute.create_server(
            name=server_name,
            flavor_id=flavor.id,
            networks=[{"port": port.id}],
            key_name=keypair.name,
            block_device_mapping_v2=block_mappings,
            disk_config="AUTO",
        )
    else:
        print(f"flavor '{flavor.name}' includes disk, using local storage")
        server = conn.compute.create_server(
            name=server_name,
            image_id=image.id,
            flavor_id=flavor.id,
            networks=[{"port": port.id}],
            key_name=keypair.name,
        )

    print(f"waiting for server to be active: {server_name}")
    server = conn.compute.wait_for_server(server)
    print(f"server is active: {server_name}")

    if not isinstance(server, Server):
        raise TypeError(f"server should be {Server} but is {type(server)}")

    return server


def main() -> (
    None
):  # pylint: disable=too-many-locals,too-many-statements,too-many-branches
    """Starting point of the program"""

    parser = argparse.ArgumentParser()
    parser.add_argument(
        "--cloud",
        help="name of the openstack cloud in your clouds.yaml, e.g: openstack",
        required=True,
    )
    parser.add_argument(
        "--flavor",
        help="name of the instance flavor to use, e.g: b2.c1r2",
        required=False,
    )
    parser.add_argument(
        "--image",
        help="name of the instance image to use, e.g: ubuntu-20.04",
        required=False,
    )
    parser.add_argument(
        "--fqdn",
        help="full name of server to create, e.g: example-ni1.cnaas.sunet.se",
        required=True,
    )
    parser.add_argument(
        "--key-name",
        help="name of existing SSH keypair to deploy server with, e.g: uid-yk1",
        required=True,
    )
    parser.add_argument(
        "--disk-size",
        help="provide disk size in GB",
        required=True,
    )
    parser.add_argument(
        "--network-name",
        help="The network name in safespring, ex 'public'",
        required=True,
    )
    parser.add_argument(
        "--verbose",
        help="include additional informational messages about what is being done",
        required=False,
        action="store_true",
    )
    parser.add_argument(
        "--debug",
        help="enable debug logging for openstack calls "
        "(lots of output, potentially sensitive information)",
        required=False,
        action="store_true",
    )
    args = parser.parse_args()

    if not args.flavor:
        print(
            "you must supply --flavor",
        )
        sys.exit(1)

    flavor_name = args.flavor

    if not args.image:
        print(
            "you must supply --image",
        )
        sys.exit(1)

    image_name = args.image

    # Configure logging. Keep in mind that debug logging will print sensitive
    # information like passwords to the screen.
    openstack.enable_logging(debug=args.debug)  # type: ignore

    # Initialize connection
    conn = openstack.connect(cloud=args.cloud)

    keypair = get_keypair(conn, args.key_name)

    network_name = args.network_name

    server = conn.compute.find_server(args.fqdn)
    if server is not None:
        print(f"server '{args.fqdn}' already exists")
        sys.exit(1)

    image = conn.image.find_image(image_name)
    if image is None:
        print(f"image '{image_name}' not found")
        sys.exit(1)

    flavor = conn.compute.find_flavor(flavor_name)
    if flavor is None:
        print(f"flavor '{flavor_name}' not found")
        sys.exit(1)

    network = conn.network.find_network(network_name)
    if network is None:
        print(f"network '{network_name}' not found")
        sys.exit(1)

    port = get_port(
        args,
        conn,
        network,
        args.fqdn,
    )

    create_server(
        conn,
        args.fqdn,
        port,
        image,
        flavor,
        keypair,
        args.disk_size,
    )

    print("DNS records (needs to be created manually):")
    for fixed_ip in port.fixed_ips:
        address = ipaddress.ip_address(fixed_ip["ip_address"])
        if isinstance(address, ipaddress.IPv4Address):
            print(f"{args.fqdn}.	A	{address}")
        if isinstance(address, ipaddress.IPv6Address):
            print(f"{args.fqdn}.	AAAA	{address}")


if __name__ == "__main__":
    main()