#!/usr/bin/env python3
import argparse
import datetime
import json
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path

from slack_common import (
    api_call,
    conversation_display_name,
    get_token,
    paginate,
    resolve_user_name,
)

INFO_TIMEOUT_SECONDS = 5
INFO_WORKERS = 12
HISTORY_WORKERS = 12
IGNORED_SUBTYPES = {
    "channel_archive",
    "channel_join",
    "channel_leave",
    "channel_name",
    "channel_purpose",
    "channel_topic",
    "channel_unarchive",
    "group_join",
    "group_leave",
    "group_name",
    "group_purpose",
    "group_topic",
    "message_deleted",
}


def _ts_to_dt(ts: str) -> str:
    try:
        return datetime.datetime.fromtimestamp(float(ts)).isoformat(sep=" ", timespec="seconds")
    except Exception:
        return ""


def _safe_ts(value) -> float:
    if isinstance(value, dict):
        value = value.get("ts") or ""
    try:
        number = float(value)
    except Exception:
        return 0.0
    if number > 1_000_000_000_000:
        return number / 1000.0
    return number


def _needs_info(conv) -> bool:
    return conv.get("last_read") is None or (conv.get("is_im") and conv.get("latest") is None)


def _fetch_conversation_info(token, conv):
    try:
        payload = api_call(
            "conversations.info",
            token,
            {"channel": conv.get("id")},
            timeout=INFO_TIMEOUT_SECONDS,
        )
    except SystemExit:
        return None
    return payload.get("channel", {})


def _enrich_conversations(convs, token):
    enriched = list(convs)
    futures = {}
    with ThreadPoolExecutor(max_workers=INFO_WORKERS) as executor:
        for idx, conv in enumerate(enriched):
            if _needs_info(conv):
                futures[executor.submit(_fetch_conversation_info, token, conv)] = idx

        for future in as_completed(futures):
            idx = futures[future]
            info = future.result()
            if info:
                enriched[idx] = {**enriched[idx], **info}
    return enriched


def _latest_unread(conv, token, last_read):
    params = {
        "channel": conv.get("id"),
        "limit": 50,
    }
    try:
        if last_read and float(last_read) > 0:
            params["oldest"] = last_read
    except Exception:
        pass
    try:
        payload = api_call("conversations.history", token, params)
    except SystemExit:
        return None, 0, False
    messages = payload.get("messages", [])
    unread = []
    for msg in messages:
        if msg.get("subtype") in IGNORED_SUBTYPES:
            continue
        if "text" not in msg:
            continue
        unread.append(msg)
    if not unread:
        return None, 0, payload.get("has_more", False)
    latest = max(unread, key=lambda m: float(m.get("ts", 0)))
    return latest, len(unread), payload.get("has_more", False)


def _should_check_history(conv) -> bool:
    unread_hint = conv.get("unread_count") or conv.get("unread_count_display") or 0
    if unread_hint:
        return True

    last_read = _safe_ts(conv.get("last_read"))
    if not last_read:
        return False

    latest = _safe_ts(conv.get("latest"))
    if latest:
        return latest > last_read

    updated = _safe_ts(conv.get("updated"))
    if updated:
        return updated > last_read

    return True


def _fetch_candidate_history(token, conv):
    last_read = conv.get("last_read") or ""
    latest_msg, unread_count, has_more = _latest_unread(conv, token, last_read)
    return conv, latest_msg, unread_count, has_more


def main():
    parser = argparse.ArgumentParser(description="List unread Slack conversations")
    parser.add_argument(
        "--types",
        default="public_channel,private_channel,im,mpim",
        help="Conversation types",
    )
    parser.add_argument("--max", type=int, default=1000)
    parser.add_argument("--json-out", default="/tmp/slack-inbox.json")
    args = parser.parse_args()

    token = get_token()
    user_cache = {}

    convs = paginate(
        "users.conversations",
        token,
        {
            "types": args.types,
            "exclude_archived": "true",
            "limit": 200,
        },
        "channels",
    )
    convs = _enrich_conversations(convs, token)

    items = []
    counter = 1
    candidates = [conv for conv in convs if _should_check_history(conv)]
    history_by_id = {}

    with ThreadPoolExecutor(max_workers=HISTORY_WORKERS) as executor:
        futures = {
            executor.submit(_fetch_candidate_history, token, conv): conv.get("id")
            for conv in candidates
        }
        for future in as_completed(futures):
            conv, latest_msg, unread_count, has_more = future.result()
            history_by_id[conv.get("id")] = (latest_msg, unread_count, has_more)

    for conv in convs:
        latest_msg, unread_count, has_more = history_by_id.get(conv.get("id"), (None, 0, False))
        if not latest_msg:
            continue

        last_read = conv.get("last_read") or ""
        display = conversation_display_name(conv, token, user_cache)
        latest_text = latest_msg.get("text", "").strip()
        latest_user = latest_msg.get("user") or latest_msg.get("username") or latest_msg.get("bot_id") or ""
        latest_user_name = ""
        if latest_msg.get("user"):
            latest_user_name = resolve_user_name(token, latest_msg.get("user"), user_cache)
        elif latest_msg.get("username"):
            latest_user_name = latest_msg.get("username")

        count_label = str(unread_count) + ("+" if has_more else "")
        preview = latest_text.replace("\n", " ")
        if len(preview) > 120:
            preview = preview[:117] + "..."
        author = latest_user_name or latest_user

        print(f"{counter}) {display} ({count_label}) - {author}: {preview}")

        items.append(
            {
                "index": counter,
                "id": conv.get("id"),
                "name": display,
                "raw_name": conv.get("name"),
                "type": "im" if conv.get("is_im") else "mpim" if conv.get("is_mpim") else "group" if conv.get("is_group") else "channel",
                "last_read": last_read,
                "latest_ts": latest_msg.get("ts"),
                "latest_text": latest_text,
                "latest_user_id": latest_msg.get("user"),
                "latest_user_name": latest_user_name,
                "unread_count": unread_count,
                "unread_has_more": has_more,
                "latest_at": _ts_to_dt(latest_msg.get("ts", "0")),
            }
        )
        counter += 1

        if counter > args.max:
            break

    Path(args.json_out).parent.mkdir(parents=True, exist_ok=True)
    with open(args.json_out, "w", encoding="utf-8") as f:
        json.dump(items, f, ensure_ascii=False, indent=2)


if __name__ == "__main__":
    main()
