from __future__ import annotations

import importlib
import inspect
import os
import re
import sys
from pathlib import Path
from typing import Callable, List, Optional, get_type_hints

from iac.runbook.discovery import parse_module_dirs
from iac.runbook.module_types import StepContext, StepResult

_SNAKE_CASE_RE = re.compile(r"^[a-z][a-z0-9_]*$")


def _ensure_iac_exports() -> None:
    """
    Ensure `from iac import StepContext, StepResult` works even in editable
    namespace-package environments.
    """
    iac_mod = importlib.import_module("iac")
    if not hasattr(iac_mod, "StepContext") or not hasattr(iac_mod, "StepResult"):
        setattr(iac_mod, "StepContext", StepContext)
        setattr(iac_mod, "StepResult", StepResult)


def _apply_module_dirs(module_dirs: List[Path]) -> None:
    for d in reversed(module_dirs):
        sd = str(d)
        if sd not in sys.path:
            sys.path.insert(0, sd)


def validate_callable_contract(
    fn: Callable[..., object], *, module_name: str, function_name: str
) -> None:
    if not _SNAKE_CASE_RE.match(function_name):
        raise ValueError(
            f"invalid function name '{function_name}' in {module_name}: must be snake_case"
        )
    if inspect.iscoroutinefunction(fn):
        raise TypeError(f"{module_name}.{function_name} must be a synchronous function")
    if "." in getattr(fn, "__qualname__", ""):
        raise TypeError(
            f"{module_name}.{function_name} must be a top-level module function"
        )
    doc = inspect.getdoc(fn) or ""
    if not doc.strip():
        raise ValueError(
            f"{module_name}.{function_name} must define a non-empty docstring"
        )

    sig = inspect.signature(fn)
    if len(sig.parameters) != 1:
        raise TypeError(
            f"{module_name}.{function_name} must accept exactly one argument: ctx: StepContext"
        )

    param = next(iter(sig.parameters.values()))
    hints = get_type_hints(
        fn,
        globalns=getattr(fn, "__globals__", {}),
        localns={"StepContext": StepContext, "StepResult": StepResult},
    )
    param_hint = hints.get(param.name)
    if param_hint is not StepContext:
        raise TypeError(
            f"{module_name}.{function_name} parameter '{param.name}' must be annotated as StepContext"
        )

    return_hint = hints.get("return")
    if return_hint is not StepResult:
        raise TypeError(
            f"{module_name}.{function_name} must return StepResult (annotation required)"
        )


def load_validated_callable(
    module_name: str, function_name: str, *, module_dirs: Optional[List[Path]] = None
) -> Callable[[StepContext], StepResult]:
    _ensure_iac_exports()
    if module_dirs is None:
        module_dirs_env = os.environ.get("IAC_MODULE_DIRS")
        module_dirs = parse_module_dirs(module_dirs_env)
    _apply_module_dirs(module_dirs)

    mod = importlib.import_module(module_name)
    fn = getattr(mod, function_name, None)
    if fn is None or not callable(fn):
        raise AttributeError(f"callable not found: {module_name}.{function_name}")
    validate_callable_contract(fn, module_name=module_name, function_name=function_name)
    return fn  # type: ignore[return-value]
