import logging
from time import sleep
from typing import Dict, List, Optional

from docker.models.containers import Container

from gefyra.cli import console
from gefyra.configuration import ClientConfiguration
from gefyra.exceptions import GefyraBridgeNotFound
from gefyra.local.cargo import get_cargo_ip_from_netaddress
from gefyra.types import GefyraLocalContainer, ExactMatchHeader

from .utils import handle_docker_run_container

logger = logging.getLogger(__name__)


def handle_create_gefyrabridge(config: ClientConfiguration, body, target: str):
    from kubernetes.client import ApiException

    try:
        ireq = config.K8S_CUSTOM_OBJECT_API.create_namespaced_custom_object(
            namespace=config.NAMESPACE,
            body=body,
            group="gefyra.dev",
            plural="gefyrabridges",
            version="v1",
        )
    except ApiException as e:
        if e.status == 409:
            raise RuntimeError(f"Workload {target} already bridged.")
        elif e.status == 500:
            import json

            raise RuntimeError(str(json.loads(e.body).get("message")))
        logger.error(
            f"A Kubernetes API Error occured. \nReason: {e.reason} \nBody: {e.body}"
        )
        raise e from None
    return ireq


def handle_delete_gefyrabridge(config: ClientConfiguration, name: str) -> bool:
    from kubernetes.client import ApiException

    try:
        ireq = config.K8S_CUSTOM_OBJECT_API.delete_namespaced_custom_object(
            namespace=config.NAMESPACE,
            name=name,
            group="gefyra.dev",
            plural="gefyrabridges",
            version="v1",
        )
        return ireq
    except ApiException as e:
        if e.status == 404:
            logger.debug(f"GefyraBridge {name} not found")
        else:
            logger.debug("Error removing GefyraBridge: " + str(e))
        return False


def get_all_gefyrabridges(config: ClientConfiguration) -> list:
    from kubernetes.client import ApiException

    try:
        ireq_list = config.K8S_CUSTOM_OBJECT_API.list_namespaced_custom_object(
            namespace=config.NAMESPACE,
            group="gefyra.dev",
            plural="gefyrabridges",
            version="v1",
            label_selector=f"gefyra.dev/client={config.CLIENT_ID}",
        )
        return ireq_list.get("items", [])
    except ApiException as e:
        if e.status != 404:
            logger.warning("Error getting GefyraBridges: " + str(e))
            raise e from None
        return []


def get_gefyrabridge(config: ClientConfiguration, name: str):
    from kubernetes.client import ApiException

    try:
        bridge = config.K8S_CUSTOM_OBJECT_API.get_namespaced_custom_object(
            name=name,
            namespace=config.NAMESPACE,
            group="gefyra.dev",
            plural="gefyrabridges",
            version="v1",
        )
        return bridge
    except ApiException as e:
        if e.status != 404:
            logger.warning("Error getting GefyraBridge: " + str(e))
            raise e from None
        raise GefyraBridgeNotFound(f"GefyraBridge with name '{name}' not found.")


def get_all_containers(config: ClientConfiguration) -> List[GefyraLocalContainer]:
    container_information = []
    gefyra_net = config.DOCKER.networks.get(f"{config.NETWORK_NAME}")
    containers = gefyra_net.containers
    # filter out gefyra-cargo container as well as fields other than name and ip
    for container in containers:
        if not container.name.startswith("gefyra-cargo"):
            try:
                address = container.attrs["NetworkSettings"]["Networks"][
                    config.NETWORK_NAME
                ]["IPAddress"].split("/")[0]
            except Exception:
                address = "unknown"
            try:
                namespace = container.attrs["HostConfig"]["DnsSearch"][0].split(".")[0]
            except Exception:
                namespace = "unknown"
            container_information.append(
                GefyraLocalContainer(
                    id=container.id,
                    short_id=container.short_id,
                    name=container.name,
                    address=address,
                    namespace=namespace,
                )
            )
    return container_information


def get_match_rules(
    rule_set: List[List] = [],
) -> List[Dict[str, Dict[str, str]]]:
    return [rule.to_dict() for rules in rule_set for rule in rules]


def get_bridge_rules(
    rules: List[ExactMatchHeader] = [],
) -> List[Dict[str, List[Dict[str, Dict[str, str]]]]]:
    return [{"match": get_match_rules(rules)}]


def get_gbridge_body(
    config: ClientConfiguration,
    name: str,
    destination_ip,
    target,
    target_namespace,
    target_container,
    port_mappings,
    handle_probes,
    match_header: List[ExactMatchHeader] = [],
):
    return {
        "apiVersion": "gefyra.dev/v1",
        "kind": "gefyrabridge",
        "metadata": {
            "name": name,
            "namespace": config.NAMESPACE,
            "labels": {
                "gefyra.dev/bridge-mount": target,
            },
        },
        "provider": "carrier2",
        "connectionProvider": "stowaway",
        "providerParameter": {"rules": get_bridge_rules(match_header)},
        "client": config.CLIENT_ID,
        "destinationIP": destination_ip,
        "target": target,
        "targetNamespace": target_namespace,
        "targetContainer": target_container,
        "portMappings": port_mappings,
        "handleProbes": handle_probes,
    }


def deploy_app_container(
    config: ClientConfiguration,
    image: str,
    name: str = "",
    command: str = "",
    volumes: Optional[List] = None,
    ports: Optional[Dict] = None,
    env: Optional[Dict] = None,
    auto_remove: bool = False,
    dns_search: Optional[List[str]] = None,
    pull: Optional[str] = "missing",
    platform: Optional[str] = "linux/amd64",
    cpu_quota: Optional[str] = None,
    mem_limit: Optional[str] = None,
    user: Optional[str] = None,
    security_opts: Optional[List[str]] = None,
    privileged: Optional[bool] = None,
) -> Container:
    import docker

    if pull == "always":
        console.info(f"Pulling image {image}...")
        if ":" in image:
            repo, tag = image.split(":")
        else:
            repo, tag = image, "latest"
        config.DOCKER.images.pull(repository=repo, tag=tag, platform=platform)
        console.info(f"Pulling image {image} done.")

    if not dns_search:
        dns_search = ["default"]

    gefyra_net = config.DOCKER.networks.get(f"{config.NETWORK_NAME}")

    net_add = gefyra_net.attrs["IPAM"]["Config"][0]["Subnet"].split("/")[0]
    cargo_ip = get_cargo_ip_from_netaddress(net_add)
    all_kwargs = {
        "network": config.NETWORK_NAME,
        "name": name,
        "command": command,
        "volumes": volumes,
        "ports": ports,
        "detach": True,
        "dns": [config.STOWAWAY_IP],
        "dns_search": dns_search,
        "auto_remove": auto_remove,
        "environment": env,
        "pid_mode": f"container:{config.CARGO_CONTAINER_NAME}",  # noqa: E231
        "cpu_quota": cpu_quota,
        "mem_limit": mem_limit,
        "user": user,
        "security_opt": security_opts,
        "privileged": privileged,
    }
    not_none_kwargs = {k: v for k, v in all_kwargs.items() if v is not None}

    container = handle_docker_run_container(config, image, **not_none_kwargs)

    cargo = config.DOCKER.containers.get(config.CARGO_CONTAINER_NAME)

    # busy wait for the container to start
    try:
        _i = 0
        while container.status == "created" and _i < (
            config.CONTAINER_RUN_TIMEOUT * 10
        ):
            sleep(0.1)
            container = config.DOCKER.containers.get(container.id)
            _i = _i + 1
    except docker.errors.NotFound:
        raise RuntimeError(
            f"Container {container.id} is not running. Did you miss a valid startup"
            " command?"
        )

    if container.status != "running":
        raise RuntimeError(
            "Container is not running. Did you miss a valid startup command?"
        )

    exit_code, output = cargo.exec_run(
        f"bash patchContainerGateway.sh {container.name} {cargo_ip}"
    )
    if exit_code == 0:
        logger.debug(f"Gateway patch applied to '{container.name}'")

    else:
        container = config.DOCKER.containers.get(container.id)
        if container.status != "running":
            raise RuntimeError(
                f"Container {name} is not running. Check whether your command is valid"
                " for the chosen image."
            )
        raise RuntimeError(
            f"Gateway patch could not be applied to '{container.name}': {output}"
        )
    return container
