"""Agent SDK session setup and main event loop for Grukk.

Grukk runs a continuous polling loop with four interleaved cycles:
  1. King messages      -- check every ~5 seconds for inbound commands.
  2. Approvals          -- check every ~10 seconds for pending approval requests.
  3. Loop heartbeats    -- check every ~30 seconds for stale or missing peon-loops.
  4. PR conflict scan   -- check every ~5 minutes for merge conflicts on open PRs.

Each cycle delegates to the appropriate handler module (chat, triage,
supervisor).  The Claude Agent SDK is used to process natural-language
interactions -- Grukk's system prompt lives in ``personality.py``.
"""

from __future__ import annotations

import asyncio
import logging
import os
import time
from typing import Any

from peon_mcp.grukk.api import GrukkAPIClient
from peon_mcp.grukk.chat import ChatHandler
from peon_mcp.grukk.personality import GRUKK_SYSTEM_PROMPT
from peon_mcp.grukk.supervisor import LoopSupervisor
from peon_mcp.grukk.triage import ApprovalTriager, TriageAction

log = logging.getLogger("peon-grukk")

# Guard the Agent SDK import -- it may not be installed yet.
try:
    from claude_agent_sdk import ClaudeAgentOptions, query  # type: ignore[import-untyped]

    _HAS_AGENT_SDK = True
except ModuleNotFoundError:
    _HAS_AGENT_SDK = False
    ClaudeAgentOptions = None  # type: ignore[assignment,misc]
    query = None  # type: ignore[assignment]


async def run_agent_loop(
    *,
    api_url: str,
    api_key: str,
    model: str,
    poll_interval: int,
) -> None:
    """Main Grukk event loop.

    Parameters
    ----------
    api_url:
        Base URL for the Peon REST API.
    api_key:
        Optional API key for authenticated requests.
    model:
        Anthropic model identifier.
    poll_interval:
        Base polling interval in seconds.  Approval checks run at 2x this
        interval, heartbeat checks at 6x.
    """

    # ---- Initialize components ----
    client = GrukkAPIClient(api_url, api_key)
    chat_handler = ChatHandler(client)
    triager = ApprovalTriager(client)
    supervisor = LoopSupervisor(client)

    agent_options: Any = None
    if _HAS_AGENT_SDK and ClaudeAgentOptions is not None:
        agent_options = ClaudeAgentOptions(
            model=model,
            system_prompt=GRUKK_SYSTEM_PROMPT,
            permission_mode="bypassPermissions",
        )
    else:
        log.warning(
            "claude_agent_sdk not installed -- Grukk will run without Agent SDK "
            "NL capabilities.  Chat responses will be canned messages."
        )

    # ---- Start Discord bot(s) ----
    # Bot tokens are mounted as files at DISCORD_BOT_TOKENS_DIR/<config_id>
    # (e.g., /etc/peon/discord/1 for config_id=1). The API masks tokens in
    # responses, so we read them from K8s-mounted secrets instead.
    try:
        from peon_mcp.grukk.discord.bot import bot_manager
        from pathlib import Path

        tokens_dir = Path(os.environ.get("DISCORD_BOT_TOKENS_DIR", "/etc/peon/discord"))

        # Retry config fetch — API may still be starting up
        grukk_config = None
        for attempt in range(10):
            configs_resp = client._request("GET", "/api/discord/configs")
            if isinstance(configs_resp, dict):
                configs = configs_resp.get("items", [])
            elif isinstance(configs_resp, list):
                configs = configs_resp
            else:
                configs = []
            # Find Grukk's config (config_id=1) — only start OUR bot
            grukk_config = next((c for c in configs if c.get("id") == 1 and c.get("enabled")), None)
            if grukk_config:
                break
            log.info("Waiting for Discord config (attempt %d/10)...", attempt + 1)
            await asyncio.sleep(5)

        if grukk_config:
            # Inject real bot token from mounted file
            token_file = tokens_dir / "1"
            if token_file.exists():
                grukk_config["bot_token"] = token_file.read_text().strip()
                await bot_manager.start_from_configs([grukk_config])
                log.info("Started Grukk Discord bot (config_id=1)")
            else:
                log.warning("No token file at %s — Grukk bot not started", token_file)
        else:
            log.info("No Discord config for Grukk — bot not started")
    except Exception as exc:
        log.warning("Failed to start Discord bot: %s", exc)

    log.info("Grukk ready to work!  Entering main loop.")

    # Stagger the four polling cycles using independent timers.
    last_king_check = 0.0
    last_approval_check = 0.0
    last_heartbeat_check = 0.0
    last_conflict_scan = 0.0

    king_interval = poll_interval           # ~5s
    approval_interval = poll_interval * 2   # ~10s
    heartbeat_interval = poll_interval * 6  # ~30s
    conflict_scan_interval = 300            # ~5 minutes

    # --- Startup: greet the King and do initial heartbeat scan ---
    await _startup_greeting(client, agent_options)

    while True:
        now = time.monotonic()

        # --- Cycle 1: King messages (every ~5s) ---
        if now - last_king_check >= king_interval:
            last_king_check = now
            await _check_king_messages(client, chat_handler, agent_options)

        # --- Cycle 2: Pending approvals (every ~10s) ---
        if now - last_approval_check >= approval_interval:
            last_approval_check = now
            await _check_pending_approvals(client, triager)

        # --- Cycle 3: Loop heartbeats (every ~30s) ---
        if now - last_heartbeat_check >= heartbeat_interval:
            last_heartbeat_check = now
            await _check_loop_heartbeats(client, supervisor)

        # --- Cycle 4: PR conflict scan (every ~5 min) ---
        if now - last_conflict_scan >= conflict_scan_interval:
            last_conflict_scan = now
            await _check_pr_conflicts(client)

        # Sleep for a short interval to avoid busy-waiting.
        await asyncio.sleep(1)


class GrukkAgent:
    """Convenience wrapper around :func:`run_agent_loop`.

    Provides a class-based interface for the Grukk foreman agent so callers
    can instantiate and run via ``agent.run()``.
    """

    def __init__(
        self,
        *,
        api_url: str = "http://127.0.0.1:8420",
        api_key: str = "",
        model: str = "claude-sonnet-4-20250514",
        poll_interval: int = 5,
    ) -> None:
        self.api_url = api_url
        self.api_key = api_key
        self.model = model
        self.poll_interval = poll_interval

    async def run(self) -> None:
        """Start the Grukk event loop."""
        await run_agent_loop(
            api_url=self.api_url,
            api_key=self.api_key,
            model=self.model,
            poll_interval=self.poll_interval,
        )


# ---------------------------------------------------------------------------
# Agent SDK helper
# ---------------------------------------------------------------------------


async def _generate_response(
    agent_options: Any,
    message: str,
    context: str = "",
) -> str:
    """Use the Agent SDK to generate Grukk's response to a King message.

    Falls back to a canned response if the SDK is not installed.
    """
    if not _HAS_AGENT_SDK or agent_options is None or query is None:
        return "Zug zug!  Grukk hear you, King.  (Agent SDK not installed -- Grukk running in basic mode.)"

    prompt = f"{context}\n\nThe King says: {message}" if context else f"The King says: {message}"
    result = ""
    try:
        async for msg in query(prompt=prompt, options=agent_options):
            if hasattr(msg, "result"):
                result = msg.result
    except Exception:
        log.exception("Agent SDK query failed.")
        result = "Grukk brain hurt!  Something went wrong with Grukk thinking.  Try again?"
    return result


# ---------------------------------------------------------------------------
# Cycle implementations
# ---------------------------------------------------------------------------


async def _startup_greeting(client: GrukkAPIClient, agent_options: Any) -> None:
    """Send Grukk's startup greeting and initial heartbeat report."""
    log.info("Grukk reporting for duty!  Zug zug!")

    # Check current loop status for the greeting.
    loops = client.get_active_loops()
    if loops:
        loop_summary = ", ".join(
            f"{l.get('id', '?')} (project {l.get('project_id', '?')})"
            for l in loops
        )
        greeting = (
            f"Ready to work!  Grukk reporting for duty, my King.  "
            f"Grukk see {len(loops)} peon(s) working: {loop_summary}."
        )
    else:
        greeting = (
            "Ready to work!  Grukk reporting for duty, my King.  "
            "No peons working right now -- camp is quiet."
        )

    # Post the greeting message.
    client.send_message(greeting)
    log.info(greeting)


async def _check_king_messages(
    client: GrukkAPIClient,
    chat_handler: ChatHandler,
    agent_options: Any,
) -> None:
    """Poll for inbound messages from the King and respond."""
    messages = client.get_pending_messages()
    if not messages:
        return

    for msg in messages:
        content = msg.get("content", "")
        user_id = msg.get("user_id")

        log.info("King message received: %s", content[:100])

        # Run through ChatHandler (logs + returns for SDK processing).
        await chat_handler.handle_message(content)

        # Generate a response via Agent SDK (or canned fallback).
        # Build context from recent loop status.
        loops = client.get_active_loops()
        context = f"Current active peon-loops: {len(loops)}"
        response = await _generate_response(agent_options, content, context)

        # Send Grukk's response back.
        metadata: dict[str, Any] = {}
        if msg.get("id"):
            metadata["in_reply_to"] = str(msg["id"])
        client.send_message(response, user_id=user_id, metadata=metadata)
        log.info("Grukk responded: %s", response[:100])


async def _check_pending_approvals(
    client: GrukkAPIClient,
    triager: ApprovalTriager,
) -> None:
    """Poll for pending approval requests and triage them."""
    approvals = client.get_pending_approvals()
    if not approvals:
        return

    log.info("Grukk found %d pending approval(s) to triage.", len(approvals))

    for approval in approvals:
        approval_id = approval.get("id", "unknown")
        title = approval.get("title", "untitled")

        decision = await triager.evaluate(approval)
        log.info(
            "Approval '%s' [%s] -> %s (%s risk): %s",
            title, approval_id, decision.action.value, decision.risk.value, decision.reason,
        )

        if decision.action == TriageAction.AUTO_APPROVE:
            # Auto-resolve without bothering the King.
            client.resolve_approval(
                approval_id,
                status="approved",
                response=decision.recommendation or decision.reason,
            )
            log.info("Auto-approved approval '%s'.", title)

        elif decision.action == TriageAction.AUTO_REJECT:
            client.resolve_approval(
                approval_id,
                status="rejected",
                response=decision.reason,
            )
            log.info("Auto-rejected approval '%s'.", title)

        elif decision.action in (TriageAction.ESCALATE, TriageAction.ESCALATE_WITH_RECOMMENDATION):
            # Notify the King via chat message.
            escalation_msg = (
                f"My King!  Peon need approval for: **{title}**\n"
                f"Risk: {decision.risk.value}\n"
                f"Grukk assessment: {decision.reason}"
            )
            if decision.recommendation:
                escalation_msg += f"\nGrukk recommendation: {decision.recommendation}"
            client.send_message(
                escalation_msg,
                metadata={"type": "approval_escalation", "approval_id": approval_id},
            )
            log.info("Escalated approval '%s' to King.", title)


async def _check_loop_heartbeats(
    client: GrukkAPIClient,
    supervisor: LoopSupervisor,
) -> None:
    """Monitor peon-loop heartbeats for staleness or stuck tasks."""
    issues = await supervisor.check_loops()
    if not issues:
        log.debug("All peons working good.  No issues detected.")
        return

    log.warning("Grukk detected %d loop issue(s)!", len(issues))

    for issue in issues:
        alert_msg = (
            f"Bad news, my King!  Problem with peon loop **{issue.loop_id}**:\n"
            f"Issue: {issue.issue_type.value}\n"
            f"{issue.description}"
        )
        if issue.recommended_action:
            alert_msg += f"\nGrukk recommend: {issue.recommended_action.value}"
        if issue.project_id:
            alert_msg += f"\nProject: {issue.project_id}"
        if issue.task_id:
            alert_msg += f"\nTask: {issue.task_id}"

        client.send_message(
            alert_msg,
            metadata={
                "type": "loop_alert",
                "loop_id": issue.loop_id,
                "issue_type": issue.issue_type.value,
            },
        )
        log.warning("Alerted King about loop %s: %s", issue.loop_id, issue.issue_type.value)


async def _check_pr_conflicts(client: GrukkAPIClient) -> None:
    """Scan open PRs for merge conflicts on projects with active loops."""
    loops = client.get_active_loops()
    if not loops:
        return

    # Collect unique project IDs from active loops.
    project_ids = {l.get("project_id") for l in loops if l.get("project_id")}
    if not project_ids:
        return

    for project_id in project_ids:
        try:
            result = client.scan_pr_conflicts(project_id)
        except Exception:
            log.debug(
                "PR conflict scan failed for project %s (non-fatal).",
                project_id,
                exc_info=True,
            )
            continue

        if not result:
            continue

        tasks_created = result.get("tasks_created", 0)
        conflicts_found = result.get("conflicts_found", 0)

        if tasks_created > 0:
            msg = (
                f"Boss!  Me found merge conflicts in {conflicts_found} PR(s) "
                f"for {project_id}.  Created {tasks_created} fix task(s)."
            )
            client.send_message(
                msg,
                metadata={
                    "type": "pr_conflict_scan",
                    "project_id": project_id,
                    "tasks_created": tasks_created,
                },
            )
            log.info("PR conflict scan: %s", msg)
        elif conflicts_found > 0:
            log.debug(
                "PR conflict scan: %d conflict(s) in %s, all already tracked.",
                conflicts_found, project_id,
            )
        else:
            log.debug("PR conflict scan: no conflicts in %s.", project_id)
