"""Autonomous sense-think-act loop for proactive agent execution."""

from __future__ import annotations

import json
import time
from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

from logging_config import get_logger

if TYPE_CHECKING:
    from agent_logger import AgentLogger
    from goals import GoalEntry, GoalStore
    from session import ContextManager, SessionStore

    from skills import Skill, SkillExecutor

log = get_logger(__name__)


@dataclass
class Decision:
    """Parsed LLM decision on whether to act."""

    should_act: bool
    reason: str


def parse_decision(raw: str) -> Decision:
    """Extract a structured decision from LLM output.

    Tries to parse JSON first, falls back to heuristic.
    """
    # Try to find JSON in the response
    for start_char in ("{",):
        idx = raw.find(start_char)
        if idx == -1:
            continue
        # Find matching closing brace
        depth = 0
        for i in range(idx, len(raw)):
            if raw[i] == "{":
                depth += 1
            elif raw[i] == "}":
                depth -= 1
                if depth == 0:
                    try:
                        obj = json.loads(raw[idx : i + 1])
                        return Decision(
                            should_act=bool(obj.get("should_act", False)),
                            reason=str(obj.get("reason", "")),
                        )
                    except json.JSONDecodeError:
                        break

    # Heuristic fallback
    lower = raw.lower()
    should_act = any(kw in lower for kw in ('should_act": true', "action needed", "yes,"))
    return Decision(should_act=should_act, reason=raw[:200])


class AutonomousLoop:
    """Runs goals on a schedule: sense → think → act."""

    def __init__(
        self,
        executor: SkillExecutor,
        skills: dict[str, Skill],
        goal_store: GoalStore,
        session_store: SessionStore,
        context_manager: ContextManager,
        agent_logger: AgentLogger | None = None,
        system_prompt: str = "",
        notify_fn: Callable[[str], None] | None = None,
    ) -> None:
        self.executor = executor
        self.skills = skills
        self.goal_store = goal_store
        self.session_store = session_store
        self.context_manager = context_manager
        self.agent_logger = agent_logger
        self._system_prompt = system_prompt
        self.notify_fn = notify_fn
        self._log = log.bind(component="autonomous")

    def run_forever(self, tick_interval: int = 30) -> None:
        """Main loop: check for due goals every tick_interval seconds."""
        self._log.info("loop_started", tick_interval=tick_interval)
        while True:
            try:
                self._tick()
            except Exception as exc:
                self._log.error("tick_error", error=str(exc), exc_info=True)
            time.sleep(tick_interval)

    def run_once(self) -> list[dict[str, Any]]:
        """Run a single tick and return results (useful for testing)."""
        return self._tick()

    def _tick(self) -> list[dict[str, Any]]:
        """Process all due goals. Returns list of run summaries."""
        due = self.goal_store.get_due()
        if not due:
            return []

        self._log.info("tick_processing", due_count=len(due))
        results = []
        for goal in due:
            try:
                summary = self._process_goal(goal)
                results.append(summary)
            except Exception as exc:
                self._log.error(
                    "goal_error",
                    goal_id=goal.id,
                    goal_name=goal.name,
                    error=str(exc),
                    exc_info=True,
                )
                self.goal_store.touch_last_run(goal.id)
                self.goal_store.record_run(
                    goal.id,
                    "error",
                    result=str(exc),
                )
                results.append(
                    {
                        "goal_id": goal.id,
                        "goal_name": goal.name,
                        "status": "error",
                        "error": str(exc),
                    }
                )
        return results

    def _process_goal(self, goal: GoalEntry) -> dict[str, Any]:
        """Sense → Think → Act for a single goal."""
        from session import Session

        goal_log = self._log.bind(goal_id=goal.id, goal_name=goal.name)
        goal_log.info("processing_goal")

        # Mark as run immediately to prevent re-triggering
        self.goal_store.touch_last_run(goal.id)

        # Create a fresh session for this run
        session = Session.new(self._system_prompt, title=f"[auto] {goal.name}")
        prepared = self.context_manager.prepare_messages(session.messages)

        # --- SENSE ---
        goal_log.info("phase_sense")
        observation = self.executor.run(
            user_message=goal.sensor_prompt,
            skills=self.skills,
            messages=prepared,
            session_id=session.id,
        )
        session.messages = prepared

        # --- THINK ---
        always_act = goal.metadata.get("always_act", False)

        if always_act:
            goal_log.info("phase_think_skipped", reason="always_act=true")
            decision = Decision(should_act=True, reason="always_act")
            decision_raw = '{"should_act": true, "reason": "always_act"}'
        else:
            goal_log.info("phase_think")
            think_prompt = (
                f"Based on the observation above, decide whether action is needed.\nGoal: {goal.description}\n"
            )
            if goal.action_prompt:
                think_prompt += f"Action to take if needed: {goal.action_prompt}\n"
            think_prompt += 'Respond ONLY with JSON: {"should_act": true/false, "reason": "brief explanation"}'

            decision_raw = self.executor.run(
                user_message=think_prompt,
                skills={},  # no tools for pure reasoning
                messages=prepared,
                session_id=session.id,
            )
            session.messages = prepared
            decision = parse_decision(decision_raw)

        goal_log.info(
            "decision_made",
            should_act=decision.should_act,
            reason=decision.reason[:100],
        )

        # --- ACT (or skip) ---
        if not decision.should_act:
            self.goal_store.record_run(
                goal.id,
                "skipped",
                observation=observation,
                decision=decision_raw,
                session_id=session.id,
            )
            self.session_store.save(session)
            return {
                "goal_id": goal.id,
                "goal_name": goal.name,
                "status": "skipped",
                "reason": decision.reason,
            }

        if goal.autonomy_level == "approve":
            self.goal_store.record_run(
                goal.id,
                "pending_approval",
                observation=observation,
                decision=decision_raw,
                session_id=session.id,
            )
            self.session_store.save(session)
            msg = f"🔔 Goal '{goal.name}' wants to act: {decision.reason}"
            if self.notify_fn:
                self.notify_fn(msg)
            goal_log.info("awaiting_approval")
            return {
                "goal_id": goal.id,
                "goal_name": goal.name,
                "status": "pending_approval",
                "reason": decision.reason,
            }

        # Execute the action
        goal_log.info("phase_act")
        action_message = (
            goal.action_prompt
            if goal.action_prompt
            else (f"Based on your analysis, take the appropriate action for: {goal.description}")
        )
        allowed_skills = self._filter_skills(goal)

        result = self.executor.run(
            user_message=action_message,
            skills=allowed_skills,
            messages=prepared,
            session_id=session.id,
        )
        session.messages = prepared
        session.touch()
        self.session_store.save(session)

        self.goal_store.record_run(
            goal.id,
            "acted",
            observation=observation,
            decision=decision_raw,
            session_id=session.id,
            result=result,
        )

        if self.notify_fn and goal.autonomy_level == "notify":
            self.notify_fn(f"✅ Goal '{goal.name}': {result[:200]}")

        goal_log.info("goal_completed", result_length=len(result))
        return {
            "goal_id": goal.id,
            "goal_name": goal.name,
            "status": "acted",
            "result_preview": result[:200],
        }

    def _filter_skills(self, goal: GoalEntry) -> dict[str, Skill]:
        """Return only skills allowed by the goal, or all if unrestricted."""
        if not goal.skills_allowed:
            return self.skills
        return {k: v for k, v in self.skills.items() if k in goal.skills_allowed}
