"""Dobby Slack bot — receive commands via Slack, dispatch to Dobby daemon.

Features:
- NL confirmation flow with react-to-confirm (checkmark/X)
- One thread per task with status update replies
- Batch brain dump (bullet/numbered list -> multiple tasks)
- Inline corrections (reply to confirmation to edit before submitting)
- Detail-status queries ("whats t-xxx doing?")
- DM shorthand ("running?", "results spare")
- Overnight summary ("summary", "what happened last night", "overnight")
"""
from __future__ import annotations

import asyncio
import contextlib
import logging
import os
from typing import Any

from dobby.cli.client import DobbyClient
from dobby.slack.approval import ApprovalMixin
from dobby.slack.blocks import BlockBuildersMixin
from dobby.slack.claude_chat import ClaudeChatMixin
from dobby.slack.commands import SlashCommandMixin

# -- Mixins ------------------------------------------------------------------
from dobby.slack.handlers import StatusHandlersMixin
from dobby.slack.parser import SlackParser

# -- Shared state & helpers (re-exported for backward compatibility) ---------
from dobby.slack.state import (  # noqa: F401  — re-exports
    _CLAUDE_SESSIONS,
    _CLAUDE_SESSIONS_FILE,
    _PENDING,
    _PENDING_PLANS,
    _PLAN_THREADS,
    _PLAN_THREADS_FILE,
    _SUMMARY_KEYWORDS,
    _SUMMARY_THREADS,
    _TASK_ID_RE,
    _TASK_THREADS,
    _build_task_payload,
    _load_claude_sessions,
    _load_plan_threads,
    _parse_task_end_time,
    _PendingBatch,
    _PendingConfirmation,
    _progress_bar,
    _save_claude_sessions,
    _save_plan_threads,
    _strip_mention,
    get_task_thread,
    register_plan_approval,
    register_plan_thread,
    register_task_thread,
)

logger = logging.getLogger(__name__)


class DobbySlackBot(
    StatusHandlersMixin,
    SlashCommandMixin,
    ApprovalMixin,
    ClaudeChatMixin,
    BlockBuildersMixin,
):
    """Async Slack bot that forwards commands to the Dobby daemon."""

    def __init__(
        self,
        slack_bot_token: str,
        slack_app_token: str,
        dobby_url: str = "http://127.0.0.1:8420",
        hub_url: str = "",
    ) -> None:
        from slack_bolt.async_app import AsyncApp

        self._app_token = slack_app_token
        self._hub_url = hub_url.rstrip("/") if hub_url else ""
        self._client_wrapper = DobbyClient(dobby_url)
        self._parser = SlackParser(
            anthropic_api_key=os.environ.get("ANTHROPIC_API_KEY")
        )

        self.app = AsyncApp(token=slack_bot_token)
        _load_claude_sessions()
        _load_plan_threads()
        self._register_handlers()

    # ------------------------------------------------------------------
    # Event registration
    # ------------------------------------------------------------------

    def _register_handlers(self) -> None:
        self.app.event("app_mention")(self._handle_mention)
        self.app.event("message")(self._handle_dm)
        self.app.event("reaction_added")(self._handle_reaction)

        # Slash command
        self.app.command("/dobby")(self._handle_slash_command)

        # Block Kit action handlers
        self.app.action("dobby_submit")(self._action_submit)
        self.app.action("dobby_cancel")(self._action_cancel)
        self.app.action("dobby_approve_plan")(self._action_approve_plan)
        self.app.action("dobby_cancel_task")(self._action_cancel_task)
        self.app.action("dobby_retry_task")(self._action_retry_task)
        # Link buttons (no server action needed, just ack)
        self.app.action("dobby_view_dashboard")(self._action_ack)
        self.app.action("dobby_view_logs")(self._action_ack)
        self.app.action("dobby_view_pr")(self._action_ack)
        self.app.action("dobby_edit_plan")(self._action_edit)

    # ------------------------------------------------------------------
    # Event handlers
    # ------------------------------------------------------------------

    async def _handle_mention(self, event: dict, say: Any, client: Any) -> None:
        """Handle @dobby mentions in channels."""
        text: str = event.get("text", "")
        clean = _strip_mention(text).strip()
        await self._dispatch(clean, event, say, client)

    async def _handle_dm(self, event: dict, say: Any, client: Any) -> None:
        """Handle direct messages to the bot."""
        if event.get("bot_id") or event.get("subtype"):
            return
        text: str = event.get("text", "").strip()
        if not text:
            return

        # Check if this is a thread reply to a pending confirmation
        thread_ts = event.get("thread_ts")
        if thread_ts and thread_ts in _PENDING:
            await self._handle_correction(thread_ts, text, event, say, client)
            return

        # Check if this is a thread reply in a plan conversation thread
        if thread_ts and thread_ts in _PLAN_THREADS:
            await self._handle_plan_reply(thread_ts, text, event, say, client)
            return

        # Check if this is a thread reply to a Claude Code conversation
        if thread_ts and thread_ts in _CLAUDE_SESSIONS:
            await self._handle_claude_thread(thread_ts, text, event, say, client)
            return

        await self._dispatch(text, event, say, client)

    async def _handle_reaction(self, event: dict, client: Any) -> None:
        """Handle reactions on confirmation and plan approval messages."""
        reaction = event.get("reaction", "")
        item = event.get("item", {})
        msg_ts = item.get("ts")
        channel = item.get("channel")

        # Plan approval reactions
        if msg_ts and msg_ts in _PENDING_PLANS:
            plan = _PENDING_PLANS[msg_ts]
            ch: str = channel or ""
            if reaction in ("white_check_mark", "heavy_check_mark"):
                del _PENDING_PLANS[msg_ts]
                await self._approve_plan(plan, ch, msg_ts, client)
            elif reaction == "x":
                del _PENDING_PLANS[msg_ts]
                await client.chat_postMessage(
                    channel=ch, thread_ts=msg_ts,
                    text=":no_entry_sign: Plan cancelled.",
                )
            return

        if not msg_ts or msg_ts not in _PENDING:
            return

        pending = _PENDING[msg_ts]
        ch: str = channel or ""

        if reaction in ("white_check_mark", "heavy_check_mark"):
            # Approve: create the task(s)
            del _PENDING[msg_ts]
            if isinstance(pending, _PendingBatch):
                await self._confirm_batch(pending, ch, msg_ts, client)
            else:
                await self._confirm_single(pending, ch, msg_ts, client)

        elif reaction == "x":
            # Cancel the pending confirmation
            del _PENDING[msg_ts]
            await client.chat_postMessage(
                channel=ch,
                thread_ts=msg_ts,
                text=":no_entry_sign: Task cancelled. Send a new message to try again.",
            )

    # ------------------------------------------------------------------
    # Block Kit action handlers
    # ------------------------------------------------------------------

    async def _action_ack(self, ack: Any, body: dict) -> None:
        """Acknowledge link button clicks (no server action needed)."""
        await ack()

    async def _action_edit(self, ack: Any, body: dict) -> None:
        """Handle Edit button -- prompt user to reply in thread."""
        await ack()
        msg_ts = body.get("message", {}).get("ts")
        channel = body.get("channel", {}).get("id", "")
        if msg_ts and channel:
            with contextlib.suppress(Exception):
                await self.app.client.chat_postMessage(
                    channel=channel, thread_ts=msg_ts,
                    text=(
                        ":pencil2: Reply in this thread with your edits."
                        " I'll update the task and re-confirm."
                    ),
                )

    async def _action_submit(self, ack: Any, body: dict) -> None:
        """Handle Submit button click on task confirmation."""
        await ack()
        msg_ts = body.get("message", {}).get("ts")
        channel = body.get("channel", {}).get("id", "")
        if not msg_ts or msg_ts not in _PENDING:
            return
        pending = _PENDING.pop(msg_ts)
        client = self.app.client
        if isinstance(pending, _PendingBatch):
            await self._confirm_batch(pending, channel, msg_ts, client)
        else:
            await self._confirm_single(pending, channel, msg_ts, client)

    async def _action_cancel(self, ack: Any, body: dict) -> None:
        """Handle Cancel button click."""
        await ack()
        msg_ts = body.get("message", {}).get("ts")
        channel = body.get("channel", {}).get("id", "")
        if msg_ts and msg_ts in _PENDING:
            del _PENDING[msg_ts]
        if msg_ts and msg_ts in _PENDING_PLANS:
            del _PENDING_PLANS[msg_ts]
        with contextlib.suppress(Exception):
            await self.app.client.chat_postMessage(
                channel=channel, thread_ts=msg_ts,
                text=":no_entry_sign: Cancelled.",
            )

    async def _action_approve_plan(self, ack: Any, body: dict) -> None:
        """Handle Approve & Execute button on plan approval."""
        await ack()
        msg_ts = body.get("message", {}).get("ts")
        channel = body.get("channel", {}).get("id", "")
        if not msg_ts or msg_ts not in _PENDING_PLANS:
            return
        plan = _PENDING_PLANS.pop(msg_ts)
        await self._approve_plan(plan, channel, msg_ts, self.app.client)

    async def _action_cancel_task(self, ack: Any, body: dict) -> None:
        """Handle Cancel button on running task notification."""
        await ack()
        action = body.get("actions", [{}])[0]
        task_id = action.get("value", "")
        channel = body.get("channel", {}).get("id", "")
        if not task_id:
            return
        try:
            await self._client_wrapper.patch(f"/tasks/{task_id}", json={"status": "cancelled"})
            await self.app.client.chat_postMessage(
                channel=channel,
                text=f":no_entry_sign: Task `{task_id}` cancelled.",
            )
        except Exception as exc:
            logger.warning("Cancel task action failed: %s", exc)

    async def _action_retry_task(self, ack: Any, body: dict) -> None:
        """Handle Retry/Run Again button."""
        await ack()
        action = body.get("actions", [{}])[0]
        task_id = action.get("value", "")
        channel = body.get("channel", {}).get("id", "")
        if not task_id:
            return
        try:
            await self._client_wrapper.post(f"/tasks/{task_id}/retry")
            await self.app.client.chat_postMessage(
                channel=channel,
                text=f":arrows_counterclockwise: Task `{task_id}` requeued.",
            )
        except Exception as exc:
            logger.warning("Retry task action failed: %s", exc)

    # ------------------------------------------------------------------
    # Confirmation flow
    # ------------------------------------------------------------------

    async def _get_spokes(self) -> list[str]:
        try:
            data = await self._client_wrapper.get("/spokes")
            if isinstance(data, list):
                return [s.get("id", "") for s in data if s.get("id")]
            return []
        except Exception:
            return []

    async def _infer_target(self, project: str) -> str:
        """Infer the best spoke target for a project from task history."""
        try:
            resp = await self._client_wrapper.get("/tasks", params={"project": project})
            tasks = resp.get("items", resp) if isinstance(resp, dict) else resp
            # Find the most common non-local target
            targets: dict[str, int] = {}
            for t in tasks:
                tgt = t.get("target", "local")
                if tgt and tgt != "local" and tgt != "any":
                    targets[tgt] = targets.get(tgt, 0) + 1
            if targets:
                return max(targets, key=targets.get)
        except Exception:
            pass
        return "mac-local"

    async def _handle_task_confirmation(
        self, text: str, event: dict, say: Any, client: Any, mode: str = "quick"
    ) -> None:
        """Parse message, show structured preview with Block Kit buttons."""
        projects = await self._get_projects()
        spokes = await self._get_spokes()
        parsed = await self._parser.parse(text, projects, spokes)
        channel = event.get("channel", "")

        # Override mode if specified by prefix
        if mode != "quick":
            parsed["mode"] = mode

        # Auto-infer target from project history if not set
        if not parsed.get("target") and parsed.get("project"):
            parsed["target"] = await self._infer_target(parsed["project"])

        if not parsed.get("project"):
            await say(
                ":thinking_face: I couldn't figure out which project this is for. "
                "Please mention the project name (e.g. \"add tests to *spare*\")."
            )
            return

        # Pass trace context from event for trajectory tracking
        if event.get("_trace_context"):
            parsed["_trace_context"] = event["_trace_context"]

        # Try Block Kit buttons; fall back to reactions for DMs
        blocks = self._build_confirmation_blocks(parsed, spokes)
        fallback_text = self._format_confirmation_preview(parsed)
        try:
            response = await client.chat_postMessage(
                channel=channel,
                blocks=blocks,
                text=fallback_text,
            )
        except Exception:
            # Fallback: plain text + reactions (DMs or permissions issue)
            response = await say(fallback_text)
            if response and response.get("ts"):
                msg_ts = response["ts"]
                _PENDING[msg_ts] = _PendingConfirmation(parsed, channel, projects)
                try:
                    await client.reactions_add(
                        channel=channel, name="white_check_mark", timestamp=msg_ts
                    )
                    await client.reactions_add(channel=channel, name="x", timestamp=msg_ts)
                except Exception as exc:
                    logger.warning("Failed to add reactions: %s", exc)
            return

        if response:
            resp_data = response if isinstance(response, dict) else getattr(response, "data", {})
            msg_ts = resp_data.get("ts")
            if msg_ts:
                _PENDING[msg_ts] = _PendingConfirmation(parsed, channel, projects)

    async def _confirm_single(
        self, pending: _PendingConfirmation, channel: str, thread_ts: str, client: Any
    ) -> None:
        """Create a single task after confirmation."""
        payload = _build_task_payload(pending.parsed)
        try:
            task = await self._client_wrapper.post("/tasks", json=payload)
        except Exception as exc:
            await client.chat_postMessage(
                channel=channel, thread_ts=thread_ts,
                text=f":warning: Failed to create task: `{exc}`",
            )
            return

        task_id = task.get("id", "?")
        register_task_thread(task_id, channel, thread_ts)

        await client.chat_postMessage(
            channel=channel, thread_ts=thread_ts,
            text=f":rocket: Task `{task_id}` queued! I'll post updates here.",
        )

    # ------------------------------------------------------------------
    # Batch brain dump
    # ------------------------------------------------------------------

    async def _handle_batch(self, text: str, event: dict, say: Any, client: Any) -> None:
        """Parse multi-item message and show batch confirmation."""
        projects = await self._get_projects()
        parsed_list = await self._parser.parse_batch(text, projects)
        channel = event.get("channel", "")

        # Filter out items with no project
        valid = [p for p in parsed_list if p.get("project")]
        if not valid:
            await say(
                ":thinking_face: I found multiple items but couldn't match any to a project. "
                "Please mention the project name."
            )
            return

        # Try Block Kit buttons; fall back to reactions for DMs
        blocks = self._build_batch_preview_blocks(valid)
        fallback_text = self._format_batch_preview(valid)
        try:
            response = await client.chat_postMessage(
                channel=channel,
                blocks=blocks,
                text=fallback_text,
            )
        except Exception:
            response = await say(fallback_text)
            if response and response.get("ts"):
                msg_ts = response["ts"]
                _PENDING[msg_ts] = _PendingBatch(valid, channel, projects)
                try:
                    await client.reactions_add(
                        channel=channel, name="white_check_mark", timestamp=msg_ts
                    )
                    await client.reactions_add(channel=channel, name="x", timestamp=msg_ts)
                except Exception as exc:
                    logger.warning("Failed to add reactions: %s", exc)
            return

        if response:
            resp_data = response if isinstance(response, dict) else getattr(response, "data", {})
            msg_ts = resp_data.get("ts")
            if msg_ts:
                _PENDING[msg_ts] = _PendingBatch(valid, channel, projects)

    async def _confirm_batch(
        self, pending: _PendingBatch, channel: str, thread_ts: str, client: Any
    ) -> None:
        """Create all tasks in a batch after confirmation."""
        tasks_payload = []
        for parsed in pending.tasks:
            tasks_payload.append(_build_task_payload(parsed))

        try:
            result = await self._client_wrapper.post("/batches", json={"tasks": tasks_payload})
        except Exception as exc:
            await client.chat_postMessage(
                channel=channel, thread_ts=thread_ts,
                text=f":warning: Failed to create batch: `{exc}`",
            )
            return

        batch_id = result.get("batch_id", "?")
        created_tasks = result.get("tasks", [])

        # Register thread for each task
        for t in created_tasks:
            register_task_thread(t.get("id", ""), channel, thread_ts)

        task_ids = [f"`{t.get('id', '?')}`" for t in created_tasks]
        await client.chat_postMessage(
            channel=channel, thread_ts=thread_ts,
            text=(
                f":rocket: Batch `{batch_id}` queued with {len(created_tasks)} tasks!\n"
                f"Task IDs: {', '.join(task_ids)}\n"
                "I'll post updates here as they progress."
            ),
        )

    # ------------------------------------------------------------------
    # Block Kit interaction handler
    # ------------------------------------------------------------------

    async def handle_interaction(self, payload: dict) -> None:
        """Handle a Slack Block Kit interaction payload (button click, menu select)."""
        actions = payload.get("actions", [])
        if not actions:
            return

        action = actions[0]
        action_id = action.get("action_id", "")
        channel_info = payload.get("channel", {})
        channel = channel_info.get("id", "")
        message = payload.get("message", {})
        msg_ts = message.get("ts", "")

        # Use the response_url for quick acks, but also need a client
        # for follow-up messages. We'll import the client from the app.
        try:
            client = self.app.client
        except Exception:
            logger.warning("No Slack client available for interaction handling")
            return

        # --- Dropdown selections: update pending state ---
        if action_id == "dobby_select_backend" and msg_ts in _PENDING:
            pending = _PENDING[msg_ts]
            selected = action.get("selected_option", {}).get("value", "")
            if isinstance(pending, _PendingConfirmation) and selected:
                pending.parsed["backend"] = selected
            return

        if action_id == "dobby_select_target" and msg_ts in _PENDING:
            pending = _PENDING[msg_ts]
            selected = action.get("selected_option", {}).get("value", "")
            if isinstance(pending, _PendingConfirmation) and selected:
                pending.parsed["target"] = selected
            return

        # --- Submit button ---
        if action_id == "dobby_submit":
            pending = _PENDING.get(msg_ts)
            if not pending:
                return
            del _PENDING[msg_ts]
            if isinstance(pending, _PendingBatch):
                await self._confirm_batch(pending, channel, msg_ts, client)
            else:
                await self._confirm_single(pending, channel, msg_ts, client)
            # Update the original message to remove buttons
            with contextlib.suppress(Exception):
                await client.chat_update(
                    channel=channel, ts=msg_ts,
                    text=":rocket: Task submitted!",
                    blocks=[{"type": "section", "text": {
                        "type": "mrkdwn", "text": ":rocket: *Task submitted!*",
                    }}],
                )
            return

        # --- Cancel button ---
        if action_id == "dobby_cancel":
            if msg_ts in _PENDING:
                del _PENDING[msg_ts]
            if msg_ts in _PENDING_PLANS:
                del _PENDING_PLANS[msg_ts]
            with contextlib.suppress(Exception):
                await client.chat_update(
                    channel=channel, ts=msg_ts,
                    text=":no_entry_sign: Cancelled.",
                    blocks=[{"type": "section", "text": {
                        "type": "mrkdwn", "text": ":no_entry_sign: *Cancelled.*",
                    }}],
                )
            return

        # --- Edit button: prompt user to reply in thread ---
        if action_id == "dobby_edit":
            await client.chat_postMessage(
                channel=channel, thread_ts=msg_ts,
                text=(
                    ":pencil2: Reply in this thread with your edits"
                    " (e.g. \"change target to ec2-orby\" or \"set priority to high\")."
                ),
            )
            return

        # --- Plan approval ---
        if action_id == "dobby_approve_plan":
            plan = _PENDING_PLANS.get(msg_ts)
            if not plan:
                return
            del _PENDING_PLANS[msg_ts]
            await self._approve_plan(plan, channel, msg_ts, client)
            with contextlib.suppress(Exception):
                await client.chat_update(
                    channel=channel, ts=msg_ts,
                    text=":rocket: Plan approved!",
                    blocks=[{"type": "section", "text": {
                        "type": "mrkdwn",
                        "text": ":rocket: *Plan approved and execution queued!*",
                    }}],
                )
            return

        if action_id == "dobby_edit_plan":
            await client.chat_postMessage(
                channel=channel, thread_ts=msg_ts,
                text=":pencil2: Reply in this thread with your edits to the plan before approving.",
            )
            return

        # --- Running task: cancel ---
        if action_id == "dobby_cancel_task":
            task_id = action.get("value", "")
            if task_id:
                try:
                    await self._client_wrapper.patch(
                        f"/tasks/{task_id}", json={"status": "cancelled"}
                    )
                    await client.chat_postMessage(
                        channel=channel, thread_ts=msg_ts,
                        text=f":no_entry_sign: Task `{task_id}` cancelled.",
                    )
                except Exception as exc:
                    await client.chat_postMessage(
                        channel=channel, thread_ts=msg_ts,
                        text=f":warning: Failed to cancel task: `{exc}`",
                    )
            return

        # --- Result: retry ---
        if action_id == "dobby_retry_task":
            task_id = action.get("value", "")
            if task_id:
                try:
                    await self._client_wrapper.post(f"/tasks/{task_id}/retry")
                    await client.chat_postMessage(
                        channel=channel, thread_ts=msg_ts,
                        text=f":arrows_counterclockwise: Task `{task_id}` queued for retry.",
                    )
                except Exception as exc:
                    await client.chat_postMessage(
                        channel=channel, thread_ts=msg_ts,
                        text=f":warning: Failed to retry task: `{exc}`",
                    )
            return

    # ------------------------------------------------------------------
    # Helpers
    # ------------------------------------------------------------------

    async def _get_projects(self) -> list[str]:
        try:
            data = await self._client_wrapper.get("/projects")
            if isinstance(data, list):
                return [p.get("name", "") for p in data if p.get("name")]
            return []
        except Exception:
            return []

    # ------------------------------------------------------------------
    # Entry point
    # ------------------------------------------------------------------

    def run(self) -> None:
        """Start the bot using Slack Socket Mode (blocking)."""
        from slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandler

        async def _start() -> None:
            handler = AsyncSocketModeHandler(self.app, self._app_token)
            await handler.start_async()

        asyncio.run(_start())
