from __future__ import annotations

import logging
import queue as queue_module
import threading
import time
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Callable

from colonyos.config import ColonyConfig
from colonyos.models import QueueItem, QueueItemStatus, QueueState, compute_priority
from colonyos.queue_runtime import (
    attach_demand_signal,
    find_similar_queue_item,
    find_related_history_items,
    reprioritize_queue_item,
    sorted_pending_items,
)
from colonyos.slack import (
    SlackClient,
    SlackWatchState,
    check_rate_limit,
    extract_base_branch,
    extract_prompt_from_mention,
    extract_prompt_text,
    find_parent_queue_item,
    format_fix_acknowledgment,
    format_fix_error,
    format_fix_round_limit,
    format_slack_as_prompt,
    has_bot_mention,
    increment_hourly_count,
    post_message,
    post_triage_acknowledgment,
    post_triage_skip,
    react_to_message,
    should_process_message,
    should_process_thread_fix,
    triage_message,
)

logger = logging.getLogger(__name__)


def _generate_id(prefix: str) -> str:
    stamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S_%f")
    return f"{prefix}-{stamp}-{uuid.uuid4().hex[:8]}"


@dataclass
class SlackQueueEngine:
    repo_root: Path
    config: ColonyConfig
    queue_state: QueueState
    watch_state: SlackWatchState
    state_lock: threading.Lock
    shutdown_event: threading.Event
    bot_user_id: str
    slack_client_ready: threading.Event
    publish_client: Callable[[SlackClient], None]
    persist_queue: Callable[[], None]
    persist_watch_state: Callable[[], None]
    is_time_exceeded: Callable[[], bool]
    is_budget_exceeded: Callable[[], bool]
    is_daily_budget_exceeded: Callable[[], bool]
    dry_run: bool = False
    # NOTE: agent_lock is accepted but intentionally NOT acquired during triage.
    # Triage (the LLM classification call) is stateless and runs lock-free so that
    # Slack intake is never blocked by pipeline execution.  Queue mutations are
    # guarded by state_lock.  agent_lock is retained for potential future use only.
    agent_lock: threading.Lock | None = None
    triage_queue_maxsize: int = 64
    _triage_queue: queue_module.Queue[dict[str, Any]] = field(init=False, repr=False)
    _pending_messages: set[str] = field(default_factory=set, init=False, repr=False)
    _triage_worker_lock: threading.Lock = field(default_factory=threading.Lock, init=False, repr=False)
    _triage_worker: threading.Thread | None = field(default=None, init=False, repr=False)

    def __post_init__(self) -> None:
        self._triage_queue = queue_module.Queue(maxsize=self.triage_queue_maxsize)

    def register(self, bolt_app: Any) -> None:
        self._ensure_triage_worker()
        bolt_app.event("app_mention")(self._handle_event)
        if self.config.slack.trigger_mode == "all":
            bolt_app.event("message")(self._handle_event)
        if self.config.slack.trigger_mode not in ("reaction", "all"):
            def _noop_reaction(_event: object, _client: object) -> None:
                return

            bolt_app.event("reaction_added")(_noop_reaction)
            return
        bolt_app.event("reaction_added")(self._handle_reaction)

    def _ensure_triage_worker(self) -> None:
        with self._triage_worker_lock:
            if self._triage_worker is not None and self._triage_worker.is_alive():
                return
            self._triage_worker = threading.Thread(
                target=self._triage_worker_loop,
                daemon=True,
                name="slack-triage-worker",
            )
            self._triage_worker.start()

    def _triage_worker_loop(self) -> None:
        while not self.shutdown_event.is_set():
            try:
                task = self._triage_queue.get(timeout=0.5)
            except queue_module.Empty:
                continue
            try:
                self._triage_and_enqueue(**task)
            except Exception:
                logger.exception(
                    "Unhandled failure in Slack triage worker for %s:%s",
                    task.get("channel", "?"),
                    task.get("ts", "?"),
                )
            finally:
                with self.state_lock:
                    self._release_pending_message(task.get("channel", ""), task.get("ts", ""))
                self._triage_queue.task_done()

    def _is_pending_message(self, channel: str, ts: str) -> bool:
        return self.watch_state.message_key(channel, ts) in self._pending_messages

    def _reserve_pending_message(self, channel: str, ts: str) -> None:
        self._pending_messages.add(self.watch_state.message_key(channel, ts))

    def _release_pending_message(self, channel: str, ts: str) -> None:
        self._pending_messages.discard(self.watch_state.message_key(channel, ts))

    def _handle_reaction(self, event: dict[str, Any], client: SlackClient) -> None:
        item = event.get("item", {})
        if item.get("type") != "message":
            return
        channel = item.get("channel", "")
        ts = item.get("ts", "")
        try:
            result = client.conversations_history(
                channel=channel,
                latest=ts,
                inclusive=True,
                limit=1,
            )
            messages = result.get("messages", [])
            if not messages:
                return
            msg = messages[0]
            synthetic_event = {
                "channel": channel,
                "ts": ts,
                "user": msg.get("user", "unknown"),
                "text": msg.get("text", ""),
            }
            self._handle_event(synthetic_event, client)
        except Exception:
            logger.debug("Failed to fetch message for reaction event", exc_info=True)

    def _handle_event(self, event: dict[str, Any], client: SlackClient) -> None:
        if not self.slack_client_ready.is_set():
            self.publish_client(client)
            self.slack_client_ready.set()

        if not should_process_message(event, self.config.slack, self.bot_user_id):
            with self.state_lock:
                items_snapshot = list(self.queue_state.items)
            if should_process_thread_fix(event, self.config.slack, self.bot_user_id, items_snapshot):
                if self.is_time_exceeded() or self.is_budget_exceeded() or self.is_daily_budget_exceeded():
                    logger.warning("Thread fix rejected due to daemon/watch limits")
                    return
                self._handle_thread_fix(event, client)
            return

        if self.is_time_exceeded() or self.is_budget_exceeded() or self.is_daily_budget_exceeded():
            logger.warning("Slack event rejected due to daemon/watch limits")
            return

        channel = event.get("channel", "")
        ts = event.get("ts", "")
        user = event.get("user", "unknown")
        raw_text = event.get("text", "")
        prompt_text = extract_prompt_text(raw_text, self.bot_user_id)
        if not prompt_text.strip():
            return

        with self.state_lock:
            if self.watch_state.is_processed(channel, ts):
                logger.info("Message %s:%s already processed, skipping", channel, ts)
                return
            if self._is_pending_message(channel, ts):
                logger.info("Message %s:%s already queued, skipping duplicate delivery", channel, ts)
                return
            if not check_rate_limit(self.watch_state, self.config.slack):
                logger.warning("Rate limit reached, skipping message %s:%s", channel, ts)
                try:
                    post_message(
                        client,
                        channel,
                        ":warning: Rate limit reached. Try again later.",
                        thread_ts=ts,
                    )
                except Exception:
                    logger.debug("Failed to post rate-limit message", exc_info=True)
                return
            self._reserve_pending_message(channel, ts)
            increment_hourly_count(self.watch_state)

        if self.dry_run:
            with self.state_lock:
                self._release_pending_message(channel, ts)
            logger.info("[DRY RUN] Would trigger pipeline for Slack prompt: %s", prompt_text[:120])
            return

        is_passive = not has_bot_mention(raw_text, self.bot_user_id)

        if not is_passive:
            try:
                react_to_message(client, channel, ts, "eyes")
            except Exception:
                logger.debug("Failed to add :eyes: reaction", exc_info=True)

        self._ensure_triage_worker()
        try:
            self._triage_queue.put_nowait({
                "client": client,
                "channel": channel,
                "ts": ts,
                "user": user,
                "prompt_text": prompt_text,
                "is_passive": is_passive,
            })
        except queue_module.Full:
            with self.state_lock:
                self._release_pending_message(channel, ts)
            logger.warning("Slack triage queue full, rejecting %s:%s", channel, ts)
            if not is_passive:
                try:
                    post_message(
                        client,
                        channel,
                        ":warning: Triage backlog is full right now. Try again in a minute.",
                        thread_ts=ts,
                    )
                except Exception:
                    logger.debug("Failed to post triage backlog message", exc_info=True)

    def _triage_and_enqueue(
        self,
        *,
        client: SlackClient,
        channel: str,
        ts: str,
        user: str,
        prompt_text: str,
        is_passive: bool = False,
    ) -> None:
        if self.shutdown_event.is_set():
            return

        triage_kwargs: dict[str, str] = {}
        if self.config.project:
            triage_kwargs["project_name"] = self.config.project.name
            triage_kwargs["project_description"] = self.config.project.description
            triage_kwargs["project_stack"] = self.config.project.stack
        if self.config.vision:
            triage_kwargs["vision"] = self.config.vision
        if self.config.slack.triage_scope:
            triage_kwargs["triage_scope"] = self.config.slack.triage_scope

        max_attempts = 2  # 1 initial + 1 retry
        triage_result = None
        for attempt in range(max_attempts):
            try:
                triage_result = triage_message(
                    prompt_text,
                    repo_root=self.repo_root,
                    **triage_kwargs,
                )
                break
            except (TimeoutError, ConnectionError, OSError) as exc:
                if attempt < max_attempts - 1 and not self.shutdown_event.is_set():
                    logger.warning(
                        "Transient triage failure (attempt %d/%d) for %s:%s: %s",
                        attempt + 1,
                        max_attempts,
                        channel,
                        ts,
                        exc,
                    )
                    time.sleep(3)
                    continue
                logger.exception("Triage failed after %d attempts for %s:%s", max_attempts, channel, ts)
                try:
                    post_message(
                        client,
                        channel,
                        ":warning: Triage failed. Check server logs for details.",
                        thread_ts=ts,
                    )
                except Exception:
                    logger.debug("Failed to post triage failure message", exc_info=True)
                with self.state_lock:
                    self.watch_state.mark_processed(channel, ts, "triage-error")
                    self.persist_watch_state()
                return
            except Exception:
                logger.exception("Triage failed for message %s:%s", channel, ts)
                try:
                    post_message(
                        client,
                        channel,
                        ":warning: Triage failed. Check server logs for details.",
                        thread_ts=ts,
                    )
                except Exception:
                    logger.debug("Failed to post triage failure message", exc_info=True)
                with self.state_lock:
                    self.watch_state.mark_processed(channel, ts, "triage-error")
                    self.persist_watch_state()
                return

        if self.shutdown_event.is_set():
            logger.info(
                "Shutdown in progress; dropping triaged message %s:%s before enqueue",
                channel,
                ts,
            )
            return

        if triage_result is None:
            return

        if not triage_result.actionable:
            try:
                if triage_result.answer:
                    post_message(client, channel, triage_result.answer, thread_ts=ts)
                elif self.config.slack.triage_verbose:
                    post_triage_skip(client, channel, ts, triage_result.reasoning)
            except Exception:
                logger.debug("Failed to post non-actionable triage result", exc_info=True)
            with self.state_lock:
                self.watch_state.mark_processed(channel, ts, "triage-skip")
                self.persist_watch_state()
            return

        if is_passive:
            try:
                react_to_message(client, channel, ts, "eyes")
            except Exception:
                logger.debug("Failed to add :eyes: reaction after triage", exc_info=True)

        base_branch = triage_result.base_branch or extract_base_branch(prompt_text)
        formatted_prompt = format_slack_as_prompt(prompt_text, channel, user)
        merged_item: QueueItem | None = None
        merged_similarity = 0.0
        queue_item: QueueItem | None = None

        with self.state_lock:
            if self.shutdown_event.is_set():
                return
            related_history = find_related_history_items(
                self.queue_state,
                prompt_text=prompt_text,
            )
            similar = find_similar_queue_item(
                self.queue_state,
                source_type="slack",
                prompt_text=prompt_text,
            )
            if similar is not None:
                merged_item = similar.item
                merged_similarity = similar.similarity
                attach_demand_signal(
                    merged_item,
                    source_type="slack",
                    source_value=prompt_text,
                    summary=triage_result.summary,
                    metadata={
                        "channel": channel,
                        "user": user,
                        "ts": ts,
                        "similarity": round(similar.similarity, 3),
                    },
                )
                reprioritize_queue_item(merged_item)
            else:
                queue_item = QueueItem(
                    id=_generate_id("slack"),
                    source_type="slack",
                    source_value=formatted_prompt,
                    raw_prompt=prompt_text,
                    summary=triage_result.summary,
                    status=QueueItemStatus.PENDING,
                    slack_ts=ts,
                    slack_channel=channel,
                    notification_channel=channel,
                    notification_thread_ts=ts,
                    base_branch=base_branch,
                    priority=compute_priority("slack"),
                    priority_reason="base:slack",
                    related_item_ids=[related.id for related in related_history],
                )
                reprioritize_queue_item(queue_item)
                self.queue_state.items.append(queue_item)
            item_for_mark = merged_item or queue_item
            assert item_for_mark is not None
            self.watch_state.mark_processed(
                channel,
                ts,
                item_for_mark.id,
            )
            self.watch_state.runs_triggered += 1
            self.persist_queue()
            self.persist_watch_state()
            pending = sorted_pending_items(self.queue_state)
            target = merged_item or queue_item
            assert target is not None
            position = pending.index(target) + 1 if target in pending else None
            total = len(pending)

        if merged_item is not None:
            try:
                post_message(
                    client,
                    channel,
                    (
                        ":link: This request matched an existing queued item.\n"
                        f"Summary: {merged_item.summary or triage_result.summary}\n"
                        f"Demand signals: {merged_item.demand_count}\n"
                        f"Queue position: {position} of {total}\n"
                        f"Similarity: {merged_similarity:.0%}"
                    ),
                    thread_ts=ts,
                )
            except Exception:
                logger.debug("Failed to post merge acknowledgment", exc_info=True)
            return

        try:
            post_triage_acknowledgment(
                client,
                channel,
                ts,
                triage_result.summary,
                needs_approval=not self.config.slack.auto_approve,
                queue_position=position,
                queue_total=total,
            )
        except Exception:
            logger.debug("Failed to post triage acknowledgment", exc_info=True)

    def _handle_thread_fix(self, event: dict[str, Any], client: SlackClient) -> None:
        channel = event.get("channel", "")
        ts = event.get("ts", "")
        thread_ts = event.get("thread_ts", "")
        user = event.get("user", "unknown")
        raw_text = event.get("text", "")
        fix_prompt_text = extract_prompt_from_mention(raw_text, self.bot_user_id)
        if not fix_prompt_text.strip():
            return

        with self.state_lock:
            parent_item = find_parent_queue_item(thread_ts, self.queue_state.items)
            if parent_item is None:
                logger.warning("Thread fix: no completed parent for thread_ts=%s", thread_ts)
                return
            if parent_item.fix_rounds >= self.config.slack.max_fix_rounds_per_thread:
                cumulative_cost = parent_item.cost_usd + sum(
                    qi.cost_usd
                    for qi in self.queue_state.items
                    if qi.parent_item_id == parent_item.id
                )
                try:
                    post_message(
                        client,
                        channel,
                        format_fix_round_limit(cumulative_cost),
                        thread_ts=thread_ts,
                    )
                except Exception:
                    logger.debug("Failed to post fix round limit message", exc_info=True)
                return
            if not parent_item.branch_name:
                try:
                    post_message(
                        client,
                        channel,
                        format_fix_error("No branch", "No branch name recorded for the original run."),
                        thread_ts=thread_ts,
                    )
                except Exception:
                    logger.debug("Failed to post no-branch message", exc_info=True)
                return

            parent_item.fix_rounds += 1
            fix_item = QueueItem(
                id=_generate_id("slack-fix"),
                source_type="slack_fix",
                source_value=format_slack_as_prompt(fix_prompt_text, channel, user),
                raw_prompt=fix_prompt_text,
                summary=f"Follow-up fix for {parent_item.summary or parent_item.branch_name}",
                status=QueueItemStatus.PENDING,
                slack_ts=thread_ts,
                slack_channel=channel,
                branch_name=parent_item.branch_name,
                parent_item_id=parent_item.id,
                pr_url=parent_item.pr_url,
                base_branch=parent_item.base_branch,
                head_sha=parent_item.head_sha,
                notification_channel=channel,
                notification_thread_ts=thread_ts,
                priority=compute_priority("slack_fix"),
                priority_reason="base:slack_fix",
            )
            reprioritize_queue_item(fix_item)
            self.queue_state.items.append(fix_item)
            self.persist_queue()
            self.persist_watch_state()

        try:
            react_to_message(client, channel, ts, "eyes")
        except Exception:
            logger.debug("Failed to add :eyes: reaction to fix request", exc_info=True)
        try:
            post_message(
                client,
                channel,
                format_fix_acknowledgment(parent_item.branch_name),
                thread_ts=thread_ts,
            )
        except Exception:
            logger.debug("Failed to post fix acknowledgment", exc_info=True)
