"""
Contains code for managing SWAI bundles with the .swaib format.

A .swaib file is a bundle of multiple SWAI apps with a single update URL.
Apps within the bundle can either define their full SWAI configuration inline, 
or reference a remote .swai file via a 'url' key.

Format:
    bundle_id: <unique bundle identifier>
    bundle_name: <display name>
    bundle_version: <version string for update checking>
    bundle_icon: <optional icon URL>
    update_url: <URL for bundle updates>
    pricing: <optional pricing tier integer>
    apps:
        filename1.swai:
            app_id: <app identifier>
            url: <URL to remote .swai file>
        filename2.swai:
            app_id: <app identifier>
            app_name: <display name>
            icon_url: <icon URL>
            categories: [list, of, categories]
            swai_file_version: <version>
            <other inline swai fields...>

Example:
    bundle_id: com.microsoft.365.bundle
    bundle_name: Microsoft 365
    bundle_version: 2026.3.25
    update_url: https://gitlab.com/stillhq/stillOS/saDB-repo/-/raw/main/swai_apps/microsoft-365.swaib
    pricing: 6
    apps:
        com-microsoft-365.swai:
            app_id: com.microsoft.365
            url: https://gitlab.com/stillhq/stillOS/saDB-repo/-/raw/main/swai_apps/com-microsoft-365.swai
        com-microsoft-word.swai:
            app_id: com.microsoft.word
            url: https://gitlab.com/stillhq/stillOS/saDB-repo/-/raw/main/swai_apps/com-microsoft-word.swai
"""

import os
import yaml
from typing import Dict, List, Tuple, Optional, Any

import swai_inst.utils as utils
import swai_inst.installer as installer
from swai_inst.exceptions import (
    SwaiBundleError, SwaiParseError, SwaiFileError,
    SwaiErrorCodes, SwaiValidationError
)


def _parse_swaib_file(swaib_file_path: str, verbose: bool = False) -> dict:
    """
    Reads and parses the content of the .swaib bundle file as YAML.

    Args:
        swaib_file_path (str): The path to the .swaib 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.
        SwaiBundleError: If the file cannot be parsed as YAML or is invalid.
    """
    try:
        with open(swaib_file_path, 'r') as f:
            data = yaml.safe_load(f)
            if data is None:
                raise SwaiBundleError(
                    f"SWAIB bundle file is empty: {swaib_file_path}",
                    error_code=SwaiErrorCodes.BUNDLE_INVALID_FORMAT
                )
            return data
    except FileNotFoundError as e:
        raise SwaiFileError(
            f"SWAIB bundle file not found: {swaib_file_path}",
            file_path=swaib_file_path,
            error_code=SwaiErrorCodes.BUNDLE_NOT_FOUND,
            original_exception=e
        )
    except yaml.YAMLError as e:
        raise SwaiBundleError(
            f"Error parsing SWAIB bundle file '{swaib_file_path}': {e}",
            error_code=SwaiErrorCodes.BUNDLE_PARSE_ERROR,
            original_exception=e
        )


def _parse_swaib_content(swaib_content: str, verbose: bool = False) -> dict:
    """
    Parses the content of a .swaib bundle from a string.

    Args:
        swaib_content (str): The content of the .swaib file.
        verbose (bool, optional): Whether to print verbose output. Defaults to False.

    Returns:
        dict: The parsed YAML data.

    Raises:
        SwaiBundleError: If the content cannot be parsed as YAML or is invalid.
    """
    try:
        data = yaml.safe_load(swaib_content)
        if data is None:
            raise SwaiBundleError(
                "SWAIB bundle content is empty",
                error_code=SwaiErrorCodes.BUNDLE_INVALID_FORMAT
            )
        return data
    except yaml.YAMLError as e:
        raise SwaiBundleError(
            f"Error parsing SWAIB bundle content: {e}",
            error_code=SwaiErrorCodes.BUNDLE_PARSE_ERROR,
            original_exception=e
        )


def _validate_bundle_data(bundle_data: dict) -> Tuple[str, str, Optional[str], Optional[str], Dict[str, dict]]:
    """
    Validates bundle data and extracts required fields.

    Args:
        bundle_data (dict): The parsed bundle data.

    Returns:
        Tuple containing (bundle_id, bundle_name, bundle_icon, update_url, apps)

    Raises:
        SwaiValidationError: If required fields are missing.
    """
    try:
        bundle_id = bundle_data['bundle_id']
        bundle_name = bundle_data['bundle_name']
    except KeyError as e:
        raise SwaiValidationError(
            f"Missing required field in SWAIB bundle: {e}",
            field_name=str(e),
            error_code=SwaiErrorCodes.MISSING_REQUIRED_FIELD,
            original_exception=e
        )

    try:
        utils.validate_path_component(bundle_id, 'bundle ID')
    except ValueError as e:
        raise SwaiValidationError(
            str(e),
            field_name='bundle_id',
            error_code=SwaiErrorCodes.BUNDLE_INVALID_FORMAT,
            original_exception=e
        )

    bundle_icon = bundle_data.get('bundle_icon')
    update_url = bundle_data.get('update_url')
    apps = bundle_data.get('apps', {})

    if not apps:
        raise SwaiValidationError(
            "SWAIB bundle must contain at least one app in 'apps' section",
            field_name='apps',
            error_code=SwaiErrorCodes.MISSING_REQUIRED_FIELD
        )

    return bundle_id, bundle_name, bundle_icon, update_url, apps


def _extract_app_ids_from_bundle(bundle_data: dict) -> List[str]:
    """
    Extracts all app IDs from a bundle.

    Args:
        bundle_data (dict): The parsed bundle data.

    Returns:
        List[str]: List of app IDs in the bundle.
    """
    app_ids = []
    apps = bundle_data.get('apps', {})
    
    for filename, app_data in apps.items():
        if isinstance(app_data, dict):
            app_id = app_data.get('app_id')
            if app_id:
                app_ids.append(app_id)
    
    return app_ids


def _generate_swai_content(app_data: dict, bundle_update_url: Optional[str] = None) -> str:
    """
    Generates SWAI file content from app data within a bundle.
    
    If the app doesn't have its own update_url, it uses the bundle's update_url.

    Args:
        app_data (dict): The app data from the bundle.
        bundle_update_url (Optional[str]): The bundle's update URL to use as fallback.

    Returns:
        str: The YAML content for the SWAI file.
    """
    # Create a copy to avoid modifying the original
    swai_data = dict(app_data)
    
    # If app doesn't have its own update_url, don't add bundle's URL
    # Individual apps in bundles are updated via the bundle, not individually
    # But we track which bundle they belong to
    
    return yaml.dump(swai_data, default_flow_style=False, allow_unicode=True)


def install_bundle_from_str(swaib_content: str, local_install: bool = False, verbose: bool = False) -> Tuple[str, str, List[Tuple[str, str]]]:
    """
    Installs a SWAI bundle from a string containing the .swaib file content.

    Args:
        swaib_content (str): The content of the .swaib file.
        local_install (bool, optional): Whether to install locally. Defaults to False.
        verbose (bool, optional): Whether to print verbose output. Defaults to False.

    Returns:
        Tuple[str, str, List[Tuple[str, str]]]: (bundle_name, bundle_id, list of (app_name, app_id) tuples)

    Raises:
        SwaiBundleError: If there are issues during bundle installation.
    """
    bundle_data = _parse_swaib_content(swaib_content, verbose=verbose)
    bundle_id, bundle_name, bundle_icon, update_url, apps = _validate_bundle_data(bundle_data)

    if verbose:
        print(f"Installing bundle '{bundle_name}' (ID: {bundle_id}) with {len(apps)} app(s)")

    installed_apps = []
    failed_apps = []

    for filename, app_data in apps.items():
        if not isinstance(app_data, dict):
            if verbose:
                print(f"Warning: Skipping invalid app entry '{filename}'")
            continue

        try:
            # If the app entry only references a remote .swai via 'url', download that file
            app_url = app_data.get('url')
            required_inline_fields = {'app_id', 'app_name', 'icon_url', 'categories'}
            has_inline_data = required_inline_fields.issubset(app_data.keys())

            if app_url and not has_inline_data:
                if verbose:
                    print(f"  Downloading app definition from '{app_url}'...")
                swai_content = utils.download_swai_to_string(app_url)
            else:
                # Full app data is inlined in the bundle, generate from it
                swai_content = _generate_swai_content(app_data, update_url)
            
            # Install the individual app
            app_name, app_id = installer.install_from_str(
                swai_content,
                local_install=local_install,
                verbose=verbose
            )
            installed_apps.append((app_name, app_id))
            
            if verbose:
                print(f"  Installed app '{app_name}' (ID: {app_id})")
                
        except Exception as e:
            app_id = app_data.get('app_id', filename)
            failed_apps.append((app_id, str(e)))
            if verbose:
                print(f"  Failed to install app '{app_id}': {e}")

    # Save the bundle metadata file
    _save_bundle_metadata(
        bundle_id, bundle_name, bundle_icon, update_url,
        swaib_content, [app_id for _, app_id in installed_apps],
        local_install, verbose
    )

    if failed_apps:
        failed_list = ", ".join([f"{app_id}: {err}" for app_id, err in failed_apps])
        raise SwaiBundleError(
            f"Bundle '{bundle_name}' partially installed. Failed apps: {failed_list}",
            bundle_id=bundle_id,
            error_code=SwaiErrorCodes.BUNDLE_APP_ERROR
        )

    if verbose:
        print(f"Successfully installed bundle '{bundle_name}' with {len(installed_apps)} app(s)")

    return bundle_name, bundle_id, installed_apps


def install_bundle_from_file(swaib_file_path: str, local_install: bool = False, verbose: bool = False) -> Tuple[str, str, List[Tuple[str, str]]]:
    """
    Installs a SWAI bundle from a local .swaib file.

    Args:
        swaib_file_path (str): The path to the .swaib file.
        local_install (bool, optional): Whether to install locally. Defaults to False.
        verbose (bool, optional): Whether to print verbose output. Defaults to False.

    Returns:
        Tuple[str, str, List[Tuple[str, str]]]: (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 during bundle installation.
    """
    if not os.path.isfile(swaib_file_path):
        raise SwaiFileError(
            f"SWAIB bundle file not found: {swaib_file_path}",
            file_path=swaib_file_path,
            error_code=SwaiErrorCodes.FILE_NOT_FOUND
        )

    try:
        with open(swaib_file_path, 'r') as f:
            swaib_content = f.read()
    except IOError as e:
        raise SwaiFileError(
            f"Error reading SWAIB bundle file '{swaib_file_path}': {e}",
            file_path=swaib_file_path,
            error_code=SwaiErrorCodes.FILE_READ_ERROR,
            original_exception=e
        )

    return install_bundle_from_str(swaib_content, local_install=local_install, verbose=verbose)


def install_bundle_from_url(url: str, local_install: bool = False, verbose: bool = False) -> Tuple[str, str, List[Tuple[str, str]]]:
    """
    Installs a SWAI bundle from a URL.

    Args:
        url (str): The URL of the .swaib file.
        local_install (bool, optional): Whether to install locally. Defaults to False.
        verbose (bool, optional): Whether to print verbose output. Defaults to False.

    Returns:
        Tuple[str, str, List[Tuple[str, str]]]: (bundle_name, bundle_id, list of (app_name, app_id) tuples)

    Raises:
        SwaiDownloadError: If there are issues downloading the file.
        SwaiBundleError: If there are issues during bundle installation.
    """
    swaib_content = utils.download_swai_to_string(url)
    return install_bundle_from_str(swaib_content, local_install=local_install, verbose=verbose)


def _save_bundle_metadata(
    bundle_id: str,
    bundle_name: str,
    bundle_icon: Optional[str],
    update_url: Optional[str],
    swaib_content: str,
    app_ids: List[str],
    local_install: bool = False,
    verbose: bool = False
):
    """
    Saves bundle metadata to track installed bundles.

    Args:
        bundle_id (str): The bundle ID.
        bundle_name (str): The bundle name.
        bundle_icon (Optional[str]): The bundle icon URL.
        update_url (Optional[str]): The bundle update URL.
        swaib_content (str): The original bundle content.
        app_ids (List[str]): List of installed app IDs.
        local_install (bool, optional): Whether to use local paths. Defaults to False.
        verbose (bool, optional): Whether to print verbose output. Defaults to False.
    """
    bundle_file_path = utils.get_bundle_path(bundle_id, local_install)

    try:
        with open(bundle_file_path, 'w') as f:
            f.write(swaib_content)
        if verbose:
            print(f"Saved bundle metadata to '{bundle_file_path}'")
    except IOError as e:
        if verbose:
            print(f"Warning: Failed to save bundle metadata: {e}")


def _load_bundle_metadata(bundle_id: str, local_install: bool = False) -> Optional[dict]:
    """
    Loads bundle metadata for a given bundle ID.

    Args:
        bundle_id (str): The bundle ID.
        local_install (bool, optional): Whether to use local paths. Defaults to False.

    Returns:
        Optional[dict]: The bundle data, or None if not found.
    """
    bundle_file_path = utils.get_bundle_path(bundle_id, local_install)

    if not os.path.exists(bundle_file_path):
        return None

    try:
        return _parse_swaib_file(bundle_file_path)
    except Exception:
        return None


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

    Args:
        bundle_id (str): The bundle ID to remove.
        local_install (bool, optional): Whether to use local paths. 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.

    Raises:
        SwaiBundleError: If the bundle cannot be found or removed.
    """
    bundle_file_path = utils.get_bundle_path(bundle_id, local_install)

    if not os.path.exists(bundle_file_path):
        raise SwaiBundleError(
            f"Bundle '{bundle_id}' not found",
            bundle_id=bundle_id,
            error_code=SwaiErrorCodes.BUNDLE_NOT_FOUND
        )

    bundle_data = _parse_swaib_file(bundle_file_path, verbose=verbose)
    bundle_name = bundle_data.get('bundle_name', bundle_id)
    apps = bundle_data.get('apps', {})

    if verbose:
        print(f"Removing bundle '{bundle_name}' (ID: {bundle_id}) with {len(apps)} app(s)")

    # Remove each app in the bundle
    for filename, app_data in apps.items():
        if not isinstance(app_data, dict):
            continue

        app_id = app_data.get('app_id')
        if not app_id:
            continue

        try:
            installer.remove_from_app_id(
                app_id,
                local_install=local_install,
                verbose=verbose,
                remove_data=remove_data
            )
            if verbose:
                print(f"  Removed app '{app_id}'")
        except Exception as e:
            if verbose:
                print(f"  Warning: Failed to remove app '{app_id}': {e}")

    # Remove the bundle metadata file
    try:
        os.remove(bundle_file_path)
        if verbose:
            print(f"Removed bundle metadata file '{bundle_file_path}'")
    except Exception as e:
        if verbose:
            print(f"Warning: Failed to remove bundle metadata file: {e}")

    if verbose:
        print(f"Successfully removed bundle '{bundle_name}'")


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

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

    Returns:
        List[str]: List of installed bundle IDs.
    """
    bundles_dir = utils.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 check_bundle_for_update(bundle_id: str, local_install: bool = False, verbose: bool = False) -> Tuple[bool, Optional[str], Optional[str], Optional[str]]:
    """
    Checks if a bundle has an update available.

    Args:
        bundle_id (str): The bundle ID to check.
        local_install (bool, optional): Whether to use local paths. Defaults to False.
        verbose (bool, optional): Whether to print verbose output. Defaults to False.

    Returns:
        Tuple[bool, Optional[str], Optional[str], Optional[str]]:
            - bool: True if update is available, False otherwise
            - Optional[str]: Current version (None if not found)
            - Optional[str]: Latest version (None if not found/error)
            - Optional[str]: Latest SWAIB content (None if no update or error)
    """
    bundle_path = utils.get_bundle_path(bundle_id, local_install)

    try:
        bundle_data = _parse_swaib_file(bundle_path, verbose=verbose)
    except Exception as e:
        if verbose:
            print(f"Error parsing bundle file for '{bundle_id}': {e}")
        return False, None, None, None

    update_url = bundle_data.get('update_url')
    current_version = bundle_data.get('bundle_version')

    if not update_url:
        return False, current_version, None, None

    if not current_version:
        if verbose:
            print(f"No version found for bundle '{bundle_id}'. Skipping.")
        return False, None, None, None

    # Download remote .swaib file and compare versions
    try:
        remote_swaib_content = utils.download_swai_to_string(update_url)
        remote_swaib_data = yaml.safe_load(remote_swaib_content)
    except Exception as e:
        if verbose:
            print(f"Error getting remote .swaib file for bundle '{bundle_id}': {e}")
        return False, current_version, None, None

    latest_version = remote_swaib_data.get('bundle_version')

    if latest_version and latest_version > current_version:
        return True, current_version, latest_version, remote_swaib_content
    else:
        return False, current_version, latest_version, None


def update_bundle(bundle_id: str, local_install: bool = False, verbose: bool = False) -> bool:
    """
    Updates a bundle to the latest version.

    Args:
        bundle_id (str): The bundle ID to update.
        local_install (bool, optional): Whether to use local paths. Defaults to False.
        verbose (bool, optional): Whether to print verbose output. Defaults to False.

    Returns:
        bool: True if the bundle was updated, False if already up to date.

    Raises:
        SwaiBundleError: If there are issues during the update process.
    """
    has_update, current_version, latest_version, latest_content = check_bundle_for_update(
        bundle_id, local_install, verbose
    )

    if not has_update:
        # Check if there's no update URL
        bundle_path = utils.get_bundle_path(bundle_id, local_install)

        try:
            bundle_data = _parse_swaib_file(bundle_path, verbose=verbose)
            update_url = bundle_data.get('update_url')
            if not update_url:
                raise SwaiBundleError(
                    f"No update URL found for bundle '{bundle_id}'",
                    bundle_id=bundle_id,
                    error_code=SwaiErrorCodes.NO_UPDATE_URL
                )
        except SwaiBundleError:
            raise
        except Exception:
            pass

        if verbose:
            print(f"Bundle '{bundle_id}' is up to date.")
        return False

    if verbose:
        print(f"Updating bundle '{bundle_id}' from version '{current_version}' to version '{latest_version}'")

    # Remove old bundle and install new one
    try:
        remove_bundle(bundle_id, local_install=local_install, verbose=verbose, remove_data=False)
        install_bundle_from_str(latest_content, local_install=local_install, verbose=verbose)
    except Exception as e:
        raise SwaiBundleError(
            f"Error updating bundle '{bundle_id}': {e}",
            bundle_id=bundle_id,
            error_code=SwaiErrorCodes.UPDATE_FAILED,
            original_exception=e
        )

    return True


def is_bundle_file(file_path: str) -> bool:
    """
    Checks if a file is 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')
