import inspect
import textwrap
from collections.abc import Callable
from pathlib import Path
from typing import Any, TypeVar, get_type_hints

import tomllib
from pydantic import BaseModel, ValidationError

from yera.config.function_config import FunctionConfig, ParameterInfo, ParameterKind

TModel = TypeVar("TModel", bound=BaseModel)


def read_config_file(cfg_path: Path | str | None = None) -> str:
    if cfg_path is None:
        home = Path.home() / ".yera"
        home.mkdir(parents=True, exist_ok=True)
        cfg_path = home / "global_cfg.toml"

    with open(cfg_path) as f:
        return f.read()


def parse_toml_string(toml_str):
    cfg = tomllib.loads(toml_str)
    validate_config(cfg)
    return cfg


def validate_config(cfg_obj):
    if not isinstance(cfg_obj, dict):
        raise TypeError("cfg_obj must be a dictionary")

    if "models" not in cfg_obj or "agents" not in cfg_obj:
        raise KeyError("Configuration must contain 'models' and 'agents' keys")


def validate_schema(d: dict, cls: type[BaseModel]) -> bool:
    try:
        cls.model_validate(d)
        return True
    except ValidationError:
        return False


def build_config_map(cfg_obj, cls: type[TModel]) -> dict[str, TModel]:
    def walk(val, results, path):
        if not isinstance(val, dict):
            raise TypeError(
                f"Something's gone wrong. The input is not a dict: {type(val)} {val}"
            )

        if validate_schema(val, cls):
            results[path] = cls.model_validate(val)
            return results
        for k, v in val.items():
            results = walk(v, results, f"{path}.{k}" if path else k)

        return results

    return walk(cfg_obj, {}, "")


def _strip_decorators(source: str) -> str:
    lines = source.splitlines()
    # Remove leading decorators until we hit 'def'
    body_start = 0
    for i, line in enumerate(lines):
        if line.lstrip().startswith("def "):
            body_start = i
            break
    return "\n".join(lines[body_start:])


def _extract_function_body(source: str) -> str:
    # source begins with 'def name(...):' line
    lines = source.splitlines()
    if not lines:
        return ""
    # Drop the signature line
    body_lines = lines[1:]
    # Dedent body
    return textwrap.dedent("\n".join(body_lines)).rstrip()


def _type_annotation_to_str(annotation: Any) -> str:
    if annotation is inspect.Signature.empty:
        return "Any"
    try:
        if hasattr(annotation, "__origin__") and annotation.__origin__ is not None:
            # This is a generic type like list[str], dict[str, int], etc.
            return str(annotation)
        if hasattr(annotation, "__name__"):
            # This is a simple type like str, int, etc.
            return annotation.__name__
        # Fallback to string representation
        return str(annotation)
    except Exception:
        return "Any"


def _map_parameter_kind_to_enum(kind: inspect._ParameterKind) -> ParameterKind:
    """Convert inspect.Parameter.kind to our enum."""
    mapping = {
        inspect.Parameter.POSITIONAL_OR_KEYWORD: ParameterKind.POSITIONAL_OR_KEYWORD,
        inspect.Parameter.VAR_POSITIONAL: ParameterKind.VAR_POSITIONAL,
        inspect.Parameter.KEYWORD_ONLY: ParameterKind.KEYWORD_ONLY,
        inspect.Parameter.VAR_KEYWORD: ParameterKind.VAR_KEYWORD,
    }
    return mapping[kind]


def extract_function_config(func: Callable) -> FunctionConfig:
    def extract_content():
        source = inspect.getsource(func)
        source = _strip_decorators(source)
        content = _extract_function_body(source)
        return content

    sig = inspect.signature(func)
    type_hints = get_type_hints(func)

    def extract_parameters():
        parameters = []
        for name, param in sig.parameters.items():
            if name == "self":
                continue

            annotation = type_hints.get(name, inspect.Signature.empty)
            type_annotation = _type_annotation_to_str(annotation)
            kind = _map_parameter_kind_to_enum(param.kind)
            default = (
                param.default if param.default is not inspect.Signature.empty else None
            )

            parameters.append(
                ParameterInfo(
                    name=name,
                    type_annotation=type_annotation,
                    kind=kind,
                    default=default,
                )
            )
        return parameters

    def extract_output_signature():
        output_annotation = type_hints.get("return", inspect.Signature.empty)
        output_signature = _type_annotation_to_str(output_annotation)
        return output_signature

    def extract_module_path():
        module_path = getattr(func, "__module__", None)
        if module_path == "__main__":
            module_path = None  # Don't store __main__ as it's not useful
        return module_path

    return FunctionConfig(
        content=extract_content(),
        name=func.__name__,
        parameters=extract_parameters(),
        output_signature=extract_output_signature(),
        module_path=extract_module_path(),
    )
