from math import e
import os
import pwd
import shutil
import urllib

import requests
import yaml

from typing import List, Tuple, Optional
from swai_inst.exceptions import SwaiDownloadError, SwaiErrorCodes

_share_directory = None


def set_share_directory(share_directory: Optional[str]) -> None:
    """Override the share root used by all installer operations."""
    global _share_directory
    _share_directory = (
        os.path.abspath(safe_expanduser(share_directory))
        if share_directory else None
    )


def safe_expanduser(path: str) -> str:
    """
    Safely expands '~' in paths, respecting the original user when running under sudo.
    """
    if path == '~' or path.startswith('~/'):
        sudo_user = os.environ.get('SUDO_USER')
        if sudo_user:
            return os.path.expanduser(f"~{sudo_user}" + path[1:])
    return os.path.expanduser(path)


def get_install_paths(local_install=False) -> Tuple[str, str, str]:
    """
    Returns the paths for the SWAI directory, desktop directory, and icon base directory.

    Args:
        local_install (bool, optional): Whether to use user-local installation paths (i.e. for the current user only). Defaults to False.

    Returns:
        tuple: A tuple containing the paths for the SWAI directory, desktop directory, and icon base directory.
    """
    if _share_directory:
        share_dir = _share_directory
    elif local_install:
        share_dir = os.path.join(safe_expanduser('~'), '.local', 'share')
    else:
        share_dir = '/usr/local/share'

    swai_dir = os.path.join(share_dir, 'swai', 'apps')
    desktop_dir = os.path.join(share_dir, 'applications')
    icon_base_dir = os.path.join(
        share_dir, 'icons', 'hicolor', 'scalable', 'apps'
    )

    try:
        # Ensure the icon base directories exists
        os.makedirs(icon_base_dir, exist_ok=True)
        os.makedirs(desktop_dir, exist_ok=True)
        os.makedirs(swai_dir, exist_ok=True)
    except PermissionError:
        pass

    return swai_dir, desktop_dir, icon_base_dir


def validate_path_component(value: str, field_name: str = 'identifier') -> str:
    """Validate a value before using it as a single filename component."""
    if (
        not isinstance(value, str)
        or not value
        or value in ('.', '..')
        or os.path.basename(value) != value
        or '\0' in value
    ):
        raise ValueError(f"Invalid {field_name}: {value!r}")
    return value


def get_app_manifest_path(app_id: str, local_install: bool = False) -> str:
    """Return an installed app manifest path."""
    validate_path_component(app_id, 'app ID')
    swai_dir, _, _ = get_install_paths(local_install)
    return os.path.join(swai_dir, f'{app_id}.swai')


def get_local_swai_directory(home_directory: Optional[str] = None) -> str:
    """Return the per-user SWAI data directory for a home directory."""
    home_directory = home_directory or os.path.expanduser('~')
    return os.path.join(home_directory, '.local', 'share', 'swai')


def get_app_data_path(app_id: str, home_directory: Optional[str] = None) -> str:
    """Return an app's per-user data path.

    App data is always user-specific, even when the app itself is installed
    system-wide or under a custom share directory.
    """
    validate_path_component(app_id, 'app ID')
    return os.path.join(
        get_local_swai_directory(home_directory), 'app_data', app_id
    )


def get_local_app_path(app_id: str, home_directory: Optional[str] = None) -> str:
    """Return the path of a user-local app manifest."""
    validate_path_component(app_id, 'app ID')
    return os.path.join(
        get_local_swai_directory(home_directory), 'apps', f'{app_id}.swai'
    )


def get_user_home_directories() -> List[str]:
    """Return unique home directories for regular local users.

    The account database is used instead of assuming homes live below
    ``/home``; immutable systems commonly expose them below ``/var/home``.
    """
    home_directories = []
    seen = set()

    for account in pwd.getpwall():
        if account.pw_uid < 1000 or account.pw_uid == 65534:
            continue

        home_directory = account.pw_dir
        if not home_directory or not os.path.isabs(home_directory):
            continue

        normalized_home = os.path.normpath(home_directory)
        if normalized_home not in seen:
            seen.add(normalized_home)
            home_directories.append(normalized_home)

    return home_directories


def get_installed_app_ids(local_install=False) -> List[str]:
    """
    Returns a list of installed app IDs.

    Args:
        local_install (bool, optional): Whether to use user-local installation paths (i.e. for the current user only). Defaults to False.

    Returns:
        list: A list of installed app IDs.
    """
    swai_dir, _, _ = get_install_paths(local_install)
    app_ids = []
    
    try:
        for file_name in os.listdir(swai_dir):
            if file_name.endswith('.swai'):
                app_id = file_name[:-5]  # Remove the '.swai' extension
                app_ids.append(app_id)
    except FileNotFoundError:
        pass
    
    return app_ids


def get_installed_file_paths(app_id, local_install=False) -> List[str]:
    """Returns a list of file paths for the given app ID."""
    
    swai_dir, _, _ = get_install_paths(local_install)
    
    file_paths = []
    
    try:
        for file_name in os.listdir(swai_dir):
            if file_name.startswith(app_id):
                file_paths.append(os.path.join(swai_dir, file_name))
    except FileNotFoundError:
        pass
    
    return file_paths


def get_bundles_dir(local_install=False) -> str:
    """
    Returns the path to the bundles directory.

    Args:
        local_install (bool, optional): Whether to use user-local installation paths. Defaults to False.

    Returns:
        str: The path to the bundles directory.
    """
    swai_dir, _, _ = get_install_paths(local_install)
    bundles_dir = os.path.join(os.path.dirname(swai_dir), 'bundles')
    
    try:
        os.makedirs(bundles_dir, exist_ok=True)
    except PermissionError:
        pass
    
    return bundles_dir


def get_bundle_path(bundle_id: str, local_install: bool = False) -> str:
    """Return an installed bundle metadata path."""
    validate_path_component(bundle_id, 'bundle ID')
    return os.path.join(get_bundles_dir(local_install), f'{bundle_id}.swaib')


def get_installed_bundle_ids(local_install=False) -> List[str]:
    """
    Returns a list of installed bundle IDs.

    Args:
        local_install (bool, optional): Whether to use user-local installation paths. Defaults to False.

    Returns:
        List[str]: A list of installed bundle IDs.
    """
    bundles_dir = get_bundles_dir(local_install)
    bundle_ids = []
    
    try:
        for file_name in os.listdir(bundles_dir):
            if file_name.endswith('.swaib'):
                bundle_id = file_name[:-6]  # Remove the '.swaib' extension
                bundle_ids.append(bundle_id)
    except FileNotFoundError:
        pass
    
    return bundle_ids


def is_bundle_file(file_path: str) -> bool:
    """
    Checks if a file path points to a .swaib bundle file.

    Args:
        file_path (str): The file path to check.

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


def is_swai_file(file_path: str) -> bool:
    """
    Checks if a file path points to a .swai file.

    Args:
        file_path (str): The file path to check.

    Returns:
        bool: True if the file is a .swai file, False otherwise.
    """
    return file_path.lower().endswith('.swai')
    

def download_swai_to_string(url) -> str:
    """
    Downloads the .swai file from the given URL and returns its content as a string.

    Args:
        url (str): The URL of the .swai file.

    Returns:
        str: The content of the .swai file.
        
    Raises:
        SwaiDownloadError: If there are issues downloading the file.
    """
    try:
        response = requests.get(url)
        response.raise_for_status()
        return response.text
    except requests.exceptions.RequestException as e:
        raise SwaiDownloadError(
            f"Error downloading .swai file from '{url}': {e}",
            url=url,
            error_code=SwaiErrorCodes.DOWNLOAD_FAILED,
            original_exception=e
        )
