"""Agentic loop — multi-step task execution with progress tracking.

Provides `run_agentic_loop()`, the engine behind the `plan_and_execute` tool.
When the model recognises a task needs 3+ sequential steps, it calls
`plan_and_execute(task)` which spawns this loop in the background.

Flow:
  1. Planning — ask the model to break the task into numbered steps
  2. Execution — run each step sequentially, feeding results forward
  3. Adaptation — on failure, ask the model to adapt the plan
  4. Summary — ask the model to summarise what was accomplished

Progress is broadcast via WebSocket so the dashboard can show live updates.
"""

from __future__ import annotations

import asyncio
import logging
import re
import threading
import time
import uuid
from collections.abc import Callable
from dataclasses import dataclass, field

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Data structures
# ---------------------------------------------------------------------------


@dataclass
class AgenticStep:
    """Result of a single step in the agentic loop."""

    step_num: int
    description: str
    result: str
    status: str  # "completed" | "failed" | "adapted"
    duration_ms: int


@dataclass
class AgenticResult:
    """Final result of the entire agentic loop."""

    task_id: str
    task: str
    summary: str
    steps: list[AgenticStep] = field(default_factory=list)
    status: str = "completed"  # "completed" | "partial" | "failed"
    total_duration_ms: int = 0


# ---------------------------------------------------------------------------
# Agent factory
# ---------------------------------------------------------------------------

_loop_agent = None
_loop_agent_lock = threading.Lock()


def _get_loop_agent():
    """Create a fresh agent for the agentic loop.

    Returns the same type of agent as `create_timmy()` but with a
    dedicated session so it doesn't pollute the main chat history.
    """
    global _loop_agent
    if _loop_agent is None:
        with _loop_agent_lock:
            if _loop_agent is None:
                from timmy.agent import create_timmy

                _loop_agent = create_timmy()
    return _loop_agent


# ---------------------------------------------------------------------------
# Plan parser
# ---------------------------------------------------------------------------

_STEP_RE = re.compile(r"^\s*(\d+)[.)]\s*(.+)$", re.MULTILINE)


def _parse_steps(plan_text: str) -> list[str]:
    """Extract numbered steps from the model's planning output."""
    matches = _STEP_RE.findall(plan_text)
    if matches:
        return [desc.strip() for _, desc in matches]
    # Fallback: split on newlines, ignore blanks
    return [line.strip() for line in plan_text.strip().splitlines() if line.strip()]


# ---------------------------------------------------------------------------
# Extracted helpers
# ---------------------------------------------------------------------------


def _extract_content(run_result) -> str:
    """Extract text content from an agent run result."""
    return run_result.content if hasattr(run_result, "content") else str(run_result)


def _clean(text: str) -> str:
    """Clean a model response using session's response cleaner."""
    from timmy.session import _clean_response

    return _clean_response(text)


async def _plan_task(
    agent, task: str, session_id: str, max_steps: int
) -> tuple[list[str], bool] | str:
    """Run the planning phase — returns (steps, was_truncated) or error string."""
    plan_prompt = (
        f"Break this task into numbered steps (max {max_steps}). "
        f"Return ONLY a numbered list, nothing else.\n\n"
        f"Task: {task}"
    )
    try:
        plan_run = await asyncio.to_thread(
            agent.run, plan_prompt, stream=False, session_id=f"{session_id}_plan"
        )
        plan_text = _extract_content(plan_run)
    except Exception as exc:  # broad catch intentional: agent.run can raise any error
        logger.error("Agentic loop: planning failed: %s", exc)
        return f"Planning failed: {exc}"

    steps = _parse_steps(plan_text)
    if not steps:
        return "Planning produced no steps."

    planned_count = len(steps)
    steps = steps[:max_steps]
    return steps, planned_count > len(steps)


async def _execute_step(
    agent,
    task: str,
    step_desc: str,
    step_num: int,
    total_steps: int,
    recent_results: list[str],
    session_id: str,
) -> AgenticStep:
    """Execute a single step, returning an AgenticStep."""
    step_start = time.monotonic()
    context = (
        f"Task: {task}\n"
        f"Step {step_num}/{total_steps}: {step_desc}\n"
        f"Recent progress: {recent_results[-2:] if recent_results else []}\n\n"
        f"Execute this step and report what you did."
    )
    step_run = await asyncio.to_thread(
        agent.run, context, stream=False, session_id=f"{session_id}_step{step_num}"
    )
    step_result = _clean(_extract_content(step_run))
    return AgenticStep(
        step_num=step_num,
        description=step_desc,
        result=step_result,
        status="completed",
        duration_ms=int((time.monotonic() - step_start) * 1000),
    )


async def _adapt_step(
    agent,
    step_desc: str,
    step_num: int,
    error: Exception,
    step_start: float,
    session_id: str,
) -> AgenticStep:
    """Attempt adaptation after a step failure."""
    adapt_prompt = (
        f"Step {step_num} failed with error: {error}\n"
        f"Original step was: {step_desc}\n"
        f"Adapt the plan and try an alternative approach for this step."
    )
    adapt_run = await asyncio.to_thread(
        agent.run, adapt_prompt, stream=False, session_id=f"{session_id}_adapt{step_num}"
    )
    adapt_result = _clean(_extract_content(adapt_run))
    return AgenticStep(
        step_num=step_num,
        description=f"[Adapted] {step_desc}",
        result=adapt_result,
        status="adapted",
        duration_ms=int((time.monotonic() - step_start) * 1000),
    )


def _summarize(result: AgenticResult, total_steps: int, was_truncated: bool) -> None:
    """Fill in summary and final status on the result object (mutates in place)."""
    completed = sum(1 for s in result.steps if s.status == "completed")
    adapted = sum(1 for s in result.steps if s.status == "adapted")
    failed = sum(1 for s in result.steps if s.status == "failed")

    parts = [f"Completed {completed}/{total_steps} steps"]
    if adapted:
        parts.append(f"{adapted} adapted")
    if failed:
        parts.append(f"{failed} failed")
    result.summary = f"{result.task}: {', '.join(parts)}."

    if was_truncated or len(result.steps) < total_steps or failed:
        result.status = "partial"
    else:
        result.status = "completed"


# ---------------------------------------------------------------------------
# Core loop
# ---------------------------------------------------------------------------


async def run_agentic_loop(
    task: str,
    *,
    session_id: str = "agentic",
    max_steps: int = 0,
    on_progress: Callable | None = None,
) -> AgenticResult:
    """Execute a multi-step task with planning, execution, and adaptation.

    Args:
        task:        Full description of the task to execute.
        session_id:  Agno session_id for conversation continuity.
        max_steps:   Max steps to execute (0 = use config default).
        on_progress: Optional async callback(description, step_num, total_steps).

    Returns:
        AgenticResult with steps, summary, and status.
    """
    from config import settings

    if max_steps <= 0:
        max_steps = getattr(settings, "max_agent_steps", 10)

    task_id = str(uuid.uuid4())[:8]
    start_time = time.monotonic()
    agent = _get_loop_agent()
    result = AgenticResult(task_id=task_id, task=task, summary="")

    # Phase 1: Planning
    plan = await _plan_task(agent, task, session_id, max_steps)
    if isinstance(plan, str):
        result.status = "failed"
        result.summary = plan
        result.total_duration_ms = int((time.monotonic() - start_time) * 1000)
        return result

    steps, was_truncated = plan
    total_steps = len(steps)

    await _broadcast_progress(
        "agentic.plan_ready",
        {"task_id": task_id, "task": task, "steps": steps, "total": total_steps},
    )

    # Phase 2: Execution
    completed_results: list[str] = []
    for i, step_desc in enumerate(steps, 1):
        step_start = time.monotonic()
        try:
            step = await _execute_step(
                agent,
                task,
                step_desc,
                i,
                total_steps,
                completed_results,
                session_id,
            )
            result.steps.append(step)
            completed_results.append(f"Step {i}: {step.result[:200]}")
            await _broadcast_progress(
                "agentic.step_complete",
                {
                    "task_id": task_id,
                    "step": i,
                    "total": total_steps,
                    "description": step_desc,
                    "result": step.result[:200],
                },
            )
            if on_progress:
                await on_progress(step_desc, i, total_steps)

        except Exception as exc:  # broad catch intentional: agent.run can raise any error
            logger.warning("Agentic loop step %d failed: %s", i, exc)
            try:
                step = await _adapt_step(agent, step_desc, i, exc, step_start, session_id)
                result.steps.append(step)
                completed_results.append(f"Step {i} (adapted): {step.result[:200]}")
                await _broadcast_progress(
                    "agentic.step_adapted",
                    {
                        "task_id": task_id,
                        "step": i,
                        "total": total_steps,
                        "description": step_desc,
                        "error": str(exc),
                        "adaptation": step.result[:200],
                    },
                )
                if on_progress:
                    await on_progress(f"[Adapted] {step_desc}", i, total_steps)
            except Exception as adapt_exc:  # broad catch intentional
                logger.error("Agentic loop adaptation also failed: %s", adapt_exc)
                result.steps.append(
                    AgenticStep(
                        step_num=i,
                        description=step_desc,
                        result=f"Failed: {exc}; Adaptation also failed: {adapt_exc}",
                        status="failed",
                        duration_ms=int((time.monotonic() - step_start) * 1000),
                    )
                )
                completed_results.append(f"Step {i}: FAILED")

    # Phase 3: Summary
    _summarize(result, total_steps, was_truncated)
    result.total_duration_ms = int((time.monotonic() - start_time) * 1000)

    await _broadcast_progress(
        "agentic.task_complete",
        {
            "task_id": task_id,
            "status": result.status,
            "steps_completed": len(result.steps),
            "summary": result.summary[:300],
            "duration_ms": result.total_duration_ms,
        },
    )

    return result


# ---------------------------------------------------------------------------
# WebSocket broadcast helper
# ---------------------------------------------------------------------------


async def _broadcast_progress(event: str, data: dict) -> None:
    """Broadcast agentic loop progress via WebSocket (best-effort)."""
    try:
        from infrastructure.ws_manager.handler import ws_manager

        await ws_manager.broadcast(event, data)
    except (ImportError, AttributeError, ConnectionError, RuntimeError) as exc:
        logger.warning("Agentic loop broadcast failed: %s", exc)
        logger.debug("Agentic loop: WS broadcast failed for %s", event)
