"""Subagent orchestration API endpoints."""

from __future__ import annotations

import json
import re
from pathlib import Path

from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel

router = APIRouter(prefix="/orchestrator", tags=["orchestrator"])


class DispatchRequest(BaseModel):
    agent_name: str
    task: str
    context: str = ""


class AgentSaveRequest(BaseModel):
    name: str
    role: str
    personality: str = ""
    capabilities: str = ""
    boundaries: str = ""
    skills: list[str] = []
    access: str = "read-only"
    status: str = "active"


# ── Known issues (from action plan) ──


def _get_known_issues(request: Request) -> dict[str, list[dict]]:
    """Load active issues per agent from the action plan. Returns {agent_key: [issues]}."""
    vault = getattr(request.app.state, "vault", None)
    if not vault:
        return {}
    if hasattr(vault, "paths") and vault.paths:
        plan_path = vault.paths.resolve("system/logs/simulation/action-plan.json")
    else:
        vault_dir = getattr(vault, "_vault_dir", None)
        if not vault_dir:
            return {}
        plan_path = Path(vault_dir) / "system" / "logs" / "simulation" / "action-plan.json"
    if not plan_path.exists():
        return {}
    try:
        items = json.loads(plan_path.read_text(encoding="utf-8"))
    except Exception:
        return {}

    # Only show unresolved items (pending, info, applied, needs_retry)
    active_statuses = {"pending", "info", "applied", "needs_retry"}
    by_agent: dict[str, list[dict]] = {}
    for item in items:
        if item.get("status") not in active_statuses:
            continue
        key = item.get("agent_key", "")
        if not key:
            continue
        by_agent.setdefault(key, []).append({
            "id": item.get("id"),
            "what": item.get("what", item.get("problem", "")),
            "severity": item.get("severity", "warn"),
            "fix_type": item.get("fix_type", ""),
            "status": item.get("status"),
        })
    return by_agent


# ── List / detail ──


@router.get("/agents")
async def list_agents(request: Request) -> list[dict]:
    """List all available subagents with known issues from action plan."""
    orchestrator = request.app.state.orchestrator
    agents = orchestrator.list_agents()
    issues_map = _get_known_issues(request)
    for agent in agents:
        agent["known_issues"] = issues_map.get(agent["key"], [])
    return agents


# ── Agent sync (hybrid upgrade of shipped agent definitions) ────────────
#
# Registered BEFORE GET /agents/{key} so `sync` isn't swallowed as a key.
# All agents are strategy-tier (no meta overwrite) — see AgentSync docstring.

def _build_agent_sync(request: Request):  # noqa: ANN202
    from agntspace.setup.init import _find_defaults_dir as _find_app_defaults
    from agntspace.skills.sync import AgentSync
    settings = request.app.state.settings
    return AgentSync(
        agents_dir=settings.vault_dir / "system" / "agents",
        defaults_dir=_find_app_defaults() / "vault" / "agents",
    )


@router.get("/agents/sync/status")
async def agent_sync_status(request: Request) -> dict:
    """Cheap status read for the sync dashboard."""
    try:
        sync = _build_agent_sync(request)
    except FileNotFoundError:
        return {"shipped_count": 0, "tracked": 0, "pending_count": 0, "pending_paths": []}
    return sync.status()


@router.get("/agents/sync/pending")
async def agent_sync_pending(request: Request) -> list[dict]:
    """List pending agent upgrades with full shipped+vault content for diffing."""
    try:
        sync = _build_agent_sync(request)
    except FileNotFoundError:
        return []
    return [
        {
            "path": p.path,
            "shipped_hash": p.shipped_hash,
            "accepted_hash": p.accepted_hash,
            "vault_hash": p.vault_hash,
            "shipped_content": p.shipped_content,
            "vault_content": p.vault_content,
            "classification": p.classification,
            "vault_edited": p.vault_edited,
        }
        for p in sync.list_pending()
    ]


@router.post("/agents/sync/run")
async def agent_sync_run(request: Request) -> dict:
    """Force an immediate sync pass (normally runs at boot)."""
    try:
        sync = _build_agent_sync(request)
    except FileNotFoundError as exc:
        raise HTTPException(status_code=500, detail=str(exc)) from exc
    report = sync.sync_on_boot()
    # Reload orchestrator's agent catalog so changes take effect without restart.
    try:
        request.app.state.orchestrator.load_agents()
    except Exception:
        pass
    return {
        "summary": report.summary(),
        "created": report.created,
        "overwritten_meta": report.overwritten_meta,
        "upgraded_clean": report.upgraded_clean,
        "skipped_user_edit": report.skipped_user_edit,
        "pending": report.pending,
    }


class AgentSyncDecisionRequest(BaseModel):
    path: str


@router.post("/agents/sync/accept")
async def agent_sync_accept(request: Request, body: AgentSyncDecisionRequest) -> dict:
    """User accepts the shipped version for a single pending path."""
    try:
        sync = _build_agent_sync(request)
    except FileNotFoundError as exc:
        raise HTTPException(status_code=500, detail=str(exc)) from exc
    ok = sync.accept_upgrade(body.path)
    if not ok:
        raise HTTPException(status_code=404, detail=f"No pending upgrade for {body.path}")
    try:
        request.app.state.orchestrator.load_agents()
    except Exception:
        pass
    return {"status": "accepted", "path": body.path}


@router.post("/agents/sync/keep-mine")
async def agent_sync_keep_mine(request: Request, body: AgentSyncDecisionRequest) -> dict:
    """User chooses to keep their vault edit; manifest is pinned to it."""
    try:
        sync = _build_agent_sync(request)
    except FileNotFoundError as exc:
        raise HTTPException(status_code=500, detail=str(exc)) from exc
    ok = sync.keep_mine(body.path)
    if not ok:
        raise HTTPException(status_code=404, detail=f"No pending upgrade for {body.path}")
    return {"status": "kept", "path": body.path}


@router.get("/agents/{key}")
async def get_agent(request: Request, key: str) -> dict:
    """Get a single subagent's details with known issues."""
    orchestrator = request.app.state.orchestrator
    agents = orchestrator.agents
    agent_def = agents.get(key)
    if not agent_def:
        raise HTTPException(status_code=404, detail=f"Subagent not found: {key}")
    issues_map = _get_known_issues(request)
    return {
        "key": key,
        "name": agent_def.name,
        "role": agent_def.role,
        "personality": agent_def.personality,
        "capabilities": agent_def.capabilities,
        "boundaries": agent_def.boundaries,
        "skills": agent_def.skills,
        "access": agent_def.access,
        "status": agent_def.status,
        "allowed_tools": agent_def.allowed_tools,
        "effective_tools": sorted(orchestrator.get_allowed_tools(key)),
        "known_issues": issues_map.get(key, []),
    }


# ── Create / update / delete ──


@router.put("/agents/{key}")
async def save_agent(request: Request, key: str, body: AgentSaveRequest) -> dict:
    """Update an existing subagent definition (writes back to vault .md file)."""
    writer = request.app.state.vault_writer
    orchestrator = request.app.state.orchestrator

    content = _build_agent_md(body)
    writer.write(f"system/agents/{key}.md", content)
    orchestrator.load_agents()
    return {"status": "updated", "key": key}


@router.post("/agents")
async def create_agent(request: Request, body: AgentSaveRequest) -> dict:
    """Create a new subagent definition."""
    writer = request.app.state.vault_writer
    orchestrator = request.app.state.orchestrator

    key = re.sub(r"[^a-z0-9-]", "", body.name.lower().replace(" ", "-"))[:60]
    if not key:
        raise HTTPException(status_code=400, detail="Invalid agent name")
    if key in orchestrator.agents:
        raise HTTPException(status_code=409, detail=f"Agent '{key}' already exists")

    content = _build_agent_md(body)
    writer.write(f"system/agents/{key}.md", content)
    orchestrator.load_agents()
    return {"status": "created", "key": key}


@router.delete("/agents/{key}")
async def delete_agent(request: Request, key: str) -> dict:
    """Delete a subagent definition."""
    orchestrator = request.app.state.orchestrator
    if key not in orchestrator.agents:
        raise HTTPException(status_code=404, detail=f"Subagent not found: {key}")

    vault = request.app.state.vault
    if hasattr(vault, "paths") and vault.paths:
        agent_path = vault.paths.resolve(f"system/agents/{key}.md")
    else:
        agent_path = vault._vault_dir / "system" / "agents" / f"{key}.md"  # noqa: SLF001
    if agent_path.is_file():
        agent_path.unlink()
    orchestrator.load_agents()
    return {"status": "deleted", "key": key}


# ── Dispatch / reload ──


@router.post("/dispatch")
async def dispatch_task(request: Request, body: DispatchRequest) -> dict:
    """Dispatch a task to a specific subagent."""
    orchestrator = request.app.state.orchestrator
    try:
        result = await orchestrator.dispatch(
            agent_name=body.agent_name,
            task=body.task,
            context=body.context,
        )
    except ValueError as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc
    return {
        "agent_name": result.agent_name,
        "content": result.content,
        "input_tokens": result.input_tokens,
        "output_tokens": result.output_tokens,
    }


@router.post("/reload")
async def reload_agents(request: Request) -> dict:
    """Reload subagent definitions from vault."""
    orchestrator = request.app.state.orchestrator
    agents = orchestrator.load_agents()
    return {"reloaded": len(agents), "agents": list(agents.keys())}


# ── Available skills (for the editor's skill picker) ──


@router.get("/available-skills")
async def list_available_skills(request: Request) -> list[dict]:
    """List all skills that can be assigned to subagents."""
    skills = request.app.state.skills
    return [
        {
            "name": s.name,
            "title": s.title,
            "description": s.description,
            "category": s.category,
            "triggers": s.triggers,
        }
        for s in skills.skills.values()
        if s.status == "active"
    ]


# ── Visualization data ──


@router.get("/stats")
async def get_agent_stats(request: Request) -> list[dict]:
    """Per-agent dispatch statistics for visualization."""
    orchestrator = request.app.state.orchestrator
    return await orchestrator.get_agent_stats()


@router.get("/dispatch-log")
async def get_dispatch_log(
    request: Request, limit: int = 50, agent: str | None = None,
) -> list[dict]:
    """Recent dispatch history."""
    orchestrator = request.app.state.orchestrator
    return await orchestrator.get_dispatch_log(limit=limit, agent_key=agent)


@router.get("/active")
async def get_active_dispatches(request: Request) -> dict:
    """Currently running dispatches."""
    orchestrator = request.app.state.orchestrator
    return orchestrator.get_active_dispatches()


# ── Helpers ──


def _build_agent_md(body: AgentSaveRequest) -> str:
    """Build the markdown file content for a subagent definition."""
    skills_yaml = "\n".join(f"  - {s}" for s in body.skills) if body.skills else ""
    skills_block = f"skills:\n{skills_yaml}" if skills_yaml else "skills: []"

    sections = []
    if body.personality.strip():
        sections.append(f"## Personality\n\n{body.personality.strip()}")
    if body.capabilities.strip():
        sections.append(f"## Capabilities\n\n{body.capabilities.strip()}")
    if body.boundaries.strip():
        sections.append(f"## Boundaries\n\n{body.boundaries.strip()}")

    body_text = "\n\n".join(sections)

    return f"""---
title: {body.name}
role: {body.role}
status: {body.status}
{skills_block}
access: {body.access}
---

{body_text}
"""


# ── Workflow execution (supervisor-driven) ──


class WorkflowRunRequest(BaseModel):
    context: str = ""


# Match ``goal:<slug>`` or ``goal=<slug>`` in the workflow context so a
# workflow run invoked against a specific goal can feed per-goal
# bookkeeping + Bayesian trust on completion. Slug format matches the
# one used by CoachService.create_goal (lowercase, alnum + hyphen).
_GOAL_IN_CONTEXT_RE = re.compile(r"\bgoal\s*[:=]\s*([a-z0-9][a-z0-9-]{0,80})\b", re.IGNORECASE)


def _extract_goal_slug(context: str) -> str:
    """Return the goal slug referenced in the workflow context, or empty string."""
    if not context:
        return ""
    m = _GOAL_IN_CONTEXT_RE.search(context)
    return m.group(1).lower() if m else ""


def _latest_dispatch_outcome(execution) -> dict:
    """Walk step_results newest-first and return the first parseable
    ``dispatch-outcome`` block. Empty dict when none present — the caller
    falls open to "no Bayesian signal this run", which is fine: a workflow
    that didn't produce a structured outcome shouldn't move the posterior.
    """
    from agntspace.coach.dispatch_outcome import parse_dispatch_outcome
    for r in reversed(execution.step_results):
        parsed = parse_dispatch_outcome(r.content or "")
        if parsed:
            return parsed
    return {}


@router.post("/workflow/{workflow_id}")
async def run_workflow(request: Request, workflow_id: str, body: WorkflowRunRequest | None = None) -> dict:
    """Execute a workflow through the supervisor.

    The supervisor adaptively decides step order, feedback loops, parallelism,
    and skips based on results from each step.

    **Per-goal Bayesian feed (2026-04-19)**: when ``context`` references a
    specific goal via ``goal:<slug>`` and any step produced a structured
    ``dispatch-outcome`` block (see ``goal-pursue`` skill), this endpoint
    records a pursuit attempt on the goal and feeds the outcome into the
    per-goal Bayesian trust posterior — same semantics as the
    heartbeat-driven ``_wq_pursue_goal`` path, so whether pursuit is
    driven implicitly (heartbeat) or explicitly (this workflow) both
    accumulate evidence on the same posterior.
    """
    supervisor = getattr(request.app.state, "supervisor", None)
    if not supervisor:
        raise HTTPException(status_code=503, detail="Workflow supervisor not initialized")

    # Resolve workflow definition from UI-registered workflows
    workflow_defs = getattr(request.app.state, "workflow_defs", {})
    workflow = workflow_defs.get(workflow_id)
    if not workflow:
        raise HTTPException(status_code=404, detail=f"Unknown workflow: {workflow_id}")

    context = body.context if body else ""
    goal_slug = _extract_goal_slug(context)

    execution = await supervisor.execute(workflow, context=context)

    # Post-execution: feed structured outcome to per-goal Bayesian trust.
    # Fail-open — a broken feed must not corrupt the HTTP response.
    bayesian_feed: dict = {"attempted": False}
    if goal_slug:
        parsed = _latest_dispatch_outcome(execution)
        if parsed.get("outcome"):
            outcome = parsed["outcome"]
            coach = getattr(request.app.state, "coach", None)
            trust_svc = getattr(request.app.state, "trust_service", None)
            bayesian_feed = {
                "attempted": True,
                "goal_slug": goal_slug,
                "outcome": outcome,
                "pursuit_recorded": False,
                "trust_signal_sent": False,
            }
            if coach is not None:
                # record_pursuit_attempt writes to goals/<slug>.md with
                # contract_action="create_task". The workflow HTTP route
                # only pre-authorized "delegate_task" (that's what the
                # user triggered), so the bookkeeping write gets blocked
                # by the VaultWriter runtime guard. Mark "create_task"
                # authorized in THIS correlation scope — this is internal
                # bookkeeping on a resource the user already owns, not a
                # privilege escalation. Live-verified as the blocking
                # cause of an aborted Bayesian feed on 2026-04-19 (run 6).
                from agntspace.activity.decisions import (
                    mark_actions_authorized,
                )
                mark_actions_authorized(["create_task"])
                try:
                    coach.record_pursuit_attempt(
                        goal_slug,
                        succeeded=(outcome == "keep"),
                        cost=0.0,  # Cost lives in cost_tracker already
                        outcome=outcome,
                        hypothesis=parsed.get("hypothesis", ""),
                        action=parsed.get("action", ""),
                        observation=parsed.get("observation", ""),
                        metric_delta=parsed.get("metric_delta", ""),
                        reason=parsed.get("reason", ""),
                    )
                    bayesian_feed["pursuit_recorded"] = True
                except FileNotFoundError:
                    # Goal referenced in context doesn't exist — not an error,
                    # workflow may be targeting an ad-hoc question.
                    bayesian_feed["pursuit_recorded"] = False
                except Exception as exc:  # pragma: no cover
                    bayesian_feed["pursuit_error"] = str(exc)[:200]
            if trust_svc is not None and coach is not None and bayesian_feed["pursuit_recorded"]:
                rating = {
                    "keep": "good",
                    "discard": "needs_improvement",
                    "crash": "redo",
                }.get(outcome)
                if rating:
                    try:
                        g_status = coach.get_pursuit_status(goal_slug)
                        goal_domain = g_status.get("domain") or "general"
                        trust_svc.record_feedback(
                            goal_domain, rating, goal_key=goal_slug,
                        )
                        bayesian_feed["trust_signal_sent"] = True
                        bayesian_feed["trust_domain"] = goal_domain
                        bayesian_feed["trust_rating"] = rating
                    except Exception as exc:  # pragma: no cover
                        bayesian_feed["trust_error"] = str(exc)[:200]

    return {
        "workflow_id": execution.workflow_id,
        "status": execution.status,
        "total_iterations": execution.total_iterations,
        "steps_executed": len(execution.step_results),
        "results": [
            {
                "step_id": r.step_id,
                "agent": r.agent_key,
                "content": r.content[:500],
                "tokens": r.input_tokens + r.output_tokens,
                "duration_ms": r.duration_ms,
                "iteration": r.iteration,
            }
            for r in execution.step_results
        ],
        "supervisor_log": execution.supervisor_log,
        "bayesian_feed": bayesian_feed,
    }


@router.get("/workflows")
async def list_workflows(request: Request) -> list[dict]:
    """List available workflow definitions."""
    workflow_defs = getattr(request.app.state, "workflow_defs", {})
    return [
        {
            "id": w.id,
            "name": w.name,
            "description": w.description,
            "supervisor": w.supervisor,
            "steps": [
                {"id": s.id, "agent_key": s.agent_key, "label": s.label}
                for s in w.steps
            ],
        }
        for w in workflow_defs.values()
    ]
