"""ThinkingAction — core built-in autonomous decision engine.

This is the "brain" of each ActionSet.  It runs every turn to evaluate
the current situation and decide what to do next.

See design doc §4.2 for the full specification.
"""

from __future__ import annotations

import json
import logging
import re
from typing import Any, AsyncGenerator, Optional

from oedix.core.action import Action, ActionOutput

_logger = logging.getLogger("oedix.thinking")
from oedix.core.context import Context, ContextType
from oedix.core.events import Event, EventType


# Default LLM system prompt for the thinking action
_THINKING_SYSTEM_PROMPT = """\
You are the reasoning engine of an AI agent. Your job is to analyze the \
current situation and decide the next action(s).

You will receive:
- The user's original query
- The current ActionSet goal (subject) and approach
- History of completed actions in this set (including their outputs)
- Available action types you can invoke
- Remaining action budget

Respond in **valid JSON** with this schema:
{
  "title": "brief goal label (max 20 chars)",
  "analysis": "1–3 sentence situation analysis",
  "approach_update": "new or updated approach (empty string to keep current)",
  "weight": 1.0,
  "decision": "goal_achieved | enqueue_actions",
  "actions": [
    {"type": "action_type", "params": { ... }}
  ],
  "reasoning": "brief justification for the decision"
}

Rules:
- "title": A concise label (≤20 characters) summarizing the immediate \
goal of this ActionSet. Set it on the first thinking turn; update only \
if the goal shifts. Examples: "파일 검색", "API 데이터 조회", "코드 리팩토링".
- If the goal is already achieved or can be answered directly, \
set decision="goal_achieved" and actions=[].
- If further work is needed, set decision="enqueue_actions" \
and list the actions to run.
- weight: 0.5 for simple tasks, 1.0 for normal, up to 3.0 for complex.
- Only use action types from the available list.
- Do NOT include "thinking" or "response" in your actions list.
- NEVER ask counter-questions to the user. You cannot communicate with \
the user during the thinking phase. Always proceed with the available \
information, make reasonable assumptions, and act autonomously.

## Action Selection Guide

The available actions are listed below in the "Available Action Types" \
section. Each action includes its type, description, and detailed usage \
guide (parameters, constraints, examples). Read each action's usage \
carefully before selecting it.

- Select the action whose capabilities best match what you need to do.
- Follow each action's constraints and parameter requirements exactly.
- Actions may be added, removed, or changed at any time — always rely \
on the current list provided below, not on prior assumptions.

### Error Recovery
- When an action returns an error, do NOT simply retry the same \
operation. Analyze the error and try a different approach.
- Each recovery step should be a new action.

## Decision Strategy
- Focus ONLY on the current ActionSet's subject. Do NOT attempt work \
outside your ActionSet's scope. Other ActionSets handle their own goals.
- Review the Completed Actions history and Context carefully. \
If the needed information is already available, \
set decision="goal_achieved" instead of running more actions.
- Each action should make MEANINGFUL PROGRESS toward the goal. \
If a previous approach failed, try a completely different strategy.
- NEVER repeat the same action with similar parameters.
- Prefer action over inaction. Do not stall.
- If you decide to use "planner", it MUST be the ONLY action in the list. \
Do NOT combine planner with any other actions. Planner creates its own \
ActionSets, so no further actions are needed in this ActionSet.

Autonomous Decision-Making:
- When the user's request is vague or ambiguous, make reasonable \
assumptions and proceed.
- State your assumptions briefly in the "analysis" field.
"""


class ThinkingAction(Action):
    """Core thinking action — autonomous decision engine (§4.2).

    Executes a 4-phase pipeline:
    1. START: Load ActionSet state.
    2. ActionSet update: Define/refine approach and weight.
    3. Decision: Analyze context and choose next actions.
    4. END: Record results.
    """

    type = "thinking"

    def describe(self) -> str:
        return "상황 분석 및 다음 행동 자율 결정"

    async def execute(
        self, context: list[Context]
    ) -> AsyncGenerator[Event, None]:
        action_set = self.get_action_set()
        agent_queue = self.get_agent_queue()

        # --- Phase 1: START — load state ---
        subject = action_set.get_subject()
        approach = action_set.get_approach()
        history = action_set.get_history()
        remaining = action_set.remaining_actions()
        query = agent_queue.get_query()

        yield self.emit(EventType.THINKING, {
            "phase": "start",
            "subject": subject,
            "remaining": remaining,
        })

        # --- Gather available actions ---
        available_actions = self._list_available_actions()

        # --- Build LLM prompt ---
        messages = self._build_messages(
            query=query,
            subject=subject,
            approach=approach,
            history=history,
            context=context,
            available_actions=available_actions,
            remaining=remaining,
        )

        # --- Phase 2 + 3: LLM call → parse decision ---
        full_response = ""
        try:
            async for chunk in self.call_llm(messages):
                full_response += chunk
                yield self.emit(EventType.THINKING, {
                    "phase": "streaming",
                    "chunk": chunk,
                })
        except Exception as e:
            yield self.emit(EventType.THINKING, {
                "phase": "error",
                "message": f"LLM call failed: {e}",
            })
            self.output = ActionOutput(
                result={"error": str(e)},
                summary=f"Thinking failed: {e}",
            )
            return

        # --- Parse structured response ---
        decision = self._parse_response(full_response)

        # --- Apply title (set once or update if goal shifts) ---
        title = decision.get("title", "")
        if title:
            action_set.set_title(title)

        # --- Phase 2: Apply approach and weight updates ---
        if decision.get("approach_update"):
            action_set.set_approach(decision["approach_update"])
            yield self.emit(EventType.APPROACH_UPDATE, {
                "approach": decision["approach_update"],
            })

        weight = decision.get("weight", 1.0)
        if isinstance(weight, (int, float)) and weight != 1.0:
            action_set.set_weight(float(weight))

        # --- Phase 3: Execute decision ---
        decision_type = decision.get("decision", "goal_achieved")
        decision_actions = decision.get("actions", [])

        yield self.emit(EventType.DECISION, {
            "decision": decision_type,
            "analysis": decision.get("analysis", ""),
            "reasoning": decision.get("reasoning", ""),
            "action_count": len(decision_actions),
        })

        if decision_type == "enqueue_actions" and decision_actions:
            enqueued_details = self._enqueue_decided_actions(
                action_set, decision_actions, decision
            )
            # Set batch remaining so ActionSet defers auto-thinking
            # until all enqueued actions have completed
            if enqueued_details:
                if self._action_set is not None:
                    self._action_set._batch_remaining = len(enqueued_details)
                # Emit ACTION_PLAN so the UI can show planned actions
                yield self.emit(EventType.ACTION_PLAN, {
                    "actions": enqueued_details,
                })
            summary_parts = [
                f"Decided: {decision_type}",
                f"Enqueued {len(enqueued_details)} action(s)",
            ]
        else:
            summary_parts = ["Decided: goal achieved, ActionSet can conclude"]

        # --- Phase 4: END — record output ---
        analysis = decision.get("analysis", "")
        reasoning = decision.get("reasoning", "")
        summary = ". ".join(summary_parts)

        # Build context entry — this is important for organizer to read
        thinking_result = (
            f"[Thinking] {analysis}\n"
            f"Decision: {decision_type}\n"
            f"Reasoning: {reasoning}"
        )
        # If organizer was enqueued, include the purpose
        organizer_purpose = decision.get("organizer_purpose", "")
        if organizer_purpose:
            thinking_result += f"\nOrganizer purpose: {organizer_purpose}"

        self.output = ActionOutput(
            result=decision,
            summary=summary,
            context_entries=[
                Context.action_result(
                    thinking_result,
                    source_action_id=self.id,
                    priority=70,
                )
            ],
        )

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _list_available_actions(self) -> list[dict[str, str]]:
        """Get available action types from the action loader."""
        if self._action_loader is not None:
            try:
                return self._action_loader.list_actions()
            except Exception:
                pass
        # Fallback: return empty (no dynamic actions available)
        return []

    def _build_messages(
        self,
        query: str,
        subject: str,
        approach: str,
        history: list,
        context: list[Context],
        available_actions: list[dict[str, str]],
        remaining: int,
    ) -> list[dict[str, str]]:
        """Build the chat messages for the LLM call."""
        # Context summary
        context_text = ""
        for ctx in context:
            label = ctx.type.value
            preview = ctx.content[:2000] if len(ctx.content) > 2000 else ctx.content
            context_text += f"[{label}] {preview}\n\n"

        # History summary (include output details for tool actions)
        history_text = ""
        if history:
            for i, action in enumerate(history):
                status = action.status.value if hasattr(action, 'status') else 'unknown'
                summary = action.output.summary if action.output else "no output"
                history_text += f"  {i+1}. [{action.type}] ({status}) {summary}\n"
                # Include result details for non-thinking actions
                if action.output and action.type != "thinking":
                    result = action.output.result
                    if isinstance(result, dict):
                        result_preview = json.dumps(result, ensure_ascii=False, default=str)[:1500]
                    else:
                        result_preview = str(result)[:1500]
                    history_text += f"     Result: {result_preview}\n"
        else:
            history_text = "  (none — this is the first action)\n"

        # Available actions (with usage guides)
        actions_text = ""
        if available_actions:
            for a in available_actions:
                actions_text += f"### {a['type']}\n{a['describe']}\n"
                usage = a.get("usage", "")
                if usage:
                    actions_text += f"{usage}\n"
                actions_text += "\n"
        else:
            actions_text = "  (no dynamic actions loaded)\n"

        user_prompt = f"""\
## Current Situation

**User Query**: {query}

**ActionSet Subject**: {subject}
**Current Approach**: {approach or "(not yet defined)"}
**Remaining Action Budget**: {remaining}

## Completed Actions in This Set
{history_text}

## Available Action Types
{actions_text}

## Context
{context_text}

Based on the above, analyze the situation and decide the next step(s).
Respond in the JSON format specified in the system prompt."""

        return [
            {"role": "system", "content": _THINKING_SYSTEM_PROMPT},
            {"role": "user", "content": user_prompt},
        ]

    def _parse_response(self, text: str) -> dict[str, Any]:
        """Parse the LLM's JSON response, with fallback."""
        original_text = text

        # Strip <think>...</think> blocks (reasoning models like DeepSeek)
        text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL).strip()

        # Handle markdown code blocks
        json_match = re.search(r"```(?:json)?\s*\n?(.*?)\n?```", text, re.DOTALL)
        if json_match:
            text = json_match.group(1)

        # Try direct JSON parse
        try:
            return json.loads(text.strip())
        except json.JSONDecodeError:
            pass

        # Try fixing trailing commas and parse again
        cleaned = self._fix_json_trailing_commas(text.strip())
        if cleaned != text.strip():
            try:
                return json.loads(cleaned)
            except json.JSONDecodeError:
                pass

        # Try to extract outermost JSON object using brace counting
        extracted = self._extract_json_object(text)
        if extracted:
            try:
                return json.loads(extracted)
            except json.JSONDecodeError:
                # Try with trailing comma fix
                cleaned = self._fix_json_trailing_commas(extracted)
                try:
                    return json.loads(cleaned)
                except json.JSONDecodeError:
                    pass

        # Fallback: treat as goal_achieved
        _logger.warning(
            "Could not parse structured LLM response. "
            "Raw response (first 500 chars): %s",
            original_text[:500],
        )
        return {
            "analysis": text[:200] if text else "Unable to parse LLM response",
            "approach_update": "",
            "weight": 1.0,
            "decision": "goal_achieved",
            "actions": [],
            "reasoning": "Fallback: could not parse structured response",
        }

    @staticmethod
    def _fix_json_trailing_commas(text: str) -> str:
        """Remove trailing commas before } or ] (common LLM JSON error)."""
        return re.sub(r",\s*([}\]])", r"\1", text)

    @staticmethod
    def _extract_json_object(text: str) -> Optional[str]:
        """Extract the outermost JSON object using brace counting."""
        start = text.find("{")
        if start == -1:
            return None
        depth = 0
        in_string = False
        escape = False
        for i in range(start, len(text)):
            ch = text[i]
            if escape:
                escape = False
                continue
            if ch == "\\":
                if in_string:
                    escape = True
                continue
            if ch == '"':
                in_string = not in_string
                continue
            if in_string:
                continue
            if ch == "{":
                depth += 1
            elif ch == "}":
                depth -= 1
                if depth == 0:
                    return text[start : i + 1]
        return None

    def _enqueue_decided_actions(
        self,
        action_set_handle,
        actions: list[dict],
        decision: dict,
    ) -> list[dict]:
        """Create and enqueue the decided actions. Returns list of action details."""
        if self._action_loader is None:
            return []

        enqueued = []
        for action_spec in actions:
            action_type = action_spec.get("type", "")
            params = action_spec.get("params", {})

            # Skip invalid types
            if not action_type or action_type in ("thinking", "response"):
                continue

            # For organizer, inject purpose from thinking decision
            if action_type == "organizer":
                purpose = (
                    params.get("purpose", "")
                    or decision.get("organizer_purpose", "")
                )
                if purpose:
                    params["purpose"] = purpose

            try:
                action_instance = self._action_loader.create(
                    action_type, **params
                )
                action_set_handle.enqueue(action_instance)
                enqueued.append({
                    "id": action_instance.id,
                    "type": action_type,
                    "purpose": params.get("purpose", ""),
                })
            except Exception as exc:
                import logging
                logging.getLogger("oedix.thinking").warning(
                    "Failed to create action '%s': %s", action_type, exc
                )
                continue

        return enqueued
