"""Slack source connector — async message fetching with rate-limit handling.

Implements the same patterns as redhat-ai-workflow's SlackSession:
- xoxc_token + d_cookie auth (enterprise Slack)
- client.counts for channel discovery
- 1-3s stealth delay between API calls
- Exponential backoff with jitter on 429s
- Day-by-day backwards fetching with date progress

Supports two authentication modes:
  1. xoxc_token + d_cookie (session credentials extracted from Chrome)
  2. Standard bot/user token (xoxb-/xoxp- Bearer auth)
"""

from __future__ import annotations

import asyncio
import hashlib
import logging
import random
from dataclasses import dataclass, field
from datetime import UTC, datetime, timedelta
from typing import TYPE_CHECKING, Any

import httpx
from rich.progress import Progress, TaskID

from redhat_oracle.core.config import OracleConfig
from redhat_oracle.core.vectordb.chunker import Chunk

if TYPE_CHECKING:
    from redhat_oracle.core.vectordb.store import VectorStore

logger = logging.getLogger(__name__)

SLACK_BASE_URL = "https://slack.com/api"

_USER_AGENT = (
    "Mozilla/5.0 (X11; Linux x86_64) "
    "AppleWebKit/537.36 (KHTML, like Gecko) "
    "Chrome/143.0.0.0 Safari/537.36"
)

# Stealth delays between API calls (matches redhat-ai-workflow pattern)
_MIN_DELAY = 1.0
_MAX_DELAY = 3.0


@dataclass
class SlackMessage:
    """A Slack message with optional thread replies."""

    channel: str
    user: str
    text: str
    timestamp: str
    thread_ts: str | None = None
    replies: list[SlackMessage] = field(default_factory=list)


def _build_client(
    xoxc_token: str | None,
    d_cookie: str | None,
    token: str | None,
    host: str = "redhat.enterprise.slack.com",
) -> httpx.AsyncClient:
    """Build an httpx client mimicking the Slack web client."""
    if xoxc_token and d_cookie:
        origin = f"https://{host}"
        return httpx.AsyncClient(
            headers={
                "User-Agent": _USER_AGENT,
                "Accept": "application/json",
                "Accept-Language": "en-US,en;q=0.9",
                "Content-Type": "application/x-www-form-urlencoded",
                "Origin": origin,
                "Referer": f"{origin}/",
                "Sec-Fetch-Dest": "empty",
                "Sec-Fetch-Mode": "cors",
                "Sec-Fetch-Site": "same-site",
            },
            cookies={"d": d_cookie},
            timeout=30.0,
        )
    return httpx.AsyncClient(timeout=30.0)


async def _slack_api(
    client: httpx.AsyncClient,
    method: str,
    token: str,
    params: dict[str, Any] | None = None,
    *,
    use_post: bool = False,
    max_retries: int = 5,
) -> dict[str, Any]:
    """Call a Slack API method with stealth delay and rate-limit handling."""
    await asyncio.sleep(random.uniform(_MIN_DELAY, _MAX_DELAY))

    for attempt in range(max_retries):
        if use_post:
            payload = dict(params or {})
            payload["token"] = token
            resp = await client.post(f"{SLACK_BASE_URL}/{method}", data=payload)
        else:
            headers = {"Authorization": f"Bearer {token}"}
            resp = await client.get(
                f"{SLACK_BASE_URL}/{method}",
                headers=headers,
                params=params or {},
            )

        if resp.status_code == 429:
            retry_after = int(resp.headers.get("Retry-After", "60"))
            backoff = min(retry_after, _MIN_DELAY * (2 ** attempt) + random.uniform(0, 1))
            logger.warning("Rate limited (429), backing off %.1fs (attempt %d/%d)",
                           backoff, attempt + 1, max_retries)
            await asyncio.sleep(backoff)
            continue

        resp.raise_for_status()
        data: dict[str, Any] = resp.json()
        if not data.get("ok"):
            error = data.get("error", "unknown")
            if error == "ratelimited":
                await asyncio.sleep(_MIN_DELAY * (2 ** attempt))
                continue
            raise RuntimeError(f"Slack API error: {error}")
        return data

    raise RuntimeError(f"Slack API rate limit exceeded after {max_retries} retries")


async def _fetch_day_messages(
    client: httpx.AsyncClient,
    token: str,
    channel_id: str,
    day_start: float,
    day_end: float,
    *,
    use_post: bool = False,
) -> list[dict[str, Any]]:
    """Fetch messages for a single day window using latest parameter.

    Slack's conversations.history returns newest-first. We set `latest`
    to the end of the day and paginate until messages are older than
    `day_start` or we run out.
    """
    messages: list[dict[str, Any]] = []
    cursor = None
    latest = str(day_end)

    while True:
        params: dict[str, Any] = {
            "channel": channel_id,
            "limit": 200,
            "latest": latest,
        }
        if cursor:
            params["cursor"] = cursor

        try:
            data = await _slack_api(
                client, "conversations.history", token, params,
                use_post=use_post,
            )
        except Exception as e:
            logger.debug("Failed to fetch history for %s: %s", channel_id, e)
            break

        batch = data.get("messages", [])
        if not batch:
            break

        for msg in batch:
            ts = float(msg.get("ts", "0"))
            if ts < day_start:
                # We've gone past the start of this day
                return messages
            messages.append(msg)

        cursor = data.get("response_metadata", {}).get("next_cursor")
        if not cursor:
            break

    return messages


async def sync_slack(
    config: OracleConfig,
    store: VectorStore | None = None,
    progress: Progress | None = None,
) -> dict[str, Any]:
    """Fetch Slack messages day-by-day from present to past.

    Goes backwards one day at a time for history_days (default 730 = 2 years).
    When store is provided, upserts chunks after each completed day for
    incremental persistence (Ctrl+C preserves all completed days).

    Returns:
        Dict with stats: messages, chunks, days_processed, days_skipped.
    """
    from redhat_oracle.core.cache import SyncCache

    slack_config = config.sources.slack

    xoxc_token = slack_config.xoxc_token
    d_cookie = slack_config.d_cookie
    bot_token = slack_config.token

    if xoxc_token and d_cookie:
        token = xoxc_token
        use_post = True
        logger.info("Using xoxc_token + d_cookie authentication")
    elif bot_token:
        token = bot_token
        use_post = False
        logger.info("Using standard bot token authentication")
    else:
        logger.warning("No Slack credentials configured, skipping Slack sync")
        return {"messages": 0, "chunks": 0, "days_processed": 0, "days_skipped": 0}

    cache = SyncCache(config)
    completed_days = cache.get_slack_completed_days()

    history_days = slack_config.history_days
    total_messages = 0
    total_chunks = 0
    days_processed = 0
    days_skipped = 0
    user_cache: dict[str, str] = {}

    host = getattr(slack_config, "host", "redhat.enterprise.slack.com")
    async with _build_client(xoxc_token, d_cookie, bot_token, host=host) as client:
        # Validate session
        if use_post:
            auth = await _slack_api(client, "auth.test", token, use_post=True)
            logger.info("Authenticated as %s", auth.get("user_id", "unknown"))

        # --- Discover channels ---
        channel_ids: list[str] = []
        if use_post:
            try:
                counts = await _slack_api(client, "client.counts", token, {
                    "thread_counts_by_channel": "true",
                    "org_wide_aware": "true",
                    "include_file_channels": "true",
                }, use_post=True)
                for ch in counts.get("channels", []):
                    channel_ids.append(ch["id"])
                logger.info("Discovered %d channels via client.counts", len(channel_ids))
            except Exception as e:
                logger.warning("client.counts failed: %s", e)

        if not channel_ids:
            try:
                cursor = None
                while True:
                    params: dict[str, Any] = {
                        "limit": 200,
                        "types": "public_channel,private_channel",
                    }
                    if cursor:
                        params["cursor"] = cursor
                    data = await _slack_api(
                        client, "users.conversations", token, params, use_post=use_post,
                    )
                    for ch in data.get("channels", []):
                        channel_ids.append(ch["id"])
                    cursor = data.get("response_metadata", {}).get("next_cursor")
                    if not cursor:
                        break
            except Exception as e:
                logger.warning("users.conversations failed: %s", e)

        if not channel_ids:
            logger.warning("No channels discovered")
            return {"messages": 0, "chunks": 0, "days_processed": 0, "days_skipped": 0}

        # Filter by configured channels
        if slack_config.channels:
            filtered: list[str] = []
            for ch_id in channel_ids:
                try:
                    info = await _slack_api(
                        client, "conversations.info", token,
                        {"channel": ch_id}, use_post=use_post,
                    )
                    name = info.get("channel", {}).get("name", "")
                    if name in slack_config.channels or ch_id in slack_config.channels:
                        filtered.append(ch_id)
                except Exception:
                    pass
            channel_ids = filtered

        # --- Resolve channel names upfront ---
        channel_names: dict[str, str] = {}
        for ch_id in channel_ids:
            try:
                info = await _slack_api(
                    client, "conversations.info", token,
                    {"channel": ch_id}, use_post=use_post,
                )
                channel_names[ch_id] = info.get("channel", {}).get("name", ch_id)
            except Exception:
                channel_names[ch_id] = ch_id

        # --- Fetch messages day-by-day, present → past ---
        now = datetime.now(UTC)
        total_days = history_days

        task_id: TaskID | None = None
        if progress:
            task_id = progress.add_task(
                "[cyan]Slack: starting...", total=total_days
            )

        for day_offset in range(total_days):
            day_end_dt = now - timedelta(days=day_offset)
            day_start_dt = day_end_dt - timedelta(days=1)
            day_start_ts = day_start_dt.timestamp()
            day_end_ts = day_end_dt.timestamp()
            date_str = day_start_dt.strftime("%Y-%m-%d")

            # Skip days already completed in a previous run
            if date_str in completed_days:
                days_skipped += 1
                if progress and task_id is not None:
                    progress.update(task_id, completed=day_offset + 1)
                continue

            if progress and task_id is not None:
                progress.update(
                    task_id,
                    description=(
                        f"[cyan]{date_str} | "
                        f"{total_messages} msgs total | "
                        f"day {day_offset + 1}/{total_days}"
                    ),
                    completed=day_offset,
                )

            day_messages: list[SlackMessage] = []
            for ch_id in channel_ids:
                ch_name = channel_names.get(ch_id, ch_id)

                if progress and task_id is not None:
                    progress.update(
                        task_id,
                        description=(
                            f"[cyan]{date_str} #{ch_name} | "
                            f"{total_messages} msgs total"
                        ),
                    )

                raw_messages = await _fetch_day_messages(
                    client, token, ch_id, day_start_ts, day_end_ts,
                    use_post=use_post,
                )

                if not raw_messages:
                    continue

                # Resolve users
                user_ids = {m.get("user", "") for m in raw_messages if m.get("user")}
                for uid in user_ids:
                    if uid not in user_cache:
                        try:
                            udata = await _slack_api(
                                client, "users.info", token,
                                {"user": uid}, use_post=use_post,
                            )
                            u = udata.get("user", {})
                            user_cache[uid] = (
                                u.get("profile", {}).get("display_name")
                                or u.get("real_name")
                                or uid
                            )
                        except Exception:
                            user_cache[uid] = uid

                # Build SlackMessage objects
                for msg in raw_messages:
                    user_name = user_cache.get(msg.get("user", ""), "unknown")
                    slack_msg = SlackMessage(
                        channel=ch_name,
                        user=user_name,
                        text=msg.get("text", ""),
                        timestamp=msg.get("ts", ""),
                        thread_ts=msg.get("thread_ts"),
                    )

                    # Fetch thread replies
                    if msg.get("reply_count", 0) > 0:
                        try:
                            tdata = await _slack_api(
                                client, "conversations.replies", token,
                                {"channel": ch_id, "ts": msg["ts"], "limit": 200},
                                use_post=use_post,
                            )
                            replies = tdata.get("messages", [])[1:]
                            for r in replies:
                                rid = r.get("user", "")
                                if rid and rid not in user_cache:
                                    try:
                                        ru = await _slack_api(
                                            client, "users.info", token,
                                            {"user": rid}, use_post=use_post,
                                        )
                                        u = ru.get("user", {})
                                        user_cache[rid] = (
                                            u.get("profile", {}).get("display_name")
                                            or u.get("real_name")
                                            or rid
                                        )
                                    except Exception:
                                        user_cache[rid] = rid

                                slack_msg.replies.append(SlackMessage(
                                    channel=ch_name,
                                    user=user_cache.get(r.get("user", ""), "unknown"),
                                    text=r.get("text", ""),
                                    timestamp=r.get("ts", ""),
                                    thread_ts=r.get("thread_ts"),
                                ))
                        except Exception:
                            logger.debug("Failed to fetch thread %s in %s",
                                         msg.get("ts"), ch_name)

                    day_messages.append(slack_msg)

            # Upsert this day's chunks to the store
            if day_messages:
                day_chunks = chunks_from_slack_messages(day_messages)
                if store and day_chunks:
                    store.upsert_chunks(day_chunks)
                total_messages += len(day_messages)
                total_chunks += len(day_chunks)
                logger.info("%s: %d messages, %d chunks indexed",
                            date_str, len(day_messages), len(day_chunks))

            cache.add_slack_completed_day(date_str)
            days_processed += 1

        if progress and task_id is not None:
            progress.update(task_id, completed=total_days)

    logger.info("Total: %d messages, %d chunks over %d days from %d channels "
                "(%d days skipped)",
                total_messages, total_chunks, history_days, len(channel_ids),
                days_skipped)
    return {
        "messages": total_messages,
        "chunks": total_chunks,
        "days_processed": days_processed,
        "days_skipped": days_skipped,
    }


# --- Chunking ---


def _generate_chunk_id(prefix: str, content: str) -> str:
    content_hash = hashlib.sha256(content.encode("utf-8")).hexdigest()[:12]
    return f"{prefix}::{content_hash}"


def _ts_to_iso(ts: str) -> str:
    try:
        return datetime.fromtimestamp(float(ts), tz=UTC).isoformat()
    except (ValueError, OSError):
        return ts


def chunks_from_slack_messages(messages: list[SlackMessage]) -> list[Chunk]:
    """Convert Slack messages and thread replies into Chunk objects."""
    chunks: list[Chunk] = []

    for msg in messages:
        parts = [f"[#{msg.channel}] {msg.user}: {msg.text}"]
        for reply in msg.replies:
            parts.append(f"  {reply.user}: {reply.text}")
        text = "\n".join(parts)

        if not text.strip():
            continue

        chunk_id = _generate_chunk_id(f"slack/{msg.channel}/{msg.timestamp}", text)
        chunks.append(Chunk(
            chunk_id=chunk_id,
            text=text,
            source="slack",
            org="",
            repo="",
            path=f"{msg.channel}/{msg.timestamp}",
            content_type="slack_message",
            author=msg.user,
            timestamp=_ts_to_iso(msg.timestamp),
        ))

    return chunks
