import os
from typing import Optional

from tach import filesystem as fs
from tach import errors
from tach.colors import BCOLORS
from tach.constants import CONFIG_FILE_NAME
from tach.core import ProjectConfig
from tach.check import check
from tach.parsing import parse_project_config, dump_project_config_to_yaml


def sync_dependency_constraints(
    root: str,
    project_config: ProjectConfig,
    filter_tags: Optional[set[str]] = None,
    exclude_paths: Optional[list[str]] = None,
) -> ProjectConfig:
    """
    Update project configuration with auto-detected dependency constraints.
    This is additive, meaning it will create dependencies to resolve existing errors,
    but will not remove any constraints.
    Passing 'filter_tags' will limit updates to only those dependencies which include one of those tags.
    """
    check_errors = check(
        root, project_config=project_config, exclude_paths=exclude_paths
    )
    for error in check_errors:
        error_info = error.error_info
        if error_info.is_tag_error:
            if not filter_tags:
                project_config.add_dependencies_to_tags(
                    error_info.source_tags, error_info.invalid_tags
                )
            else:
                source_tags = set(error_info.source_tags)
                invalid_tags = set(error_info.invalid_tags)
                if source_tags & filter_tags:
                    # A package with one of the added tags caused this error and should update its dependencies
                    project_config.add_dependencies_to_tags(
                        error_info.source_tags, error_info.invalid_tags
                    )
                if invalid_tags & filter_tags:
                    # A package now depends on one of the added tags and should add the newly added tags
                    # Note that we should leave pre-existing invalid tags
                    project_config.add_dependencies_to_tags(
                        error_info.source_tags, list(invalid_tags & filter_tags)
                    )

    return project_config


def prune_dependency_constraints(
    root: str,
    project_config: Optional[ProjectConfig] = None,
    exclude_paths: Optional[list[str]] = None,
) -> ProjectConfig:
    """
    Build a minimal project configuration with auto-detected dependency constraints.
    """
    if project_config is not None:
        # Force constraints to be empty in case we received configuration with pre-existing constraints
        project_config = project_config.model_copy(update={"constraints": []})
    else:
        project_config = ProjectConfig()

    sync_dependency_constraints(
        root, project_config=project_config, exclude_paths=exclude_paths
    )

    return project_config


def sync_project(
    prune: bool = False, exclude_paths: Optional[list[str]] = None
) -> None:
    original_cwd = fs.get_cwd()
    try:
        root = fs.find_project_config_root(original_cwd)
        if root is None:
            raise errors.TachSetupError(
                f"{BCOLORS.WARNING}Could not find project configuration.{BCOLORS.ENDC}"
            )
        fs.chdir(root)
        project_config = parse_project_config(root=root)
        if exclude_paths is not None and project_config.exclude is not None:
            exclude_paths.extend(project_config.exclude)
        else:
            exclude_paths = project_config.exclude

        if prune:
            project_config = prune_dependency_constraints(
                root, project_config=project_config, exclude_paths=exclude_paths
            )
        else:
            project_config = sync_dependency_constraints(
                root, project_config=project_config, exclude_paths=exclude_paths
            )

        tach_yml_path = os.path.join(root, f"{CONFIG_FILE_NAME}.yml")
        tach_yml_content = dump_project_config_to_yaml(project_config)
        fs.write_file(tach_yml_path, tach_yml_content)

    finally:
        fs.chdir(original_cwd)
