#!/usr/bin/env python3
# pylint: disable=invalid-name
"""Checks the health of a Docker Compose deployment."""


import argparse
import datetime
import os
import sys
import time
import docker  # pylint: disable=import-error
import dotenv  # pylint: disable=import-error


def get_project_containers_health(client, project):
    """Get a Docker connection and the Docker Compose project name and return a
    dictionary of container objects and their health status (if available, else
    their status)."""
    return {
        i: i.attrs["State"]["Health"]["Status"]
        if "Health" in i.attrs["State"]
        else i.attrs["State"]["Status"]
        for i in client.containers.list(
            filters={"label": f"com.docker.compose.project={project}"}
        )
    }


def check_project_health(project, timeout):
    """Checks if a Docker Compose project is healthy.

    If all of the containers are healthy or running (because there's no
    healthcheck set), return True. If any of the containers is starting (health
    check hasn't run yet or in the grace period), sleep 10 and try again
    (ignoring the timeout). Otherwise, if haven't passed the timeout, sleep 10
    and try again, if over the timeout, return if all the containers are
    healthy or running.
    """
    starttime = datetime.datetime.now()
    deadline = starttime + datetime.timedelta(seconds=timeout)
    docker_client = docker.from_env()
    docker_client.ping()
    while True:
        healths = get_project_containers_health(docker_client, project)
        # pylint: disable=no-else-return
        if all(map(lambda x: x in ["healthy", "running"], healths.values())):
            return True
        elif any(map(lambda x: x == "starting", healths.values())):
            time.sleep(10)
            continue
        elif datetime.datetime.now() > deadline:
            break
        else:
            time.sleep(10)

    if all(map(lambda x: x in ["healthy", "running"], healths.values())):
        return True
    unhealthy = ", ".join(
        [
            f"{x.id}({x.name})"
            for x in healths
            if healths[x] not in ["healthy", "running"]
        ]
    )
    print(f"""Containers {unhealthy} are not healthy.""")
    return False


def main():
    """Main entrypoint."""

    epilog = (
        "The Docker Compose project name is resolved in the following order:"  # noqa: E501
        "\n1. From the command line parameter."
        "\n2. From the COMPOSE_PROJECT_NAME variable in the .env file."
        "\n3. From the COMPOSE_PROJECT_NAME environment variable."
    )
    arg_parser = argparse.ArgumentParser(
        description=__doc__,
        epilog=epilog,
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    arg_parser.add_argument(
        "project", help="Name of the Compose Project.", nargs="?"
    )
    arg_parser.add_argument(
        "-t",
        "--timeout",
        help="Number of seconds to wait for the deployment to be healthy, defaults to 300.",  # noqa: E501
        type=int,
        default=300,
    )
    args = arg_parser.parse_args()
    if args.project:
        project = args.project
    else:
        env = dotenv.dotenv_values(dotenv_path=".env")
        if "COMPOSE_PROJECT_NAME" in env:
            project = env["COMPOSE_PROJECT_NAME"]
        elif "COMPOSE_PROJECT_NAME" in os.environ:
            project = os.environ["COMPOSE_PROJECT_NAME"]
        else:
            arg_parser.error(
                "Compose project wasn't specified, the COMPOSE_PROJECT_NAME variable is missing from the environment and from the .env file."  # noqa: E501
            )

    if check_project_health(project, args.timeout):
        return 0
    return 1


if __name__ == "__main__":
    sys.exit(main())
