#!/usr/bin/env python3
import argparse
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from typing import Self

from python_on_whales import DockerClient

PROJECT_ROOT_DIRECTORY = Path(__file__).parent.parent.resolve()
SERVICES_DIRECTORY = PROJECT_ROOT_DIRECTORY / "services"
AVAILABLE_SERVICES = list(
    service.name for service in SERVICES_DIRECTORY.iterdir()
)


@dataclass
class Server:
    server_name: str
    services: list[str]
    context_name: str | None = None

    @property
    def context(self) -> str:
        return self.context_name if self.context_name else self.server_name

    @classmethod
    def parse_dict(cls, data: dict[str, Any]) -> Self:
        if not (server_name := data.get("server_name")):
            raise ValueError("The service is missing server_name property.")

        if not (services := data.get("services")):
            raise ValueError("The config is missing services property.")

        if not isinstance(services, list):
            raise ValueError("The services property should be an array.")

        return cls(server_name=server_name, services=services)


@dataclass
class ServicesConfig:
    servers: list[Server]

    def search_service(self, service_name: str) -> Server:
        servers_matched = []
        for server in self.servers:
            if service_name in server.services:
                servers_matched.append(server)

        if not servers_matched:
            raise ValueError(f"Service `{service_name}` not found.")

        if len(servers_matched) > 1:
            raise ValueError(
                f"Two services with the same name (`{service_name}`) found.",
            )

        return servers_matched.pop()

    @classmethod
    def parse_dict(cls, data: dict[str, Any]) -> Self:
        if not (all_servers := data.get("servers")):
            raise ValueError("The config is missing servers property.")
        if not isinstance(all_servers, list):
            raise ValueError("The servers property should be an array.")
        servers = []
        for server in all_servers:
            if not isinstance(server, dict):
                raise ValueError("The server should be an object.")
            servers.append(Server.parse_dict(server))

        return cls(servers=servers)


def _get_service_context(service_name: str, config: ServicesConfig, docker_client: DockerClient) -> str:
    context_names = [ctx.name for ctx in docker_client.context.list()]
    server_config = config.search_service(service_name)

    if server_config.context not in context_names:
        raise ValueError("Context not found on docker host.")

    return server_config.context


def deploy_service(service_name: str, config: ServicesConfig):
    _tmp_client = DockerClient()
    context = _get_service_context(service_name, config, _tmp_client)
    service_dir = SERVICES_DIRECTORY / service_name

    docker = DockerClient(context=context)

    compose_file = service_dir / "docker-compose.yml"
    env_file = service_dir / ".env"

    if not compose_file.exists():
        raise ValueError(
            f"Service {service_name} is missing docker-compose.yml",
        )

    print(f"INFO: deploying service {service_name} with context {context}")
    docker.stack.deploy(
        name=service_name,
        compose_files=[compose_file],
        env_files=[env_file] if env_file else [],
    )


def load_config(config_file: Path | str) -> ServicesConfig:
    config_file = Path(config_file)
    unknown_data = json.loads(config_file.read_text())
    return ServicesConfig.parse_dict(unknown_data)


def _add_args(parser: argparse.ArgumentParser):
    parser.add_argument("services_file")
    parser.add_argument("service_name", choices=AVAILABLE_SERVICES)


def main() -> int:
    parser = argparse.ArgumentParser("deployservice")
    _add_args(parser)
    parsed_args = parser.parse_args()
    config = load_config(parsed_args.services_file)
    deploy_service(parsed_args.service_name, config)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())