import json
import os
import re
from datetime import datetime
from time import sleep
from typing import NoReturn

from colorama import Fore
from gitlab import Gitlab
from yaml import SafeLoader, dump, load

from argocd import ArgoCDApp, ArgoCDCenter
from common import (
    ExitCode,
    end_collapsible_section,
    exec_command,
    exit_message,
    get_env_variable,
    get_env_variable_required,
    print_message,
    print_messages,
    start_collapsible_section,
)


class SyncDeployments(ArgoCDCenter):
    """
    Atualiza os manifestos de deploy com base no diff do commit atual e
    replica a alteração para a branch padrão após sucesso.

    Deve ser executado apenas em branches criadas a partir da branch default, nunca na branch default.
    """

    def __init__(self) -> NoReturn:
        super().__init__()

        ci_commit_sha = get_env_variable_required("CI_COMMIT_SHA")
        ci_commit_branch = get_env_variable_required("CI_COMMIT_BRANCH")
        gitlab_protocol = get_env_variable_required("CI_SERVER_PROTOCOL")
        gitlab_host = get_env_variable_required("CI_SERVER_HOST")

        sci_argocd_project = get_env_variable_required("SCI_ARGOCD_PROJECT")
        sci_argocd_dest_name = get_env_variable_required("SCI_ARGOCD_DEST_NAME")
        sci_argocd_sync_options = get_env_variable(
            "SCI_ARGOCD_SYNC_OPT",
            "--sync-option CreateNamespace=true",
        )
        sci_argocd_wait_timeout = get_env_variable(
            "SCI_ARGOCD_WAIT_TIMEOUT",
            "300",
        )
        skip_argocd_sync = get_env_variable("SKIP_ARGOCD_SYNC")
        sci_argocd_queued_deploy = get_env_variable("SCI_ARGOCD_QUEUED_DEPLOY")
        sci_argocd_queue_type = get_env_variable("SCI_ARGOCD_QUEUE_TYPE")

        argocd_server = get_env_variable_required("ARGOCD_SERVER")

        gitlab_manager = Gitlab(
            url=f"{gitlab_protocol}://{gitlab_host}",
            private_token=get_env_variable_required("SCI_ARGOCD_TOKEN"),
            per_page=100,
        )

        project = gitlab_manager.projects.get(
            get_env_variable_required("CI_PROJECT_PATH")
        )

        if sci_argocd_queued_deploy:
            pipelines = self.get_commits_not_finished_pipelines(project)
            self._wait_for_pipelines(project, pipelines)

        section_id = start_collapsible_section(
            f"Começando processo de criação/atualização das aplicações para o commit {ci_commit_sha}.",
            Fore.CYAN,
        )

        commit = self.manifest_project.commits.get(ci_commit_sha)

        apps: dict[str, ArgoCDApp] = {}
        any_failure = False

        diffs = True
        do_commit_application_ref = False
        page = 1

        while diffs:
            diffs = commit.diff(per_page=100, page=page)

            for diff in diffs:
                yaml_path = diff["new_path"]
                if "/" not in yaml_path:
                    continue

                yaml_name = yaml_path.split("/")[-2]
                yaml_path_dir = "/".join(yaml_path.split("/")[:-1])
                yaml_namespace = None
                yaml_label = None
                yaml_domain = None
                to_delete = False

                if not diff["deleted_file"]:
                    if not self._is_yaml(yaml_path):
                        continue
                    if "application-reference" in yaml_path:
                        do_commit_application_ref = True

                    yaml = self.get_yaml_by_path(yaml_path)

                    if not yaml:
                        print_message(
                            f"Não foi possível realizar o parse do manifesto: {yaml_path}",
                            Fore.RED,
                        )
                        any_failure = True
                        continue

                    if "kind" not in yaml:
                        print_message(
                            f"Manifesto {yaml_path} não possui a chave 'kind'. Pulando...",
                            Fore.YELLOW,
                        )
                        continue

                    yaml_kind = yaml["kind"]
                    yaml_namespace = self._get_k8s_namespace(yaml_kind, yaml, yaml_path)
                    yaml_label = self._get_label_by_kind(yaml_kind, yaml)
                    yaml_domain = self._get_yaml_domain(yaml)

                elif diff["deleted_file"] and "application-reference" in yaml_path:
                    to_delete = True

                if (not diff["deleted_file"] or to_delete) and not apps.get(
                    yaml_path_dir
                ):
                    apps[yaml_path_dir] = ArgoCDApp(
                        name=yaml_name,
                        path=yaml_path_dir,
                        namespace=yaml_namespace,
                        domain=yaml_domain,
                        label=yaml_label,
                        to_delete=to_delete,
                    )

            page += 1

        if not apps:
            exit_message("Nenhuma aplicação encontrada para criar/atualizar/deletar!")

        if any_failure:
            exit_message(
                "Ocorreu falha ao encontrar algumas aplicações. Verifique os logs acima para detalhes."
            )

        if sci_argocd_queued_deploy:
            self._wait_app_already_in_progress()

        apps_to_upsert = dict(filter(lambda item: not item[1].to_delete, apps.items()))
        apps_to_delete = dict(filter(lambda item: item[1].to_delete, apps.items()))

        length_apps_to_upsert_leading_zeros = f"{len(apps_to_upsert):02d}"
        length_apps_to_delete_leading_zeros = f"{len(apps_to_delete):02d}"

        print(" ")
        print_message(
            f"Foram encontradas {length_apps_to_delete_leading_zeros} aplicações para serem deletadas."
        )

        if get_env_variable("SCI_ARGOCD_SKIP_DELETE_APPS"):
            apps_to_delete = {}

            print(" ")
            print_message("Pulando deleção das aplicações!")

        for index, app in enumerate(apps_to_delete.values()):
            app.name = f"{app.name}-{sci_argocd_project}"
            index_leading_zeros = f"{index+1:02d}"
            progress = f"{index_leading_zeros}/{length_apps_to_upsert_leading_zeros}"

            print(" ")
            print_message(
                f"{{{progress}}} Iniciando deleção da aplicação {app.name}...",
                Fore.CYAN,
            )

            app_delete_command = exec_command(f"argocd app delete {app.name}")

            if app_delete_command.exit_code == ExitCode.ERROR:
                any_failure = True
                print(" ")
                print_message(
                    f"Ocorreu um erro ao tentar deletar a aplicação {app.name}, verifique o log acima.",
                    Fore.YELLOW,
                )
                continue

            print(" ")
            print_message("Aplicação deletada com sucesso!")

        print(" ")
        print_message(
            f"Foram encontradas {length_apps_to_upsert_leading_zeros} aplicações para serem criadas/atualizadas."
        )

        for index, app in enumerate(apps_to_upsert.values()):
            app.name = f"{app.name}-{sci_argocd_project}"
            index_leading_zeros = f"{index+1:02d}"
            progress = f"{index_leading_zeros}/{length_apps_to_upsert_leading_zeros}"
            app_reference_path = f"{app.path}/application-reference.yaml"

            if self._app_name_already_exists(app, sci_argocd_project):
                continue

            print(" ")
            print_message(
                f"{{{progress}}} Iniciando criação/atualização da aplicação {app.name}...",
                Fore.CYAN,
            )
            raw_app_yaml = None
            try:
                raw_app_yaml = project.files.raw(
                    file_path=app_reference_path,
                    ref="develop",
                )
            except Exception:
                print_message(
                    "Arquivo de referência da application não existe. criando um novo..."
                )
            argocd_namespace = None
            yaml_template = None
            with open(
                "senior-ci/argocd/templates/application-reference.yaml",
                "r",
                encoding="utf-8",
            ) as app_yaml_template:
                yaml_template = load(app_yaml_template, Loader=SafeLoader)
                argocd_namespace = yaml_template["metadata"]["namespace"]

            if raw_app_yaml:
                yaml_template = load(raw_app_yaml, Loader=SafeLoader)

            content = self.create_application_yaml(
                yaml_template,
                sci_argocd_sync_options,
                raw_app_yaml,
                app,
                sci_argocd_project,
                sci_argocd_dest_name,
                argocd_namespace,
            )
            if not raw_app_yaml or (
                raw_app_yaml and content != raw_app_yaml.decode("utf-8")
            ):
                do_commit_application_ref = True

            # If application exists on Argo CD or will be created
            # to prevent empty commits
            if do_commit_application_ref or not raw_app_yaml:
                self._commit_application(
                    project,
                    app_reference_path,
                    content,
                    self._get_commit_action(raw_app_yaml),
                    app.name,
                )

            raw_app_yaml = project.files.raw(
                file_path=app_reference_path,
                ref="develop",
            )

            with open(
                f"senior-ci/argocd/templates/{app.name}.yaml", "w", encoding="utf-8"
            ) as file_to_read:
                raw_app_yaml = self._set_target_revision(
                    ci_commit_branch, raw_app_yaml.decode("utf-8")
                )
                file_to_read.write(raw_app_yaml)

            app_create_command = exec_command(
                f"argocd app create {app.name} --upsert -f senior-ci/argocd/templates/{app.name}.yaml"
            )
            os.remove(f"senior-ci/argocd/templates/{app.name}.yaml")

            if app_create_command.exit_code == ExitCode.ERROR:
                any_failure = True
                print(" ")
                print_message(
                    f"Ocorreu um erro ao tentar criar/atualizar a aplicação {app.name}, verifique o log acima.",
                    Fore.YELLOW,
                )
                continue
            print_message("Mudanças geradas na application:", fore_color=Fore.CYAN)
            print(" ")
            app_diff = self._clean_text_mess(
                exec_command(
                    f"argocd app diff {app.name} --refresh", print_command=False
                ).output
            )
            if not app_diff:
                print_message(
                    "Nenhuma mudança foi encontrada no cluster a partir desse job."
                )

            if not skip_argocd_sync:
                exec_command(f"argocd app sync {app.name}", print_output=False)

                app_wait_command = (
                    f"argocd app wait {app.name} --timeout {sci_argocd_wait_timeout}"
                )

                if sci_argocd_queue_type:
                    app_wait_command = f"{app_wait_command} {sci_argocd_queue_type}"

                exec_command(app_wait_command)

                self._check_app_health(app.name)

            print(" ")
            print_messages(
                [
                    "Aplicação criada/atualizada com sucesso!",
                    f"Você pode visualizar ela em https://{argocd_server}/applications/{app.name}",
                ]
            )

        print(" ")
        if any_failure:
            exit_message(
                "Ocorreu falha ao criar/atualizar algumas aplicações. Verifique os logs acima para detalhes."
            )
        elif ci_commit_branch != self.ci_default_branch:
            commit.cherry_pick(branch=self.ci_default_branch)
            self.manifest_project.branches.delete(ci_commit_branch)

        print_message("Todas as aplicações foram criadas/atualizadas.", Fore.GREEN)

        end_collapsible_section(section_id)

    def _check_app_health(self, app_name):
        try:
            app_data = json.loads(
                self._clean_text_mess(
                    exec_command(
                        f"argocd app get {app_name} -o json",
                        print_command=False,
                        print_output=False,
                    ).output
                )
            )
            app_status = app_data["status"]["health"]["status"]
            if app_status == "Degraded":
                exit_message(
                    f"Deploy não foi concluido com súcesso. O status da aplicação {app_name} é {app_status}!"
                )
        except json.JSONDecodeError:
            print_message(
                f"Não foi possível realizar a checagem de saúda do serviço {app_name}."
            )

    def _clean_text_mess(self, app_data):
        if "GOMAXPROCS" in app_data:
            app_data = re.sub(
                r"^.*?maxprocs:.*?quota undefined\s*", "", app_data, flags=re.DOTALL
            )
        return app_data

    def _set_target_revision(self, ci_commit_branch, raw_app_yaml):
        return raw_app_yaml.replace(
            "targetRevision: HEAD", f"targetRevision: {ci_commit_branch}"
        )

    def _wait_app_already_in_progress(self) -> NoReturn:
        sci_argocd_project = get_env_variable_required("SCI_ARGOCD_PROJECT")
        sci_argocd_queue_type = get_env_variable("SCI_ARGOCD_QUEUE_TYPE")
        sci_argocd_wait_timeout = get_env_variable(
            "SCI_ARGOCD_WAIT_TIMEOUT",
            "300",
        )

        print_message("Validando alguma pendência de atualização existente")

        apps_json_raw = exec_command(
            f"argocd app list -p {sci_argocd_project} -o json",
            print_output=False,
            error_message=f"Não foi possível listar as aplicações do projeto {sci_argocd_project}",
        ).output

        apps_json = json.loads(apps_json_raw)

        everything_up_to_date = True

        for app in apps_json:
            app_name = app["metadata"]["name"]
            app_status_health = app["status"]["health"]["status"]

            if app_status_health == "Progressing":
                everything_up_to_date = False

                print(" ")
                print_message(f"Aguardando o deployment da aplicação {app_name}...")

                app_wait_command = (
                    f"argocd app wait {app_name} --timeout {sci_argocd_wait_timeout}"
                )

                if sci_argocd_queue_type:
                    app_wait_command = f"{app_wait_command} {sci_argocd_queue_type}"

                exec_command(app_wait_command)

                print(" ")
                print_message(
                    f"Deployment da aplicação {app_name} concluído com sucesso!"
                )

        print(" ")
        if not everything_up_to_date:
            sleep(5)
            self._wait_app_already_in_progress()

        print_message("Todas as pendências existentes estão resolvidas!")

    def create_application_yaml(
        self,
        yaml_template,
        sync_options,
        ref_yaml,
        app,
        argo_project,
        cluster_name,
        argocd_namespace,
    ):
        if ref_yaml:
            ref_yaml = load(ref_yaml, SafeLoader)

        sync_opt = self.get_sync_options_list(sync_options)
        ci_project_url = get_env_variable_required("CI_PROJECT_URL")

        yaml_template["metadata"]["namespace"] = argocd_namespace
        yaml_template["metadata"]["labels"]["type"] = app.label

        if app.domain:
            yaml_template["metadata"]["labels"]["domain"] = app.domain

        yaml_template["metadata"]["name"] = app.name

        if ref_yaml and "annotations" in ref_yaml:
            yaml_template["metadata"]["annotations"] = ref_yaml["metadata"][
                "annotations"
            ]

        yaml_template["spec"]["project"] = argo_project

        yaml_template["spec"]["source"]["repoURL"] = f"{ci_project_url}.git"

        yaml_template["spec"]["source"]["path"] = app.path

        yaml_template["spec"]["destination"]["server"] = self._get_cluster_server(
            cluster_name
        )

        yaml_template["spec"]["destination"]["namespace"] = app.namespace

        yaml_template["spec"]["syncPolicy"]["syncOptions"] = sync_opt

        if "--sync-policy automated" in sync_options:
            yaml_template["spec"]["syncPolicy"]["automated"] = {}

            if "--auto-prune" in sync_options:
                yaml_template["spec"]["syncPolicy"]["automated"]["prune"] = True

            if "--self-heal" in sync_options:
                yaml_template["spec"]["syncPolicy"]["automated"]["selfHeal"] = True

            if "--allow-empty" in sync_options:
                yaml_template["spec"]["syncPolicy"]["automated"]["allowEmpty"] = True
        elif "automated" in yaml_template["spec"]["syncPolicy"] and not bool(
            yaml_template["spec"]["syncPolicy"]["automated"]
        ):
            try:
                yaml_template["spec"]["syncPolicy"].pop("automated")
            except Exception:
                print_message(
                    "Não foi possível remover as sync policies da sua aplicação",
                    Fore.YELLOW,
                )

        return dump(yaml_template)

    @staticmethod
    def _app_name_already_exists(app, sci_argocd_project):
        skip_check_duplicated_name = get_env_variable("SKIP_CHECK_DUPLICATED_NAME")

        if skip_check_duplicated_name:
            return False

        existing_apps_raw = exec_command(
            f"argocd app list -p {sci_argocd_project} -o json",
            print_output=False,
            error_message="Não foi possível listar as aplicações.",
        ).output

        existing_apps_json = json.loads(existing_apps_raw)

        for existing_app in existing_apps_json:
            existing_app_path = None

            if "spec" in existing_app and "source" in existing_app["spec"]:
                existing_app_path = existing_app["spec"]["source"]["path"]
            if (
                existing_app["metadata"]["name"] == app.name
                and existing_app_path != app.path
            ):
                print_message(
                    (
                        f"Application com o nome {app.name} já existe no path {existing_app_path} "
                        f"e esta em conflito com o path {app.path}. "
                        f"Altere o nome da application para continuar com esse deploy."
                    ),
                    Fore.RED,
                )
                return True
        return False

    def get_sync_options_list(self, sync_cli: str):
        return sync_cli.split("--sync-option")[1].split("--")[0].strip().split()

    @staticmethod
    def _get_k8s_namespace(yaml_kind, yaml, yaml_path):
        yaml_namespace = "default"

        if yaml_kind == "Namespace":
            yaml_namespace = yaml["metadata"]["name"]
        elif yaml_kind == "Application":
            yaml_namespace = yaml["spec"]["destination"]["namespace"]
        elif yaml_kind == "StorageClass":
            yaml_namespace = "cluster-resources"
        elif yaml_kind == "List":
            first_yaml = yaml["items"][0]
            if "metadata" in first_yaml and "namespace" in first_yaml["metadata"]:
                yaml_namespace = first_yaml["metadata"]["namespace"]
        elif "namespace" in yaml["metadata"]:
            yaml_namespace = yaml["metadata"]["namespace"]
        else:
            print_message(
                f"Manifesto {yaml_path} não tem um namespace definido. Definindo como default...",
                Fore.YELLOW,
            )
            return "default"
        return yaml_namespace

    @staticmethod
    def _get_label_by_kind(kind, yaml) -> str:
        label = "general"
        if "Deployment" in kind:
            label = "service"
        if "Application" in kind:
            label = yaml["metadata"]["labels"]["type"]
        return label

    @staticmethod
    def _get_yaml_domain(yaml) -> str:
        try:
            return yaml["metadata"]["labels"]["domain"]
        except Exception:
            return None

    @staticmethod
    def _get_commit_action(file):
        if file:
            return "update"
        return "create"

    @staticmethod
    def _get_cluster_server(cluster_name: str):
        cluster_info = json.loads(
            exec_command(
                "argocd cluster list -o json",
                print_command=False,
                print_output=False,
            ).output
        )
        for cluster in cluster_info:
            if cluster["name"] == cluster_name:
                return cluster["server"]

        return None

    @staticmethod
    def _commit_application(
        gitlab_project, path: str, content: str, action: str, app_name: str
    ):
        data = {
            "branch": "develop",
            "commit_message": f"Criando referência da aplicação {app_name}.",
            "actions": [
                {
                    "action": action,
                    "file_path": path,
                    "content": content,
                }
            ],
        }

        return gitlab_project.commits.create(data)

    @staticmethod
    def get_commits_not_finished_pipelines(project):
        pipelines = []
        page = 1

        commits = project.commits.list(
            per_page=100, page=page, until=get_env_variable("CI_COMMIT_TIMESTAMP")
        )

        while commits:
            for commit in commits:
                commit_data = project.commits.get(commit.id)
                if commit_data.last_pipeline:
                    if commit_data.last_pipeline["status"] in [
                        "success",
                        "failed",
                        "canceled",
                        "skipped",
                    ]:
                        return pipelines[::-1]

                    if str(commit_data.last_pipeline["id"]) != get_env_variable(
                        "CI_PIPELINE_ID"
                    ):
                        pipelines.append(commit_data.last_pipeline["id"])

            page += 1

            commits = project.commits.list(
                per_page=100, page=page, until=get_env_variable("CI_COMMIT_TIMESTAMP")
            )

        return pipelines[::-1]

    @staticmethod
    def _wait_for_pipelines(project, pipelines):
        if pipelines:
            for pipeline_id in pipelines:
                pipeline = project.pipelines.get(pipeline_id)
                while pipeline.status not in [
                    "success",
                    "failed",
                    "canceled",
                    "skipped",
                ]:
                    print_message(
                        f"Esperando a finalização da seguinte pipeline: {project.web_url}/-/pipelines/{pipeline_id}"
                    )
                    sleep(60)
                    pipeline = project.pipelines.get(pipeline_id)

    @staticmethod
    def _is_yaml(yaml):
        conditions = 0
        yaml_extensions = [".yaml", ".yml"]

        for extension in yaml_extensions:
            if not yaml.endswith(extension):
                conditions += 1

        if conditions == 2:
            return False
        return True
