import gzip
import os
import os.path
import pwd
import tempfile
import time
from typing import List, Dict, Optional
from xml.etree import ElementTree as etree
from datetime import datetime

from sam.managers import Manager
from sam.actions import Action

import threading

import gi.repository
gi.require_version('Flatpak', '1.0')
gi.require_version('AppStream', '1.0')
from gi.repository import AppStream, Flatpak, GLib, Gio


def ensure_flatpak_environment():
    home = os.environ.get("HOME")
    if not home:
        home = pwd.getpwuid(os.geteuid()).pw_dir
        os.environ["HOME"] = home

    os.environ.setdefault("XDG_DATA_HOME", os.path.join(home, ".local", "share"))

    required_dirs = [
        os.path.join(home, ".local", "share", "flatpak", "exports", "share"),
        "/var/lib/flatpak/exports/share",
        "/usr/local/share",
        "/usr/share",
    ]
    existing_dirs = [
        entry for entry in os.environ.get("XDG_DATA_DIRS", "").split(":") if entry
    ]
    merged_dirs = []
    for entry in required_dirs + existing_dirs:
        if entry not in merged_dirs:
            merged_dirs.append(entry)
    os.environ["XDG_DATA_DIRS"] = ":".join(merged_dirs)


def flatpak_log(message: str):
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    print(f"[{timestamp}] Flatpak: {message}", flush=True)


def package_from_ref(ref: Flatpak.Ref):
    kind = ""
    if ref.get_kind() == Flatpak.RefKind.APP:
        kind = "app"
    elif ref.get_kind() == Flatpak.RefKind.RUNTIME:
        kind = "runtime"

    return f"{kind}/{ref.get_name()}/{ref.get_arch()}/{ref.get_branch()}"


def ref_from_package(package: str):
    ref_parts = package.split("/")
    kind = Flatpak.RefKind.APP if ref_parts[0] == "app" else Flatpak.RefKind.RUNTIME
    name = ref_parts[1]
    arch = ref_parts[2]
    branch = ref_parts[3]
    return kind, name, arch, branch


class FlatpakManager(Manager):
    title: str
    manager_id: str
    current_action: Action = None
    flatpak_operation: Flatpak.TransactionOperation = None
    flatpak_progress: Flatpak.TransactionProgress = None
    appstream_pool: Optional[AppStream.Pool] = None
    
    # Transaction progress tracking
    total_operation_count: int = 0
    current_operation_index: int = 0
    current_operation_progress: float = 0.0
    
    @property
    def manager_type(self) -> str:  # Hardcoded property
        return "flatpak"

    #  Load app stream data into memory to prevent stillCenter taking too long to launch
    def load_app_stream(self):
        if self.appstream_pool is None:
            self.appstream_pool = AppStream.Pool()
            self.appstream_pool.add_flags(AppStream.PoolFlags.LOAD_FLATPAK)
            self.appstream_pool.load()

    def calculate_overall_progress(self) -> float:
        """Calculate overall progress across all operations in the transaction."""
        if self.total_operation_count == 0:
            return 0.0
        
        # Each operation contributes equally to overall progress
        overall = (self.current_operation_index + self.current_operation_progress) / self.total_operation_count
        return min(overall * 100, 100.0)

    def on_ready(self, transaction):
        """Called when the transaction has resolved all dependencies.
        At this point we can get the full list of operations to know the total count."""
        operations = transaction.get_operations()
        self.total_operation_count = len(operations)
        return True  # Proceed with the transaction

    def update_progress(self, progress):
        if self.current_action is not None:
            self.current_operation_progress = progress.get_progress() / 100.0
            overall_progress = self.calculate_overall_progress()
            self.current_action.progress = int(overall_progress)
            self.current_action.status = progress.get_status()

    def new_operation(self, transaction, operation, progress):
        # If a previous operation existed, it's now complete
        if self.flatpak_operation is not None:
            self.current_operation_index += 1

        # Fallback if ready signal didn't provide total count
        if self.total_operation_count == 0:
            self.total_operation_count = self.current_operation_index + 1

        self.current_operation_progress = 0.0
        self.flatpak_operation = operation
        progress.connect("changed", self.update_progress)

    def reset_transaction_state(self):
        """Reset transaction tracking state for new transactions."""
        self.total_operation_count = 0
        self.current_operation_index = 0
        self.current_operation_progress = 0.0
        self.flatpak_operation = None

    # Used for error handling
    def run_transaction(self, transaction):
        try:
            transaction.run(self.cancellable)
        except GLib.GError as error:
            self.current_action.error = error.message
        finally:
            # Reset transaction state when transaction completes
            self.reset_transaction_state()

    def on_error(self, _transaction, _operation, error, _error_details):
        self.current_action.error = error.message

    def create_transaction(self):
        transaction = Flatpak.Transaction.new_for_installation(self.flatpak_installation, self.cancellable)
        transaction.connect("ready", self.on_ready)
        transaction.connect("new-operation", self.new_operation)
        transaction.connect("operation-error", self.on_error)
        return transaction

    def __init__(self, title: str, manager_id: str):
        super().__init__(title, manager_id)
        ensure_flatpak_environment()
        self.remote = title
        self.flatpak_installation = Flatpak.Installation.new_system()
        self.cancellable = Gio.Cancellable()
        self.reset_transaction_state()

    def install(self, action: Action):
        self.current_action = action
        self.reset_transaction_state()  # Ensure clean state for new transaction
        transaction = self.create_transaction()
        transaction.add_install(self.manager_id, action.package_id, None)
        self.run_transaction(transaction)

    def remove(self, action: Action):
        self.current_action = action
        self.reset_transaction_state()  # Ensure clean state for new transaction
        transaction = self.create_transaction()
        transaction.add_uninstall(action.package_id)
        self.run_transaction(transaction)

    def update(self, action: Action):
        self.current_action = action
        self.reset_transaction_state()  # Ensure clean state for new transaction
        transaction = self.create_transaction()
        transaction.add_update(action.package_id)
        self.run_transaction(transaction)

    def check_update(self, package: str) -> bool:
        # Parsing the package name to get the RefKind
        ref = package.split("/")
        if ref[0] == "app":
            ref_kind = Flatpak.RefKind.APP
        else:
            ref_kind = Flatpak.RefKind.RUNTIME
        arch = ref[2]
        branch = ref[3]
        installed_ref = self.flatpak_installation.get_installed_ref(ref_kind, ref[1], arch, branch, self.cancellable)
        return not installed_ref.get_is_current()

    def bare_app_info(self, package: str) -> Optional[dict]:
        ref = package.split("/")
        kind, name, arch, branch = ref_from_package(package)
        installed_ref = self.flatpak_installation.get_installed_ref(kind, name, arch, branch, self.cancellable)

        if installed_ref.get_origin() != self.manager_id:
            return None

        self.load_app_stream()
        components = self.appstream_pool.get_components_by_bundle_id(
            AppStream.BundleKind.FLATPAK, package, True
        )
        app_array = components.as_array()
        app = app_array[0] if len(app_array) > 0 else None

        if app is None:
            return {
                "app_id": f"{self.manager_id}-{ref[1].replace(".", "-").lower()}",
                "name": ref[1],
                "author": "Unknown Author",
                "primary_src": self.manager_id,
                "src_package_name": package,
                "summary": "Unknown app",
                "description": "Unknown app"
            }

        icon = app.get_icon_by_size(128, 128)
        if icon is not None:
            if icon.get_kind == AppStream.IconKind.LOCAL:
                icon_url = icon.get_filename()
            elif icon.get_kind == AppStream.IconKind.REMOTE:
                icon_url = icon.get_url()
            else:
                icon_url = ""
        else:
            icon_url = ""

        return {
            "app_id": f"{self.manager_id}-{ref[1].replace(".", "-")}",
            "name": str(app.get_name()),
            "author": str(app.get_developer().get_name()),
            "primary_src": self.manager_id,
            "src_package_name": package,
            "icon_url": icon_url,
            "summary": str(app.get_summary()),
            "description": str(app.get_description()).replace("<p>", "").replace("</p>", ""),
            "categories": str(",".join(app.get_categories()))
        }

    def check_updates(self) -> List[str]:
        try:
            flatpak_log(f"Checking updates for remote {self.manager_id} via libflatpak")
            refs = self.flatpak_installation.list_installed_refs_for_update()
            updates = [package_from_ref(ref) for ref in refs if self.check_ref_source(ref)]
            flatpak_log(f"Remote {self.manager_id} returned {len(updates)} update(s) via libflatpak")
            return updates
        except GLib.GError as error:
            flatpak_log(
                f"Primary libflatpak update check failed for remote {self.manager_id}: {error.message}; "
                "falling back to installed-ref scan"
            )
            refs = self.flatpak_installation.list_installed_refs()
            updates = []
            for ref in refs:
                if not self.check_ref_source(ref):
                    continue
                try:
                    if not ref.get_is_current():
                        updates.append(package_from_ref(ref))
                except GLib.GError as ref_error:
                    flatpak_log(
                        f"Skipping ref {package_from_ref(ref)} for remote {self.manager_id}: {ref_error.message}"
                    )
            flatpak_log(f"Fallback remote {self.manager_id} returned {len(updates)} update(s)")
            return updates

    def check_installed(self) -> List[str]:
        refs = self.flatpak_installation.list_installed_refs()
        return [package_from_ref(ref) for ref in refs if self.check_ref_source(ref)]

    def check_ref_source(self, ref) -> bool:
        if ref.get_origin() != self.manager_id:
            return False
        return True


def get_managers_for_remotes() -> Dict[str, FlatpakManager]:
    installation = Flatpak.Installation.new_system()
    remotes = installation.list_remotes()
    managers = {}
    for remote in remotes:
        title = remote.get_title() if remote.get_title() else remote.get_name()
        managers[remote.get_name()] = FlatpakManager(title, remote.get_name())
    return managers
