import ast as _ast
import json
import re
import shutil as _shutil
import sys
from datetime import datetime
from pathlib import Path

from backend.config import (
    MEMORY_DIR,
    TOOLS_DIR,
    WORKSPACE_ROOT,
    _BUILTIN_TOOL_NAMES,
    _load_settings,
    _normalize_tool_name,
    _save_settings,
)


_dynamic_tools: dict = {}
_CUSTOM_TOOL_HEALTH_FILE = MEMORY_DIR / "custom_tool_health.json"


def _tool_exec_globals(file_path: Path) -> dict:
    """Build isolated globals for dynamic tool exec with stable import paths."""
    try:
        root = str(WORKSPACE_ROOT.resolve())
    except Exception:
        root = str(WORKSPACE_ROOT)
    try:
        tools = str(TOOLS_DIR.resolve())
    except Exception:
        tools = str(TOOLS_DIR)
    try:
        tool_parent = str(file_path.resolve().parent)
    except Exception:
        tool_parent = str(file_path.parent)

    for path_value in (root, tools, tool_parent):
        if path_value and path_value not in sys.path:
            sys.path.insert(0, path_value)

    return {
        "__name__": f"dynamic_tool_{file_path.stem}",
        "__file__": str(file_path),
    }


def _load_custom_tool_health() -> dict:
    try:
        if _CUSTOM_TOOL_HEALTH_FILE.exists():
            data = json.loads(_CUSTOM_TOOL_HEALTH_FILE.read_text(encoding="utf-8"))
            if isinstance(data, dict):
                return data
    except Exception:
        pass
    return {"version": 1, "tools": {}}


def _save_custom_tool_health(store: dict) -> None:
    try:
        MEMORY_DIR.mkdir(parents=True, exist_ok=True)
        _CUSTOM_TOOL_HEALTH_FILE.write_text(
            json.dumps(store, ensure_ascii=False, indent=2),
            encoding="utf-8",
        )
    except Exception:
        pass


def _log_skill_health_event(message: str, details: dict | None = None) -> None:
    try:
        from backend.memory import log_system_event

        log_system_event("learning", message[:250], details or {})
    except Exception:
        pass


def _set_tool_enabled(tool_name: str, enabled: bool) -> None:
    try:
        settings = _load_settings()
        tool_map = dict(settings.get("tool_enabled_map") or {})
        tool_map[_normalize_tool_name(tool_name)] = bool(enabled)
        settings["tool_enabled_map"] = tool_map
        _save_settings(settings)
    except Exception:
        pass


def _record_custom_tool_health(
    tool_name: str,
    *,
    ok: bool,
    stage: str,
    reason: str = "",
    allow_quarantine: bool = True,
) -> bool:
    """Record tool health. Returns True if the tool was quarantined by this call."""
    name = _normalize_tool_name(tool_name)
    if not name:
        return False
    store = _load_custom_tool_health()
    tools = store.get("tools") if isinstance(store.get("tools"), dict) else {}
    row = tools.get(name) if isinstance(tools.get(name), dict) else {}

    now_iso = datetime.now().isoformat()
    row["last_stage"] = str(stage or "runtime")[:40]
    row["last_seen"] = now_iso
    row["last_reason"] = str(reason or "")[:300]
    row["attempts"] = int(row.get("attempts") or 0) + 1

    should_quarantine = False
    if ok:
        row["successes"] = int(row.get("successes") or 0) + 1
        row["consecutive_runtime_failures"] = 0
        row["consecutive_quality_issues"] = 0
        row["quarantined"] = False
        row["requires_hitl_update"] = False
        _set_tool_enabled(name, True)
    else:
        stage_low = str(stage or "").lower()
        if stage_low in {"quality", "validator"}:
            row["consecutive_quality_issues"] = int(row.get("consecutive_quality_issues") or 0) + 1
        else:
            row["consecutive_runtime_failures"] = int(row.get("consecutive_runtime_failures") or 0) + 1

        settings = _load_settings()
        runtime_threshold = int(settings.get("custom_tool_runtime_failure_threshold", 3) or 3)
        quality_threshold = int(settings.get("custom_tool_quality_issue_threshold", 3) or 3)
        auto_disable = bool(settings.get("custom_tool_auto_disable_on_failure", True))
        hitl_required = bool(settings.get("custom_tool_require_hitl_for_updates", True))

        should_quarantine = allow_quarantine and auto_disable and (
            int(row.get("consecutive_runtime_failures") or 0) >= max(1, runtime_threshold)
            or int(row.get("consecutive_quality_issues") or 0) >= max(1, quality_threshold)
        )
        if should_quarantine:
            row["quarantined"] = True
            row["requires_hitl_update"] = hitl_required
            _set_tool_enabled(name, False)
            _log_skill_health_event(
                f"Custom skill '{name}' quarantined after repeated failures",
                {
                    "tool": name,
                    "stage": stage_low,
                    "runtime_failures": str(row.get("consecutive_runtime_failures") or 0),
                    "quality_issues": str(row.get("consecutive_quality_issues") or 0),
                    "reason": str(reason or "")[:220],
                },
            )

    tools[name] = row
    store["tools"] = tools
    _save_custom_tool_health(store)
    if ok:
        return False
    return bool(should_quarantine)


def record_custom_tool_feedback(tool_name: str, verdict: str, reason: str = "") -> bool:
    """Called by orchestration flow to report validator quality outcomes for custom tools."""
    name = _normalize_tool_name(tool_name)
    if name not in _dynamic_tools:
        return False
    bad = str(verdict or "").upper().startswith("ISSUE")
    return _record_custom_tool_health(
        name,
        ok=not bad,
        stage="quality",
        reason=(reason or verdict or "")[:300],
        allow_quarantine=True,
    )


def _load_dynamic_tools() -> dict:
    """Load / hot-reload all custom tools from TOOLS_DIR."""
    loaded: dict = {}
    for file_path in sorted(TOOLS_DIR.rglob("*.py")):
        if file_path.name.startswith("_"):
            continue
        if any(part.startswith(".") for part in file_path.parts):
            continue
        try:
            source = file_path.read_text(encoding="utf-8")
            namespace = _tool_exec_globals(file_path)
            exec(compile(source, str(file_path), "exec"), namespace)  # noqa: S102
            name = str(namespace.get("TOOL_NAME", "")).strip()
            if not name:
                continue
            loaded[name] = {
                "description": str(namespace.get("TOOL_DESCRIPTION", "Custom tool.")).strip(),
                "args_desc": str(namespace.get("TOOL_ARGS", "{}")).strip(),
                "source": source,
                "file": file_path,
                "_compiled_run": namespace.get("run"),
            }
        except Exception as exc:
            _record_custom_tool_health(
                file_path.stem,
                ok=False,
                stage="load",
                reason=f"{type(exc).__name__}: {exc}",
            )
            print(f"[tools] Failed to load {file_path.name}: {exc}")
    return loaded


def _reload_dynamic_tools() -> None:
    loaded = _load_dynamic_tools()
    _dynamic_tools.clear()
    _dynamic_tools.update(loaded)
    names = ", ".join(_dynamic_tools) or "none"
    print(f"[tools] {len(_dynamic_tools)} custom tool(s) loaded: {names}")


def _execute_dynamic_tool(name: str, args: dict) -> str:
    """Execute a custom skill using its cached compiled run() function."""
    name = _normalize_tool_name(name)
    tool = _dynamic_tools.get(name)
    if not tool:
        return f"Unknown skill: {name}"
    health = _load_custom_tool_health()
    row = ((health.get("tools") or {}).get(name) or {}) if isinstance(health, dict) else {}
    if bool(row.get("quarantined")):
        return (
            f"TOOL_DISABLED: Skill '{name}' is quarantined after repeated failures. "
            "Update/fix the skill with approval before re-enabling."
        )
    try:
        run_fn = tool.get("_compiled_run")
        if run_fn is None:
            namespace = _tool_exec_globals(tool["file"])
            exec(compile(tool["source"], str(tool["file"]), "exec"), namespace)  # noqa: S102
            run_fn = namespace["run"]
            tool["_compiled_run"] = run_fn
        result = str(run_fn(args))
        looks_bad = result.startswith(("ERROR", "TOOL_DISABLED", "HITL_REQUIRED", "POLICY_DENIED"))
        _record_custom_tool_health(
            name,
            ok=not looks_bad,
            stage="runtime",
            reason=(result[:280] if looks_bad else ""),
            allow_quarantine=True,
        )
        return result
    except Exception as exc:
        tool.pop("_compiled_run", None)
        _record_custom_tool_health(name, ok=False, stage="runtime", reason=f"{type(exc).__name__}: {exc}")
        return f"Skill '{name}' error: {exc}"


def _create_or_update_skill(
    *,
    raw_name: str,
    description: str,
    args_schema: str,
    code: str,
    overwrite: bool,
    approved: bool,
    require_existing: bool,
    op_label: str,
) -> str:
    name = re.sub(r"[^\w]", "_", str(raw_name or "").strip().lower())
    description = str(description or "").strip()
    args_schema = str(args_schema or "").strip()
    code = str(code or "").strip()
    if not name:
        return f"{op_label}: 'name' is required."
    if name in _BUILTIN_TOOL_NAMES:
        return (
            f"{op_label}: '{name}' is an existing built-in skill and cannot be modified this way. "
            "If it is disabled, enable it in Settings > Skills instead of creating a copy skill."
        )

    tool_file = TOOLS_DIR / f"{name}.py"
    is_update = tool_file.exists()
    if require_existing and not is_update:
        return f"{op_label}: skill '{name}' does not exist. Use create_skill first."
    if is_update and not overwrite:
        return (
            f"{op_label}: skill '{name}' already exists. "
            'Pass "overwrite": "true" to replace it.'
        )
    if (not is_update) and require_existing:
        return f"{op_label}: skill '{name}' does not exist."

    existing = _dynamic_tools.get(name) or {}
    if is_update and not description:
        description = str(existing.get("description") or "Custom tool.").strip()
    if is_update and not args_schema:
        args_schema = str(existing.get("args_desc") or "{}").strip()
    if not args_schema:
        args_schema = "{}"

    if "def run(args" not in code:
        return f"{op_label}: 'code' must contain a 'def run(args: dict) -> str:' function."
    try:
        _ast.parse(code)
    except SyntaxError as syntax_error:
        return f"{op_label}: syntax error in code — {syntax_error}"

    if is_update and bool(_load_settings().get("custom_tool_require_hitl_for_updates", True)) and not approved:
        return (
            f"HITL_REQUIRED: updating existing skill '{name}' requires user approval. "
            f"Re-run {op_label} with approved=true after explicit approval."
        )

    if is_update:
        _shutil.copy2(tool_file, tool_file.with_suffix(".py.bak"))
    file_content = (
        f'TOOL_NAME = "{name}"\n'
        f'TOOL_DESCRIPTION = """{description}"""\n'
        f"TOOL_ARGS = '{args_schema}'\n\n"
        f"{code}\n"
    )
    tool_file.write_text(file_content, encoding="utf-8")
    try:
        probe_namespace = _tool_exec_globals(tool_file)
        exec(compile(file_content, str(tool_file), "exec"), probe_namespace)  # noqa: S102
        probe_namespace["run"]({})
    except Exception as probe_error:
        _record_custom_tool_health(name, ok=False, stage="probe", reason=f"{type(probe_error).__name__}: {probe_error}")
        tool_file.unlink(missing_ok=True)
        if is_update:
            _shutil.copy2(tool_file.with_suffix(".py.bak"), tool_file)
        return (
            f"{op_label}: probe-run failed — {probe_error}. "
            "Fix the error and try again. Previous version restored."
        )

    _record_custom_tool_health(name, ok=True, stage="update", reason="probe-pass", allow_quarantine=False)
    _reload_dynamic_tools()
    verb = "updated" if is_update else "created"
    bak_note = " Previous version saved as .py.bak." if is_update else ""
    return (
        f"Skill '{name}' {verb}, saved to tools/{name}.py and registered."
        f"{bak_note} Available immediately and in all future sessions."
    )