"""Shared module-level state, data classes, and helpers for the Dobby Slack bot.

All mutable registries (pending confirmations, task threads, Claude sessions,
plan threads) live here so they can be imported by any mixin or by external
callers (notifier, tests) without pulling in the full bot.
"""
from __future__ import annotations

import json
import os
import re
from datetime import datetime
from typing import Any

# Task ID pattern: t-YYYYMMDD-XXXX
_TASK_ID_RE = re.compile(r"\bt-\d{8}-[0-9a-f]{4}\b")

# ---------------------------------------------------------------------------
# Pending confirmation state
# ---------------------------------------------------------------------------

# Maps message_ts -> PendingConfirmation for single tasks
# Maps message_ts -> PendingBatch for batch confirmations
_PENDING: dict[str, _PendingConfirmation | _PendingBatch] = {}

# Maps task_id -> (channel, thread_ts) so the notifier knows where to post
# Shared with SlackNotifier via get_task_thread()
_TASK_THREADS: dict[str, tuple[str, str]] = {}

# Maps thread_ts -> project filter (or None) for summary drill-down replies
_SUMMARY_THREADS: dict[str, str | None] = {}

# Maps thread_ts -> {session_id, cwd} for free-form Claude Code conversations
_CLAUDE_SESSIONS: dict[str, dict] = {}

# Maps message_ts -> {task, result, session_id} for plan approval flow
_PENDING_PLANS: dict[str, dict] = {}

# Maps thread_ts -> {session_id, task_id, project, target, backend, channel}
# for plan conversation threads (reply-to-continue)
_PLAN_THREADS: dict[str, dict] = {}


# ---------------------------------------------------------------------------
# Data classes
# ---------------------------------------------------------------------------

class _PendingConfirmation:
    """Tracks a single task awaiting react-to-confirm."""
    __slots__ = ("parsed", "channel", "projects")

    def __init__(self, parsed: dict, channel: str, projects: list[str]) -> None:
        self.parsed = parsed
        self.channel = channel
        self.projects = projects


class _PendingBatch:
    """Tracks a batch of tasks awaiting react-to-confirm."""
    __slots__ = ("tasks", "channel", "projects")

    def __init__(self, tasks: list[dict], channel: str, projects: list[str]) -> None:
        self.tasks = tasks
        self.channel = channel
        self.projects = projects


# ---------------------------------------------------------------------------
# Thread registration helpers
# ---------------------------------------------------------------------------

def get_task_thread(task_id: str) -> tuple[str, str] | None:
    """Return (channel, thread_ts) for a task, or None if not tracked."""
    return _TASK_THREADS.get(task_id)


def register_task_thread(task_id: str, channel: str, thread_ts: str) -> None:
    """Register a Slack thread for a task (used by bot and notifier)."""
    _TASK_THREADS[task_id] = (channel, thread_ts)


def register_plan_approval(msg_ts: str, task: dict, result: dict, session_id: str | None) -> None:
    """Register a plan approval message for reaction handling."""
    _PENDING_PLANS[msg_ts] = {
        "task": task,
        "result": result,
        "session_id": session_id,
    }


def register_plan_thread(thread_ts: str, task: dict, session_id: str | None) -> None:
    """Register a plan thread for reply-to-continue conversations."""
    _PLAN_THREADS[thread_ts] = {
        "session_id": session_id,
        "task_id": task.get("id"),
        "project": task.get("project"),
        "target": task.get("target", "local"),
        "backend": task.get("backend", "claude"),
    }
    _save_plan_threads()


# ---------------------------------------------------------------------------
# Persistence
# ---------------------------------------------------------------------------

_CLAUDE_SESSIONS_FILE = os.path.join(
    os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ".claude_sessions.json"
)
_PLAN_THREADS_FILE = os.path.join(
    os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ".plan_threads.json"
)


def _load_claude_sessions() -> None:
    """Load persisted Claude sessions from disk."""
    global _CLAUDE_SESSIONS
    try:
        with open(_CLAUDE_SESSIONS_FILE, encoding="utf-8") as f:
            _CLAUDE_SESSIONS = json.load(f)
    except (OSError, json.JSONDecodeError):
        _CLAUDE_SESSIONS = {}


def _save_claude_sessions() -> None:
    """Persist Claude sessions to disk."""
    try:
        with open(_CLAUDE_SESSIONS_FILE, "w", encoding="utf-8") as f:
            json.dump(_CLAUDE_SESSIONS, f)
    except OSError:
        pass


def _load_plan_threads() -> None:
    """Load persisted plan threads from disk."""
    global _PLAN_THREADS
    try:
        with open(_PLAN_THREADS_FILE, encoding="utf-8") as f:
            _PLAN_THREADS = json.load(f)
    except (OSError, json.JSONDecodeError):
        _PLAN_THREADS = {}


def _save_plan_threads() -> None:
    """Persist plan threads to disk."""
    try:
        with open(_PLAN_THREADS_FILE, "w", encoding="utf-8") as f:
            json.dump(_PLAN_THREADS, f)
    except OSError:
        pass


# Keywords that trigger the summary handler
_SUMMARY_KEYWORDS = ("summary", "overnight", "what happened last night", "what happened overnight")


# ---------------------------------------------------------------------------
# Pure-function helpers
# ---------------------------------------------------------------------------

def _strip_mention(text: str) -> str:
    """Remove the leading <@UXXXXXXXX> mention from a message."""
    return re.sub(r"^<@[A-Z0-9]+>\s*", "", text)


def _parse_task_end_time(task: dict) -> datetime | None:
    """Extract the completion/failure time from a task dict.

    Uses the last event timestamp, falling back to started_at or created_at.
    """
    events = task.get("events", [])
    for ev in reversed(events):
        if ev.get("status") in ("completed", "failed"):
            try:
                return datetime.fromisoformat(ev["timestamp"])
            except (KeyError, ValueError):
                pass
    # Fallback: try started_at, then created_at
    for field in ("started_at", "created_at"):
        val = task.get(field)
        if val:
            try:
                return datetime.fromisoformat(val)
            except ValueError:
                pass
    return None


def _build_task_payload(parsed: dict) -> dict:
    """Convert a parser result dict into a POST /tasks payload."""
    payload: dict[str, Any] = {
        "project": parsed.get("project"),
        "description": parsed.get("description", ""),
        "mode": parsed.get("mode", "quick"),
        "priority": parsed.get("priority") or "normal",
    }

    if parsed.get("exit_criteria"):
        payload["exit_criteria"] = parsed["exit_criteria"]

    if parsed.get("backend"):
        payload["backend"] = parsed["backend"]

    if parsed.get("target"):
        payload["target"] = parsed["target"]

    stype = parsed.get("schedule_type")
    sexpr = parsed.get("schedule_expr")
    if stype:
        schedule: dict[str, Any] = {"type": stype}
        if stype == "cron":
            schedule["expr"] = sexpr
        elif stype == "interval":
            schedule["every"] = sexpr
        payload["schedule"] = schedule

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

    return payload


def _progress_bar(pct: int | float, width: int = 10) -> str:
    """Render a simple text progress bar."""
    filled = int(round(pct / 100 * width))
    return "\u2588" * filled + "\u2591" * (width - filled)
