"""Filesystem discovery for Agentbyte WebUI entities."""

from __future__ import annotations

import importlib
import importlib.util
import sys
from pathlib import Path
from typing import Any

from agentbyte.agents.base import BaseAgent
from agentbyte.orchestration.base import BaseOrchestrator
from agentbyte.workflow.core.workflow import BaseWorkflow

from .models import AgentInfo, Entity, OrchestratorInfo, WorkflowInfo


class AgentbyteScanner:
    """Convention-based scanner for agent, orchestrator, and workflow modules."""

    def __init__(self, entities_dir: str) -> None:
        self.entities_dir = Path(entities_dir).resolve()
        self._module_cache: dict[str, Any] = {}

    def discover_entities(self) -> list[Entity]:
        """Discover top-level entities under the configured directory."""
        if not self.entities_dir.exists():
            return []

        if str(self.entities_dir) not in sys.path:
            sys.path.insert(0, str(self.entities_dir))

        discovered: list[Entity] = []
        for item in self.entities_dir.iterdir():
            if item.name.startswith(".") or item.name == "__pycache__":
                continue

            if item.is_dir():
                discovered.extend(self._discover_entities_in_directory(item))
            elif item.is_file() and item.suffix == ".py" and not item.name.startswith("_"):
                discovered.extend(self._discover_entities_in_file(item))
        return discovered

    def get_entity_object(self, entity_id: str) -> Any | None:
        """Load and return a discovered entity object."""
        module = self._module_cache.get(entity_id)
        if module is not None:
            return self._find_entity_in_module(module)

        for py_file in self.entities_dir.rglob("*.py"):
            module_candidate_id = self._get_entity_id(py_file)
            if entity_id == module_candidate_id or entity_id.startswith(f"{module_candidate_id}."):
                module = self._load_module(py_file, module_candidate_id.replace(".", "_"))
                if module is None:
                    continue
                self._module_cache[entity_id] = module
                return self._find_entity_in_module(module)

        return None

    def clear_cache(self) -> None:
        """Clear the cached imported modules."""
        for module_name in list(self._module_cache):
            sys.modules.pop(module_name, None)
        self._module_cache.clear()

    def _discover_entities_in_directory(self, dir_path: Path) -> list[Entity]:
        base_id = dir_path.name
        discovered: list[Entity] = []
        import_patterns = [
            base_id,
            f"{base_id}.agent",
            f"{base_id}.workflow",
            f"{base_id}.orchestrator",
        ]
        for pattern in import_patterns:
            module = self._load_module_from_pattern(pattern)
            if module is None:
                continue
            discovered.extend(self._find_entities_in_module(module, base_id, str(dir_path)))
            if discovered:
                break
        return discovered

    def _discover_entities_in_file(self, file_path: Path) -> list[Entity]:
        base_id = self._get_entity_id(file_path)
        module = self._load_module(file_path, base_id.replace(".", "_"))
        if module is None:
            return []
        return self._find_entities_in_module(module, base_id, str(file_path))

    def _load_module_from_pattern(self, pattern: str) -> Any | None:
        try:
            spec = importlib.util.find_spec(pattern)
            if spec is None:
                return None
            return importlib.import_module(pattern)
        except Exception:
            return None

    def _load_module(self, file_path: Path, module_name: str) -> Any | None:
        try:
            spec = importlib.util.spec_from_file_location(module_name, file_path)
            if spec is None or spec.loader is None:
                return None
            module = importlib.util.module_from_spec(spec)
            spec.loader.exec_module(module)
            return module
        except Exception:
            return None

    def _find_entities_in_module(
        self,
        module: Any,
        base_id: str,
        module_path: str,
    ) -> list[Entity]:
        discovered: list[Entity] = []
        candidates = [
            ("agent", getattr(module, "agent", None)),
            ("orchestrator", getattr(module, "orchestrator", None)),
            ("workflow", getattr(module, "workflow", None)),
        ]
        for obj_type, obj in candidates:
            if obj is None or not self._is_valid_entity(obj, obj_type):
                continue
            info = self._extract_entity_info(obj, obj_type, base_id, module_path)
            if info is not None:
                discovered.append(info)
                self._module_cache[info.id] = module
        return discovered

    def _find_entity_in_module(self, module: Any) -> Any | None:
        for attr_name in ("agent", "orchestrator", "workflow"):
            candidate = getattr(module, attr_name, None)
            if candidate is not None:
                return candidate
        return None

    def _is_valid_entity(self, obj: Any, obj_type: str) -> bool:
        if obj_type == "agent":
            return isinstance(obj, BaseAgent)
        if obj_type == "orchestrator":
            return isinstance(obj, BaseOrchestrator)
        if obj_type == "workflow":
            return isinstance(obj, BaseWorkflow)
        return False

    def _extract_entity_info(
        self,
        obj: Any,
        obj_type: str,
        base_id: str,
        module_path: str,
    ) -> Entity | None:
        entity_id = f"{base_id}.{obj_type}"
        common = {
            "id": entity_id,
            "name": getattr(obj, "name", base_id),
            "description": getattr(obj, "description", None)
            or getattr(getattr(obj, "config", None), "description", None),
            "source": "directory",
            "module_path": module_path,
            "example_tasks": list(getattr(obj, "example_tasks", []) or []),
            "has_env": (Path(module_path).parent / ".env").exists()
            if Path(module_path).is_file()
            else (Path(module_path) / ".env").exists(),
        }

        if obj_type == "agent":
            model_client = getattr(obj, "model_client", None)
            model = getattr(model_client, "model", None)
            return AgentInfo(
                **common,
                model=str(model) if model else None,
                tools=self._extract_agent_tools(obj),
                memory_type=type(obj.memory).__name__ if getattr(obj, "memory", None) else None,
                middleware_count=len(getattr(obj.middleware_chain, "middlewares", [])),
            )

        if obj_type == "orchestrator":
            return OrchestratorInfo(
                **common,
                orchestrator_type=obj.__class__.__name__,
                agents=[agent.name for agent in getattr(obj, "agents", [])],
                termination_conditions=self._extract_termination_conditions(
                    getattr(obj, "termination", None)
                ),
            )

        if obj_type == "workflow":
            start_step_id = getattr(obj, "start_step_id", None)
            input_schema = None
            if start_step_id and start_step_id in getattr(obj, "steps", {}):
                input_type = getattr(obj.steps[start_step_id], "input_type", None)
                if input_type and hasattr(input_type, "model_json_schema"):
                    input_schema = input_type.model_json_schema()

            return WorkflowInfo(
                **common,
                name=getattr(getattr(obj, "config", None), "name", common["name"]),
                steps=sorted(getattr(obj, "steps", {}).keys()),
                start_step=start_step_id,
                start_step_id=start_step_id,
                end_step_ids=list(getattr(obj, "end_step_ids", [])),
                input_schema=input_schema,
            )

        return None

    def _extract_agent_tools(self, agent: BaseAgent) -> list[str]:
        return [tool.name for tool in getattr(agent, "tools", [])]

    def _extract_termination_conditions(self, termination: Any) -> list[str]:
        if termination is None:
            return []
        metadata = getattr(termination, "get_metadata", None)
        if callable(metadata):
            details = metadata()
            if isinstance(details, dict) and "conditions" in details:
                conditions = details.get("conditions")
                if isinstance(conditions, list):
                    return [str(item) for item in conditions]
        return [termination.__class__.__name__]

    def _get_entity_id(self, py_file: Path) -> str:
        return py_file.relative_to(self.entities_dir).with_suffix("").as_posix().replace("/", ".")


__all__ = ["AgentbyteScanner"]
