from __future__ import annotations

import json
import os
from pathlib import Path
import re
from typing import Any
from uuid import uuid4

from src.bootstrap_config import apply_initial_env_from_config
from src.prompt_manager import _resolve_prompt_bundle
from src.state_manager import _get_checkpointer, _resolve_checkpointer_sqlite_path
from src.tools import (
    build_langfuse_openapi_operation_tools,
    set_langfuse_settings_override,
    langfuse_annotation_create_tool,
    langfuse_control_execute_tool,
    langfuse_control_message_tool,
    langfuse_dataset_items_upsert_tool,
    langfuse_datasets_create_tool,
    langfuse_datasets_import_from_traces_tool,
    langfuse_datasets_list_tool,
    langfuse_observations_list_tool,
    langfuse_openapi_build_tools_tool,
    langfuse_openapi_execute_tool,
    langfuse_openapi_list_operations_tool,
    langfuse_openapi_spec_tool,
    langfuse_prompt_delete_tool,
    langfuse_prompt_get_tool,
    langfuse_prompt_list_tool,
    langfuse_prompt_promote_label_tool,
    langfuse_prompt_upsert_tool,
    langfuse_score_create_tool,
    langfuse_trace_get_tool,
    langfuse_traces_list_tool,
    run_langfuse_control_message,
    run_tool_pipeline,
    user_reports_list_tool,
    user_report_detail_tool,
    report_session_turns_tool,
    trace_replay_tool,
    experiment_manager_tool,
    langfuse_eval_tool,
    response_comparison_tool,
)

ROOT = Path(__file__).resolve().parent.parent
SRC_DIR = Path(__file__).resolve().parent
ARTIFACTS = ROOT / "artifacts"
ARTIFACTS.mkdir(exist_ok=True)
MEMORIES_DIR = ROOT / "data" / "memories"
MEMORIES_DIR.mkdir(parents=True, exist_ok=True)
TOOL_PRESET_PATH = SRC_DIR / "config" / "agent_tool_profile_presets.json"

apply_initial_env_from_config()

_DEEP_AGENT = None
_DEEP_AGENT_ERROR: str | None = None
_DEEP_AGENT_INITIALIZED = False

# ---------------------------------------------------------------------------
# Model catalog & multi-provider registry
# ---------------------------------------------------------------------------

_MODEL_CATALOG: dict[str, dict] = {
    "openai": {
        "env_key": "OPENAI_API_KEY",
        "models": [
            {"id": "openai:gpt-4o", "label": "GPT-4o"},
            {"id": "openai:gpt-4o-mini", "label": "GPT-4o Mini"},
            {"id": "openai:gpt-4.1", "label": "GPT-4.1"},
            {"id": "openai:gpt-4.1-mini", "label": "GPT-4.1 Mini"},
            {"id": "openai:gpt-4.1-nano", "label": "GPT-4.1 Nano"},
        ],
    },
    "anthropic": {
        "env_key": "ANTHROPIC_API_KEY",
        "models": [
            {"id": "anthropic:claude-sonnet-4-20250514", "label": "Claude Sonnet 4"},
            {"id": "anthropic:claude-3-5-sonnet-latest", "label": "Claude 3.5 Sonnet"},
            {"id": "anthropic:claude-3-5-haiku-latest", "label": "Claude 3.5 Haiku"},
        ],
    },
    "google_genai": {
        "env_key": "GOOGLE_API_KEY",
        "alt_env_key": "GEMINI_API_KEY",
        "models": [
            {"id": "google_genai:gemini-2.5-flash-preview-05-20", "label": "Gemini 2.5 Flash"},
            {"id": "google_genai:gemini-2.0-flash", "label": "Gemini 2.0 Flash"},
        ],
    },
    "openrouter": {
        "env_key": "OPENROUTER_API_KEY",
        "models": [
            {"id": "openrouter:openai/gpt-4o", "label": "GPT-4o (OpenRouter)"},
            {"id": "openrouter:anthropic/claude-sonnet-4", "label": "Claude Sonnet 4 (OpenRouter)"},
            {"id": "openrouter:google/gemini-2.5-flash-preview", "label": "Gemini 2.5 Flash (OpenRouter)"},
            {"id": "openrouter:meta-llama/llama-4-maverick", "label": "Llama 4 Maverick (OpenRouter)"},
            {"id": "openrouter:deepseek/deepseek-r1", "label": "DeepSeek R1 (OpenRouter)"},
        ],
    },
    "ollama": {
        "env_key": "OLLAMA_BASE_URL",
        "models": [],
    },
    "vllm": {
        "env_key": "VLLM_BASE_URL",
        "models": [],
    },
    "lmstudio": {
        "env_key": "LMSTUDIO_BASE_URL",
        "models": [],
    },
}


def _fetch_local_models(provider: str, base_url: str) -> list[dict]:
    """Fetch model list from Ollama/vLLM/LM Studio local servers."""
    import urllib.request

    try:
        if provider == "ollama":
            url = f"{base_url.rstrip('/')}/api/tags"
            resp = urllib.request.urlopen(url, timeout=3)
            data = json.loads(resp.read())
            return [
                {"id": f"ollama:{m['name']}", "label": f"{m['name']} (Ollama)"}
                for m in data.get("models", [])
            ]
        else:
            url = f"{base_url.rstrip('/')}/v1/models"
            resp = urllib.request.urlopen(url, timeout=3)
            data = json.loads(resp.read())
            return [
                {"id": f"{provider}:{m['id']}", "label": f"{m['id']} ({provider})"}
                for m in data.get("data", [])
            ]
    except Exception:
        return []


def get_available_models() -> dict:
    """Return available models grouped by provider based on configured API keys."""
    groups: list[dict] = []
    default_model = _resolve_deep_model()
    default_id = default_model if isinstance(default_model, str) else None

    for provider, cfg in _MODEL_CATALOG.items():
        key = os.getenv(cfg["env_key"], "").strip()
        alt_key = os.getenv(cfg.get("alt_env_key", ""), "").strip() if cfg.get("alt_env_key") else ""
        if not key and not alt_key:
            continue

        if provider in ("ollama", "vllm", "lmstudio"):
            models = _fetch_local_models(provider, key or alt_key)
        else:
            models = list(cfg["models"])

        if models:
            groups.append({"provider": provider, "models": models})

    return {"groups": groups, "default": default_id}

def plan_task(message: str) -> str:
    return (
        "# Plan\n"
        f"- request: {message}\n"
        "- stack: deepagents(create_deep_agent)\n"
        "- features: skills + long_term_memory + subagents + composite_backends\n"
        "- profile: langfuse_trace_analysis\n"
        "- mode: hybrid"
    )


def _resolve_deep_model():
    explicit = os.getenv("AGENT_DEEP_MODEL", "").strip()
    if explicit:
        return explicit

    if os.getenv("OPENAI_API_KEY", "").strip():
        return f"openai:{os.getenv('AGENT_OPENAI_MODEL', 'gpt-4o-mini')}"
    if os.getenv("ANTHROPIC_API_KEY", "").strip():
        return f"anthropic:{os.getenv('AGENT_ANTHROPIC_MODEL', 'claude-3-5-sonnet-latest')}"
    if os.getenv("GOOGLE_API_KEY", "").strip() or os.getenv("GEMINI_API_KEY", "").strip():
        return f"google_genai:{os.getenv('AGENT_GEMINI_MODEL', 'gemini-2.0-flash')}"

    openrouter_key = os.getenv("OPENROUTER_API_KEY", "").strip()
    if openrouter_key:
        try:
            from langchain.chat_models import init_chat_model
        except Exception:
            return None
        return init_chat_model(
            model=os.getenv("OPENROUTER_MODEL", "openai/gpt-4o-mini"),
            model_provider="openai",
            api_key=openrouter_key,
            base_url=os.getenv("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1"),
            temperature=0,
        )
    return None


def run_langfuse_lens_query(message: str) -> str:
    """Run Langfuse Lens query and return markdown analysis."""
    return run_tool_pipeline(message)


def _with_mode(message: str, mode: str, *, force_compare: bool = False) -> str:
    text = str(message or "").strip()
    if not text:
        text = f"mode={mode}"
    elif re.search(r"\bmode=", text):
        text = re.sub(r"\bmode=[^\s]+", f"mode={mode}", text, count=1)
    else:
        text = f"{text} mode={mode}"

    if force_compare:
        if re.search(r"\bcompare_models=", text):
            text = re.sub(r"\bcompare_models=[^\s]+", "compare_models=true", text, count=1)
        else:
            text = f"{text} compare_models=true"
    return text


def run_lens_overview(message: str) -> str:
    """Run Lens overview mode."""
    return run_tool_pipeline(_with_mode(message, "overview"))


def run_lens_observability(message: str) -> str:
    """Run Lens observability deep-dive mode."""
    return run_tool_pipeline(_with_mode(message, "observability"))


def run_lens_prompt_engineering(message: str) -> str:
    """Run Lens prompt engineering mode."""
    return run_tool_pipeline(_with_mode(message, "prompt"))


def run_lens_evaluation(message: str) -> str:
    """Run Lens evaluation mode."""
    return run_tool_pipeline(_with_mode(message, "evaluation"))


def run_lens_model_comparison(message: str) -> str:
    """Run Lens model comparison mode."""
    return run_tool_pipeline(_with_mode(message, "overview", force_compare=True))


def run_lens_langfuse_control(message: str) -> str:
    """Run Langfuse control operation via SDK-first/http-fallback service."""
    try:
        result = run_langfuse_control_message(message)
    except Exception as exc:
        return (
            "# Langfuse Control Result\n"
            f"- ok: false\n"
            f"- error: {exc}\n"
            "- hint: provide JSON like "
            '{"operation":"prompt.get","params":{"name":"your.prompt","label":"latest"}}'
        )

    import json

    return (
        "# Langfuse Control Result\n"
        "- ok: true\n\n"
        "```json\n"
        f"{json.dumps(result, ensure_ascii=False, indent=2)}\n"
        "```"
    )


def _parse_csv(raw: str) -> list[str]:
    return [item.strip() for item in str(raw or "").split(",") if item.strip()]


def _memory_sources() -> list[str]:
    configured = _parse_csv(os.getenv("AGENT_DEEP_MEMORY_SOURCES", "").strip())
    if configured:
        return configured
    return ["/AGENTS.md", "/memories/AGENTS.md"]


def _skills_sources() -> list[str]:
    configured = _parse_csv(os.getenv("AGENT_DEEP_SKILLS_SOURCES", "").strip())
    if configured:
        return configured
    return [str(SRC_DIR / "skills")]


def _ensure_default_memory_file() -> None:
    memory_file = MEMORIES_DIR / "AGENTS.md"
    if memory_file.exists():
        return
    memory_file.write_text(
        (
            "# Long-term Memory (Langfuse Lens)\n\n"
            "- Store durable user/team preferences here.\n"
            "- Keep secrets/credentials out of this file.\n"
            "- Use concise bullet points for reusable operational patterns.\n"
        ),
        encoding="utf-8",
    )


def _env_bool(name: str, default: bool = False) -> bool:
    raw = os.getenv(name, "").strip().lower()
    if not raw:
        return default
    return raw in {"1", "true", "yes", "y", "on"}


def _env_int(name: str, default: int) -> int:
    raw = os.getenv(name, "").strip()
    if not raw:
        return default
    try:
        return int(raw)
    except Exception:
        return default


def _parse_bool(value: Any, default: bool = False) -> bool:
    if isinstance(value, bool):
        return value
    raw = str(value or "").strip().lower()
    if not raw:
        return default
    return raw in {"1", "true", "yes", "y", "on"}


def _load_tool_preset_catalog() -> dict[str, Any]:
    if not TOOL_PRESET_PATH.exists():
        return {"default_preset": "analyst-balanced", "presets": {}}
    try:
        payload = json.loads(TOOL_PRESET_PATH.read_text(encoding="utf-8"))
    except Exception:
        return {"default_preset": "analyst-balanced", "presets": {}}
    if not isinstance(payload, dict):
        return {"default_preset": "analyst-balanced", "presets": {}}
    default_preset = str(payload.get("default_preset") or "analyst-balanced").strip() or "analyst-balanced"
    presets = payload.get("presets")
    if not isinstance(presets, dict):
        presets = {}
    normalized: dict[str, dict[str, Any]] = {}
    for key, value in presets.items():
        if not isinstance(value, dict):
            continue
        normalized[str(key)] = dict(value)
    return {"default_preset": default_preset, "presets": normalized}


def _load_selected_tool_preset() -> tuple[str, dict[str, Any], dict[str, Any]]:
    catalog = _load_tool_preset_catalog()
    default_name = str(catalog.get("default_preset") or "analyst-balanced")
    selected_name = str(os.getenv("AGENT_LANGFUSE_TOOL_PRESET", default_name)).strip() or default_name
    presets = catalog.get("presets") if isinstance(catalog.get("presets"), dict) else {}
    selected = presets.get(selected_name)
    if not isinstance(selected, dict):
        selected_name = default_name
        selected = presets.get(selected_name)
    if not isinstance(selected, dict):
        selected = {}
    return selected_name, dict(selected), catalog


def get_agent_tool_presets() -> dict[str, Any]:
    selected_name, selected_preset, catalog = _load_selected_tool_preset()
    presets = catalog.get("presets") if isinstance(catalog.get("presets"), dict) else {}
    items: list[dict[str, Any]] = []
    for name, payload in presets.items():
        if not isinstance(payload, dict):
            continue
        items.append(
            {
                "name": str(name),
                "description": str(payload.get("description", "")).strip(),
                "profile": str(payload.get("profile", "")).strip(),
                "enable_control": _parse_bool(payload.get("enable_control"), default=False),
                "enable_openapi_execute": _parse_bool(payload.get("enable_openapi_execute"), default=False),
                "enable_mutation_tools": _parse_bool(payload.get("enable_mutation_tools"), default=False),
                "enable_dynamic_openapi": _parse_bool(payload.get("enable_dynamic_openapi"), default=False),
                "dynamic_include_mutating": _parse_bool(payload.get("dynamic_include_mutating"), default=False),
                "dynamic_max_tools": int(payload.get("dynamic_max_tools") or 0),
            }
        )
    items.sort(key=lambda item: item["name"])
    return {
        "default_preset": str(catalog.get("default_preset") or "analyst-balanced"),
        "selected_preset": selected_name,
        "selected": selected_preset,
        "presets": items,
    }


def _build_backend():
    from deepagents.backends import CompositeBackend, FilesystemBackend, LocalShellBackend, StateBackend

    mode = os.getenv("AGENT_DEEP_BACKEND_MODE", "filesystem").strip().lower()
    if mode in {"state", "statebackend", "state_backend"}:
        return StateBackend

    enable_shell = _env_bool("AGENT_DEEP_ENABLE_SHELL", default=True)
    if enable_shell:
        timeout = int(os.getenv("AGENT_DEEP_SHELL_TIMEOUT_SEC", "120"))
        workspace_backend = LocalShellBackend(root_dir=ROOT, virtual_mode=False, timeout=max(10, timeout))
    else:
        workspace_backend = FilesystemBackend(root_dir=ROOT, virtual_mode=False)
    memory_backend = FilesystemBackend(root_dir=MEMORIES_DIR, virtual_mode=False)
    return CompositeBackend(default=workspace_backend, routes={"/memories/": memory_backend})


def _agent_subagents(prompts: dict[str, str]) -> list[dict]:
    return [
        {
            "name": "lens-overview-analyst",
            "description": "Use for KPI-style overview and trace filter summaries.",
            "system_prompt": prompts["subagent_overview"],
            "tools": [run_lens_overview],
        },
        {
            "name": "lens-observability-investigator",
            "description": "Use for root-cause and observability deep-dive.",
            "system_prompt": prompts["subagent_observability"],
            "tools": [run_lens_observability],
        },
        {
            "name": "lens-prompt-engineer",
            "description": "Use for prompt rewrite, scoring, and recommended prompt.",
            "system_prompt": prompts["subagent_prompt"],
            "tools": [run_lens_prompt_engineering],
        },
        {
            "name": "lens-evaluation-analyst",
            "description": "Use for rubric/cohort/regression evaluation reports.",
            "system_prompt": prompts["subagent_evaluation"],
            "tools": [run_lens_evaluation],
        },
        {
            "name": "lens-model-comparison-analyst",
            "description": "Use for same-context multi-model comparison analysis.",
            "system_prompt": prompts["subagent_model_compare"],
            "tools": [run_lens_model_comparison],
        },
        {
            "name": "lens-langfuse-controller",
            "description": "Use for Langfuse control operations (prompt/traces/observations/http control).",
            "system_prompt": prompts["subagent_langfuse_control"],
            "tools": [run_lens_langfuse_control],
        },
    ]


def _normalize_profile_value(raw: str, default: str = "analyst") -> str:
    normalized_default = str(default or "analyst").strip().lower() or "analyst"
    raw = str(raw or "").strip().lower()
    if raw in {"readonly", "read_only", "read-only"}:
        return "readonly"
    if raw in {"admin", "owner", "ops"}:
        return "admin"
    if raw == "analyst":
        return "analyst"
    return normalized_default if normalized_default in {"readonly", "analyst", "admin"} else "analyst"


def _normalized_tool_profile(default: str = "analyst") -> str:
    raw = os.getenv("AGENT_LANGFUSE_TOOL_PROFILE", "").strip().lower()
    return _normalize_profile_value(raw, default=default)


def _mutation_tools_allowed() -> bool:
    return _env_bool("AGENT_LANGFUSE_CONTROL_ALLOW_MUTATION", default=False)


def _dedupe_tools(tools: list[Any]) -> list[Any]:
    seen: set[str] = set()
    deduped: list[Any] = []
    for tool in tools:
        name = str(getattr(tool, "name", "") or getattr(tool, "__name__", "")).strip()
        if not name:
            name = repr(tool)
        if name in seen:
            continue
        seen.add(name)
        deduped.append(tool)
    return deduped


def _build_agent_tools() -> tuple[list[Any], dict[str, Any]]:
    preset_name, preset, _catalog = _load_selected_tool_preset()
    preset_profile = _normalize_profile_value(str(preset.get("profile", "analyst")))
    profile = _normalized_tool_profile(default=preset_profile)
    mutation_allowed = _mutation_tools_allowed()

    preset_enable_control = _parse_bool(preset.get("enable_control"), default=(profile != "readonly"))
    preset_enable_openapi_execute = _parse_bool(preset.get("enable_openapi_execute"), default=(profile == "admin"))
    preset_enable_mutation_tools = _parse_bool(
        preset.get("enable_mutation_tools"),
        default=(profile == "admin" and mutation_allowed),
    )
    preset_enable_dynamic_openapi = _parse_bool(preset.get("enable_dynamic_openapi"), default=True)
    if profile == "readonly":
        preset_enable_dynamic_openapi = _parse_bool(preset.get("enable_dynamic_openapi"), default=False)
    preset_dynamic_include_mutating = _parse_bool(
        preset.get("dynamic_include_mutating"),
        default=(profile == "admin"),
    )
    preset_dynamic_max_tools = _env_int("AGENT_LANGFUSE_OPENAPI_DYNAMIC_TOOL_MAX", int(preset.get("dynamic_max_tools") or 300))

    include_mutation_tools = _env_bool(
        "AGENT_LANGFUSE_TOOLS_ENABLE_MUTATION",
        default=preset_enable_mutation_tools,
    )
    include_mutation_tools = include_mutation_tools and mutation_allowed

    enable_control = _env_bool("AGENT_LANGFUSE_TOOLS_ENABLE_CONTROL", default=preset_enable_control)
    enable_openapi_execute = _env_bool("AGENT_LANGFUSE_TOOLS_ENABLE_OPENAPI_EXECUTE", default=preset_enable_openapi_execute)
    enable_dynamic_openapi = _env_bool("AGENT_LANGFUSE_OPENAPI_DYNAMIC_TOOLS", preset_enable_dynamic_openapi)
    if profile == "readonly":
        enable_dynamic_openapi = _env_bool("AGENT_LANGFUSE_OPENAPI_DYNAMIC_TOOLS_READONLY", False)

    tools: list[Any] = [
        run_langfuse_lens_query,
        run_lens_overview,
        run_lens_observability,
        run_lens_prompt_engineering,
        run_lens_evaluation,
        run_lens_model_comparison,
        run_lens_langfuse_control,
        langfuse_traces_list_tool,
        langfuse_trace_get_tool,
        langfuse_observations_list_tool,
        langfuse_prompt_get_tool,
        langfuse_prompt_list_tool,
        langfuse_datasets_list_tool,
        langfuse_openapi_spec_tool,
        langfuse_openapi_list_operations_tool,
        langfuse_openapi_build_tools_tool,
        user_reports_list_tool,
        user_report_detail_tool,
        report_session_turns_tool,
        trace_replay_tool,
        experiment_manager_tool,
        langfuse_eval_tool,
        response_comparison_tool,
    ]

    if include_mutation_tools:
        tools.extend(
            [
                langfuse_score_create_tool,
                langfuse_annotation_create_tool,
                langfuse_prompt_upsert_tool,
                langfuse_prompt_delete_tool,
                langfuse_prompt_promote_label_tool,
                langfuse_datasets_create_tool,
                langfuse_dataset_items_upsert_tool,
                langfuse_datasets_import_from_traces_tool,
            ]
        )

    if enable_control:
        tools.extend(
            [
                langfuse_control_execute_tool,
                langfuse_control_message_tool,
            ]
        )

    if enable_openapi_execute:
        tools.append(langfuse_openapi_execute_tool)

    dynamic_openapi_tools: list[Any] = []
    include_dynamic_mutating_value: bool | None = None
    if enable_dynamic_openapi:
        include_dynamic_mutating = os.getenv("AGENT_LANGFUSE_OPENAPI_DYNAMIC_INCLUDE_MUTATING", "").strip().lower()
        if include_dynamic_mutating in {"1", "true", "yes", "y", "on"}:
            include_dynamic_mutating_value = True
        elif include_dynamic_mutating in {"0", "false", "no", "n", "off"}:
            include_dynamic_mutating_value = False
        else:
            include_dynamic_mutating_value = preset_dynamic_include_mutating
        if include_mutation_tools is False:
            include_dynamic_mutating_value = False
        max_tools = preset_dynamic_max_tools
        try:
            dynamic_openapi_tools = build_langfuse_openapi_operation_tools(
                refresh=False,
                include_mutating=include_dynamic_mutating_value,
                max_tools=max_tools,
            )
        except Exception:
            dynamic_openapi_tools = []
        tools.extend(dynamic_openapi_tools)

    deduped = _dedupe_tools(tools)
    summary = {
        "profile": profile,
        "preset_name": preset_name,
        "preset_profile": preset_profile,
        "mutation_allowed": mutation_allowed,
        "include_mutation_tools": include_mutation_tools,
        "enable_control": enable_control,
        "enable_openapi_execute": enable_openapi_execute,
        "enable_dynamic_openapi": enable_dynamic_openapi,
        "dynamic_include_mutating": include_dynamic_mutating_value if enable_dynamic_openapi else False,
        "dynamic_max_tools": preset_dynamic_max_tools,
        "dynamic_openapi_tool_count": len(dynamic_openapi_tools),
        "tool_count": len(deduped),
    }
    return deduped, summary


def get_agent_tool_runtime_summary() -> dict[str, Any]:
    tools, summary = _build_agent_tools()
    summary = dict(summary)
    summary["tools"] = [
        {
            "name": str(getattr(tool, "name", "") or getattr(tool, "__name__", "")),
            "description": str(getattr(tool, "description", "") or getattr(tool, "__doc__", "")),
        }
        for tool in tools
    ]
    return summary


def get_agent_state_runtime_summary() -> dict[str, Any]:
    checkpointer, checkpointer_error = _get_checkpointer()
    sqlite_path = _resolve_checkpointer_sqlite_path()
    return {
        "enabled": _env_bool("AGENT_DEEP_STATE_ENABLED", default=True),
        "required": _env_bool("AGENT_DEEP_STATE_REQUIRED", default=False),
        "backend_mode": os.getenv("AGENT_DEEP_BACKEND_MODE", "filesystem").strip().lower() or "filesystem",
        "sqlite_path": str(sqlite_path),
        "ready": checkpointer is not None and not checkpointer_error,
        "checkpointer_class": checkpointer.__class__.__name__ if checkpointer is not None else None,
        "error": checkpointer_error,
    }


def list_agent_state_checkpoints(thread_id: str, limit: int = 20) -> dict[str, Any]:
    checkpointer, checkpointer_error = _get_checkpointer()
    resolved_thread_id = str(thread_id or "").strip()
    if not resolved_thread_id:
        raise ValueError("thread_id is required")
    if checkpointer is None:
        raise RuntimeError(checkpointer_error or "checkpointer not initialized")

    config = {"configurable": {"thread_id": resolved_thread_id}}
    rows = []
    for item in checkpointer.list(config=config, limit=max(1, int(limit))):
        item_config = item.config if isinstance(getattr(item, "config", None), dict) else {}
        item_parent = item.parent_config if isinstance(getattr(item, "parent_config", None), dict) else {}
        item_meta = item.metadata if isinstance(getattr(item, "metadata", None), dict) else {}
        item_checkpoint = item.checkpoint if isinstance(getattr(item, "checkpoint", None), dict) else {}
        rows.append(
            {
                "checkpoint_id": str(item_config.get("configurable", {}).get("checkpoint_id") or ""),
                "parent_checkpoint_id": str(item_parent.get("configurable", {}).get("checkpoint_id") or ""),
                "created_at": item_meta.get("created_at"),
                "source": item_meta.get("source"),
                "step": item_meta.get("step"),
                "channel_versions": item_checkpoint.get("channel_versions", {}),
            }
        )

    return {
        "thread_id": resolved_thread_id,
        "count": len(rows),
        "checkpoints": rows,
    }


def _resolve_model_from_spec(spec: str):
    """Convert 'provider:model_name' spec to init_chat_model-compatible form."""
    parts = spec.split(":", 1)
    if len(parts) != 2:
        return spec
    provider, model_name = parts

    if provider == "openrouter":
        from langchain.chat_models import init_chat_model

        return init_chat_model(
            model=model_name,
            model_provider="openai",
            api_key=os.getenv("OPENROUTER_API_KEY", ""),
            base_url=os.getenv("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1"),
            temperature=0,
        )
    if provider == "ollama":
        from langchain.chat_models import init_chat_model

        base_url = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")
        return init_chat_model(
            model=model_name,
            model_provider="openai",
            api_key="ollama",
            base_url=f"{base_url.rstrip('/')}/v1",
            temperature=0,
        )
    if provider in ("vllm", "lmstudio"):
        from langchain.chat_models import init_chat_model

        env_key = "VLLM_BASE_URL" if provider == "vllm" else "LMSTUDIO_BASE_URL"
        default_url = "http://localhost:8000" if provider == "vllm" else "http://localhost:1234"
        base_url = os.getenv(env_key, default_url)
        return init_chat_model(
            model=model_name,
            model_provider="openai",
            api_key=os.getenv(f"{provider.upper()}_API_KEY", "none"),
            base_url=f"{base_url.rstrip('/')}/v1",
            temperature=0,
        )
    # Direct providers (openai, anthropic, google_genai)
    return spec


def _build_deep_agent(model_spec: str | None = None):
    if model_spec:
        model = _resolve_model_from_spec(model_spec)
    else:
        model = _resolve_deep_model()
    if not model:
        return (
            None,
            "LLM API key/model not configured for create_deep_agent. "
            "Set AGENT_DEEP_MODEL or one of OPENAI_API_KEY/ANTHROPIC_API_KEY/GOOGLE_API_KEY/GEMINI_API_KEY/OPENROUTER_API_KEY.",
        )

    try:
        from deepagents import create_deep_agent
    except Exception as exc:
        return None, f"deepagents import failed: {exc}"

    _ensure_default_memory_file()

    backend = _build_backend()
    skills = _skills_sources()
    memory = _memory_sources()
    prompt_bundle = _resolve_prompt_bundle()
    subagents = _agent_subagents(prompt_bundle)
    checkpointer, checkpointer_error = _get_checkpointer()
    if checkpointer is None and _env_bool("AGENT_DEEP_STATE_REQUIRED", default=False):
        return None, checkpointer_error or "state checkpointer is required but unavailable"

    system_prompt = prompt_bundle["orchestrator_system"]
    tools, _tool_summary = _build_agent_tools()

    try:
        agent = create_deep_agent(
            model=model,
            tools=tools,
            system_prompt=system_prompt,
            subagents=subagents,
            skills=skills,
            memory=memory,
            checkpointer=checkpointer,
            backend=backend,
            name="langfuse-lens-deep-agent",
        )
    except Exception as exc:
        return None, f"create_deep_agent failed: {exc}"

    return agent, None


_DEEP_AGENT_CACHE: dict[str, tuple[Any, str | None]] = {}
_DEFAULT_MODEL_SPEC: str | None = None


def _get_deep_agent(model_override: str | None = None):
    global _DEEP_AGENT_INITIALIZED, _DEEP_AGENT, _DEEP_AGENT_ERROR, _DEFAULT_MODEL_SPEC

    if model_override:
        cache_key = model_override
    else:
        # Use legacy singleton path for default model
        if _DEEP_AGENT_INITIALIZED:
            return _DEEP_AGENT, _DEEP_AGENT_ERROR
        _DEEP_AGENT, _DEEP_AGENT_ERROR = _build_deep_agent()
        _DEEP_AGENT_INITIALIZED = True
        return _DEEP_AGENT, _DEEP_AGENT_ERROR

    # Model override path — use per-model cache
    if cache_key in _DEEP_AGENT_CACHE:
        return _DEEP_AGENT_CACHE[cache_key]

    agent, error = _build_deep_agent(model_spec=model_override)
    _DEEP_AGENT_CACHE[cache_key] = (agent, error)
    # Cap cache size at 5 entries
    if len(_DEEP_AGENT_CACHE) > 5:
        oldest_key = next(iter(_DEEP_AGENT_CACHE))
        if oldest_key != cache_key:
            del _DEEP_AGENT_CACHE[oldest_key]
    return agent, error


def _stringify_content(content: Any) -> str:
    if content is None:
        return ""
    if isinstance(content, str):
        return content.strip()
    if isinstance(content, list):
        chunks: list[str] = []
        for item in content:
            if isinstance(item, str):
                if item.strip():
                    chunks.append(item.strip())
                continue
            if isinstance(item, dict):
                text = str(item.get("text", "")).strip()
                if text:
                    chunks.append(text)
        return "\n".join(chunks).strip()
    return str(content).strip()


def _extract_agent_output(result: Any) -> str:
    messages = result.get("messages") if isinstance(result, dict) else getattr(result, "messages", None)
    if isinstance(messages, list):
        for msg in reversed(messages):
            role = ""
            if isinstance(msg, dict):
                role = str(msg.get("role") or msg.get("type") or "").lower()
                if role in {"assistant", "ai"}:
                    text = _stringify_content(msg.get("content"))
                    if text:
                        return text
            else:
                role = str(getattr(msg, "type", "") or getattr(msg, "role", "")).lower()
                if role in {"assistant", "ai"}:
                    text = _stringify_content(getattr(msg, "content", None))
                    if text:
                        return text
    return _stringify_content(result)


def _agent_local_commands_enabled() -> bool:
    return _env_bool("AGENT_LOCAL_COMMANDS_ENABLED", default=True)


def _render_json_markdown(title: str, payload: dict[str, Any]) -> str:
    return (
        f"# {title}\n\n"
        "```json\n"
        f"{json.dumps(payload, ensure_ascii=False, indent=2)}\n"
        "```"
    )


def _handle_local_agent_command(message: str) -> str | None:
    if not _agent_local_commands_enabled():
        return None
    text = str(message or "").strip()
    if not text:
        return None
    normalized = text.lower()

    if normalized in {"/agent", "/agent help", "agent help"}:
        return (
            "# Agent Command Help\n"
            "- `/agent help`: show this help\n"
            "- `/agent tools`: current runtime tool summary\n"
            "- `/agent presets`: available tool presets and selected preset\n"
            "- `/agent diagnose`: presets + runtime summary + dynamic tool counts\n"
        )
    if normalized in {"/agent tools", "agent tools"}:
        return _render_json_markdown("Agent Runtime Tools", get_agent_tool_runtime_summary())
    if normalized in {"/agent presets", "agent presets"}:
        return _render_json_markdown("Agent Tool Presets", get_agent_tool_presets())
    if normalized in {"/agent diagnose", "/agent diagnostics", "agent diagnose", "agent diagnostics"}:
        return _render_json_markdown(
            "Agent Diagnostics",
            {
                "presets": get_agent_tool_presets(),
                "runtime": get_agent_tool_runtime_summary(),
            },
        )
    return None


def _resolve_thread_id(thread_id: str | None) -> str:
    raw = str(thread_id or "").strip()
    if raw:
        return raw[:160]
    return f"thread_{uuid4().hex[:24]}"


def run(
    message: str,
    *,
    thread_id: str | None = None,
    user_id: str | None = None,
    langfuse_settings: dict | None = None,
    model: str | None = None,
) -> str:
    if langfuse_settings:
        set_langfuse_settings_override(langfuse_settings)
    try:
        return _run_inner(message, thread_id=thread_id, user_id=user_id, model=model)
    finally:
        set_langfuse_settings_override(None)


def _run_inner(
    message: str,
    *,
    thread_id: str | None = None,
    user_id: str | None = None,
    model: str | None = None,
) -> str:
    plan = plan_task(message)
    (ARTIFACTS / "request.md").write_text(plan, encoding="utf-8")

    output: str
    resolved_thread_id = _resolve_thread_id(thread_id)
    local_command_output = _handle_local_agent_command(message)
    if local_command_output is not None:
        (ARTIFACTS / "state.json").write_text(
            json.dumps(
                {
                    "thread_id": resolved_thread_id,
                    "user_id": str(user_id or "").strip() or None,
                    "local_command": True,
                },
                ensure_ascii=False,
                indent=2,
            ),
            encoding="utf-8",
        )
        final = f"# Final Output\n{local_command_output}"
        (ARTIFACTS / "final.md").write_text(final, encoding="utf-8")
        return final

    agent, error = _get_deep_agent(model_override=model)
    if agent is None:
        output = (
            "Deep Agent initialization failed.\n"
            f"- reason: {error or 'unknown error'}\n"
            "- note: fallback pipeline has been intentionally disabled.\n"
        )
    else:
        try:
            config = {"configurable": {"thread_id": resolved_thread_id}}
            resolved_user_id = str(user_id or "").strip()
            if resolved_user_id:
                config["configurable"]["user_id"] = resolved_user_id
            result = agent.invoke({"messages": [{"role": "user", "content": message}]}, config=config)
            output = _extract_agent_output(result) or "(Deep Agent returned empty output)"
        except Exception as exc:
            output = (
                "Deep Agent execution failed.\n"
                f"- reason: {exc}\n"
                "- note: fallback pipeline has been intentionally disabled.\n"
            )

    (ARTIFACTS / "state.json").write_text(
        json.dumps(
            {
                "thread_id": resolved_thread_id,
                "user_id": str(user_id or "").strip() or None,
                "state": get_agent_state_runtime_summary(),
            },
            ensure_ascii=False,
            indent=2,
        ),
        encoding="utf-8",
    )
    final = f"# Final Output\n{output}"
    (ARTIFACTS / "final.md").write_text(final, encoding="utf-8")
    return final
