"""
lambdagent.providers.claude_code_native — 原生 Claude-Code 执行车道

工作区对话型 agent(workspace.assistant)专用:让 claude-code **当 agent 本身**
在所选文件夹里用 native 工具干活,而不是把它阉割成纯文本生成器套进 react-over-text
封装。见 docs/NATIVE_CLAUDE_CODE_DESIGN.md。

与 claude_code_provider(react 车道)的区别:
  · react 车道:`claude -p --tools ""` → 纯文本 → 平台正则抠 {"tool"} 自己执行。
  · native 车道(本模块):`claude -p --output-format stream-json` + native 工具开
    → claude-code 自己结构化执行工具、自管上下文 → 平台只解析事件流 + 落盘。
"""
from __future__ import annotations

import json
import os
import select
import subprocess
import time
from dataclasses import dataclass, field
from typing import Callable, Dict, List, Optional

from lambdagent.providers.base import ProviderError
from lambdagent.providers.claude_code_provider import (
    _find_working_claude, _is_auth_error, _AUTH_HINT, logger,
)

# 默认工具白名单:单体助手该有的全套(读/写/改/跑/搜/笔记/待办)。可经 config.extra 覆盖。
_DEFAULT_ALLOWED_TOOLS = [
    "Read", "Write", "Edit", "Bash", "Glob", "Grep", "NotebookEdit", "TodoWrite",
]
_DEFAULT_FIRST_BYTE_TIMEOUT_S = 90.0   # 原生长跑首个事件可能稍慢(要先想)
# idle 只在**没有工具在飞**时生效(模型 turn 间静默)。工具在飞时(Bash 跑实验/
# pdflatex 编译可达数分钟,期间 stream-json 完全静默)不按 idle 杀——靠硬超时兜底。
_DEFAULT_IDLE_TIMEOUT_S = 300.0        # 模型 turn 间最长静默(无工具在飞)
_DEFAULT_HARD_TIMEOUT_S = 3600.0       # 60 分钟总硬顶(重任务:重做实验+出 PDF)


@dataclass
class NativeResult:
    text: str = ""
    session_id: Optional[str] = None
    input_tokens: int = 0
    output_tokens: int = 0
    cache_read_tokens: int = 0
    cache_creation_tokens: int = 0
    cost_usd: float = 0.0
    num_turns: int = 0
    is_error: bool = False
    trace: List[dict] = field(default_factory=list)
    pending_tools: int = 0   # 在飞工具数(tool_use 未等到 tool_result);>0 时禁 idle 超时


def build_cmd(claude_bin: str, prompt: str, *, cwd: str, model: str,
              system_append: str = "", session_id: Optional[str] = None,
              allowed_tools: Optional[List[str]] = None,
              strict_mcp: bool = True) -> List[str]:
    """构造 native claude-code 命令。cwd 由调用方在 spawn 时设置。"""
    cmd = [
        claude_bin, "-p", prompt,
        "--output-format", "stream-json", "--verbose",
        "--model", model,
        # native 工具限定在工作文件夹(claude-code 的文件操作 root)。
        "--add-dir", cwd,
        # -p 非交互:必须 bypass,否则工具卡在权限审批等不到回应。
        "--permission-mode", "bypassPermissions",
        "--allowedTools", *(allowed_tools or _DEFAULT_ALLOWED_TOOLS),
    ]
    if strict_mcp:
        cmd.append("--strict-mcp-config")  # 隔离用户 MCP(Gmail/Calendar 等)
    if system_append:
        cmd += ["--append-system-prompt", system_append]
    if session_id:
        cmd += ["--resume", session_id]
    return cmd


def _emit(on_event, etype: str, payload: dict):
    if on_event is not None:
        try:
            on_event({"event": etype, **payload})
        except Exception as e:  # 回调异常不能拖垮主流程
            logger.warning("native on_event(%s) failed: %s", etype, e)


def _handle_event(obj: dict, on_event, result: NativeResult):
    """把一条 claude stream-json 事件映射成平台 SSE 事件 + 累积结果。"""
    t = obj.get("type")
    if t == "system" and obj.get("subtype") == "init":
        result.session_id = obj.get("session_id") or result.session_id
        return
    if t == "assistant":
        for c in (obj.get("message", {}) or {}).get("content", []) or []:
            if not isinstance(c, dict):
                continue
            if c.get("type") == "text" and c.get("text"):
                _emit(on_event, "think_chunk", {"text": c["text"]})
            elif c.get("type") == "tool_use":
                result.pending_tools += 1   # 工具开飞 → 禁 idle 超时直到结果回来
                _emit(on_event, "tool_call",
                      {"tool": c.get("name", ""), "input": c.get("input", {})})
                result.trace.append({"tool": c.get("name", ""),
                                     "input": str(c.get("input", ""))[:300]})
        return
    if t == "user":
        for c in (obj.get("message", {}) or {}).get("content", []) or []:
            if isinstance(c, dict) and c.get("type") == "tool_result":
                result.pending_tools = max(0, result.pending_tools - 1)
                out = c.get("content", "")
                if isinstance(out, list):
                    out = " ".join(x.get("text", "") for x in out if isinstance(x, dict))
                _emit(on_event, "tool_result", {"output": str(out)[:2000]})
        return
    if t == "result":
        result.is_error = bool(obj.get("is_error"))
        result.text = str(obj.get("result", "") or "")
        result.session_id = obj.get("session_id") or result.session_id
        result.num_turns = obj.get("num_turns", 0) or 0
        result.cost_usd = float(obj.get("total_cost_usd", 0) or 0)
        u = obj.get("usage", {}) or {}
        result.input_tokens = u.get("input_tokens", 0) or 0
        result.output_tokens = u.get("output_tokens", 0) or 0
        result.cache_read_tokens = u.get("cache_read_input_tokens", 0) or 0
        result.cache_creation_tokens = u.get("cache_creation_input_tokens", 0) or 0


def run_native(prompt: str, *, cwd: str, model: str = "sonnet",
               system_append: str = "", session_id: Optional[str] = None,
               on_event: Optional[Callable[[dict], None]] = None,
               config_extra: Optional[dict] = None,
               cancel=None) -> NativeResult:
    """在 cwd 里原生跑一次 claude-code,流式回调事件,返回最终结果。

    auth(401)→ 抛 ProviderError(_AUTH_HINT)。卡死/超时 → 抛 ProviderError(retryable)。
    """
    extra = config_extra or {}
    claude_bin = _find_working_claude(extra.get("claude_bin", "claude")) or "claude"
    allowed = extra.get("native_allowed_tools") or _DEFAULT_ALLOWED_TOOLS
    strict_mcp = not bool(extra.get("native_allow_mcp", False))
    cmd = build_cmd(claude_bin, prompt, cwd=cwd, model=model,
                    system_append=system_append, session_id=session_id,
                    allowed_tools=allowed, strict_mcp=strict_mcp)

    first_byte_timeout = float(extra.get("native_first_byte_timeout", _DEFAULT_FIRST_BYTE_TIMEOUT_S))
    idle_timeout = float(extra.get("native_idle_timeout", _DEFAULT_IDLE_TIMEOUT_S))
    hard_timeout = float(extra.get("native_hard_timeout", _DEFAULT_HARD_TIMEOUT_S))

    logger.info("claude-code NATIVE spawn: cwd=%s model=%s resume=%s tools=%s strict_mcp=%s",
                cwd, model, bool(session_id), ",".join(allowed), strict_mcp)

    result = NativeResult()
    t0 = time.time()
    proc = subprocess.Popen(
        cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
        bufsize=0,  # 二进制非阻塞读,不用行缓冲(bufsize=1 在 binary 模式会告警)
    )
    buf = b""
    last_byte_at: Optional[float] = None
    err_chunks: List[bytes] = []
    saw_result = False

    def _process_line(line: str):
        nonlocal saw_result
        line = line.strip()
        if not line:
            return
        try:
            obj = json.loads(line)
        except json.JSONDecodeError:
            return
        _handle_event(obj, on_event, result)
        if obj.get("type") == "result":
            saw_result = True

    try:
        import fcntl
        for f in (proc.stdout, proc.stderr):
            fl = fcntl.fcntl(f, fcntl.F_GETFL)
            fcntl.fcntl(f, fcntl.F_SETFL, fl | os.O_NONBLOCK)

        while proc.poll() is None:
            now = time.time()
            if cancel is not None and cancel.is_cancelled():
                proc.kill()
                raise cancel.CancelledRun()
            if now - t0 > hard_timeout:
                proc.kill()
                raise ProviderError(
                    f"claude-code native hard-timeout after {now-t0:.0f}s", "claude-code",
                    retryable=True)
            if last_byte_at is None and now - t0 > first_byte_timeout:
                proc.kill()
                raise ProviderError(
                    f"claude-code native first-byte-timeout after {now-t0:.0f}s", "claude-code",
                    retryable=True)
            # idle 只在没有工具在飞时判定。工具在飞(Bash 跑实验/编译可达数分钟,
            # 期间 stream-json 静默)时不杀,靠上面的 hard_timeout 兜底——否则会把
            # 正常长跑命令误判成卡死(run_84f6fdb6f11b: idle-timeout after 181s)。
            if (last_byte_at is not None and result.pending_tools == 0
                    and now - last_byte_at > idle_timeout):
                proc.kill()
                raise ProviderError(
                    f"claude-code native idle-timeout after {now-last_byte_at:.0f}s "
                    f"(no tool in flight)", "claude-code", retryable=True)

            ready, _, _ = select.select([proc.stdout, proc.stderr], [], [], 0.5)
            for fobj in ready:
                try:
                    chunk = fobj.read()
                except (BlockingIOError, OSError):
                    chunk = b""
                if not chunk:
                    continue
                last_byte_at = now
                if fobj is proc.stderr:
                    err_chunks.append(chunk)
                    continue
                buf += chunk
                while b"\n" in buf:
                    line, buf = buf.split(b"\n", 1)
                    _process_line(line.decode(errors="replace"))

        # drain
        try:
            rem_out, rem_err = proc.communicate(timeout=5)
        except subprocess.TimeoutExpired:
            rem_out, rem_err = b"", b""
        if rem_err:
            err_chunks.append(rem_err)
        buf += rem_out or b""
        for line in buf.split(b"\n"):
            _process_line(line.decode(errors="replace"))
    finally:
        if proc.poll() is None:
            try:
                proc.kill()
            except Exception:
                pass

    stderr_txt = b"".join(err_chunks).decode(errors="replace")

    # 认证失败:result is_error + 401,或 stderr 含 auth 标记。
    if (result.is_error and _is_auth_error(result.text)) or _is_auth_error(stderr_txt):
        raise ProviderError(_AUTH_HINT, "claude-code", retryable=False)
    if result.is_error:
        raise ProviderError(
            f"claude-code native error: {result.text[:300] or stderr_txt[:300] or '(no detail)'}",
            "claude-code", retryable=False)
    if not saw_result:
        # 进程结束却没拿到 result 事件 = 异常退出
        raise ProviderError(
            f"claude-code native produced no result (exit {proc.returncode}). "
            f"stderr: {stderr_txt[:300] or '(empty)'}",
            "claude-code", retryable=True)

    logger.info("claude-code NATIVE ok: %.1fs turns=%d in=%d out=%d cost=$%.4f session=%s",
                time.time()-t0, result.num_turns, result.input_tokens,
                result.output_tokens, result.cost_usd, result.session_id)
    return result
