"""StatusHandlersMixin — core message router, status, cost, summary, and query handlers."""
from __future__ import annotations

import contextlib
import logging
import re
from datetime import UTC, datetime, timedelta
from typing import Any

from dobby.slack.state import (
    _CLAUDE_SESSIONS,
    _PENDING,
    _PLAN_THREADS,
    _SUMMARY_KEYWORDS,
    _SUMMARY_THREADS,
    _TASK_ID_RE,
    _progress_bar,
)

logger = logging.getLogger(__name__)


class StatusHandlersMixin:
    """Message routing and status/summary query methods."""

    # These attributes are provided by DobbySlackBot.__init__:
    #   self._client_wrapper, self._parser, self.app, self._hub_url

    async def _dispatch(self, text: str, event: dict, say: Any, client: Any) -> None:
        """Route a cleaned message to the appropriate action."""
        from dobby.slack.parser import looks_like_batch

        lower = text.lower().strip()
        channel = event.get("channel", "")
        msg_ts = event.get("ts", "")
        user_id = event.get("user", "")

        # Acknowledge every message with eyes emoji
        if client and channel and msg_ts:
            with contextlib.suppress(Exception):
                await client.reactions_add(channel=channel, name="eyes", timestamp=msg_ts)

        # Stash trace context in event so downstream handlers can pass it to API
        event["_trace_context"] = {
            "source": "slack",
            "source_id": msg_ts,
            "user_id": user_id,
            "input_text": text[:500],
            "channel": channel,
            "thread_ts": event.get("thread_ts"),
        }

        # Help command
        if lower in ("help", "--help", "-h", "commands"):
            await self._handle_help(say)
            return

        # Projects list
        if lower in ("projects", "project help", "project list", "list projects"):
            await self._handle_projects(say)
            return

        # DM shorthand: "running?"
        if lower in ("running?", "running", "whats running", "what's running"):
            await self._handle_running_query(say)
            return

        # DM shorthand: "cost <project>"
        if lower.startswith("cost ") or lower == "cost":
            project = text[len("cost"):].strip() or None
            await self._handle_cost(say, project=project)
            return

        # DM shorthand: "results <project>"
        if lower.startswith("results ") or lower == "results":
            project = text[len("results"):].strip() or None
            await self._handle_results_query(project, say)
            return

        # Summary / overnight report: "summary", "summary 8h", "summary spare", "summary spare 24h"
        if any(lower.startswith(kw) or lower == kw for kw in _SUMMARY_KEYWORDS):
            hours, project = self._parse_time_and_project(text, _SUMMARY_KEYWORDS, default_hours=12)
            await self._handle_summary(say, hours=hours, project=project)
            return

        # Detail-status: any message containing a task ID
        task_match = _TASK_ID_RE.search(text)
        if task_match:
            await self._handle_detail_status(task_match.group(), say)
            return

        # Status command: "status", "status 8h", "status spare", "status spare 24h"
        if lower == "status" or lower.startswith("status "):
            hours, project = self._parse_time_and_project(text, ["status"], default_hours=4)
            await self._handle_status(say, hours=hours, project=project)
            return

        # Check if this is a thread reply to a summary thread (drill-down)
        thread_ts = event.get("thread_ts")
        if thread_ts and thread_ts in _SUMMARY_THREADS:
            await self._handle_summary_detail(text, say)
            return

        # Check if this is a thread reply to a pending confirmation
        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 an active Claude Code conversation
        if thread_ts and thread_ts in _CLAUDE_SESSIONS:
            await self._handle_claude_thread(thread_ts, text, event, say, client)
            return

        # ---------------------------------------------------------------
        # Prefix-based routing:
        #   do: / run:   -> task (immediate execution)
        #   plan:        -> planned task (explore, then approve)
        #   ask:         -> force free-form Claude
        #   batch:       -> batch of tasks
        #   (default)    -> free-form Claude conversation
        # ---------------------------------------------------------------

        # do: / run: -> task submission
        for prefix in ("do:", "run:"):
            if lower.startswith(prefix):
                task_text = text[len(prefix):].strip()
                if task_text:
                    await self._handle_task_confirmation(task_text, event, say, client)
                    return

        # plan: -> planned task (explore first, approve before execute)
        if lower.startswith("plan:"):
            plan_text = text[len("plan:"):].strip()
            if plan_text:
                await self._handle_task_confirmation(plan_text, event, say, client, mode="planned")
                return

        # ask: -> force free-form Claude
        if lower.startswith("ask:"):
            ask_text = text[len("ask:"):].strip()
            if ask_text:
                event_copy = dict(event)
                await self._handle_claude_freeform(ask_text, event_copy, say, client)
                return

        # batch: -> batch of tasks (or auto-detect bullet list)
        if lower.startswith("batch:"):
            batch_text = text[len("batch:"):].strip()
            if batch_text:
                await self._handle_batch(batch_text, event, say, client)
                return
        if looks_like_batch(text):
            await self._handle_batch(text, event, say, client)
            return

        # Default: free-form Claude conversation
        await self._handle_claude_freeform(text, event, say, client)

    # ------------------------------------------------------------------
    # Status queries
    # ------------------------------------------------------------------

    def _parse_time_and_project(
        self, text: str, keywords: list | tuple, default_hours: int = 4
    ) -> tuple[int, str | None]:
        """Parse 'command [project] [Nh]' -> (hours, project|None)."""
        # Strip the keyword prefix
        rest = text
        for kw in sorted(keywords, key=len, reverse=True):
            if rest.lower().startswith(kw):
                rest = rest[len(kw):].strip()
                break

        hours = default_hours
        project = None

        # Extract hours pattern: "8h", "24h"
        hours_match = re.search(r"(\d+)\s*h\b", rest.lower())
        if hours_match:
            hours = int(hours_match.group(1))
            rest = rest[:hours_match.start()] + rest[hours_match.end():]

        # Remaining text is project name
        rest = rest.strip()
        if rest:
            project = rest

        return hours, project

    async def _handle_status(self, say: Any, hours: int = 4, project: str | None = None) -> None:
        """Concise bullet-point status per project."""
        recent, results_map = await self._fetch_recent(hours, project)
        if not recent:
            label = f" for *{project}*" if project else ""
            await say(f":white_check_mark: No task activity in the last {hours}h{label}.")
            return

        # Group by project
        by_project: dict[str, list] = {}
        for t in recent:
            p = t.get("project", "?")
            by_project.setdefault(p, []).append(t)

        lines = [f"*Status:* :clipboard: Last {hours}h\n"]
        for proj, tasks in by_project.items():
            completed = sum(1 for t in tasks if t.get("status") == "completed")
            failed = sum(1 for t in tasks if t.get("status") == "failed")
            running = sum(1 for t in tasks if t.get("status") == "running")
            parts = []
            if completed:
                parts.append(f":white_check_mark: {completed} done")
            if failed:
                parts.append(f":x: {failed} failed")
            if running:
                parts.append(f":runner: {running} running")
            lines.append(f"\u2022 *{proj}*: {', '.join(parts) if parts else 'no activity'}")
        await say("\n".join(lines))

    async def _handle_cost(self, say: Any, project: str | None = None) -> None:
        """Show cost breakdown by project and time range."""
        try:
            data = await self._client_wrapper.get("/stats/cost")
        except Exception as exc:
            await say(f":warning: Could not fetch cost data: `{exc}`")
            return

        by_project = data.get("by_project", {})

        if project:
            # Filter to a single project
            info = by_project.get(project)
            if not info:
                await say(f":thinking_face: No cost data found for project *{project}*.")
                return
            lines = [
                f":money_with_wings: *Cost \u2014 {project}*\n",
                f"\u2022 *Today:* ${info.get('cost_today', 0):.4f}",
                f"\u2022 *This Week:* ${info.get('cost_week', 0):.4f}",
                f"\u2022 *All Time:* ${info.get('cost_usd', 0):.4f}",
                f"\u2022 *Tasks:* {info.get('task_count', 0)}",
                f"\u2022 *Tokens:* {info.get('total_tokens', 0):,}",
                "\n_Equivalent API cost covered by subscription._",
            ]
            await say("\n".join(lines))
            return

        cost_today = data.get("cost_today", 0)
        cost_week = data.get("cost_week", 0)
        cost_total = data.get("cost_total", 0)

        lines = [
            ":money_with_wings: *Cost Breakdown*\n",
            f"*Today:* ${cost_today:.4f}  |  *This Week:* ${cost_week:.4f}"
            f"  |  *All Time:* ${cost_total:.4f}\n",
        ]

        # Sort projects by total cost descending
        sorted_projects = sorted(
            by_project.items(), key=lambda x: x[1].get("cost_usd", 0), reverse=True
        )
        if sorted_projects:
            lines.append("*By Project:*")
            for proj_name, info in sorted_projects:
                proj_cost = info.get("cost_usd", 0)
                proj_tasks = info.get("task_count", 0)
                proj_today = info.get("cost_today", 0)
                parts = [f"${proj_cost:.4f} total"]
                if proj_today > 0:
                    parts.append(f"${proj_today:.4f} today")
                parts.append(f"{proj_tasks} tasks")
                lines.append(f"\u2022 *{proj_name}*: {', '.join(parts)}")

        lines.append(f"\n_Equivalent API cost: ${cost_total:.4f} (covered by subscription)_")
        await say("\n".join(lines))

    async def _handle_summary(
        self, say: Any, hours: int = 12, project: str | None = None,
        channel_id: str | None = None,
    ) -> None:
        """Detailed LLM-generated summary per project."""
        recent, results_map = await self._fetch_recent(hours, project)
        if not recent:
            label = f" for *{project}*" if project else ""
            await say(f":white_check_mark: No task activity in the last {hours}h{label}.")
            return

        # Post loading indicator
        loading_ts = None
        if channel_id:
            try:
                resp = await self.app.client.chat_postMessage(
                    channel=channel_id,
                    text=(
                        f"*Summary:* :hourglass_flowing_sand:"
                        f" Generating summary for the last {hours}h..."
                    )
                )
                loading_ts = resp.get("ts")
            except Exception:
                pass
        if not loading_ts:
            await say(
                f"*Summary:* :hourglass_flowing_sand:"
                f" Generating summary for the last {hours}h..."
            )

        # Build rich context for Claude
        task_details = []
        for t in recent:
            tid = t.get("id", "?")
            proj = t.get("project", "?")
            status = t.get("status", "?")
            title = t.get("title") or t.get("description", "")[:80]
            target = t.get("target", "local")
            result = results_map.get(tid, {})
            summary = result.get("summary", "")
            error = result.get("error", "")
            cost = result.get("cost", {}).get("cost_usd", 0)
            task_details.append(
                f"- [{status}] {proj}: {title} (spoke: {target})"
                + (f"\n  Result: {summary[:200]}" if summary else "")
                + (f"\n  Error: {error[:200]}" if error and status == "failed" else "")
                + (f" (${cost:.4f})" if cost else "")
            )

        context = "\n".join(task_details)
        prompt = (
            f"You are Dobby, a meta-agent orchestrator."
            f" Write a detailed summary of task activity from the last {hours} hours.\n\n"
            f"Tasks:\n{context}\n\n"
            "Write a Slack message organized by project:\n"
            "- For each active project, summarize what was implemented/completed and what failed\n"
            "- For failures, briefly note what went wrong\n"
            "- Mention which spoke servers were used\n"
            "- Include total cost if significant\n"
            "Use Slack formatting (*bold*, _italic_, bullet points). Be detailed but not verbose."
        )

        try:
            output, _ = await self._run_claude(prompt, session_id=None)
            final_text = f"*Summary:*\n{output}"
            if loading_ts and channel_id:
                await self.app.client.chat_update(
                    channel=channel_id, ts=loading_ts, text=final_text
                )
            else:
                await say(final_text)
        except Exception:
            # Fallback
            completed = len([t for t in recent if t.get("status") == "completed"])
            failed = len([t for t in recent if t.get("status") == "failed"])
            fallback = (
                f"*Summary:* :clipboard: Last {hours}h \u2014"
                f" {len(recent)} tasks, {completed} completed, {failed} failed"
            )
            if loading_ts and channel_id:
                await self.app.client.chat_update(channel=channel_id, ts=loading_ts, text=fallback)
            else:
                await say(fallback)

    async def _fetch_recent(self, hours: int, project: str | None = None) -> tuple[list, dict]:
        """Fetch recent tasks and their results."""
        cutoff = datetime.now(UTC) - timedelta(hours=hours)

        try:
            resp = await self._client_wrapper.get("/tasks")
            tasks = resp.get("items", resp) if isinstance(resp, dict) else resp
        except Exception:
            return [], {}

        recent = []
        for t in tasks:
            task_time = t.get("started_at") or t.get("created_at") or ""
            if task_time:
                try:
                    if datetime.fromisoformat(task_time.replace("Z", "+00:00")) >= cutoff:
                        recent.append(t)
                        continue
                except (ValueError, TypeError):
                    pass
            if t.get("status") == "running":
                recent.append(t)

        if project:
            recent = [t for t in recent if t.get("project") == project]

        results_map = {}
        for t in recent:
            if t.get("status") in ("completed", "failed"):
                try:
                    result = await self._client_wrapper.get(f"/results/{t['id']}")
                    results_map[t["id"]] = result
                except Exception:
                    pass

        return recent, results_map

    async def _handle_running_query(self, say: Any) -> None:
        """Show currently running tasks."""
        try:
            resp = await self._client_wrapper.get("/tasks", params={"status": "running"})
        except Exception as exc:
            await say(f":warning: Could not reach Dobby daemon: `{exc}`")
            return

        tasks = resp.get("items", resp) if isinstance(resp, dict) else resp
        if not tasks:
            await say(":white_check_mark: No tasks currently running.")
            return

        lines = [f"*Running:* :runner: {len(tasks)} task(s)\n"]
        for t in tasks:
            lines.append(
                f"  `{t.get('id', '?')}` [{t.get('project', '?')}] "
                f"\u2014 {t.get('description', '')[:60]}"
            )
        await say("\n".join(lines))

    async def _handle_results_query(self, project: str | None, say: Any) -> None:
        """Show recent results, optionally filtered by project."""
        try:
            params: dict[str, str] = {"last_n": "5"}
            if project:
                params["project"] = project
            resp = await self._client_wrapper.get("/results", params=params)
        except Exception as exc:
            await say(f":warning: Could not reach Dobby daemon: `{exc}`")
            return

        results = resp.get("items", resp) if isinstance(resp, dict) else resp
        if not results:
            label = f" for *{project}*" if project else ""
            await say(f":file_folder: No results found{label}.")
            return

        label = f" for *{project}*" if project else ""
        lines = [f":file_folder: *Recent results{label}:*\n"]
        for r in results[:5]:
            emoji = ":white_check_mark:" if r.get("status") == "completed" else ":x:"
            cost = r.get("cost", {}).get("cost_usd", 0.0)
            cost_str = f"${cost:.4f}" if cost else "\u2014"
            lines.append(
                f"  {emoji} `{r.get('task_id', '?')}` \u2014"
                f" {r.get('summary', 'No summary')[:80]} ({cost_str})"
            )
        await say("\n".join(lines))

    async def _handle_detail_status(self, task_id: str, say: Any) -> None:
        """Fetch and format detail-status for a specific task."""
        try:
            detail = await self._client_wrapper.get(f"/tasks/{task_id}/detail-status")
        except Exception as exc:
            await say(f":warning: Could not get status for `{task_id}`: `{exc}`")
            return

        status = detail.get("status", "unknown")
        phase = detail.get("phase", "")
        progress = detail.get("progress_pct")
        current_file = detail.get("current_file", "")
        recent_actions = detail.get("recent_actions", [])
        errors = detail.get("errors", [])

        lines = [f":mag: *Detail status for `{task_id}`*\n"]
        lines.append(f"*Status:* {status}")
        if phase:
            lines.append(f"*Phase:* {phase}")
        if progress is not None:
            bar = _progress_bar(progress)
            lines.append(f"*Progress:* {bar} {progress}%")
        if current_file:
            lines.append(f"*Current file:* `{current_file}`")
        if recent_actions:
            lines.append("*Recent actions:*")
            for action in recent_actions[-5:]:
                lines.append(f"  \u2022 {action}")
        if errors:
            lines.append("*Errors:*")
            for err in errors[-3:]:
                lines.append(f"  :warning: {err}")

        await say("\n".join(lines))

    async def _handle_summary_detail(self, text: str, say: Any) -> None:
        """Handle drill-down replies in a summary thread."""
        task_match = _TASK_ID_RE.search(text)

        if not task_match:
            await say(
                ":thinking_face: Please mention a task ID (e.g. `t-20260322-ab12`) "
                "to see its details."
            )
            return

        task_id = task_match.group()

        # Fetch task and result
        try:
            task = await self._client_wrapper.get(f"/tasks/{task_id}")
        except Exception as exc:
            await say(f":warning: Could not find task `{task_id}`: `{exc}`")
            return

        try:
            result = await self._client_wrapper.get(f"/results/{task_id}")
        except Exception:
            result = None

        blocks = self._build_task_detail_blocks(task, result)
        await say(blocks=blocks, text=f"Details for {task_id}")

    async def _handle_help(self, say: Any) -> None:
        """Show available commands."""
        dashboard_line = (
            f":globe_with_meridians: Dashboard: {self._hub_url}/dashboard\n"
            if self._hub_url else ""
        )
        await say(
            ":book: *Dobby Slack Commands*\n\n"
            "*Prefixes (how to route your message):*\n"
            "\u2022 `do:` / `run:` \u2014 submit a task \u2192 `do: fix tests in spare`\n"
            "\u2022 `plan:` \u2014 plan first, approve before executing"
            " \u2192 `plan: redesign ADPO training`\n"
            "\u2022 `ask:` \u2014 force free-form Claude chat"
            " \u2192 `ask: what does the config do?`\n"
            "\u2022 `batch:` \u2014 multiple tasks \u2192 `batch: - fix tests - add docs`\n"
            "\u2022 _(no prefix)_ \u2014 free-form Claude conversation (default)\n\n"
            "*Status & Reports*\n"
            "\u2022 `status` \u2014 quick overview (last 4h)\n"
            "\u2022 `status 12h` / `status spare` \u2014 with time or project filter\n"
            "\u2022 `summary` \u2014 detailed AI summary (last 12h)\n"
            "\u2022 `running` \u2014 currently running tasks\n"
            "\u2022 `results` / `results <project>` \u2014 recent results\n"
            "\u2022 `t-XXXXXXXX-XXXX` \u2014 detail status for a task\n"
            "\u2022 `projects` \u2014 list all projects with spoke servers\n\n"
            "*Conversations*\n"
            "\u2022 Any message without a prefix \u2192 Claude Code chat\n"
            "\u2022 Reply in thread to continue the conversation\n\n"
            + dashboard_line +
            "\u2022 `help` \u2014 this message"
        )

    async def _handle_projects(self, say: Any) -> None:
        """List all registered projects with their spoke servers and paths."""
        try:
            projects = await self._client_wrapper.get("/projects")
        except Exception as exc:
            await say(f":warning: Could not reach Dobby hub: `{exc}`")
            return

        if not isinstance(projects, list):
            projects = projects.get("items", []) if isinstance(projects, dict) else []

        if not projects:
            await say(":file_folder: No projects registered. Use the API or MCP to register one.")
            return

        # Fetch spokes for mapping
        try:
            spokes = await self._client_wrapper.get("/spokes")
            if not isinstance(spokes, list):
                spokes = []
        except Exception:
            spokes = []

        # Build project -> spoke mapping from tasks
        try:
            resp = await self._client_wrapper.get("/tasks")
            tasks = resp.get("items", resp) if isinstance(resp, dict) else resp
        except Exception:
            tasks = []

        project_targets: dict[str, set] = {}
        for t in tasks:
            target = t.get("target", "local")
            proj = t.get("project", "")
            if proj:
                if proj not in project_targets:
                    project_targets[proj] = set()
                project_targets[proj].add(target)

        spoke_map = {s.get("id", ""): s for s in spokes}

        lines = [":file_folder: *Registered Projects*\n"]
        for p in projects:
            name = p.get("name", "?")
            repo = p.get("repo", "\u2014")
            context = p.get("context", "")

            # Find spoke(s) for this project
            targets = project_targets.get(name, set())
            spoke_labels = []
            for t in targets:
                if t == "local":
                    continue
                spoke = spoke_map.get(t)
                status_emoji = (
                    ":large_green_circle:"
                    if spoke and spoke.get("status") != "offline"
                    else ":red_circle:"
                )
                spoke_labels.append(f"{status_emoji} `{t}`")
            if not spoke_labels:
                spoke_labels.append(":large_green_circle: `local`")

            line = f"*{name}*  {' '.join(spoke_labels)}\n"
            line += f"  :file_folder: `{repo}`"
            if context:
                line += f"\n  _{context}_"
            lines.append(line)

        await say("\n".join(lines))
