from typing import List, Optional
import json
from pathlib import Path

from .approval import ApprovalOptions, prompt_approval
from .plan import PlanContext
from .llm_client import LLMClient, LLMResult
from .logger import Logger
from .prompt_builder import build_messages
from .tool_registry import ToolRegistry
from .tools import ToolError


class ChatSession:
    """Single-session orchestrator for chat + tool calls."""

    def __init__(
        self,
        llm_client: LLMClient,
        registry: Optional[ToolRegistry] = None,
        logger: Optional[Logger] = None,
        approval: Optional[ApprovalOptions] = None,
        plan_context: Optional[PlanContext] = None,
        max_steps: Optional[int] = None,
    ):
        self.client = llm_client
        self.registry = registry or ToolRegistry(logger=logger)
        self.log = logger or Logger()
        self.approval = approval or ApprovalOptions()
        self.history: List[dict] = []
        self.plan_context = plan_context
        self.max_steps = max_steps
        self.completed_steps = 0

    def run_turn(self, user_input: str, *, debug: bool = False) -> str:
        plan_summary = self.plan_context.summary() if self.plan_context else None
        plan_guidance = (
            " planに従ってpendingのステップを順に処理し、完了したらupdate_planでstatus=completedに更新してください。"
            " 作業ディレクトリは cwd を使ってください: {cwd}。"
            " apply_patchは必ず*** Begin Patch...*** End Patch形式で与えてください。"
            " update_planは必ず {{\"plan\":[...全ステップ...]}} 形式で現在のplanをそのまま反映して呼んでください。"
            " 実行できない場合は理由を述べてpendingに戻すか保留を提案してください。"
        )
        plan_guidance = plan_guidance.format(cwd=str(Path.cwd()))
        messages = build_messages(
            user_input,
            self.registry.specs(),
            history=self.history,
            plan_summary=plan_summary,
            plan_guidance=plan_guidance if self.plan_context else None,
        )
        last_tool_outputs: List[str] = []
        max_rounds = 5
        for _ in range(max_rounds):
            if debug:
                self.log.info("[DEBUG prompt] " + str(messages), to_stdout=True)
            try:
                result = self.client.invoke(messages, tools=self.registry.specs())
            except Exception as err:
                self.log.error(f"LLM call failed: {err}")
                return "\n".join(last_tool_outputs)
            if debug:
                self.log.info(
                    f"[DEBUG llm] content={result.content!r} tool_calls={len(result.tool_calls)}",
                    to_stdout=True,
                )
                for call in result.tool_calls:
                    self.log.info(
                        f"[DEBUG tool_call] id={call.id} name={call.name} args={call.arguments}",
                        to_stdout=True,
                    )

            if not result.tool_calls:
                content = result.content or ""
                if not content and last_tool_outputs:
                    content = "\n".join(last_tool_outputs)
                self.history.append({"role": "user", "content": user_input})
                self.history.append({"role": "assistant", "content": content})
                return content

            # Execute tool calls
            assistant_msg = {"role": "assistant", "content": None, "tool_calls": []}
            tool_messages: List[dict] = []
            tool_outputs: List[str] = []
            for call in result.tool_calls:
                if not prompt_approval(
                    f"Run tool {call.name}?",
                    options=self.approval,
                    input_func=None,
                ):
                    self.log.warn(f"Tool {call.name} skipped by user.")
                    continue
                if debug:
                    self.log.info(f"[DEBUG tool] {call.name} {call.arguments}", to_stdout=True)
                tool_result = self.registry.execute(call.name, call.arguments)
                assistant_msg["tool_calls"].append(
                    {
                        "id": call.id,
                        "type": "function",
                        "function": {"name": call.name, "arguments": json.dumps(call.arguments or {})},
                    }
                )
                tool_messages.append(
                    {
                        "role": "tool",
                        "tool_call_id": call.id,
                        "content": str(tool_result),
                    }
                )
                tool_outputs.append(tool_result)
                if call.name == "update_plan" and self.plan_context:
                    try:
                        self.plan_context.update_from_json_text(str(tool_result))
                        self.completed_steps += 1
                    except ToolError as err:
                        self.log.warn(f"Plan update failed: {err}")
                if debug:
                    self.log.info(f"[DEBUG result] {tool_result}", to_stdout=True)

            last_tool_outputs = tool_outputs
            messages.append(assistant_msg)
            messages.extend(tool_messages)
            messages.append(
                {
                    "role": "user",
                    "content": "上記ツール結果を踏まえて依頼に答えてください。"
                    "必要なら全文を示し、十分なら要約で返してください。",
                }
            )
            if debug:
                self.log.info(f"[DEBUG followup messages] {messages}", to_stdout=True)

            if self.max_steps is not None and self.completed_steps >= self.max_steps:
                summary = "\n".join(last_tool_outputs) if last_tool_outputs else ""
                return summary or "max steps reached"

        # Safeguard: if loop exhausts without content, return last tool outputs.
        return "\n".join(last_tool_outputs)
