import os.path
from typing import List, Union

from actions_logging.app_logging import logger
from common.common import raise_with_context, run_command
from git.constants import DEFAULT_BRANCH_NAMES
from github.env import exit_on_error_and_write_summary, get_required_env_var


def init_git_user():
    try:
        logger.info("initializing git user, if not already initialized")
        run_command('git config --get user.email || git config --global user.email "svc-github@perimeter81.com"')
        run_command('git config --get user.name || git config --global user.name "Sayuser"')
        logger.info_green("git user initialized")
    except Exception as e:
        exit_on_error_and_write_summary(f"error in init_git_user: {e}")


def get_modified_files(ignored_files: List[str] =[]):
    modified_files = run_command(f'git status --porcelain --untracked-files=no').strip()
    cleaned_modified_files = []
    if not modified_files:
        logger.info_green("No modified files")
        return cleaned_modified_files
    for git_modified in modified_files.split('\n'):
        file = git_modified.strip().split()[1]
        if file not in ignored_files:
            cleaned_modified_files.append(file)
    return cleaned_modified_files

def git_add_commit_and_push(files_to_add: Union[str, list], commit_message: str, branch: str, no_verify=False, ignored_files: list[str] =[]):
    """
    Adds, commits, and force-pushes specified files to the given branch.

    :param files_to_add: A string or list of file paths to add to the commit.
    :param commit_message: The message to include with the commit.
    :param branch: The target branch to push changes to.
    :param no_verify: A boolean indicating whether to skip pre-commit checks.
    """
    try:
        init_git_user()
        no_verify_arg = '--no-verify' if no_verify else ''
        run_command(f'git status')
        if isinstance(files_to_add, list):
            for file in files_to_add:
                run_command(f'git add {file}')
        elif isinstance(files_to_add, str):
            run_command(f'git add {files_to_add}')
        else:
            logger.error("Invalid type for 'files_to_add'. Must be a string or list.")
            return
        logger.info("git status after git add")
        modified_files = get_modified_files(ignored_files)
        if modified_files:
            run_command(f"git commit --only {files_to_add} {no_verify_arg} -m '{commit_message}'")
            run_command(f"git fetch origin {branch}", exit_on_error=False)
            run_command(f"git pull origin {branch}", exit_on_error=False)
            run_command(f"git push --force origin {branch}")
            logger.info_green(
                f"Files {files_to_add} successfully added, committed, and force-pushed to branch '{branch}'.")
        else:
            logger.info_green(f"No changes, nothing to commit.")
    except Exception as e:
        logger.error(f"Error in git_add_commit_and_push: {e}")
        exit_on_error_and_write_summary(f"Error in git_add_commit_and_push: {e}")


def tag_and_push(tags: Union[str, list], tagging_file: str = ''):
    try:
        logger.info(f"will tag and push: {tags}")
        if isinstance(tags, list):
            for t in tags:
                if tagging_file and os.path.exists(tagging_file) and os.path.isfile(tagging_file):
                    run_command(f'git tag {t} -F {tagging_file}')
                else:
                    run_command(f'git tag {t}')
            run_command(f'git push origin --tags')
        elif isinstance(tags, str):
            tag = tags
            if tagging_file and os.path.exists(tagging_file) and os.path.isfile(tagging_file):
                run_command(f'git tag {tag} -F {tagging_file}')
            else:
                run_command(f'git tag {tag}')
            run_command(f'git push origin {tag}')
        logger.info_green(f"tag {tags} created and pushed")
    except Exception as e:
        exit_on_error_and_write_summary(f"error in tag_and_push: {e}")


def get_commit_hash(short=True) -> str:
    try:
        if short:
            commit_hash = run_command('git rev-parse --short HEAD', print_output=False, print_command=False)
        else:
            commit_hash = run_command('git rev-parse HEAD', print_output=False, print_command=False)
        logger.info_green(f"got commit hash: {commit_hash}")
        return commit_hash.strip()
    except Exception as e:
        exit_on_error_and_write_summary(f"error in get commit hash: {e}")


def get_current_branch() -> str:
    try:
        current_branch = run_command("git branch --show-current",print_output=False, print_command=False)
        branch = current_branch.strip()
        logger.info(f"current branch is: {branch}")
        return branch
    except Exception as e:
        exit_on_error_and_write_summary(f"error in get_current_branch: {e}")


def check_if_default_branch() -> bool:
    try:
        current_branch = get_current_branch()
        if current_branch in DEFAULT_BRANCH_NAMES:
            logger.info_green(f"current branch is default branch: {current_branch}")
            return True
        logger.warning(f"current branch is not default branch: {current_branch}")
        return False
    except Exception as e:
        exit_on_error_and_write_summary(f"error in check_if_default_branch: {e}")


def get_dev_version(env_name, suffix=''):
    commit_sha = get_commit_hash()
    build_num = get_required_env_var("GITHUB_RUN_NUMBER")
    return f"{env_name}-{build_num}-{commit_sha}{suffix}"


def checkout_branch(git_repo_path: str, git_branch_name: str):
    """
    Checks out the specified branch.

    :param git_repo_path: The path to the repository.
    :param git_branch_name: The name of the branch to checkout.
    """
    if not os.path.exists(git_repo_path):
        raise_with_context(None, ValueError, f"repository path does not exist: {git_repo_path}")
    if not git_branch_name:
        raise_with_context(None, ValueError, "git_branch_name is not set")

    try:
        os.chdir(git_repo_path)
        logger.debug(f"checking out branch {git_branch_name}")
        run_command(f'git checkout {git_branch_name}')
        logger.info_green(f"checked out branch {git_branch_name}")
    except Exception as e:
        raise_with_context(e)


def create_branch(git_repo_path: str, git_branch_name_to_create: str):
    """
    Creates a new branch with the given name.

    :param branch_name: The name of the new branch.
    """
    if not os.path.exists(git_repo_path):
        raise_with_context(None, ValueError, f"repository path does not exist: {git_repo_path}")
    if not git_branch_name_to_create:
        raise_with_context(None, ValueError, "git_branch_name_to_create is not set")

    try:
        os.chdir(git_repo_path)
        logger.debug(f"getting current branch for repository in path: {git_repo_path}")
        current_branch = get_current_branch()
        logger.debug(f"creating a new branch {git_branch_name_to_create} from {current_branch}")
        run_command(f'git checkout -b {git_branch_name_to_create}')
        logger.info_green(f"branch {git_branch_name_to_create} created from {current_branch}")
    except Exception as e:
        raise_with_context(e)


def is_path_contain_valid_git_repo(git_repo_path: str, log_level_if_false: str = "error") -> bool:
    """
    Checks if the specified path contains a valid git repository.

    :param git_repo_path: The path to check.
    :param log_level_if_false: The log level to use if the path is not a valid git repository. When you expect the path to contain a git repo, set this to "error" or "warning". Otherwise, "debug" or "info" make more sense.
    :return: True if the path contains a valid git repository, False otherwise.
    """

    log_level_if_false = log_level_if_false.lower()
    if not os.path.exists(git_repo_path):
        log_message = f"repository path does not exist: {git_repo_path}"
        return_value = False
    elif not os.path.isdir(git_repo_path):
        log_message = f"repository path is not a directory: {git_repo_path}"
        return_value = False
    elif os.path.exists(os.path.join(git_repo_path, '.git')) and os.path.isdir(os.path.join(git_repo_path, '.git')):
        log_message = f"repository path is a valid git repository: {git_repo_path}"
        return_value = True
    else:
        log_message = f"repository path does not contain a valid git repository: {git_repo_path}"
        return_value = False

    if return_value or log_level_if_false == "info":
        logger.info(log_message)
    elif log_level_if_false == "critical":
        logger.critical(log_message)
    elif log_level_if_false == "error":
        logger.error(log_message)
    elif log_level_if_false == "warning" or log_level_if_false == "warn":
        logger.warning(log_message)
    else:
        logger.debug(log_message)

    return return_value


def clone_repository(git_repository_url: str, git_repo_path: str, branch: str = None):
    """
    Clones the specified repository to the given path.

    :param git_repository_url: The URL of the repository to clone.
    :param git_repo_path: The path to clone the repository to.
    :param branch: The branch to checkout after cloning. If None, the default branch will be used.
    """
    if not git_repository_url:
        raise_with_context(None, ValueError, "git_repository_url is not set")
    if not git_repo_path:
        raise_with_context(None, ValueError, "git_repo_path is not set")

    try:
        if is_path_contain_valid_git_repo(git_repo_path, log_level_if_false="debug"):
            logger.warning(f"repository path {git_repo_path} already exists contains a valid git repository")
            return
        logger.debug(f"Creating directory {git_repo_path} for git repository")
        os.makedirs(git_repo_path, exist_ok=True)
        os.chdir(git_repo_path)

        branch_switch = f"--branch {branch}" if branch else ""
        logger.debug(f"cloning repository {git_repository_url} to {git_repo_path}")
        run_command(f"git clone --depth 1 {branch_switch} {git_repository_url} .")
        logger.info_green(f"repository {git_repository_url} cloned to {git_repo_path}")
    except Exception as e:
        raise_with_context(e)

def clone_repository_ssh(full_repo_name: str, repo_path: str, branch: str = None):
    """
    Clones the specified repository to the given path using ssh.

    :param full_repo_name: The full name of the repository to clone, including the organization prefix.
    :param repo_path: The path to clone the repository to.
    :param branch: The branch to checkout after cloning. If None, the default branch will be used.
    """
    repo_url = f"git@github.com:{full_repo_name}.git"
    try:
        logger.debug(f"cloning repository {repo_url} to {repo_path} using ssh")
        clone_repository(repo_url, repo_path, branch)
    except Exception as e:
        raise_with_context(e)


def clone_repository_https(full_repo_name: str, repo_path: str, branch: str = None):
    """
    Clones the specified repository to the given path using https with a git token.

    :param full_repo_name: The full name of the repository to clone, including the organization prefix.
    :param repo_path: The path to clone the repository to.
    :param branch: The branch to checkout after cloning. If None, the default branch will be used.
    """
    git_user = 'sayuser'
    git_token = get_required_env_var("GLOBAL_CICD_GIT_TOKEN")
    repo_url = f"https://{git_user}:{git_token}@github.com/{full_repo_name}.git"
    try:
        logger.debug(f"cloning repository {repo_url} to {repo_path} using token from git user {git_user}")
        clone_repository(repo_url, repo_path, branch)
    except Exception as e:
        raise_with_context(e)
