"""Module for common methods used on entire application"""

import os
import random
import shutil
import string
import sys
from datetime import datetime, timezone
from email import message
from enum import Enum
from glob import glob
from subprocess import PIPE, STDOUT, CalledProcessError, Popen
from typing import List, NoReturn

from colorama import Back, Fore, Style
from packaging.version import InvalidVersion, parse
from yaml import SafeLoader, add_constructor, load


class ExitCode(Enum):
    SUCCESS = 0
    ERROR = 1


class Command(object):
    output = ""
    cmd: str
    exit_code: ExitCode

    def __init__(self, cmd: str, error_message: str, raise_on_error: bool = False):
        self.cmd = cmd
        self.error_message = error_message
        self.raise_on_error = raise_on_error

    def run(self, print_command=True, print_output=True, env: dict = None):
        if print_command:
            print_message(self.cmd, Fore.LIGHTGREEN_EX)
        if env is None:
            env = os.environ.copy()

        with Popen(
            self.cmd,
            env=env,
            stdout=PIPE,
            stderr=STDOUT,
            shell=True,
        ) as process:
            if print_output:
                print(" ")

            while True:
                line = process.stdout.readline()
                if not line:
                    break

                self.output += line.decode()

                if print_output:
                    print(line.decode().replace("\n", ""))

            exit_code = process.wait()

            self._set_exit_code(exit_code)

            if self.exit_code == ExitCode.ERROR:
                if self.raise_on_error:
                    raise RuntimeError(
                        self.error_message or f"Comando falhou: {self.cmd}"
                    )
                elif self.error_message:
                    exit_message(self.error_message)

            return self

    def _set_exit_code(self, exit_code: int):
        if exit_code == 0 or (exit_code == 65 and "flutter pub" in self.cmd):
            self.exit_code = ExitCode.SUCCESS
        else:
            self.exit_code = ExitCode.ERROR


def get_env_variable(variable_name: str, default_value: str = None) -> str:
    """Get the environment variable"""

    var = os.getenv(variable_name)

    if not var:
        var = default_value

    return var


def get_env_variable_required(variable_name: str) -> str:
    """Get the environment variable and check if exist"""

    var = get_env_variable(variable_name)

    if not var:
        exit_message(f"Variável obrigatória não informada: {variable_name}")

    return var


def convert_tag_to_semver(tag: str) -> str:
    """Convert a Gitlab Tag to semver format (vX-Y-Z => X.Y.Z)"""

    return tag.replace("v", "").replace("-", ".")


def get_commit_tag_as_semver() -> str:
    """Get version from gitlab variable CI_COMMIT_TAG
    and format it to semver format (vX-Y-Z => X.Y.Z)
    """

    commit_tag = get_env_variable("CI_COMMIT_TAG")

    if commit_tag and not is_semantic_released():
        return convert_tag_to_semver(commit_tag)

    return commit_tag.replace("v", "") if commit_tag else None


def get_version_from_gitlab() -> str:
    """
    DEPRECATED

    Use directly `get_version()`
    """

    return get_version()


def get_version() -> str:
    """
    Retrieve the version information based on GitLab CI/CD environment variables.

    This function determines the version of the project by checking if a Git tag
    is associated with the current commit. If a tag is found, it is returned as the version.
    Otherwise, it constructs a version string using the last known version, the branch name,
    and the short commit hash.

    Returns:
        str: The version string in the format:
             - "<tag>" if a tag is associated with the commit.
             - "<last_version>-<branch_name>-<short_commit_hash>" if no tag is found.

    Raises:
        KeyError: If required environment variables (CI_COMMIT_REF_SLUG or CI_COMMIT_SHORT_SHA)
                  are not set or accessible.
    """

    version = get_commit_tag_as_semver()

    if not version:
        commit_ref_slug = get_env_variable_required("CI_COMMIT_REF_SLUG")
        commit_short_sha = get_env_variable_required("CI_COMMIT_SHORT_SHA")

        last_version = get_last_version()

        if not last_version:
            last_version = "0.0.0"

        version = f"{last_version}-{commit_ref_slug}-{commit_short_sha}"

    return version


def print_message(message: str, fore_color=None) -> NoReturn:
    """Print a colored message"""

    colored_message = get_colored_message(message, fore_color)

    print(colored_message)


def print_messages(messages: List[str], fore_color=None) -> NoReturn:
    """Print many colored messages"""

    for message in messages:
        print_message(message, fore_color)


def get_colored_message(
    message, fore_color: Fore = None, back_color: Back = None
) -> str:
    """Color a message"""

    fore_color_code = fore_color if fore_color else ""
    back_color_code = back_color if back_color else ""

    reset_style_code = Style.RESET_ALL if back_color or fore_color else ""

    return f"{back_color_code}{fore_color_code}[CI] {message}{reset_style_code}"


def start_collapsible_section(header: str, fore_color: Fore = Fore.CYAN) -> str:
    """Print a message with the GitLab syntax to create a collapse section
    and return the id for use when close the section

    https://docs.gitlab.com/ee/ci/jobs/index.html#custom-collapsible-sections
    """

    section_id = "".join(random.choices(string.ascii_uppercase + string.digits, k=10))

    colored_header = get_colored_message(header, fore_color)

    print(" ")
    print(
        "\033[0Ksection_start:"
        f"{str(datetime.now(timezone.utc).timestamp()).split('.', maxsplit=1)[0]}:{section_id}"
        f"\r\033[0K{colored_header}"
    )
    print(" ")

    return section_id


def end_collapsible_section(section_id: str) -> NoReturn:
    """Close the section"""

    print(
        "\033[0Ksection_end:"
        f"{str(datetime.now(timezone.utc).timestamp()).split('.', maxsplit=1)[0]}:{section_id}"
        "\r\033[0K"
    )


def exit_message(message: str) -> NoReturn:
    """Print a red error message and stop the execution"""

    print(" ")
    print_message(message, Fore.LIGHTRED_EX)
    sys.exit(2)


def exec_command(
    command: str,
    print_command=True,
    print_output=True,
    error_message: str = None,
    env: dict = None,
    raise_on_error: bool = False,
) -> Command:
    """Run a command"""

    cmd = Command(command, error_message, raise_on_error)

    return cmd.run(print_command, print_output, env)


def cd_path(path: str) -> NoReturn:
    """Print a path then navigate"""

    print_message(f"cd {path}", Fore.LIGHTGREEN_EX)

    os.chdir(path)


def move_path(source: str, destination: str) -> NoReturn:
    """Print a move command then execute it"""

    print_message(f"mv {source} {destination}", Fore.LIGHTGREEN_EX)

    shutil.move(source, destination)


def unshallow_repo() -> NoReturn:
    """Get all references of repository"""

    print_message("Atualizando repositório git com todas as referências")

    is_shallow = exec_command(
        "git rev-parse --is-shallow-repository", print_output=False
    )

    if is_shallow.output.rstrip() == "true":
        exec_command("git fetch --unshallow", print_output=False)

    exec_command("git fetch --all", print_output=False)

    print_message("Repositório atualizado")


def is_path_exist(path: str) -> bool:
    """Validate the existence of a path"""

    return os.path.exists(path)


def regex_path_exists(path: str) -> bool:
    return len(glob(path)) > 0


def is_frontend() -> bool:
    """Verify if project is frontend"""

    ci_project_name = get_env_variable_required("CI_PROJECT_NAME")
    sci_project_type = get_env_variable_required("SCI_PROJECT_TYPE")

    return (
        sci_project_type
        in ["ANGULAR_SENIORX", "ANGULAR_GENERATED", "ANGULAR_APP", "ANGULAR_LIB"]
        or "-frontend" in ci_project_name
    )


def uses_json_translations() -> bool:
    """Verify if project uses JSON translation files (frontend or Python apps)"""

    sci_project_type = get_env_variable_required("SCI_PROJECT_TYPE")

    return sci_project_type == "PYTHON_APP" or is_frontend()


def get_last_version():
    """Returns last released version tag"""
    unshallow_repo()

    version = exec_command(
        "git describe --tags --abbrev=0", print_output=False
    ).output.strip()

    version_converted = convert_tag_to_semver(version)

    return version_converted if is_semver(version_converted) else None


def is_semver(version: str):
    """Validate string is semantic version"""

    try:
        parse(version)
        return True
    except InvalidVersion:
        return False


def repository_is_flex() -> bool:
    """Check if the repository is flex or not"""

    sci_flex = get_env_variable("SCI_FLEX")

    return bool(sci_flex)


def repository_is_sdl_flex() -> bool:
    """Check if the repository is sdl and flex or not"""

    sci_project_type = get_env_variable_required("SCI_PROJECT_TYPE")

    return "MAVEN_SDL" in sci_project_type and repository_is_flex()


def set_env_variable(variable_name, value):
    os.environ[variable_name] = value


def get_go_version():
    """
    DEPRECATED

    Use directly `get_version()`
    """

    ci_commit_sha = get_env_variable("CI_COMMIT_SHA")

    commit_hash = ci_commit_sha[:12]

    ci_commit_timestamp = get_env_variable("CI_COMMIT_TIMESTAMP")

    timestamp = datetime.fromisoformat(ci_commit_timestamp)
    timestamp = timestamp.astimezone(timezone.utc)
    timestamp = timestamp.strftime("%Y%m%d%H%M%S")

    version = get_version()
    last_version = get_last_version()

    if not last_version:
        last_version = "0.0.0"

        project_version = f"v{last_version}-0.{timestamp}-{commit_hash}"
    else:
        project_version = f"v{version}"

    return project_version


def is_semantic_released() -> bool:
    """
    Checks if the GitLab CI configuration includes the semantic release template.

    It uses a custom YAML loader to handle `!reference` tags, as described in the issue:
    https://github.com/yaml/pyyaml/issues/825.
    """

    class GitlabLoader(SafeLoader):
        pass

    def construct_reference(l, node):
        data = l.construct_sequence(node)
        return data

    add_constructor("!reference", construct_reference, GitlabLoader)
    ci_yaml_path = f"{get_env_variable_required('CI_PROJECT_DIR')}/{get_env_variable_required('CI_CONFIG_PATH')}"

    with open(ci_yaml_path, "r", encoding="utf-8") as file:
        ci_yaml = load(file, Loader=GitlabLoader)
    includes = ci_yaml.get("include", {})

    if isinstance(includes, list):
        for item in includes:
            if (
                isinstance(item, dict)
                and item.get("project") == "$SCI_PROJECT_TEMPLATES"
            ):
                includes = item

    return "flow/release/semantic.gitlab-ci.yml" in includes.get("file", [])


def get_homologx_manifest_project() -> str:
    """Returns the HomologX manifest project path based on whether the project is flex or not."""
    if repository_is_flex():
        return "devops/homologx-platform-flex-k8s-manifests"
    return "devops/homologx-platform-k8s-manifests"
