from __future__ import annotations

import json
import os
import re
from pathlib import Path
from typing import Any, cast

from pydantic import BaseModel, Field

from cleveragents.actor.yaml_template_engine import YAMLTemplateEngine

try:  # Optional dependency; present in dev/test extras
    import yaml
except Exception:  # pragma: no cover - defensive optional import
    yaml = None


class ActorConfiguration(BaseModel):
    """Canonical actor configuration parsed from user-provided blobs."""

    name: str | None = Field(default=None, description="Actor name (optional)")
    provider: str = Field(..., min_length=1, description="Provider identifier")
    model: str = Field(..., min_length=1, description="Model identifier")
    graph_descriptor: dict[str, Any] | None = Field(
        default=None, description="Graph descriptor emitted by actor config"
    )
    options: dict[str, Any] = Field(
        default_factory=dict, description="Opaque actor options stored in config blob"
    )
    unsafe: bool = Field(
        default=False, description="True when the actor config is marked unsafe"
    )

    @staticmethod
    def load_yaml_text(text: str) -> dict[str, Any]:
        """Parse YAML text into a configuration blob.

        Args:
            text: Raw YAML string.

        Returns:
            Parsed configuration dictionary.
        """
        try:
            data = json.loads(text)
        except json.JSONDecodeError:
            data = ActorConfiguration._load_v2_yaml_content(text)
        else:
            if data is None:
                return {}
            if not isinstance(data, dict):
                raise ValueError("Config must be a JSON or YAML object")
            return cast(dict[str, Any], data)
        return data

    @staticmethod
    def load_blob_from_file(config_path: Path) -> dict[str, Any]:
        """Load a configuration blob from a JSON or YAML file (v2 parity)."""

        path = config_path.expanduser()
        if not path.exists():
            raise ValueError(f"Config file not found: {path}")

        text = path.read_text()
        try:
            data = json.loads(text)
        except json.JSONDecodeError:
            data = ActorConfiguration._load_v2_yaml_content(text)
        else:
            if data is None:
                return {}
            if not isinstance(data, dict):
                raise ValueError("Config must be a JSON or YAML object")
            return cast(dict[str, Any], data)

        return data

    @staticmethod
    def _load_v2_yaml_content(text: str) -> dict[str, Any]:
        if yaml is None:
            raise ValueError("PyYAML is required for YAML actor configs")

        raw: Any
        try:
            if "{%" in text or "{{" in text:
                protected = re.sub(r"{{", "<<<TEMPLATE_START>>>", text)
                protected = re.sub(r"}}", "<<<TEMPLATE_END>>>", protected)
                protected = re.sub(r"{%", "<<<BLOCK_START>>>", protected)
                protected = re.sub(r"%}", "<<<BLOCK_END>>>", protected)

                engine = YAMLTemplateEngine()
                minimal_context: dict[str, dict[str, Any]] = {
                    "context": {
                        "paper_details": {
                            "topic": "",
                            "length": "",
                            "audience": "",
                            "publication": "",
                            "format": "",
                            "other": "",
                        },
                        "brainstorming_summary": "",
                        "vetting_sources": list[str](),
                        "table_of_contents": "",
                        "deep_research_sources": "",
                        "current_section_to_write": "",
                        "paper_content": dict[str, Any](),
                        "final_paper_text": "",
                        "proofread_paper": "",
                    }
                }
                raw = engine.load_string(protected, context=minimal_context)
                raw = ActorConfiguration._restore_template_syntax(raw)
            else:
                raw = yaml.safe_load(text)
        except Exception as exc:  # pragma: no cover - defensive
            raise ValueError(f"Failed to parse config: {exc}") from exc

        if raw is None:
            raw = {}
        if not isinstance(raw, dict):
            raise ValueError("Config must be a JSON or YAML object")

        interpolated = ActorConfiguration._interpolate_env_vars(raw)
        if not isinstance(interpolated, dict):
            raise ValueError("Config must be a JSON or YAML object")
        return cast(dict[str, Any], interpolated)

    @staticmethod
    def _restore_template_syntax(config: Any) -> Any:
        if isinstance(config, dict):
            config_dict = cast(dict[str, Any], config)
            restored: dict[str, Any] = {}
            for key, value in config_dict.items():
                if key == "system_prompt" and isinstance(value, str):
                    updated = value.replace("<<<TEMPLATE_START>>>", "{{").replace(
                        "<<<TEMPLATE_END>>>", "}}"
                    )
                    updated = updated.replace("<<<BLOCK_START>>>", "{%")
                    updated = updated.replace("<<<BLOCK_END>>>", "%}")
                    restored[key] = updated
                else:
                    restored[key] = ActorConfiguration._restore_template_syntax(value)
            return restored
        if isinstance(config, list):
            config_list = cast(list[Any], config)
            return [
                ActorConfiguration._restore_template_syntax(item)
                for item in config_list
            ]
        return config

    @staticmethod
    def _interpolate_env_vars(config: Any) -> Any:
        if isinstance(config, dict):
            config_dict = cast(dict[str, Any], config)
            return {
                k: ActorConfiguration._interpolate_env_vars(v)
                for k, v in config_dict.items()
            }
        if isinstance(config, list):
            config_list = cast(list[Any], config)
            return [ActorConfiguration._interpolate_env_vars(i) for i in config_list]
        if isinstance(config, str):
            env_var_pattern = r"\${([A-Za-z0-9_]+)(?::([^}]*))?\}"

            def replace_env_var(match: re.Match[str]) -> str:
                env_var = match.group(1)
                default_value = match.group(2) if match.group(2) is not None else None

                env_value = os.environ.get(env_var)
                if env_value is None:
                    if default_value is not None:
                        if default_value.lower() in {"true", "false"}:
                            return str(default_value.lower() == "true")
                        if default_value.lstrip("-").isdigit():
                            return default_value
                        return default_value
                    raise ValueError(f"Environment variable '{env_var}' is not set")
                return env_value

            substituted = re.sub(env_var_pattern, replace_env_var, config)

            lowered = substituted.lower()
            if lowered in {"true", "false"}:
                return lowered == "true"
            if substituted.lstrip("-").isdigit():
                return int(substituted)
            if (
                substituted.lstrip("-").replace(".", "", 1).isdigit()
                and substituted.count(".") == 1
            ):
                return float(substituted)
            return substituted
        return config

    @classmethod
    def from_file(
        cls,
        *,
        path: Path,
        name: str | None = None,
        provider: str | None = None,
        model: str | None = None,
        graph_descriptor: dict[str, Any] | None = None,
        unsafe: bool = False,
    ) -> ActorConfiguration:
        """Parse and validate a configuration file into an ActorConfiguration."""

        blob = cls.load_blob_from_file(path)
        return cls.from_blob(
            blob=blob,
            name=name,
            provider=provider,
            model=model,
            graph_descriptor=graph_descriptor,
            unsafe=unsafe,
        )

    @classmethod
    def from_blob(
        cls,
        *,
        blob: dict[str, Any] | None,
        name: str | None = None,
        provider: str | None = None,
        model: str | None = None,
        graph_descriptor: dict[str, Any] | None = None,
        unsafe: bool = False,
        default_options: dict[str, Any] | None = None,
        option_overrides: dict[str, Any] | None = None,
    ) -> ActorConfiguration:
        """Coerce an incoming config blob plus CLI overrides into a validated model."""

        data: dict[str, Any] = dict(blob) if isinstance(blob, dict) else {}

        v2_provider, v2_model, v2_graph, v2_unsafe = cls._extract_v2_actor(data)
        v2_options = cls._extract_v2_options(data)

        resolved_provider = (
            provider or data.get("provider") or data.get("provider_type") or v2_provider
        )
        resolved_model = model or data.get("model") or data.get("model_id") or v2_model
        resolved_graph = (
            graph_descriptor
            or data.get("graph_descriptor")
            or data.get("graph")
            or v2_graph
        )

        options_raw = data.get("options")
        options_value: dict[str, Any] | None = (
            cast(dict[str, Any], options_raw) if isinstance(options_raw, dict) else None
        )
        merged_options: dict[str, Any] = {}
        if default_options:
            merged_options.update(default_options)
        if v2_options:
            merged_options.update(v2_options)
        if options_value:
            merged_options.update(options_value)
        if option_overrides:
            merged_options.update(option_overrides)

        resolved_unsafe = bool(data.get("unsafe", False)) or v2_unsafe or unsafe

        if not resolved_provider:
            raise ValueError("provider is required")

        if not resolved_model:
            raise ValueError("model is required")

        graph_blob = (
            cast(dict[str, Any], resolved_graph)
            if isinstance(resolved_graph, dict)
            else None
        )

        return cls(
            name=name,
            provider=str(resolved_provider),
            model=str(resolved_model),
            graph_descriptor=graph_blob,
            options=merged_options,
            unsafe=resolved_unsafe,
        )

    @staticmethod
    def _extract_v2_actor(
        data: dict[str, Any],
    ) -> tuple[str | None, str | None, dict[str, Any] | None, bool]:
        """Derive provider/model/graph from the v2 YAML actor config format."""

        agents_obj = data.get("agents")
        if isinstance(agents_obj, dict) and agents_obj:
            agents_dict = cast(dict[str, Any], agents_obj)
            for first_agent, first_entry in agents_dict.items():
                if isinstance(first_entry, dict):
                    entry_dict = cast(dict[str, Any], first_entry)
                    config_block = entry_dict.get("config")
                    if isinstance(config_block, dict):
                        config_dict = cast(dict[str, Any], config_block)
                        provider_value = config_dict.get("provider") or config_dict.get(
                            "provider_type"
                        )
                        model_value = config_dict.get("model") or config_dict.get(
                            "model_id"
                        )
                        unsafe_flag = bool(config_dict.get("unsafe", False))
                        descriptor: dict[str, Any] = {
                            "agent": first_agent,
                            "agents": agents_dict,
                        }
                        for key in (
                            "routes",
                            "merges",
                            "templates",
                            "cleveragents",
                        ):
                            if key in data and data[key] is not None:
                                descriptor[key] = data[key]
                        return (
                            str(provider_value) if provider_value else None,
                            str(model_value) if model_value else None,
                            descriptor,
                            unsafe_flag,
                        )
                break
        return None, None, None, False

    @staticmethod
    def _extract_v2_options(data: dict[str, Any]) -> dict[str, Any] | None:
        """Extract option defaults from the v2 actor config format."""

        agents_obj = data.get("agents")
        if isinstance(agents_obj, dict) and agents_obj:
            agents_dict = cast(dict[str, Any], agents_obj)
            for _, first_entry in agents_dict.items():
                if isinstance(first_entry, dict):
                    entry_dict = cast(dict[str, Any], first_entry)
                    config_block = entry_dict.get("config")
                    if isinstance(config_block, dict):
                        config_dict = cast(dict[str, Any], config_block)
                        options = config_dict.get("options")
                        if isinstance(options, dict):
                            return cast(dict[str, Any], options)
                break
        return None
