#! /usr/bin/env python3
import argparse
import datetime
import re
import sys

from collections import OrderedDict

import docker
import yaml

IGNORE_VALUES = [None, "", [], "null", {}, "default", 0, ",", "no"]

default_networks = ["bridge", "host", "none"]

dclient = None
DOCKER_API = '1.52'


def client():
    """
    Init docker client
    """
    global dclient
    if not dclient:
        dclient = docker.DockerClient(version = DOCKER_API)
    return dclient


def list_container_names():
    return [container.name for container in client().containers.list(all=True)]


def list_network_names():
    return [network.name for network in client().networks.list()]


def generate_network_info():
    networks = {}

    for network_name in list_network_names():
        network_attributes = client().networks.get(network_name).attrs

        values = {
            "name": network_attributes.get("Name"),
            "scope": network_attributes.get("Scope", "local"),
            "driver": network_attributes.get("Driver", None),
            "enable_ipv6": network_attributes.get("EnableIPv6", False),
            "internal": network_attributes.get("Internal", False),
            "ipam": {
                "driver": network_attributes.get("IPAM", {}).get("Driver", "default"),
                "config": [
                    {key.lower(): value for key, value in config.items()}
                    for config in network_attributes.get("IPAM", {}).get("Config") or []
                ],
            },
        }

        networks[network_name] = {key: value for key, value in values.items()}

    return networks


def main():
    parser = argparse.ArgumentParser(
        description="Generate docker-compose yaml definition from running container.",
    )
    parser.add_argument(
        "-a",
        "--all",
        action="store_true",
        help="Include all active containers",
    )
    parser.add_argument(
        "-v",
        "--version",
        type=int,
        default=3,
        help="Compose file version (1 or 3)",
    )
    parser.add_argument(
        "cnames",
        nargs="*",
        type=str,
        help="The name of the container to process.",
    )
    parser.add_argument(
        "-c",
        "--createvolumes",
        action="store_true",
        help="Create new volumes instead of reusing existing ones",
    )
    parser.add_argument(
        "-f",
        "--filter",
        type=str,
        help="Filter containers by regex",
    )
    parser.add_argument(
        "-i",
        "--keepip",
        action="store_true",
        help="Keep ip address",
    )
    args = parser.parse_args()

    container_names = args.cnames

    if args.all:
        container_names.extend(list_container_names())

    if args.filter:
        cfilter = re.compile(args.filter)
        container_names = [c for c in container_names if cfilter.search(c)]

    struct = {}
    networks = {}
    volumes = {}
    containers = {}

    for cname in container_names:
        cfile, c_networks, c_volumes = generate(cname, createvolumes=args.createvolumes, keepip = args.keepip)

        struct.update(cfile)

        if not c_networks == None:
            networks.update(c_networks)
        if not c_volumes == None:
            volumes.update(c_volumes)

    # moving the networks = None statements outside of the for loop. Otherwise any container could reset it.
    if len(networks) == 0:
        networks = None
    if len(volumes) == 0:
        volumes = None

    if args.all:
        host_networks = generate_network_info()
        networks = host_networks

    render(struct, args, networks, volumes)


def render(struct, args, networks, volumes):
    # Render yaml file
    if args.version == 1:
        yaml_str = yaml.dump(OrderedDict(struct), default_flow_style=False, allow_unicode=True)
    else:
        ans = {"services": struct}

        if networks is not None:
            ans["networks"] = networks

        if volumes is not None:
            ans["volumes"] = volumes

        yaml_str = yaml.dump(ans, default_flow_style=False, allow_unicode=True)
    print(yaml_str)


def generate(cname, createvolumes=False, keepip= False):
    try:
        cid = [x.short_id for x in client().containers.list(all=True) if cname == x.name or x.short_id in cname][0]
    except IndexError:
        print("That container is not available.", file=sys.stderr)
        sys.exit(1)

    cattrs = client().containers.get(cid).attrs

    # Build yaml dict structure

    cfile = {}
    cfile[cattrs.get("Name")[1:]] = {}
    ct = cfile[cattrs.get("Name")[1:]]

    values = {
        "cap_drop": cattrs.get("HostConfig", {}).get("CapDrop", None),
        "cgroup_parent": cattrs.get("HostConfig", {}).get("CgroupParent", None),
        "container_name": cattrs.get("Name")[1:],
        "devices": [],
        "dns": cattrs.get("HostConfig", {}).get("Dns", None),
        "dns_search": cattrs.get("HostConfig", {}).get("DnsSearch", None),
        "environment": cattrs.get("Config", {}).get("Env", None),
        "extra_hosts": cattrs.get("HostConfig", {}).get("ExtraHosts", None),
        "image": cattrs.get("Config", {}).get("Image", None),
        "labels": cattrs.get("Config", {}).get("Labels", {}),
        "links": cattrs.get("HostConfig", {}).get("Links"),
        #'log_driver': cattrs.get('HostConfig']['LogConfig']['Type'],
        #'log_opt': cattrs.get('HostConfig']['LogConfig']['Config'],
        "logging": {
            "driver": cattrs.get("HostConfig", {}).get("LogConfig", {}).get("Type", None),
            "options": cattrs.get("HostConfig", {}).get("LogConfig", {}).get("Config", None),
        },
        "networks": network_info(cattrs.get("NetworkSettings", {}).get("Networks", {}), keepip),
        "security_opt": cattrs.get("HostConfig", {}).get("SecurityOpt"),
        "ulimits": cattrs.get("HostConfig", {}).get("Ulimits"),
        # the line below would not handle type bind
        #        'volumes': [f'{m["Name"]}:{m["Destination"]}' for m in cattrs.get('Mounts'] if m['Type'] == 'volume'],
        "mounts": cattrs.get("Mounts"),  # this could be moved outside of the dict. will only use it for generate
        "volume_driver": cattrs.get("HostConfig", {}).get("VolumeDriver", None),
        "volumes_from": cattrs.get("HostConfig", {}).get("VolumesFrom", None),
        "entrypoint": cattrs.get("Config", {}).get("Entrypoint", None),
        "user": cattrs.get("Config", {}).get("User", None),
        "working_dir": cattrs.get("Config", {}).get("WorkingDir", None),
        "domainname": cattrs.get("Config", {}).get("Domainname", None),
        "hostname": cattrs.get("Config", {}).get("Hostname", None),
        "ipc": cattrs.get("HostConfig", {}).get("IpcMode", None),
        "mac_address": cattrs.get("Config", {}).get("MacAddress", None),
        "privileged": cattrs.get("HostConfig", {}).get("Privileged", None),
        "restart": cattrs.get("HostConfig", {}).get("RestartPolicy", {}).get("Name", None),
        "read_only": cattrs.get("HostConfig", {}).get("ReadonlyRootfs", None),
        "stdin_open": cattrs.get("Config", {}).get("OpenStdin", None),
        "tty": cattrs.get("Config", {}).get("Tty", None),
    }

    # Populate devices key if device values are present
    if cattrs.get("HostConfig", {}).get("Devices"):
        values["devices"] = [
            x["PathOnHost"] + ":" + x["PathInContainer"] for x in cattrs.get("HostConfig", {}).get("Devices")
        ]

    # Environment env may contain '$'. Turn it into '$$' to be correctly analyse
    if values["environment"] is not None:
        envs = []
        for env_var in values["environment"]:
            envs.append(env_var.replace('$', '$$'))
        values["environment"] = envs

    networks = {}
    if len(values["networks"]) == 0:
        del values["networks"]

        if len(cattrs.get("NetworkSettings", {}).get("Networks", {}).keys()) > 0:
            assumed_default_network = list(cattrs.get("NetworkSettings", {}).get("Networks", {}).keys())[0]
            values["network_mode"] = assumed_default_network
            networks = None
    else:
        networklist = client().networks.list()
        for network in networklist:
            if network.attrs["Name"] in values["networks"] and network.attrs["Name"] not in default_networks:
                networks[network.attrs["Name"]] = {
                    "external": (not network.attrs["Internal"]),
                    "name": network.attrs["Name"],
                }
    #     volumes = {}
    #     if values['volumes'] is not None:
    #         for volume in values['volumes']:
    #             volume_name = volume.split(':')[0]
    #             volumes[volume_name] = {'external': True}
    #     else:
    #         volumes = None

    # handles both the returned values['volumes'] (in c_file) and volumes for both, the bind and volume types
    # also includes the read only option
    volumes = {}
    mountpoints = []
    if values["mounts"] is not None:
        for mount in values["mounts"]:
            destination = mount["Destination"]
            if not mount["RW"]:
                destination = destination + ":ro"
            if mount["Type"] == "volume":
                mountpoints.append(mount["Name"] + ":" + destination)
                if not createvolumes:
                    volumes[mount["Name"]] = {
                        "external": True
                    }  # to reuse an existing volume ... better to make that a choice? (cli argument)
            elif mount["Type"] == "bind":
                mountpoints.append(mount["Source"] + ":" + destination)
        values["volumes"] = mountpoints
    if len(volumes) == 0:
        volumes = None
    values["mounts"] = None  # remove this temporary data from the returned data

    # Check for command and add it if present.
    if cattrs.get("Config", {}).get("Cmd") is not None:
        values["command"] = cattrs.get("Config", {}).get("Cmd")

    # Check for exposed/bound ports and add them if needed.
    try:
        expose_value = list(cattrs.get("Config", {}).get("ExposedPorts", {}).keys())
        ports_value = [
            cattrs.get("HostConfig", {}).get("PortBindings", {})[key][0]["HostIp"]
            + ":"
            + cattrs.get("HostConfig", {}).get("PortBindings", {})[key][0]["HostPort"]
            + ":"
            + key
            for key in cattrs.get("HostConfig", {}).get("PortBindings")
        ]

        # If bound ports found, don't use the 'expose' value.
        if ports_value not in IGNORE_VALUES:
            for index, port in enumerate(ports_value):
                if port[0] == ":":
                    ports_value[index] = port[1:]

            values["ports"] = ports_value
        else:
            values["expose"] = expose_value

    except (KeyError, TypeError):
        # No ports exposed/bound. Continue without them.
        ports = None

    # Iterate through values to finish building yaml dict.
    for key in values:
        value = values[key]
        if value not in IGNORE_VALUES:
            ct[key] = value

    return cfile, networks, volumes

def network_info(networks, keepip = False) :
    """
    Get network info from dict
    """
    if keepip:
        nets = {}
    else:
        nets = []

    if isinstance(networks, dict):
        for net, conf in networks.items():
            if net not in default_networks:
                if keepip:
                    nets[net] = {}
                    if "IPAMConfig" in conf and isinstance(conf["IPAMConfig"], dict):
                        if "IPv4Address" in conf["IPAMConfig"] and len(conf["IPAMConfig"]["IPv4Address"]):
                            nets[net]["ipv4_address"] = conf["IPAMConfig"]["IPv4Address"]
                        if "IPv6Address" in conf["IPAMConfig"] and len(conf["IPAMConfig"]["IPv6Address"]):
                            nets[net]["ipv6_address"] = conf["IPAMConfig"]["IPv6Address"]
                else:
                    nets.append(net)
    return nets

if __name__ == "__main__":
    main()
