"""YAML parsing and loading for DepartmentSupervisor."""
from __future__ import annotations

from pathlib import Path
from typing import Any

import yaml

from logos_adk.config import AgentConfig
from logos_adk.config_parser import parse_agent_config_data
from logos_adk.crew import CrewTask
from logos_adk.dynamic_orchestration_config import (
    DepartmentConfig,
    DepartmentSupervisorConfig,
    SwarmSubscriptionConfig,
)
from logos_adk.errors import ConfigError
from logos_adk.team_config import EventBusConfig, TeamConfig
from logos_adk.team_config_parser import load_team_config, parse_team_config_data


_ALLOWED_FIELDS = {
    "name",
    "type",
    "router",
    "fallback_department",
    "departments",
}


def parse_department_supervisor_config_data(
    data: Any,
    *,
    base_dir: str | Path | None = None,
) -> DepartmentSupervisorConfig:
    """Parse a raw YAML mapping into a DepartmentSupervisorConfig."""
    if not isinstance(data, dict):
        raise ConfigError("department supervisor config must be a mapping")

    unknown = set(data) - _ALLOWED_FIELDS
    if unknown:
        raise ConfigError(
            f"Unknown department supervisor config fields: {', '.join(sorted(unknown))}"
        )

    name = data.get("name")
    if not isinstance(name, str) or not name.strip():
        raise ConfigError("department supervisor config requires a non-empty string 'name'")

    cfg_type = data.get("type")
    if cfg_type != "department_supervisor":
        raise ConfigError(
            "department supervisor config requires 'type: department_supervisor' "
            f"(got {cfg_type!r})"
        )

    router = _parse_router_config(data.get("router"), base_dir=base_dir)
    fallback_department = data.get("fallback_department")
    if fallback_department is not None and (
        not isinstance(fallback_department, str) or not fallback_department.strip()
    ):
        raise ConfigError("'fallback_department' must be a non-empty string")

    raw_departments = data.get("departments", [])
    if not isinstance(raw_departments, list) or not raw_departments:
        raise ConfigError("'departments' must be a non-empty list")
    departments = [
        _parse_department(item, index=index, base_dir=base_dir)
        for index, item in enumerate(raw_departments)
    ]
    known_names = {department.name for department in departments}
    if fallback_department is not None and fallback_department not in known_names:
        raise ConfigError(
            f"'fallback_department' references unknown department '{fallback_department}'"
        )

    return DepartmentSupervisorConfig(
        name=name.strip(),
        router=router,
        fallback_department=fallback_department.strip() if isinstance(fallback_department, str) else None,
        departments=departments,
    )


def load_department_supervisor_config(path: str) -> DepartmentSupervisorConfig:
    """Load a DepartmentSupervisorConfig from a YAML file."""
    config_path = Path(path)
    with config_path.open() as f:
        data = yaml.safe_load(f)
    return parse_department_supervisor_config_data(data, base_dir=config_path.parent)


def load_department_supervisor_from_config(path: str):
    """Load a DepartmentSupervisor instance from a YAML config file."""
    from logos_adk.dynamic_orchestration import DepartmentSupervisor

    return DepartmentSupervisor.from_definition(load_department_supervisor_config(path))


def _parse_router_config(raw_router: Any, *, base_dir: str | Path | None) -> AgentConfig | None:
    if raw_router is None:
        return None
    if isinstance(raw_router, str):
        return _load_agent_config_from_path(raw_router, base_dir=base_dir)
    if isinstance(raw_router, dict):
        return parse_agent_config_data(raw_router)
    raise ConfigError("'router' must be a mapping or a string path")


def _parse_department(
    raw_department: Any,
    *,
    index: int,
    base_dir: str | Path | None,
) -> DepartmentConfig:
    if not isinstance(raw_department, dict):
        raise ConfigError(f"'departments[{index}]' must be a mapping")
    unknown = set(raw_department) - {
        "name",
        "kind",
        "description",
        "capability_tags",
        "metadata",
        "team",
        "tasks",
        "process",
        "manager_role",
        "max_parallel_tasks",
        "event_bus",
        "request_topic",
        "response_topic",
        "subscriptions",
        "retry_delay_seconds",
        "lease_seconds",
    }
    if unknown:
        raise ConfigError(
            f"Unknown departments[{index}] fields: {', '.join(sorted(unknown))}"
        )

    name = raw_department.get("name")
    if not isinstance(name, str) or not name.strip():
        raise ConfigError(f"'departments[{index}].name' must be a non-empty string")
    kind = raw_department.get("kind")
    if kind not in {"team", "crew", "swarm"}:
        raise ConfigError(
            f"'departments[{index}].kind' must be one of: team, crew, swarm"
        )
    description = raw_department.get("description", "")
    if not isinstance(description, str):
        raise ConfigError(f"'departments[{index}].description' must be a string")
    capability_tags = _parse_string_list(
        raw_department.get("capability_tags", []),
        field_name=f"departments[{index}].capability_tags",
    )
    metadata = raw_department.get("metadata", {})
    if not isinstance(metadata, dict):
        raise ConfigError(f"'departments[{index}].metadata' must be a mapping")

    department = DepartmentConfig(
        name=name.strip(),
        kind=kind,
        description=description,
        capability_tags=capability_tags,
        metadata=dict(metadata),
    )

    if kind in {"team", "crew"}:
        department.team = _parse_team_reference(
            raw_department.get("team"),
            field_name=f"departments[{index}].team",
            base_dir=base_dir,
        )

    if kind == "crew":
        department.tasks = _parse_crew_tasks(
            raw_department.get("tasks"),
            field_name=f"departments[{index}].tasks",
        )
        process = raw_department.get("process", "sequential")
        if process not in {"sequential", "hierarchical"}:
            raise ConfigError(f"'departments[{index}].process' must be 'sequential' or 'hierarchical'")
        department.process = process
        manager_role = raw_department.get("manager_role")
        if manager_role is not None and (not isinstance(manager_role, str) or not manager_role.strip()):
            raise ConfigError(f"'departments[{index}].manager_role' must be a string")
        department.manager_role = manager_role
        max_parallel_tasks = raw_department.get("max_parallel_tasks")
        if max_parallel_tasks is not None and (
            not isinstance(max_parallel_tasks, int) or max_parallel_tasks <= 0
        ):
            raise ConfigError(
                f"'departments[{index}].max_parallel_tasks' must be a positive integer"
            )
        department.max_parallel_tasks = max_parallel_tasks

    if kind == "swarm":
        department.event_bus = _parse_event_bus_config(
            raw_department.get("event_bus"),
            field_name=f"departments[{index}].event_bus",
        )
        department.request_topic = _parse_optional_string(
            raw_department.get("request_topic"),
            field_name=f"departments[{index}].request_topic",
        )
        department.response_topic = _parse_optional_string(
            raw_department.get("response_topic"),
            field_name=f"departments[{index}].response_topic",
        )
        department.subscriptions = _parse_swarm_subscriptions(
            raw_department.get("subscriptions", []),
            field_name=f"departments[{index}].subscriptions",
        )
        retry_delay_seconds = raw_department.get("retry_delay_seconds", 0.0)
        lease_seconds = raw_department.get("lease_seconds", 60.0)
        if not isinstance(retry_delay_seconds, (int, float)) or retry_delay_seconds < 0:
            raise ConfigError(
                f"'departments[{index}].retry_delay_seconds' must be a non-negative number"
            )
        if not isinstance(lease_seconds, (int, float)) or lease_seconds <= 0:
            raise ConfigError(
                f"'departments[{index}].lease_seconds' must be a positive number"
            )
        department.retry_delay_seconds = float(retry_delay_seconds)
        department.lease_seconds = float(lease_seconds)

    return department


def _parse_team_reference(
    raw_team: Any,
    *,
    field_name: str,
    base_dir: str | Path | None,
) -> TeamConfig:
    if isinstance(raw_team, str):
        return load_team_config(_resolve_path(raw_team, base_dir=base_dir))
    if isinstance(raw_team, dict):
        return parse_team_config_data(raw_team)
    raise ConfigError(f"'{field_name}' must be a mapping or a string path")


def _load_agent_config_from_path(path: str, *, base_dir: str | Path | None) -> AgentConfig:
    resolved = _resolve_path(path, base_dir=base_dir)
    with open(resolved) as f:
        data = yaml.safe_load(f)
    return parse_agent_config_data(data)


def _resolve_path(path: str, *, base_dir: str | Path | None) -> str:
    candidate = Path(path)
    if candidate.is_absolute() or base_dir is None:
        return str(candidate)
    return str(Path(base_dir) / candidate)


def _parse_event_bus_config(raw_event_bus: Any, *, field_name: str) -> EventBusConfig | None:
    if raw_event_bus is None:
        return None
    if not isinstance(raw_event_bus, dict):
        raise ConfigError(f"'{field_name}' must be a mapping")
    unknown = set(raw_event_bus) - {"backend", "path", "dsn"}
    if unknown:
        raise ConfigError(f"Unknown {field_name} fields: {', '.join(sorted(unknown))}")
    backend = raw_event_bus.get("backend", "memory")
    if backend == "postgres":
        backend = "postgresql"
    if backend not in {"memory", "sqlite", "postgresql"}:
        raise ConfigError(f"'{field_name}.backend' must be 'memory', 'sqlite', or 'postgresql'")
    path = raw_event_bus.get("path")
    dsn = raw_event_bus.get("dsn")
    if path is not None and not isinstance(path, str):
        raise ConfigError(f"'{field_name}.path' must be a string")
    if dsn is not None and not isinstance(dsn, str):
        raise ConfigError(f"'{field_name}.dsn' must be a string")
    if backend == "sqlite" and (not isinstance(path, str) or not path):
        raise ConfigError(f"'{field_name}.path' is required when backend is 'sqlite'")
    return EventBusConfig(backend=backend, path=path, dsn=dsn)


def _parse_swarm_subscriptions(raw_subscriptions: Any, *, field_name: str) -> list[SwarmSubscriptionConfig]:
    if not isinstance(raw_subscriptions, list):
        raise ConfigError(f"'{field_name}' must be a list")
    subscriptions: list[SwarmSubscriptionConfig] = []
    for index, item in enumerate(raw_subscriptions):
        if not isinstance(item, dict):
            raise ConfigError(f"'{field_name}[{index}]' must be a mapping")
        unknown = set(item) - {"topic", "handler", "publish_to", "name"}
        if unknown:
            raise ConfigError(
                f"Unknown {field_name}[{index}] fields: {', '.join(sorted(unknown))}"
            )
        topic = item.get("topic")
        handler = item.get("handler")
        if not isinstance(topic, str) or not topic.strip():
            raise ConfigError(f"'{field_name}[{index}].topic' must be a non-empty string")
        if not isinstance(handler, str) or not handler.strip():
            raise ConfigError(f"'{field_name}[{index}].handler' must be a non-empty string")
        publish_to = _parse_optional_string(
            item.get("publish_to"),
            field_name=f"{field_name}[{index}].publish_to",
        )
        name = _parse_optional_string(
            item.get("name"),
            field_name=f"{field_name}[{index}].name",
        )
        subscriptions.append(
            SwarmSubscriptionConfig(
                topic=topic.strip(),
                handler=handler.strip(),
                publish_to=publish_to,
                name=name,
            )
        )
    return subscriptions


def _parse_crew_tasks(raw_tasks: Any, *, field_name: str) -> list[CrewTask]:
    if not isinstance(raw_tasks, list) or not raw_tasks:
        raise ConfigError(f"'{field_name}' must be a non-empty list")
    tasks: list[CrewTask] = []
    for index, item in enumerate(raw_tasks):
        if not isinstance(item, dict):
            raise ConfigError(f"'{field_name}[{index}]' must be a mapping")
        unknown = set(item) - {
            "name",
            "description",
            "task_type",
            "role",
            "agent_name",
            "expected_output",
            "depends_on",
            "context",
            "async_execution",
            "approval_message",
            "output_key",
            "handoff_to_role",
            "handoff_to_agent",
            "metadata",
        }
        if unknown:
            raise ConfigError(
                f"Unknown {field_name}[{index}] fields: {', '.join(sorted(unknown))}"
            )
        name = item.get("name")
        description = item.get("description")
        if not isinstance(name, str) or not name.strip():
            raise ConfigError(f"'{field_name}[{index}].name' must be a non-empty string")
        if not isinstance(description, str) or not description.strip():
            raise ConfigError(f"'{field_name}[{index}].description' must be a non-empty string")
        task_type = item.get("task_type", "work")
        if task_type not in {"work", "approval"}:
            raise ConfigError(f"'{field_name}[{index}].task_type' must be 'work' or 'approval'")
        metadata = item.get("metadata", {})
        if not isinstance(metadata, dict):
            raise ConfigError(f"'{field_name}[{index}].metadata' must be a mapping")
        async_execution = item.get("async_execution", False)
        if not isinstance(async_execution, bool):
            raise ConfigError(f"'{field_name}[{index}].async_execution' must be a boolean")
        tasks.append(
            CrewTask(
                name=name.strip(),
                description=description.strip(),
                task_type=task_type,
                role=_parse_optional_string(item.get("role"), field_name=f"{field_name}[{index}].role"),
                agent_name=_parse_optional_string(
                    item.get("agent_name"),
                    field_name=f"{field_name}[{index}].agent_name",
                ),
                expected_output=_parse_optional_string(
                    item.get("expected_output"),
                    field_name=f"{field_name}[{index}].expected_output",
                ),
                depends_on=_parse_string_list(
                    item.get("depends_on", []),
                    field_name=f"{field_name}[{index}].depends_on",
                ),
                context=_parse_string_list(
                    item.get("context", []),
                    field_name=f"{field_name}[{index}].context",
                ),
                async_execution=async_execution,
                approval_message=_parse_optional_string(
                    item.get("approval_message"),
                    field_name=f"{field_name}[{index}].approval_message",
                ),
                output_key=_parse_optional_string(
                    item.get("output_key"),
                    field_name=f"{field_name}[{index}].output_key",
                ),
                handoff_to_role=_parse_optional_string(
                    item.get("handoff_to_role"),
                    field_name=f"{field_name}[{index}].handoff_to_role",
                ),
                handoff_to_agent=_parse_optional_string(
                    item.get("handoff_to_agent"),
                    field_name=f"{field_name}[{index}].handoff_to_agent",
                ),
                metadata=dict(metadata),
            )
        )
    return tasks


def _parse_string_list(raw_value: Any, *, field_name: str) -> list[str]:
    if not isinstance(raw_value, list):
        raise ConfigError(f"'{field_name}' must be a list of non-empty strings")
    values: list[str] = []
    for index, item in enumerate(raw_value):
        if not isinstance(item, str) or not item.strip():
            raise ConfigError(f"'{field_name}[{index}]' must be a non-empty string")
        values.append(item.strip())
    return values


def _parse_optional_string(raw_value: Any, *, field_name: str) -> str | None:
    if raw_value is None:
        return None
    if not isinstance(raw_value, str) or not raw_value.strip():
        raise ConfigError(f"'{field_name}' must be a non-empty string")
    return raw_value.strip()
