"""
Commands for generating new projects and services.
"""
import getpass
import logging
import os
import random
import shutil
import string
import sys
from typing import Optional

import typer

from fastiot import __version__
from fastiot.cli.commands.build_lib import update_pyproject_toml
from fastiot.cli.constants import DEPLOYMENTS_CONFIG_DIR, CONFIGURE_FILE_NAME, MANIFEST_FILENAME, \
    DEPLOYMENTS_CONFIG_FILE
from fastiot.cli.helper_fn import get_jinja_env
from fastiot.cli.model import DeploymentConfig
from fastiot.cli.model.infrastructure_service import InfrastructureService
from fastiot.cli.model.project import ProjectContext
from fastiot.cli.typer_app import create_cmd


def _create_random_password(length: int = 16) -> str:
    chars = list(string.ascii_letters + string.digits + "_-.,:%?!*")
    password = [random.choice(chars) for _ in range(length)]
    return "".join(password)


def create_toml(path: str, project_name: str, short_description: str = ""):

    python_version = f"{sys.version_info.major}.{sys.version_info.minor}"

    with open(path, "w") as template_file:
        template = get_jinja_env().get_template("pyproject.toml.j2")
        template_file.write(template.render(
            projectname=project_name,
            authors=getpass.getuser(),
            description=short_description,
            python_version=python_version,
        ))


@create_cmd.command()
def new_project(project_name: str = typer.Argument(None, help="The project name to generate"),
                force: bool = typer.Option(False, '-f', '--force',
                                           help="Create project even if an existing project has been found."),
                target_directory: str = typer.Option('.', '-d', '--directory',
                                                     help="The directory the project will be stored"),
                description: str = typer.Option('', '--description',
                                                help="The short description of the project")
                ):
    """
    Function to create a new project with its directory structure and all needed files for a quick start.

    You may use the function `new-service` afterwards to create your first service stubs.
    """

    if os.path.isfile(os.path.join(target_directory, CONFIGURE_FILE_NAME)) and not force:
        logging.error("An existing project configuration has been found! "
                      "To force overwriting this you may use the --force argument.")
        raise typer.Exit(2)

    context = ProjectContext.default

    if target_directory.startswith('/'):
        context.project_root_dir = target_directory
    else:
        context.project_root_dir = os.path.join(context.project_root_dir, target_directory)

    # Check for valid project name (no space, no /, …)
    project_name = project_name.replace(" ", "_").replace("-", '_')
    for letter in project_name:
        if not (letter.isalnum() or letter == "_" or letter == "."):
            logging.error("Please enter valid Name. Only use a-z, A-Z, 0-9, '.' and '_' .")
            raise typer.Exit(3)
    logging.info("The project %s will be created in %s", project_name, context.project_root_dir)

    # Create necessary directories
    if not os.path.exists(context.project_root_dir):
        os.makedirs(context.project_root_dir)
    for directory in ['requirements',
                      os.path.join(DEPLOYMENTS_CONFIG_DIR, "integration_test"),
                      os.path.join(DEPLOYMENTS_CONFIG_DIR, "production"),
                      os.path.join(DEPLOYMENTS_CONFIG_DIR, "full"),
                      os.path.join("src", f"{project_name}"),
                      os.path.join("src", f"{project_name}_tests"),
                      os.path.join("src", f"{project_name}_services")]:
        os.makedirs(os.path.join(context.project_root_dir, directory), exist_ok=True)

    # Copy static files like .gitignore
    templates_dir = os.path.join(os.path.dirname(__file__), '..', 'templates', 'new_project')
    for src, dest in [('.dockerignore', '.'),
                      ('.gitignore', '.'),
                      ('install.sh', '.'),
                      ('__init__.py', os.path.join("src", f"{project_name}")),
                      ('deployment.yaml', os.path.join(DEPLOYMENTS_CONFIG_DIR, 'integration_test')),
                      ('dummy_test.py', os.path.join("src", f"{project_name}_tests"))]:
        shutil.copy(os.path.join(templates_dir, src), os.path.join(context.project_root_dir, dest))

    # Loop over many templates used to create a new project
    for temp, dest in [('configure.py.j2', CONFIGURE_FILE_NAME),
                       ('README.md.j2', 'README.md'),
                       ('.env.j2', os.path.join('deployments', 'integration_test', '.env')),
                       ('deployment.yaml.j2', os.path.join(DEPLOYMENTS_CONFIG_DIR,
                                                           'production', DEPLOYMENTS_CONFIG_FILE)),
                       ('deployment.yaml.j2', os.path.join(DEPLOYMENTS_CONFIG_DIR, 'full', DEPLOYMENTS_CONFIG_FILE)),
                       ('.env.production.j2', os.path.join(DEPLOYMENTS_CONFIG_DIR, 'production', '.env')),
                       ('.env.production.j2', os.path.join(DEPLOYMENTS_CONFIG_DIR, 'full', '.env')),
                       ('requirements.txt.j2', os.path.join('requirements', 'requirements.txt'))]:

        # For each potential service create a unique password for the production deployment.
        password_fields = []
        for service in InfrastructureService.all.values():
            password_fields += service.password_env_vars
        env_vars = [f"{f}={_create_random_password(24)}" for f in password_fields]

        with open(os.path.join(context.project_root_dir, dest), "w") as template_file:
            template = get_jinja_env().get_template(os.path.join('new_project', temp))
            template_file.write(template.render(
                project_namespace=project_name,
                version=__version__,
                major_version=int(__version__.split(".", maxsplit=1)[0]),
                env_vars=env_vars,
                user=getpass.getuser()
            ))
    # create toml
    create_toml(path=os.path.join(context.project_root_dir, "pyproject.toml"),
                short_description=description,
                project_name=project_name)

    logging.info("Project created successfully. Check pyproject.toml for correct configuration")


@create_cmd.command()
def new_service(service_name: str = typer.Argument("", help="The service name to generate"),
                service_package: Optional[str] = typer.Option(None, '-p', '--package',
                                                              help="Specify the package to create the "
                                                                   "service in. If left empty and only one package is "
                                                                   "found this one will be used."),
                force: Optional[bool] = typer.Option(False, '-f', '--force',
                                                     help="Create service even if if an existing service has the same "
                                                          "name."),
                ):
    """ Create a new service with some method stubs for further programming. """
    # service name
    service_name = _sanitize_service_name(service_name)
    # service location
    context = ProjectContext.default
    service_package = _find_service_package(context, service_package)
    service_location = os.path.join(context.project_root_dir, "src", service_package, service_name)
    logging.info("The service %s will be created in %s.", service_name, service_location)

    # service package
    if not os.path.isdir(service_location):
        os.makedirs(service_location)
    elif not force:
        logging.warning("Service with same name found. Will not overwrite unless --force is used.")
        raise typer.Exit(5)

    # __init__.py
    with open(os.path.join(service_location, "__init__.py"), "w") as file:
        file.write("\n")

    # class name
    words_list = service_name.split("_")
    service_classname: str = ""
    for word in words_list:
        service_classname = service_classname + word.capitalize()

    # Templates
    for dest, temp in [(MANIFEST_FILENAME, os.path.join('new_service', 'manifest.yaml.j2')),
                       (f'{service_name}_service.py', os.path.join('new_service', 'servicename_service.py.j2')),
                       ('run.py', os.path.join('new_service', '.run.py.j2'))]:
        with open(os.path.join(service_location, dest), "w") as file:
            configure_py_template = get_jinja_env().get_template(temp)
            file.write(configure_py_template.render(
                service_name=service_name,
                service_name_class=service_classname,
                path=f"{service_package}.{service_name}.{service_name}_service"
            ))

    _add_service_to_deployment(service_name)
    logging.info("Service %s.%s created successfully", service_package, service_name)


@create_cmd.command()
def pyproject_toml(description: Optional[str] = typer.Option("", '-d', '--description',
                                                             help="The short description of the project"),
                   force: Optional[bool] = typer.Option(False, '-f', '--force',
                                                        help="Overwrites existing pyproject.toml")):
    """ Creates a new pyproject.toml to build the library."""
    logging.info("Creating a pyproject.toml")

    context = ProjectContext.default
    pyproject_file = os.path.join(context.project_root_dir, "pyproject.toml")

    if not os.path.isfile(pyproject_file) or force:
        create_toml(path=pyproject_file,
                    short_description=description,
                    project_name=context.project_namespace)

        update_pyproject_toml()
        logging.info("Successfully created pyproject.toml. Please see the file and make adjustments as needed.")
    else:
        logging.error("Not overwriting existing pyproject.toml. Use --force option to overwrite anyways.")


def _sanitize_service_name(service_name):
    service_name = service_name.replace(" ", "_").replace("-", '_')
    for letter in service_name:
        if not ((letter.isalnum() and (letter.islower() or letter.isnumeric())) or letter == "_"):
            logging.error("Please enter valid name for the service. Only use a-z, 0-9, and '_' .")
            raise typer.Exit(3)
    return service_name


def _find_service_package(project_config, service_package):
    """
    Find *the* package to place the service if not defined. Raise errors when no or to many packages are possible.
    """
    service_package_list = []
    for package in os.listdir(os.path.join(project_config.project_root_dir, "src")):
        if package != project_config.library_package and package != project_config.test_package and \
                os.path.isdir(os.path.join(project_config.project_root_dir, 'src', package)):
            service_package_list.append(package)
    if len(service_package_list) == 0:
        logging.error("No service package found in project to place the service.")
        raise typer.Exit(4)
    # nothing given
    if not service_package and len(service_package_list) > 1:
        logging.error("More than one service package found. Please choose one manually using the option `-p`. "
                      "Detected packages: %s", ", ".join(service_package_list))
        raise typer.Exit(4)
    # package given
    if service_package and service_package not in service_package_list:
        logging.error("Package %s not found in project. Found service packages %s.",
                      service_package, ", ".join(service_package_list))
        raise typer.Exit(4)
    service_package = service_package or service_package_list[0]
    return service_package


def _add_service_to_deployment(service_name: str):
    """
    Add the fresh service to the "full" deployment.
    """
    deployment_file = os.path.join(DEPLOYMENTS_CONFIG_DIR, 'full', DEPLOYMENTS_CONFIG_FILE)
    if not os.path.exists(deployment_file):
        logging.debug("No deployment `full` found to add new service automatically. Skipping.")
        return
    deployment = DeploymentConfig.from_yaml_file(deployment_file)

    if service_name not in deployment.services:
        deployment.services[service_name] = None
        deployment.to_yaml_file(deployment_file)
