"use client";

import { Suspense, useEffect, useMemo, useRef, useState, type FormEvent, type ReactNode } from "react";
import type { BlocksV1, Message } from "@slackwsh/contracts";
import { AppShell, useWorkspaceIdParam } from "../../components/AppShell";
import { UserAvatar } from "../../components/UserAvatar";
import { RichTextEditor, type RichTextEditorHandle } from "../../components/RichTextEditor";
import { RichTextMessage } from "../../components/RichTextMessage";
import {
  IconAt,
  IconCheck,
  IconChevron,
  IconLock,
  IconMessages,
  IconSearch,
  IconSend,
  IconSliders,
  IconThreads,
} from "../../components/icons";
import { api, ApiError } from "../../lib/api";

interface MentionActivityHit {
  message: Message;
  channelName: string | null;
  authorName: string;
  mentionType: string;
  readAt: string | null;
}

interface ActivityMember {
  user: { id: string | number; name: string; avatarUrl?: string | null };
}

type ActivityTab = "all" | "dms" | "mentions" | "threads";
type Density = "comfortable" | "compact";

function activityKind(hit: MentionActivityHit): Exclude<ActivityTab, "all"> {
  if (hit.message.parentId) return "threads";
  if (hit.channelName?.toLowerCase().startsWith("dm-")) return "dms";
  return "mentions";
}

function activityLabel(hit: MentionActivityHit) {
  const kind = activityKind(hit);
  if (kind === "threads") return "Thread in";
  if (kind === "dms") return "Direct message";
  if (hit.mentionType === "user") return "Mentioned you in";
  return "Group mention in";
}

function relativeTime(iso: string) {
  const elapsed = Math.max(0, Date.now() - new Date(iso).getTime());
  const minutes = Math.floor(elapsed / 60_000);
  if (minutes < 1) return "now";
  if (minutes < 60) return `${minutes} min${minutes === 1 ? "" : "s"}`;
  const hours = Math.floor(minutes / 60);
  if (hours < 24) return `${hours} hr${hours === 1 ? "" : "s"}`;
  const days = Math.floor(hours / 24);
  if (days < 7) return `${days} day${days === 1 ? "" : "s"}`;
  return new Date(iso).toLocaleDateString([], { month: "short", day: "numeric" });
}

function exactTime(iso: string) {
  return new Date(iso).toLocaleString([], {
    weekday: "short",
    month: "short",
    day: "numeric",
    hour: "numeric",
    minute: "2-digit",
  });
}

function highlightedText(text: string): ReactNode {
  const parts = text.split(/(@[a-zA-Z0-9._-]+)/g);
  return parts.map((part, index) =>
    part.startsWith("@") ? <mark key={`${part}-${index}`}>{part}</mark> : <span key={index}>{part}</span>,
  );
}

function legacyReadStorageKey(workspaceId: string) {
  return `connecthub.activity.read.v1:${workspaceId}`;
}

function ActivityView() {
  const workspaceId = useWorkspaceIdParam();
  const [results, setResults] = useState<MentionActivityHit[]>([]);
  const [members, setMembers] = useState<ActivityMember[]>([]);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [activeTab, setActiveTab] = useState<ActivityTab>("all");
  const [selectedId, setSelectedId] = useState<string | null>(null);
  const [readIds, setReadIds] = useState<Set<string>>(new Set());
  const [unreadOnly, setUnreadOnly] = useState(false);
  const [searchOpen, setSearchOpen] = useState(false);
  const [query, setQuery] = useState("");
  const [density, setDensity] = useState<Density>("comfortable");
  const [threadRoot, setThreadRoot] = useState<Message | null>(null);
  const [threadReplies, setThreadReplies] = useState<Message[]>([]);
  const [threadBusy, setThreadBusy] = useState(false);
  const [threadError, setThreadError] = useState<string | null>(null);
  const [threadDraft, setThreadDraft] = useState("");
  const [threadDraftBlocks, setThreadDraftBlocks] = useState<BlocksV1 | null>(null);
  const [threadSending, setThreadSending] = useState(false);
  const threadEditorRef = useRef<RichTextEditorHandle | null>(null);

  useEffect(() => {
    if (!workspaceId) return;
    let cancelled = false;
    setBusy(true);
    setError(null);
    Promise.all([api.mentionActivity(workspaceId), api.members(workspaceId)])
      .then(async ([activity, workspaceMembers]) => {
        let legacyIds: string[] = [];
        try {
          const parsed = JSON.parse(window.localStorage.getItem(legacyReadStorageKey(workspaceId)) ?? "[]") as unknown;
          if (Array.isArray(parsed)) legacyIds = parsed.map(String);
        } catch {
          legacyIds = [];
        }
        const visibleLegacyIds = activity.results
          .map((hit) => String(hit.message.id))
          .filter((id) => legacyIds.includes(id));
        if (legacyIds.length > 0) {
          for (let index = 0; index < legacyIds.length; index += 100) {
            await api.updateActivityRead(workspaceId, legacyIds.slice(index, index + 100), true);
          }
          activity.results = activity.results.map((hit) => visibleLegacyIds.includes(String(hit.message.id))
            ? { ...hit, readAt: hit.readAt ?? new Date().toISOString() }
            : hit);
          window.localStorage.removeItem(legacyReadStorageKey(workspaceId));
        }
        return [activity, workspaceMembers] as const;
      })
      .then(([activity, workspaceMembers]) => {
        if (cancelled) return;
        setResults(activity.results);
        setReadIds(new Set(activity.results.filter((hit) => hit.readAt).map((hit) => String(hit.message.id))));
        setMembers(workspaceMembers.members);
      })
      .catch((err) => {
        if (!cancelled) setError(err instanceof ApiError ? JSON.stringify(err.body) : String(err));
      })
      .finally(() => {
        if (!cancelled) setBusy(false);
      });
    return () => {
      cancelled = true;
    };
  }, [workspaceId]);

  useEffect(() => {
    if (!workspaceId) return;
    const onReadUpdated = (event: Event) => {
      const detail = (event as CustomEvent<{
        workspaceId?: string | number;
        messageIds?: Array<string | number>;
        read?: boolean;
      }>).detail;
      if (detail?.workspaceId == null || String(detail.workspaceId) !== workspaceId || !detail.messageIds) return;
      setReadIds((current) => {
        const next = new Set(current);
        for (const id of detail.messageIds ?? []) {
          if (detail.read) next.add(String(id));
          else next.delete(String(id));
        }
        return next;
      });
    };
    window.addEventListener("slackwsh:activity-read-updated", onReadUpdated);
    return () => window.removeEventListener("slackwsh:activity-read-updated", onReadUpdated);
  }, [workspaceId]);

  const counts = useMemo(() => {
    const next = { all: results.length, dms: 0, mentions: 0, threads: 0 };
    results.forEach((hit) => {
      next[activityKind(hit)] += 1;
    });
    return next;
  }, [results]);

  const visibleResults = useMemo(() => {
    const normalizedQuery = query.trim().toLowerCase();
    return results.filter((hit) => {
      if (activeTab !== "all" && activityKind(hit) !== activeTab) return false;
      if (unreadOnly && readIds.has(String(hit.message.id))) return false;
      if (!normalizedQuery) return true;
      return [hit.authorName, hit.channelName ?? "", hit.message.text]
        .join(" ")
        .toLowerCase()
        .includes(normalizedQuery);
    });
  }, [activeTab, query, readIds, results, unreadOnly]);

  const selected = results.find((hit) => String(hit.message.id) === selectedId) ?? null;
  const threadRootId = selected ? String(selected.message.parentId ?? selected.message.id) : null;

  useEffect(() => {
    if (!workspaceId || !selected || !threadRootId) {
      setThreadRoot(null);
      setThreadReplies([]);
      return;
    }

    let cancelled = false;
    const selectedIsRoot = selected.message.parentId == null;
    const knownRoot = selectedIsRoot
      ? selected.message
      : results.find((hit) => String(hit.message.id) === threadRootId)?.message ?? null;

    setThreadRoot(knownRoot);
    setThreadReplies([]);
    setThreadBusy(true);
    setThreadError(null);

    const rootRequest = knownRoot
      ? Promise.resolve(knownRoot)
      : api
          .scrollback(workspaceId, String(selected.message.channelId), {
            beforeSeq: selected.message.seq + 1,
            limit: 100,
          })
          .then((res) => (res.messages as Message[]).find((message) => String(message.id) === threadRootId) ?? null);

    Promise.all([
      rootRequest,
      api.threadMessages(workspaceId, String(selected.message.channelId), threadRootId),
    ])
      .then(([root, thread]) => {
        if (cancelled) return;
        setThreadRoot(root);
        setThreadReplies(thread.messages.filter((message) => !message.deletedAt));
        const latestSeq = thread.messages.reduce((max, message) => Math.max(max, message.seq), 0);
        if (latestSeq > 0) {
          void api
            .markThreadRead(workspaceId, String(selected.message.channelId), threadRootId, latestSeq)
            .catch(() => undefined);
        }
      })
      .catch((err) => {
        if (!cancelled) setThreadError(err instanceof ApiError ? JSON.stringify(err.body) : String(err));
      })
      .finally(() => {
        if (!cancelled) setThreadBusy(false);
      });

    return () => {
      cancelled = true;
    };
  }, [results, selected, threadRootId, workspaceId]);

  function markRead(id: string) {
    if (!workspaceId) return;
    if (readIds.has(id)) return;
    setReadIds((current) => new Set(current).add(id));
    void api.updateActivityRead(workspaceId, [id], true).catch(() => {
      setReadIds((current) => {
        const next = new Set(current);
        next.delete(id);
        return next;
      });
    });
  }

  function selectHit(hit: MentionActivityHit) {
    setSelectedId(String(hit.message.id));
    markRead(String(hit.message.id));
  }

  function markAllRead() {
    if (!workspaceId || results.length === 0) return;
    const previous = readIds;
    const next = new Set(results.map((hit) => String(hit.message.id)));
    setReadIds(next);
    void api.updateActivityRead(workspaceId, [...next], true).catch(() => setReadIds(previous));
  }

  async function sendThreadReply(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const richValue = threadEditorRef.current?.getValue();
    const text = (richValue?.text ?? threadDraft).trim();
    if (!workspaceId || !selected || !threadRootId || !text || threadSending) return;
    setThreadSending(true);
    setThreadError(null);
    try {
      const sent = (await api.sendMessage(workspaceId, String(selected.message.channelId), {
        clientMsgId: crypto.randomUUID(),
        text,
        blocks: richValue?.blocks ?? threadDraftBlocks ?? undefined,
        parentId: threadRootId,
      })) as Message;
      setThreadReplies((current) => [...current, sent]);
      setThreadDraft("");
      setThreadDraftBlocks(null);
    } catch (err) {
      setThreadError(err instanceof ApiError ? JSON.stringify(err.body) : String(err));
    } finally {
      setThreadSending(false);
    }
  }

  const tabs: Array<{ id: ActivityTab; label: string; Icon?: typeof IconAt }> = [
    { id: "all", label: "All" },
    { id: "dms", label: "DMs", Icon: IconMessages },
    { id: "mentions", label: "Mentions", Icon: IconAt },
    { id: "threads", label: "Threads", Icon: IconThreads },
  ];

  return (
    <AppShell active="home">
      <div className="activity-page">
        <section className="activity-inbox" aria-label="Activity inbox">
          <header className="activity-header">
            <div className="activity-title-row">
              <h1>Activity</h1>
            </div>
            <nav className="activity-tabs" aria-label="Activity filters">
              {tabs.map(({ id, label, Icon }) => (
                <button
                  className={activeTab === id ? "activity-tab active" : "activity-tab"}
                  key={id}
                  type="button"
                  onClick={() => setActiveTab(id)}
                >
                  {Icon && <Icon size={17} />}
                  <span>{label}</span>
                  {counts[id] > 0 && <span className="activity-tab-count">{counts[id]}</span>}
                </button>
              ))}
            </nav>
          </header>

          <div className="activity-toolbar">
            <button
              className="activity-check-btn"
              type="button"
              onClick={markAllRead}
              aria-label="Mark all as read"
              title="Mark all as read"
            >
              <span className="activity-checkbox">{readIds.size >= results.length && results.length > 0 && <IconCheck size={12} />}</span>
              <IconChevron size={13} />
            </button>
            <button
              className={unreadOnly ? "activity-filter-btn active" : "activity-filter-btn"}
              type="button"
              onClick={() => setUnreadOnly((value) => !value)}
            >
              <span className="activity-unread-icon" />
              Unreads
            </button>
            <label className="activity-filter-select">
              <IconSliders size={15} />
              <select aria-label="Activity type" value={activeTab} onChange={(event) => setActiveTab(event.target.value as ActivityTab)}>
                <option value="all">All types</option>
                <option value="dms">DMs</option>
                <option value="mentions">Mentions</option>
                <option value="threads">Threads</option>
              </select>
              <IconChevron size={13} />
            </label>
            {searchOpen ? (
              <label className="activity-search-field">
                <IconSearch size={16} />
                <input autoFocus value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Search activity" />
                <button type="button" onClick={() => { setQuery(""); setSearchOpen(false); }} aria-label="Close search">×</button>
              </label>
            ) : (
              <button className="activity-icon-btn bordered" type="button" onClick={() => setSearchOpen(true)} aria-label="Search activity">
                <IconSearch size={19} />
              </button>
            )}
            <div className="activity-density" aria-label="Display density">
              <button
                className={density === "comfortable" ? "active" : ""}
                type="button"
                onClick={() => setDensity("comfortable")}
                aria-label="Comfortable view"
                title="Comfortable view"
              ><span className="density-card" /></button>
              <button
                className={density === "compact" ? "active" : ""}
                type="button"
                onClick={() => setDensity("compact")}
                aria-label="Compact view"
                title="Compact view"
              ><span className="density-lines" /></button>
            </div>
          </div>

          <div className={`activity-feed ${density}`}>
            {error && <div className="activity-status error">Could not load activity. {error}</div>}
            {busy && <div className="activity-status">Loading activity…</div>}
            {!busy && !error && visibleResults.map((hit) => {
              const member = members.find((row) => String(row.user.id) === String(hit.message.authorId))?.user;
              const unread = !readIds.has(String(hit.message.id));
              const active = selectedId === String(hit.message.id);
              return (
                <button
                  className={`activity-card${unread ? " unread" : ""}${active ? " selected" : ""}`}
                  type="button"
                  key={`${hit.message.id}-${hit.mentionType}`}
                  onClick={() => selectHit(hit)}
                >
                  <UserAvatar
                    userId={hit.message.authorId}
                    name={hit.authorName}
                    avatarUrl={member?.avatarUrl}
                    className="activity-avatar"
                  />
                  <span className="activity-card-body">
                    <span className="activity-card-head">
                      <strong>{hit.authorName}</strong>
                      <time dateTime={hit.message.createdAt}>{relativeTime(hit.message.createdAt)}</time>
                      {unread && <span className="activity-unread-count">1</span>}
                    </span>
                    <span className="activity-context">
                      {activityKind(hit) === "threads" ? <IconThreads size={15} /> : <IconAt size={15} />}
                      <b>{activityLabel(hit)}</b>
                      <span className="activity-channel"><IconLock size={12} /> {hit.channelName ?? "channel"}</span>
                    </span>
                    <span className="activity-message">{highlightedText(hit.message.text || "(attachment)")}</span>
                  </span>
                </button>
              );
            })}
            {!busy && !error && visibleResults.length === 0 && (
              <div className="activity-status empty">
                <span className="activity-empty-icon"><IconCheck size={22} /></span>
                <strong>You&apos;re all caught up</strong>
                <span>{query ? "No activity matches your search." : unreadOnly ? "There are no unread notifications." : "New mentions and thread replies will appear here."}</span>
              </div>
            )}
          </div>
        </section>

        <aside className={`activity-detail${selected ? " has-selection" : ""}`} aria-label="Thread details">
          {selected ? (
            <>
              <header className="activity-detail-head">
                <div>
                  <h2><IconThreads size={19} /> Thread</h2>
                  <span className="activity-thread-channel"><IconLock size={12} /> {selected.channelName ?? "channel"}</span>
                </div>
                <button type="button" onClick={() => setSelectedId(null)} aria-label="Close notification details">×</button>
              </header>
              <div className="activity-thread-scroll">
                {threadRoot && (
                  <ActivityThreadMessage
                    message={threadRoot}
                    fallbackName={String(threadRoot.id) === String(selected.message.id) ? selected.authorName : "Member"}
                    members={members}
                    root
                  />
                )}

                <div className="activity-thread-count">
                  <span>{threadBusy ? "Loading replies…" : `${threadReplies.length} ${threadReplies.length === 1 ? "reply" : "replies"}`}</span>
                  <i />
                </div>

                {threadError && <p className="activity-thread-error">{threadError}</p>}

                {threadReplies.map((reply) => (
                  <ActivityThreadMessage
                    key={reply.id}
                    message={reply}
                    fallbackName={String(reply.id) === String(selected.message.id) ? selected.authorName : "Member"}
                    members={members}
                  />
                ))}

                {!threadBusy && !threadRoot && (
                  <ActivityThreadMessage
                    message={selected.message}
                    fallbackName={selected.authorName}
                    members={members}
                    root
                  />
                )}
              </div>

              <form className="activity-thread-composer" onSubmit={sendThreadReply}>
                <RichTextEditor
                  ref={threadEditorRef}
                  value={threadDraft}
                  blocks={threadDraftBlocks}
                  compact
                  onChange={(next) => {
                    setThreadDraft(next.text);
                    setThreadDraftBlocks(next.blocks);
                  }}
                  onSubmit={() => document.activeElement?.closest("form")?.requestSubmit()}
                  placeholder="Reply…"
                  ariaLabel="Reply in thread"
                />
                <div className="activity-thread-compose-actions">
                  <button className="activity-thread-send" type="submit" disabled={!threadDraft.trim() || threadSending} aria-label="Send reply">
                    <IconSend size={16} />
                  </button>
                </div>
              </form>
            </>
          ) : (
            <div className="activity-detail-empty">
              <span className="activity-detail-empty-icon"><IconMessages size={23} /></span>
              <p>Select a notification to view the details.</p>
            </div>
          )}
        </aside>
      </div>
    </AppShell>
  );
}

function ActivityThreadMessage({
  message,
  fallbackName,
  members,
  root = false,
}: {
  message: Message;
  fallbackName: string;
  members: ActivityMember[];
  root?: boolean;
}) {
  const author = members.find((row) => String(row.user.id) === String(message.authorId))?.user;
  const name = author?.name ?? fallbackName;
  return (
    <article className={root ? "activity-thread-message root" : "activity-thread-message"}>
      <UserAvatar
        userId={message.authorId}
        name={name}
        avatarUrl={author?.avatarUrl}
        className="activity-detail-avatar"
      />
      <div className="activity-detail-message">
        <div className="activity-detail-author">
          <strong>{name}</strong>
          <time dateTime={message.createdAt}>{exactTime(message.createdAt)}</time>
          {message.editedAt && <span>(edited)</span>}
        </div>
        <RichTextMessage
          blocks={message.blocks}
          fallback={message.text || (message.deletedAt ? "This message was deleted." : "(attachment)")}
          className="activity-thread-rich-text"
          renderText={highlightedText}
        />
        {(message.reactions ?? []).length > 0 && (
          <div className="activity-thread-reactions">
            {message.reactions?.map((reaction) => (
              <span key={`${message.id}-${reaction.emoji}`}>{reaction.emoji} {reaction.count}</span>
            ))}
          </div>
        )}
      </div>
    </article>
  );
}

export default function ActivityPage() {
  return (
    <Suspense fallback={<main className="page"><div className="empty-state">Loading activity…</div></main>}>
      <ActivityView />
    </Suspense>
  );
}
