"""
Contains code for installing and removing SWAI applications with the .swai format.
Also handles .swaib bundle files transparently.
"""

import os
import shutil
import glob
import yaml
import requests
import urllib.parse
import swai_inst.utils as utils
from swai_inst.exceptions import (
    SwaiFileError, SwaiParseError, SwaiDownloadError, SwaiInstallationError,
    SwaiDesktopFileError, SwaiValidationError, SwaiErrorCodes, create_swai_error_from_exception
)


def _is_bundle_source(source: str) -> bool:
    """
    Checks if a source (file path or URL) is a .swaib bundle.

    Args:
        source (str): The source path or URL.

    Returns:
        bool: True if the source is a .swaib bundle, False otherwise.
    """
    return source.lower().endswith('.swaib')

def _parse_swai_file(swai_file_path, verbose=False) -> dict:
    """
    Reads and parses the content of the .swai file as YAML.

    Args:
        swai_file_path (str): The path to the .swai file.
        verbose (bool, optional): Whether to print verbose output. Defaults to False.

    Returns:
        dict: The parsed YAML data.

    Raises:
        SwaiFileError: If the file cannot be read.
        SwaiParseError: If the file cannot be parsed as YAML.
    """
    try:
        with open(swai_file_path, 'r') as f:
            # Use safe_load to prevent potential security issues
            data = yaml.safe_load(f)
            if data is None:
                raise SwaiParseError(
                    f"SWAI file is empty: {swai_file_path}",
                    file_path=swai_file_path,
                    error_code=SwaiErrorCodes.INVALID_SWAI_FORMAT
                )
            return data
    except FileNotFoundError as e:
        raise SwaiFileError(
            f"SWAI file not found: {swai_file_path}",
            file_path=swai_file_path,
            error_code=SwaiErrorCodes.FILE_NOT_FOUND,
            original_exception=e
        )
    except yaml.YAMLError as e:
        raise SwaiParseError(
            f"Error parsing SWAI file '{swai_file_path}': {e}",
            file_path=swai_file_path,
            error_code=SwaiErrorCodes.YAML_PARSE_ERROR,
            original_exception=e
        )
    except Exception as e:
        raise create_swai_error_from_exception(
            e,
            f"Unexpected error reading SWAI file '{swai_file_path}'"
        )


def _download_icon(icon_url, icon_base_dir, app_id, verbose=False, swai_file_path=None) -> str | None:
    """
    Downloads the icon from the given URL to the specified directory.

    Args:
        icon_url (str): The URL of the icon.
        icon_base_dir (str): The base directory to save the icon.
        app_id (str): The application ID (used for filename).
        verbose (bool, optional): Whether to print verbose output. Defaults to False.
        swai_file_path (str, optional): The path to the swai file. Defaults to None.

    Returns:
        str: The full path to the downloaded icon file, or None if download fails or icon_url is None/empty.
        None: if download fails or icon_url is None/empty.
    """
    if not icon_url:
        return None

    if not (icon_url.startswith('http://') or icon_url.startswith('https://')):
        icon_path = utils.safe_expanduser(icon_url)
        if not os.path.isabs(icon_path):
            if swai_file_path:
                swai_dir = os.path.dirname(os.path.abspath(swai_file_path))
                icon_path = os.path.join(swai_dir, icon_path)
            else:
                icon_path = os.path.abspath(icon_path)
        
        if not os.path.isfile(icon_path):
            if verbose:
                print(f"Warning: Local icon file not found at '{icon_path}'. Skipping.")
            return None

        # Determine file extension from the path
        ext = os.path.splitext(icon_path)[1].lower()
        
        # Sanitize app_id for filename and ensure it's not empty
        if not app_id:
            app_id = "unknown_app"
            if verbose:
                print("Warning: 'app_id' not found in SWAI file, using 'unknown_app' for icon filename.")

        # Create a safe filename
        icon_filename = f"io.stillhq.swai.{app_id}{ext}"
        icon_file_path = os.path.join(icon_base_dir, icon_filename)

        # Ensure the directory exists
        os.makedirs(icon_base_dir, exist_ok=True)

        # Copy the icon content to the file
        try:
            shutil.copy2(icon_path, icon_file_path)
            if verbose:
                print(f"Successfully copied local icon from '{icon_path}' to '{icon_file_path}'")
            return icon_file_path
        except (IOError, OSError) as e:
            if verbose:
                print(f"Error copying local icon from '{icon_path}' to '{icon_file_path}': {e}")
            return None

    try:
        # Send a GET request to the icon URL
        response = requests.get(icon_url, stream=True, timeout=10)
        response.raise_for_status()  # Raise an HTTPError for bad responses (4xx or 5xx)

        # Determine file extension from the URL
        parsed_url = urllib.parse.urlparse(icon_url)
        path = parsed_url.path
        ext = os.path.splitext(path)[1].lower()
        
        if not ext:
            # If no extension in URL, try to guess from content type
            content_type = response.headers.get('Content-Type', '').lower()
            if 'png' in content_type:
                ext = '.png'
            elif 'jpeg' in content_type or 'jpg' in content_type:
                ext = '.jpg'
            elif 'svg' in content_type:
                ext = '.svg'
            else:
                if verbose:
                    print(f"Warning: Could not determine icon file extension from URL or Content-Type for '{icon_url}'. Skipping download.")
                return None

        # Sanitize app_id for filename and ensure it's not empty
        if not app_id:
            app_id = "unknown_app"
            if verbose:
                print("Warning: 'app_id' not found in SWAI file, using 'unknown_app' for icon filename.")

        # Create a safe filename
        icon_filename = f"io.stillhq.swai.{app_id}{ext}"
        icon_file_path = os.path.join(icon_base_dir, icon_filename)

        # Ensure the directory exists
        os.makedirs(icon_base_dir, exist_ok=True)

        # Save the icon content to the file
        with open(icon_file_path, 'wb') as f:
            for chunk in response.iter_content(chunk_size=8192):
                if chunk:  # Filter out keep-alive chunks
                    f.write(chunk)

        if verbose:
            print(f"Successfully downloaded icon from '{icon_url}' to '{icon_file_path}'")
            
        return icon_file_path

    except requests.exceptions.RequestException as e:
        if verbose:
            print(f"Error downloading icon from '{icon_url}': {e}")
        return None
    except (IOError, OSError) as e:
        if verbose:
            print(f"Error saving downloaded icon to '{icon_file_path}': {e}")
        return None


def _generate_desktop_content(app_name, icon_path, swai_file_path, app_id, categories=None, comment=None, keywords=None, local_install=False):
    """
    Generates the content for the .desktop file.

    Args:
        app_name (str): The name of the application.
        icon_path (str): The local path to the application icon file, or None.
        swai_file_path (str): The full path to the copied .swai file.
        app_id (str): The application ID.
        categories (list, optional): A list of application categories. Defaults to None.
        local_install (bool, optional): Whether this is a local installation. Defaults to False.

    Returns:
        str: The content of the .desktop file.
    """
    # Generate the StartupWMClass with the io.stillhq.swai prefix
    wm_class = f"io.stillhq.swai.{app_id}"
    
    content = f"""[Desktop Entry]
Version=1.0
Type=Application
Name={app_name}
Exec=/bin/swai "{swai_file_path}"
Terminal=false
StartupWMClass={wm_class}
"""
    if local_install:
        content += "Actions=uninstall;uninstall-data;\n"
    if icon_path:
        content += f"Icon={icon_path}\n" # Use the local icon path

    if categories:
        content += f"Categories={';'.join(categories)};\n"

    if comment:
        content += f"Comment={comment}\n"
        
    if keywords:
        content += f"Keywords={';'.join(keywords)};\n"

    if local_install:
        uninstall_cmd = f"swai-inst --local remove {app_id}"
        
        content += f"""
[Desktop Action uninstall]
Name=Uninstall
Exec={uninstall_cmd} --keep-data

[Desktop Action uninstall-data]
Name=Uninstall (with data)
Exec={uninstall_cmd}
"""

    return content


def _write_desktop_file(desktop_dir, app_id, desktop_content, verbose=False):
    """
    Writes the generated .desktop file content to the destination directory.

    Args:
        desktop_dir (str): The destination directory for the .desktop file.
        app_id (str): The application ID (used for the filename).
        desktop_content (str): The content to write to the .desktop file.
        verbose (bool, optional): Whether to print verbose output. Defaults to False.

    Returns:
        str: The full path to the created .desktop file.

    Raises:
        SwaiDesktopFileError: If the desktop file cannot be created.
    """
    if not app_id:
        app_id = "unknown_app"  # Fallback if app_id is missing
        if verbose:
            print("Warning: Using 'unknown_app' for desktop filename due to missing app_id")

    # Create a safe filename
    desktop_filename = f"io.stillhq.swai.{app_id}.desktop"
    desktop_file_path = os.path.join(desktop_dir, desktop_filename)

    try:
        # Ensure the directory exists
        os.makedirs(desktop_dir, exist_ok=True)
        
        # Write the desktop file
        with open(desktop_file_path, 'w', encoding='utf-8') as f:
            f.write(desktop_content)

        # Make the desktop file executable
        os.chmod(desktop_file_path, 0o755)
        
        if verbose:
            print(f"Successfully created desktop file at '{desktop_file_path}'")
            
        return desktop_file_path
        
    except (IOError, OSError) as e:
        raise SwaiDesktopFileError(
            f"Error writing desktop file '{desktop_file_path}': {e}",
            desktop_file_path=desktop_file_path,
            error_code=SwaiErrorCodes.DESKTOP_FILE_ERROR,
            original_exception=e
        )


def _install_requirements(requires, local_install=False, verbose=False):
    """
    Installs required dependencies for a SWAI application.

    Args:
        requires (list): List of requirement dicts, each containing 'app_id' and 'url'.
        local_install (bool, optional): Whether to install locally. Defaults to False.
        verbose (bool, optional): Whether to print verbose output. Defaults to False.

    Raises:
        SwaiInstallationError: If a required dependency cannot be installed.
    """
    if not requires:
        return

    installed_app_ids = utils.get_installed_app_ids(local_install)

    for req in requires:
        if not isinstance(req, dict):
            if verbose:
                print(f"Warning: Skipping invalid requirement entry: {req}")
            continue

        req_app_id = req.get('app_id')
        req_url = req.get('url')

        if not req_app_id or not req_url:
            if verbose:
                print(f"Warning: Requirement missing 'app_id' or 'url': {req}")
            continue

        # Check if the required app is already installed
        if req_app_id in installed_app_ids:
            if verbose:
                print(f"Requirement '{req_app_id}' is already installed.")
            continue

        # Install the required app from the URL
        if verbose:
            print(f"Installing required dependency '{req_app_id}' from '{req_url}'...")

        try:
            install_from_url(req_url, local_install=local_install, verbose=verbose)
            if verbose:
                print(f"Successfully installed requirement '{req_app_id}'.")
        except Exception as e:
            raise SwaiInstallationError(
                f"Failed to install required dependency '{req_app_id}' from '{req_url}': {e}",
                app_id=req_app_id,
                installation_step='requirement_installation',
                error_code=SwaiErrorCodes.INSTALLATION_FAILED,
                original_exception=e
            )


def install_from_str(swai_content, local_install=False, verbose=False, swai_file_path=None):
    """
    Installs a SWAI application from a string containing the .swai file content.

    Args:
        swai_content (str): The content of the .swai file.
        local_install (bool, optional): Whether to install the application locally (i.e. for the current user only). Defaults to False.
        verbose (bool, optional): Whether to print verbose output. Defaults to False.
        swai_file_path (str, optional): The path to the swai file. Defaults to None.
    
    Returns:
        tuple: (app_name, app_id) of the installed application
        
    Raises:
        SwaiParseError: If the SWAI content cannot be parsed.
        SwaiValidationError: If required fields are missing from the SWAI content.
        SwaiInstallationError: If there are issues during the installation process.
    
    The .swai file can optionally include a 'requires' field with a list of dependencies:
        requires:
            - app_id: com.example.dependency
              url: https://example.com/dependency.swai
            - app_id: com.example.another
              url: https://example.com/another.swai
    
    Each required app will be installed if not already present.
    """
    # Parse the YAML content
    try:
        swai_data = yaml.safe_load(swai_content)
    except yaml.YAMLError as e:
        raise SwaiParseError(
            f"Invalid YAML in SWAI content: {e}",
            error_code=SwaiErrorCodes.YAML_PARSE_ERROR,
            original_exception=e
        )

    # Extract app information
    try:
        app_name = swai_data['app_name']
        app_id = swai_data['app_id']
        icon_url = swai_data['icon_url']
        categories = swai_data['categories']

    except KeyError as e:
        raise SwaiValidationError(
            f"Missing required field in SWAI content: {e}",
            field_name=str(e),
            error_code=SwaiErrorCodes.MISSING_REQUIRED_FIELD,
            original_exception=e
        )

    try:
        utils.validate_path_component(app_id, 'app ID')
    except ValueError as e:
        raise SwaiValidationError(
            str(e),
            field_name='app_id',
            error_code=SwaiErrorCodes.INVALID_SWAI_FORMAT,
            original_exception=e
        )

    comment = swai_data.get('comment')
    keywords = swai_data.get('keywords')

    # Install requirements first (if any)
    requires = swai_data.get('requires', [])
    if requires:
        if verbose:
            print(f"Checking {len(requires)} requirement(s) for '{app_name}'...")
        _install_requirements(requires, local_install=local_install, verbose=verbose)

    # Get installation paths
    swai_dir, desktop_dir, icon_base_dir = utils.get_install_paths(local_install=local_install)
    
    # Create the SWAI directory if it doesn't exist
    os.makedirs(swai_dir, exist_ok=True)

    # Create a filename based on app_id
    swai_filename = f"{app_id}.swai"
    installed_swai_path = os.path.join(swai_dir, swai_filename)

    # Write the .swai file to the installation directory
    try:
        with open(installed_swai_path, 'w') as f:
            f.write(swai_content)
        if verbose:
            print(f"Installed SWAI file to {installed_swai_path}")
    except IOError as e:
        raise SwaiInstallationError(
            f"Error writing SWAI file to {installed_swai_path}: {e}",
            app_id=app_id,
            installation_step='swai_file_creation',
            error_code=SwaiErrorCodes.FILE_WRITE_ERROR,
            original_exception=e
        )

    # Download the icon if it exists
    downloaded_icon_path = None
    if icon_url:
        downloaded_icon_path = _download_icon(icon_url, icon_base_dir, app_id, verbose=verbose, swai_file_path=swai_file_path)
        if downloaded_icon_path is None and verbose:
            print(f"Warning: Failed to download icon from '{icon_url}'. The desktop entry may not display an icon.")

    # Generate the .desktop file content
    desktop_content = _generate_desktop_content(
        app_name,
        downloaded_icon_path,
        installed_swai_path,
        app_id,
        categories,
        comment=comment,
        keywords=keywords,
        local_install=local_install
    )

    try:
        # Write the desktop file using app_id for the filename
        desktop_file_path = _write_desktop_file(desktop_dir, app_id, desktop_content, verbose=verbose)
    except SwaiDesktopFileError as e:
        # Clean up the installed file if writing desktop file failed
        if os.path.exists(installed_swai_path):
            try:
                os.remove(installed_swai_path)
                if verbose:
                    print(f"Cleaned up installed file '{installed_swai_path}' due to desktop file writing error.")
            except IOError as cleanup_e:
                if verbose:
                    print(f"Error cleaning up installed file '{installed_swai_path}': {cleanup_e}")
        # Clean up downloaded icon if it exists
        if downloaded_icon_path and os.path.exists(downloaded_icon_path):
            try:
                os.remove(downloaded_icon_path)
                if verbose:
                    print(f"Cleaned up downloaded icon file '{downloaded_icon_path}' due to desktop file writing error.")
            except IOError as cleanup_e:
                if verbose:
                    print(f"Error cleaning up downloaded icon file '{downloaded_icon_path}': {cleanup_e}")
        raise SwaiInstallationError(
            f"Failed to write desktop file: {e}",
            app_id=app_id,
            installation_step='desktop_file_creation',
            error_code=SwaiErrorCodes.INSTALLATION_FAILED,
            original_exception=e
        )

    if verbose:
        print(f"Successfully installed '{app_name}' to '{desktop_file_path}'")
    
    return (app_name, app_id)


def install_from_file(file_path, local_install=False, verbose=False):
    """
    Installs a SWAI application from a local .swai file or a .swaib bundle.

    Args:
        file_path (str): The path to the .swai or .swaib file to install.
        local_install (bool, optional): Whether to install the application locally (i.e. for the current user only). Defaults to False.
        verbose (bool, optional): Whether to print verbose output. Defaults to False.
    
    Returns:
        tuple: For .swai files: (app_name, app_id)
               For .swaib bundles: (bundle_name, bundle_id, list of (app_name, app_id) tuples)
        
    Raises:
        SwaiFileError: If the file cannot be found or read.
        SwaiBundleError: If there are issues with a bundle installation.
    """
    # Check if this is a bundle file
    if _is_bundle_source(file_path):
        # Import bundle module here to avoid circular imports
        import swai_inst.bundle as bundle
        return bundle.install_bundle_from_file(file_path, local_install=local_install, verbose=verbose)
    
    # Check if the file exists
    if not os.path.isfile(file_path):
        raise SwaiFileError(
            f"SWAI file not found: {file_path}",
            file_path=file_path,
            error_code=SwaiErrorCodes.FILE_NOT_FOUND
        )

    # Read the .swai file
    try:
        with open(file_path, 'r') as f:
            swai_content = f.read()
    except IOError as e:
        raise SwaiFileError(
            f"Error reading SWAI file '{file_path}': {e}",
            file_path=file_path,
            error_code=SwaiErrorCodes.FILE_READ_ERROR,
            original_exception=e
        )
    
    # Install from the file content
    return install_from_str(swai_content, local_install=local_install, verbose=verbose, swai_file_path=file_path)


def install_from_url(url, local_install=False, verbose=False):
    """
    Installs a SWAI application or bundle from a URL.

    Args:
        url (str): The URL of the .swai or .swaib file to install.
        local_install (bool, optional): Whether to install the application locally (i.e. for the current user only). Defaults to False.
        verbose (bool, optional): Whether to print verbose output. Defaults to False.
    
    Returns:
        tuple: For .swai files: (app_name, app_id)
               For .swaib bundles: (bundle_name, bundle_id, list of (app_name, app_id) tuples)
        
    Raises:
        SwaiDownloadError: If there are issues downloading the file from the URL.
        SwaiBundleError: If there are issues with a bundle installation.
    """
    # Check if this is a bundle URL
    if _is_bundle_source(url):
        # Import bundle module here to avoid circular imports
        import swai_inst.bundle as bundle
        return bundle.install_bundle_from_url(url, local_install=local_install, verbose=verbose)
    
    # Install from the downloaded content
    return install_from_str(utils.download_swai_to_string(url), local_install=local_install, verbose=verbose)


def remove_from_file(swai_file_path, local_install=False, verbose=False, remove_data=True):
    """
    Removes a SWAI application from a .swai file or a .swaib bundle.

    Args:
        swai_file_path (str): The path to the .swai or .swaib file to remove.
        local_install (bool, optional): Whether to remove the application locally (i.e. for the current user only). Defaults to False.
        verbose (bool, optional): Whether to print verbose output. Defaults to False.
        remove_data (bool, optional): Whether to remove application data. Defaults to True.
    
    Returns:
        For .swai files: None
        For .swaib bundles: None (removes all apps in the bundle)
    """
    # Check if this is a bundle file
    if _is_bundle_source(swai_file_path):
        # Parse the bundle to get the bundle_id
        import swai_inst.bundle as bundle
        try:
            bundle_data = bundle._parse_swaib_file(swai_file_path, verbose=verbose)
            bundle_id = bundle_data.get('bundle_id')
            if bundle_id:
                bundle.remove_bundle(bundle_id, local_install=local_install, verbose=verbose, remove_data=remove_data)
                return
            else:
                raise SwaiFileError(
                    f"Bundle file '{swai_file_path}' does not contain a bundle_id",
                    file_path=swai_file_path,
                    error_code=SwaiErrorCodes.MISSING_REQUIRED_FIELD
                )
        except Exception as e:
            if isinstance(e, SwaiFileError):
                raise
            raise SwaiFileError(
                f"Error removing bundle from file '{swai_file_path}': {e}",
                file_path=swai_file_path,
                error_code=SwaiErrorCodes.FILE_READ_ERROR,
                original_exception=e
            )
    
    swai_dir, desktop_dir, icon_base_dir = utils.get_install_paths(local_install)
    
    # Parse the .swai file
    try:
        swai_data = _parse_swai_file(swai_file_path, verbose=verbose)
    except Exception as e:
        raise Exception(f"Error parsing SWAI file '{swai_file_path}': {e}")
    
    # Extract required information
    app_name = swai_data.get('app_name')
    app_id = swai_data.get('app_id')
    icon_url = swai_data.get('icon_url')
    categories = swai_data.get('categories')

    try:
        utils.validate_path_component(app_id, 'app ID')
    except ValueError as e:
        raise SwaiValidationError(
            str(e),
            field_name='app_id',
            error_code=SwaiErrorCodes.INVALID_SWAI_FORMAT,
            original_exception=e
        )
    
    # Remove the .swai file
    try:
        os.remove(swai_file_path)
    except Exception as e:
        if verbose:
            print(f"Warning: Failed to remove .swai file '{swai_file_path}': {e}")
        
        # If the SWAI file is not found, it's already removed, so continue
        # Otherwise, re-raise the exception
        if not isinstance(e, FileNotFoundError):
            raise

    # Remove the desktop file
    desktop_file_path = os.path.join(desktop_dir, f"io.stillhq.swai.{app_id}.desktop")
    try:
        os.remove(desktop_file_path)        
    except FileNotFoundError:
        # Desktop file not found, likely already removed or never created, ignore
        pass
    except Exception as e:
        if verbose:
            print(f"Warning: Failed to remove desktop file '{desktop_file_path}': {e}")
            # Re-raise other exceptions
            raise

    # Remove the icon if it exists
    if icon_url:
        try:
            # Determine file extension from the URL, similar to _download_icon
            parsed_url = urllib.parse.urlparse(icon_url)
            path = parsed_url.path
            ext = os.path.splitext(path)[1].lower()
            
            if not ext:
                 # If no extension in URL, try to guess from content type (requires downloading/checking headers - skipping for simplicity in removal)
                 # For removal, we'll just try a common extension like .png or .svg
                 # A more robust solution would involve storing the actual installed icon path in the .swai file.
                 # For now, we'll attempt removal with common extensions or rely on the io.stillhq.swai.app_id.* pattern.
                 # Let's try to glob for io.stillhq.swai.app_id.*
                 escaped_app_id = glob.escape(app_id)
                 icon_pattern = os.path.join(
                     icon_base_dir,
                     f"io.stillhq.swai.{escaped_app_id}.*"
                 )
                 matching_icons = glob.glob(icon_pattern)
                 for icon_to_remove in matching_icons:
                     try:
                         os.remove(icon_to_remove)
                         if verbose:
                             print(f"Successfully removed icon file '{icon_to_remove}'")
                     except Exception as e:
                         if verbose:
                             print(f"Warning: Failed to remove icon file '{icon_to_remove}': {e}")
            else:
                icon_path = os.path.join(icon_base_dir, f"io.stillhq.swai.{app_id}{ext}")
                os.remove(icon_path)
                if verbose:
                    print(f"Successfully removed icon file '{icon_path}'")
        except FileNotFoundError:
            # Icon file not found, likely already removed or never created, ignore
            pass
        except Exception as e:
            if verbose:
                print(f"Warning: Failed to remove icon file for app ID '{app_id}': {e}")
            # Re-raise other exceptions
            raise

    # Remove data directory if remove_data is True and it exists
    if remove_data:
        home_directories = (
            [os.path.expanduser("~")]
            if local_install else utils.get_user_home_directories()
        )

        for home_directory in home_directories:
            data_path = utils.get_app_data_path(app_id, home_directory)

            # A user-local copy still uses this data after the global copy is
            # removed, so preserve it during a system-wide uninstall.
            if not local_install and os.path.exists(
                utils.get_local_app_path(app_id, home_directory)
            ):
                if verbose:
                    print(
                        f"Skipping data removal for '{app_id}' in "
                        f"'{home_directory}' as a local installation exists."
                    )
                continue

            if os.path.isdir(data_path) and os.access(
                data_path, os.W_OK | os.X_OK
            ):
                try:
                    shutil.rmtree(data_path)
                    if verbose:
                        print(
                            f"Successfully removed data directory "
                            f"'{data_path}' for app '{app_id}'."
                        )
                except Exception as e:
                    if verbose:
                        print(
                            f"Warning: Failed to remove data directory "
                            f"'{data_path}' for app '{app_id}': {e}"
                        )
                    raise
            elif verbose:
                if not os.path.isdir(data_path):
                    print(
                        f"No data directory found at '{data_path}' for app "
                        f"'{app_id}'."
                    )
                else:
                    print(
                        f"Data directory '{data_path}' for app '{app_id}' "
                        "is not writable or executable by the current user. "
                        "Skipping removal."
                    )

    if verbose and app_name:
        print(f"Successfully removed '{app_name}' (ID: {app_id}).")
    elif verbose:
        print(f"Successfully removed app with ID: {app_id}.")


def _find_apps_requiring(app_id, local_install=False, verbose=False):
    """
    Finds all installed apps that require the given app_id.

    Args:
        app_id (str): The app_id to search for in requirements.
        local_install (bool, optional): Whether to use local paths. Defaults to False.
        verbose (bool, optional): Whether to print verbose output. Defaults to False.

    Returns:
        list: List of app_ids that require the given app_id.
    """
    dependent_apps = []

    installed_app_ids = utils.get_installed_app_ids(local_install)

    for installed_app_id in installed_app_ids:
        if installed_app_id == app_id:
            continue  # Skip the app itself

        swai_file_path = utils.get_app_manifest_path(
            installed_app_id, local_install
        )

        try:
            swai_data = _parse_swai_file(swai_file_path, verbose=False)
            requires = swai_data.get('requires', [])

            for req in requires:
                if isinstance(req, dict) and req.get('app_id') == app_id:
                    dependent_apps.append(installed_app_id)
                    break
        except Exception:
            # Skip apps that can't be parsed
            continue

    return dependent_apps


def remove_from_app_id(app_id, local_install=False, verbose=False, remove_data=True, remove_dependents=True):
    """
    Removes a SWAI application by its app_id.
    
    Also removes any apps that depend on this app (have it in their 'requires' list).

    Args:
        app_id (str): The app_id of the application to remove.
        local_install (bool, optional): Whether to remove the application locally (i.e. for the current user only). Defaults to False.
        verbose (bool, optional): Whether to print verbose output. Defaults to False.
        remove_data (bool, optional): Whether to remove application data. Defaults to True.
        remove_dependents (bool, optional): Whether to also remove apps that depend on this app. Defaults to True.
    """
    utils.validate_path_component(app_id, 'app ID')

    # First, find and remove any apps that depend on this app
    if remove_dependents:
        dependent_apps = _find_apps_requiring(app_id, local_install=local_install, verbose=verbose)
        
        if dependent_apps and verbose:
            print(f"Found {len(dependent_apps)} app(s) that require '{app_id}': {', '.join(dependent_apps)}")
        
        for dependent_app_id in dependent_apps:
            if verbose:
                print(f"Removing dependent app '{dependent_app_id}'...")
            try:
                # Recursively remove dependent apps (which will also remove their dependents)
                remove_from_app_id(
                    dependent_app_id,
                    local_install=local_install,
                    verbose=verbose,
                    remove_data=remove_data,
                    remove_dependents=True
                )
            except Exception as e:
                if verbose:
                    print(f"Warning: Failed to remove dependent app '{dependent_app_id}': {e}")
    
    # Get the .swai file path
    swai_file_path = utils.get_app_manifest_path(app_id, local_install)
    
    # Remove the .swai file
    remove_from_file(swai_file_path, local_install=local_install, verbose=verbose, remove_data=remove_data)


def remove_bundle_from_id(bundle_id, local_install=False, verbose=False, remove_data=True):
    """
    Removes a SWAI bundle and all its apps by bundle_id.

    Args:
        bundle_id (str): The bundle_id of the bundle to remove.
        local_install (bool, optional): Whether to remove locally. Defaults to False.
        verbose (bool, optional): Whether to print verbose output. Defaults to False.
        remove_data (bool, optional): Whether to remove app data. Defaults to True.
    """
    # Import bundle module here to avoid circular imports
    import swai_inst.bundle as bundle
    bundle.remove_bundle(bundle_id, local_install=local_install, verbose=verbose, remove_data=remove_data)
