import traceback
from collections import defaultdict, deque
from typing import Dict, Any
from packaging.specifiers import SpecifierSet
from packaging.version import Version
from fastpluggy.core.plugin_state import PluginState
from loguru import logger


class PluginDependencyResolver:
    @staticmethod
    def get_sorted_modules_by_dependency(modules: Dict[str, PluginState]) -> Dict[str, Any]:
        """
        Perform dependency resolution and return full topological order, loadable status,
        and error details. All modules that are enabled & initialized are analyzed.
        """
        output = {
            "success": True,
            "sorted_modules": [],       # Only loadable modules (based on version + presence)
            "computed_order": [],       # Full topo sort result, including unresolvable modules
            "unresolved": [],
            "error": None,
            "modules": {},
        }

        graph = defaultdict(set)  # dependency -> set of dependents
        in_degree = defaultdict(int)  # dependent -> count of unresolved deps
        module_versions = {}
        module_deps = {}
        module_optional_deps = {}

        logger.info("🔍 Gathering module versions and declared dependencies...")
        for module in modules.values():
            if module.enabled and module.initialized:
                name = module.plugin.module_name
                version_str = module.plugin.module_version or "0.0.0"
                version = Version(version_str)
                deps: dict = module.plugin.depends_on or {}
                optional_deps: dict = module.plugin.optional_depends_on or {}

                module_versions[name] = version
                module_deps[name] = deps
                module_optional_deps[name] = optional_deps
                in_degree[name] = 0

                output["modules"][name] = {
                    "version": str(version),
                    "depends_on": deps,
                    "optional_depends_on": {
                        dep: {"constraint": constraint, "status": "pending"}
                        for dep, constraint in optional_deps.items()
                    },
                    "status": "pending",
                    "dependency_issues": [],
                    "optional_dependency_warnings": [],
                    "loadable": True,
                }

        logger.info("🔧 Validating and building dependency graph...")
        for name, deps in module_deps.items():
            for dep_name, version_constraint in deps.items():
                module_output = output["modules"][name]

                if dep_name not in modules or not modules[dep_name].initialized:
                    issue = f"Missing dependency: {dep_name} (version:{version_constraint or '(any version)'})"
                    module_output["dependency_issues"].append(issue)
                    module_output["status"] = "error"
                    module_output["loadable"] = False
                    logger.warning(f"❌ {issue} for module '{name}'")
                    continue

                actual_version = module_versions.get(dep_name, Version("0.0.0"))
                spec = SpecifierSet(version_constraint)

                if actual_version not in spec:
                    issue = f"Version mismatch for '{dep_name}': required {version_constraint}, found {actual_version}"
                    module_output["dependency_issues"].append(issue)
                    module_output["status"] = "error"
                    module_output["loadable"] = False
                    logger.error(f"❌ {issue} (in module '{name}')")
                    continue

                graph[dep_name].add(name)
                in_degree[name] += 1
                logger.debug(f"✅ '{name}' depends on '{dep_name}' {version_constraint} (actual: {actual_version})")

        # Process optional dependencies — these influence load order but never block loading
        optional_edges = []  # track edges added so we can remove them if they cause cycles
        logger.info("🔧 Processing optional dependencies...")
        for name, optional_deps in module_optional_deps.items():
            for dep_name, version_constraint in optional_deps.items():
                module_output = output["modules"][name]
                opt_dep_info = module_output["optional_depends_on"][dep_name]

                if dep_name not in module_versions:
                    opt_dep_info["status"] = "not_loaded"
                    warning = f"Optional dep '{dep_name}' not loaded — related features disabled"
                    module_output["optional_dependency_warnings"].append(warning)
                    logger.info(f"ℹ️ {warning} (in module '{name}')")
                    continue

                actual_version = module_versions[dep_name]
                spec = SpecifierSet(version_constraint)

                if actual_version not in spec:
                    opt_dep_info["status"] = "version_mismatch"
                    opt_dep_info["actual_version"] = str(actual_version)
                    warning = f"Optional dep '{dep_name}' version mismatch: need {version_constraint}, found {actual_version}"
                    module_output["optional_dependency_warnings"].append(warning)
                    logger.warning(f"⚠️ {warning} (in module '{name}')")
                    continue

                # Optional dep is present and version matches — add soft edge for load ordering
                opt_dep_info["status"] = "available"
                opt_dep_info["actual_version"] = str(actual_version)
                graph[dep_name].add(name)
                in_degree[name] += 1
                optional_edges.append((dep_name, name))
                logger.debug(f"✅ '{name}' optionally depends on '{dep_name}' {version_constraint} (actual: {actual_version})")

        # Update module status if not already in error
        for name, info in output["modules"].items():
            if info["status"] == "pending":
                info["status"] = "ok"

        logger.info("📐 Performing topological sort...")
        queue = deque([name for name in module_deps if in_degree[name] == 0])
        full_order = []

        while queue:
            current = queue.popleft()
            full_order.append(current)
            for dependent in graph[current]:
                in_degree[dependent] -= 1
                if in_degree[dependent] == 0:
                    queue.append(dependent)

        # If optional edges caused a cycle, drop them and retry
        if len(full_order) != len(module_deps) and optional_edges:
            logger.warning("⚠️ Optional dependency edges caused a cycle — dropping optional edges and retrying")
            # Remove all optional edges
            for dep_name, name in optional_edges:
                graph[dep_name].discard(name)

            # Rebuild in_degree from scratch
            in_degree.clear()
            for name in module_deps:
                in_degree[name] = 0
            for dep_name, dependents in graph.items():
                for dependent in dependents:
                    if dependent in in_degree:
                        in_degree[dependent] += 1

            # Re-run topo sort
            queue = deque([name for name in module_deps if in_degree[name] == 0])
            full_order = []
            while queue:
                current = queue.popleft()
                full_order.append(current)
                for dependent in graph[current]:
                    in_degree[dependent] -= 1
                    if in_degree[dependent] == 0:
                        queue.append(dependent)

        output["computed_order"] = full_order

        if len(full_order) != len(module_deps):
            output["success"] = False
            output["error"] = "Cyclic or unsatisfiable dependencies detected."
            unresolved = [name for name in module_deps if name not in full_order]
            output["unresolved"] = unresolved

            for name in unresolved:
                output["modules"][name]["status"] = "unresolved"
                output["modules"][name]["loadable"] = False
                logger.error(f"🔗 Module '{name}' still has unresolved dependencies: {module_deps[name]}")
        else:
            logger.info(f"✅ Modules sorted in dependency order: {full_order}")

        output["sorted_modules"] = [
            name for name in full_order if output["modules"][name]["loadable"]
        ]

        # build a new order: first all sorted/loadable, then whatever is left
        # this avoid lost module not loadable of have dependency issue
        reordered = PluginDependencyResolver.reorder_modules(modules=modules, sorted_modules=output["sorted_modules"])
        modules.clear()
        modules.update(reordered)
        return output

    @staticmethod
    def reorder_modules(modules: dict, sorted_modules: list) -> dict:
        """
        Reorder `modules` so that keys in `sorted_modules` come first (in order),
        then all the other keys (in their original order).
        """
        ordered_names = sorted_modules + [n for n in modules if n not in sorted_modules]
        return {name: modules[name] for name in ordered_names}

    @staticmethod
    def update_plugin_states_from_result(result: dict, modules: Dict[str, PluginState]):
        """
        Updates the PluginState instances in `modules` with error/warning info based on
        the output from `get_sorted_modules_by_dependency`.

        This does not re-run dependency resolution — it only reflects the result.
        """
        for name, mod_info in result["modules"].items():
            plugin_state = modules.get(name)
            if not plugin_state:
                continue

            if mod_info["status"] in {"error", "unresolved"}:
                for issue in mod_info.get("dependency_issues", []):
                    plugin_state.dependency_issues.append(issue)

            for warning in mod_info.get("optional_dependency_warnings", []):
                plugin_state.optional_dependency_warnings.append(warning)

            plugin_state.optional_dependency_status = mod_info.get("optional_depends_on", {})

            try:
                plugin_state.process()
            except Exception as err:
                plugin_state.error.append(f"Error in process(): {err}")
                plugin_state.traceback.append(traceback.format_exc())
