"use client";

import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
import { useSearchParams } from "next/navigation";
import type { CalendarEvent, Call, CallContact } from "@slackwsh/contracts";
import { AppShell, useWorkspaceIdParam } from "../../components/AppShell";
import { CallOverlay } from "../../components/CallOverlay";
import { NewCallModal, type NewCallSubmit } from "../../components/NewCallModal";
import { api, getCurrentUserId } from "../../lib/api";
import { avatarColor, initials } from "../../lib/avatar";
import { addDays, formatDurationLabel, formatWhenLabel } from "../../lib/datetime";
import { IconPhone, IconPlus, IconUsers, IconVideo } from "../../components/icons";

interface MemberRow {
  member: { role: string; deactivatedAt?: string | null };
  user: { id: string | number; name: string; email: string; avatarUrl?: string | null };
}

type Tab = "all" | "missed" | "incoming" | "outgoing";

const TABS: Array<{ key: Tab; label: string }> = [
  { key: "all", label: "All" },
  { key: "missed", label: "Missed" },
  { key: "incoming", label: "Incoming" },
  { key: "outgoing", label: "Outgoing" },
];

/** How far ahead the "scheduled" panel looks. Long enough to be useful as a
 * "what's coming" list, short enough that it isn't a second calendar. */
const UPCOMING_DAYS = 7;

const PRESENCE_RANK: Record<string, number> = { active: 0, away: 1, offline: 2 };

function CallsView() {
  const workspaceId = useWorkspaceIdParam();
  const params = useSearchParams();
  const openCallId = params.get("callId");

  const [myUserId, setMyUserId] = useState<number | null>(null);
  const [members, setMembers] = useState<MemberRow[]>([]);
  const [presence, setPresence] = useState<Record<string, { status: string }>>({});
  const [history, setHistory] = useState<Call[]>([]);
  const [live, setLive] = useState<Call[]>([]);
  const [contacts, setContacts] = useState<CallContact[]>([]);
  const [upcoming, setUpcoming] = useState<CalendarEvent[]>([]);
  const [tab, setTab] = useState<Tab>("all");
  const [loading, setLoading] = useState(true);
  const [actionError, setActionError] = useState<string | null>(null);
  const [busy, setBusy] = useState(false);
  const [pickerOpen, setPickerOpen] = useState(false);
  // The call this tab currently holds media for. Kept separate from `live`
  // because being *on* a call is a property of this browser, not of the
  // workspace — another tab of the same account is not in this one's session.
  const [activeCall, setActiveCall] = useState<Call | null>(null);

  useEffect(() => {
    getCurrentUserId().then(setMyUserId);
  }, []);

  useEffect(() => {
    if (!workspaceId) return;
    api
      .members(workspaceId)
      .then((res) => setMembers(res.members as MemberRow[]))
      .catch(() => undefined);
  }, [workspaceId]);

  /**
   * Availability is polled, not pushed. `presence:changed` is published to the
   * `ws:{workspaceId}` room, which only a socket that has joined a channel is
   * in — so on this page there is no live feed to subscribe to, and a snapshot
   * on a timer is the honest way to keep "who's around" from going stale. It
   * also refreshes on focus, which covers the common case of coming back to a
   * tab left open for an hour.
   */
  useEffect(() => {
    if (!workspaceId) return;
    let cancelled = false;
    function refreshPresence() {
      api
        .presenceSnapshot(workspaceId!)
        .then((res) => {
          if (!cancelled) setPresence(res.presence);
        })
        .catch(() => undefined);
    }
    refreshPresence();
    const timer = window.setInterval(refreshPresence, 30_000);
    window.addEventListener("focus", refreshPresence);
    return () => {
      cancelled = true;
      window.clearInterval(timer);
      window.removeEventListener("focus", refreshPresence);
    };
  }, [workspaceId]);

  const load = useCallback(() => {
    if (!workspaceId) return;
    const from = new Date().toISOString();
    const to = addDays(new Date(), UPCOMING_DAYS).toISOString();
    setLoading(true);
    Promise.all([
      api
        .listCalls(workspaceId, {
          missed: tab === "missed" || undefined,
          direction: tab === "incoming" ? "incoming" : tab === "outgoing" ? "outgoing" : undefined,
          limit: 50,
        })
        .then((res) => res.calls),
      api.listCalls(workspaceId, { active: true }).then((res) => res.calls),
      api
        .callContacts(workspaceId)
        .then((res) => res.contacts)
        .catch(() => [] as CallContact[]),
      // Scheduled calls are calendar events, not a second source of truth —
      // anything on your calendar in the next week is something you might be
      // about to dial into.
      api
        .listEvents(workspaceId, { from, to, mine: true })
        .then((res) => res.events)
        .catch(() => [] as CalendarEvent[]),
    ])
      .then(([nextHistory, nextLive, nextContacts, nextUpcoming]) => {
        setHistory(nextHistory);
        setLive(nextLive);
        setContacts(nextContacts);
        setUpcoming(nextUpcoming.slice(0, 5));
      })
      .catch(() => {
        setHistory([]);
        setLive([]);
      })
      .finally(() => setLoading(false));
  }, [workspaceId, tab]);

  useEffect(() => {
    load();
  }, [load]);

  // Opening the page is the acknowledgement — the badge clears here rather than
  // per-row, matching how an inbox behaves.
  useEffect(() => {
    if (!workspaceId) return;
    api
      .markCallsSeen(workspaceId)
      .then((res) => {
        if (res.seen > 0) window.dispatchEvent(new CustomEvent("slackwsh:calls-seen"));
      })
      .catch(() => undefined);
  }, [workspaceId]);

  /**
   * Realtime. Unlike the calendar, call events carry the complete row and there
   * is no window they might fall outside of, so they are merged rather than
   * triggering a refetch — which matters here because the merged participant
   * list is what drives the mesh's peer set mid-call.
   */
  useEffect(() => {
    function onCallEvent(e: Event) {
      const detail = (e as CustomEvent).detail as { type?: string; call?: Call } | undefined;
      const call = detail?.call;
      if (!call || !workspaceId || String(call.workspaceId) !== String(workspaceId)) return;

      const isLive = call.status === "ringing" || call.status === "active";
      const iAmOnIt =
        myUserId != null &&
        call.participants.some(
          (p) => Number(p.userId) === myUserId && (p.state === "ringing" || p.state === "joined"),
        );
      setLive((current) => {
        const rest = current.filter((c) => Number(c.id) !== Number(call.id));
        return isLive && iAmOnIt ? [call, ...rest] : rest;
      });
      setHistory((current) => {
        const index = current.findIndex((c) => Number(c.id) === Number(call.id));
        if (index === -1) return isLive ? current : [call, ...current];
        const next = current.slice();
        next[index] = call;
        return next;
      });
      setActiveCall((current) => {
        if (!current || Number(current.id) !== Number(call.id)) return current;
        // Everyone else hung up: drop the overlay rather than leaving a dead
        // grid on screen.
        return call.status === "ended" || call.status === "missed" ? null : call;
      });
    }
    window.addEventListener("slackwsh:call", onCallEvent);
    return () => window.removeEventListener("slackwsh:call", onCallEvent);
  }, [workspaceId, myUserId]);

  // Deep link from the ringer: /calls?callId=… means "you answered, show it".
  useEffect(() => {
    if (!workspaceId || !openCallId || myUserId == null) return;
    let cancelled = false;
    api
      .getCall(workspaceId, openCallId)
      .then((call) => {
        if (cancelled) return;
        const me = call.participants.find((p) => Number(p.userId) === myUserId);
        if (me?.state === "joined" && (call.status === "ringing" || call.status === "active")) setActiveCall(call);
      })
      .catch(() => undefined);
    return () => {
      cancelled = true;
    };
  }, [workspaceId, openCallId, myUserId]);

  const nameById = useMemo(
    () => new Map(members.map((row) => [String(row.user.id), row.user.name])),
    [members],
  );
  const avatarById = useMemo(
    () => new Map(members.map((row) => [String(row.user.id), row.user.avatarUrl])),
    [members],
  );

  /**
   * The people-to-call directory: everyone you have called, ranked by how often
   * and how recently, then everyone else. Presence breaks ties within each
   * group — someone you call weekly is still the better suggestion than someone
   * you have never called who happens to be online, but among equals, reachable
   * wins.
   */
  const directory = useMemo(() => {
    const statsById = new Map(contacts.map((c) => [String(c.userId), c]));
    return members
      .filter((row) => !row.member.deactivatedAt && String(row.user.id) !== String(myUserId))
      .map((row) => {
        const stats = statsById.get(String(row.user.id));
        return {
          user: row.user,
          callCount: stats?.callCount ?? 0,
          lastCallAt: stats?.lastCallAt ?? null,
          status: presence[String(row.user.id)]?.status ?? "offline",
        };
      })
      .sort((a, b) => {
        if (b.callCount !== a.callCount) return b.callCount - a.callCount;
        const rank = (PRESENCE_RANK[a.status] ?? 2) - (PRESENCE_RANK[b.status] ?? 2);
        if (rank !== 0) return rank;
        if (a.lastCallAt !== b.lastCallAt) return (b.lastCallAt ?? "").localeCompare(a.lastCallAt ?? "");
        return a.user.name.localeCompare(b.user.name);
      });
  }, [members, contacts, presence, myUserId]);

  const missedCount = useMemo(
    () =>
      history.filter((call) =>
        call.participants.some((p) => Number(p.userId) === myUserId && p.state === "missed" && !p.seenAt),
      ).length,
    [history, myUserId],
  );

  async function guard<T>(work: () => Promise<T>, fallback: string): Promise<T | null> {
    setBusy(true);
    setActionError(null);
    try {
      return await work();
    } catch (err) {
      setActionError(err instanceof Error ? err.message : fallback);
      return null;
    } finally {
      setBusy(false);
    }
  }

  async function dial(inviteeUserIds: Array<string | number>, kind: "audio" | "video", extra: { eventId?: string | null; title?: string | null } = {}) {
    if (!workspaceId) return;
    const call = await guard(
      () => api.startCall(workspaceId, { kind, inviteeUserIds, ...extra }),
      "Couldn't start the call.",
    );
    if (call) {
      setActiveCall(call);
      load();
    }
  }

  async function join(call: Call) {
    if (!workspaceId) return;
    const joined = await guard(() => api.joinCall(workspaceId, String(call.id)), "Couldn't join the call.");
    if (joined) setActiveCall(joined);
  }

  async function leave() {
    const call = activeCall;
    if (!workspaceId || !call) return;
    // The overlay comes down first: the local session is already gone as far as
    // this user is concerned, and a slow request shouldn't leave them staring at
    // a call they have left.
    setActiveCall(null);
    await guard(() => api.leaveCall(workspaceId, String(call.id)), "Couldn't hang up cleanly.");
    load();
  }

  async function endForAll() {
    const call = activeCall;
    if (!workspaceId || !call) return;
    setActiveCall(null);
    await guard(() => api.endCall(workspaceId, String(call.id)), "Couldn't end the call.");
    load();
  }

  async function submitNewCall(values: NewCallSubmit) {
    setPickerOpen(false);
    await dial(values.userIds, values.kind, { title: values.title || null });
  }

  const rows = tab === "missed" ? history : history.filter((call) => !live.some((l) => Number(l.id) === Number(call.id)));

  return (
    <AppShell
      active="calls"
      title="Calls"
      subtitle={
        loading
          ? "Loading…"
          : live.length > 0
            ? `${live.length} call${live.length > 1 ? "s" : ""} happening now`
            : `${history.length} in your history`
      }
      actions={
        <button className="head-action primary" type="button" onClick={() => setPickerOpen(true)}>
          <IconPlus size={14} />
          <span>New call</span>
        </button>
      }
    >
      <div className="calls-body">
        <div className="calls-main">
          {actionError && <p className="error-text calls-error">{actionError}</p>}

          {live.length > 0 && (
            <section className="calls-live" aria-label="Happening now">
              <div className="calls-live-head">
                <span className="calls-live-dot" aria-hidden="true" />
                Happening now
              </div>
              {live.map((call) => {
                const joinedNames = call.participants
                  .filter((p) => p.state === "joined")
                  .map((p) => nameById.get(String(p.userId)) ?? `User ${p.userId}`);
                const amOn = call.participants.some((p) => Number(p.userId) === myUserId && p.state === "joined");
                return (
                  <div className="calls-live-row" key={call.id}>
                    <span className="calls-live-kind">
                      {call.kind === "video" ? <IconVideo size={15} /> : <IconPhone size={15} />}
                    </span>
                    <div className="calls-live-who">
                      <strong>{call.title ?? joinedNames.slice(0, 3).join(", ") ?? "Call"}</strong>
                      <span>
                        {call.status === "ringing" ? "Ringing…" : `${joinedNames.length} on the call`}
                        {call.channelId != null && " · from a conversation"}
                      </span>
                    </div>
                    <button
                      className="calls-join"
                      type="button"
                      disabled={busy}
                      onClick={() => (amOn ? setActiveCall(call) : void join(call))}
                    >
                      {amOn ? "Return" : "Join"}
                    </button>
                  </div>
                );
              })}
            </section>
          )}

          <div className="calls-tabs" role="tablist" aria-label="Call history filter">
            {TABS.map((entry) => (
              <button
                key={entry.key}
                type="button"
                role="tab"
                aria-selected={tab === entry.key}
                className={tab === entry.key ? "calls-tab active" : "calls-tab"}
                onClick={() => setTab(entry.key)}
              >
                {entry.label}
                {entry.key === "missed" && missedCount > 0 && <span className="calls-tab-badge">{missedCount}</span>}
              </button>
            ))}
          </div>

          <ul className="calls-list">
            {rows.map((call) => {
              const mine = call.participants.find((p) => Number(p.userId) === myUserId);
              const outgoing = Number(call.startedBy) === myUserId;
              const others = call.participants.filter((p) => Number(p.userId) !== myUserId);
              const otherNames = others.map((p) => nameById.get(String(p.userId)) ?? `User ${p.userId}`);
              const headline = call.title ?? (otherNames.length > 0 ? otherNames.join(", ") : "Call");
              const missed = mine?.state === "missed";
              const primaryOther = others[0];
              const color = avatarColor(String(primaryOther?.userId ?? call.startedBy));
              return (
                <li className={missed ? "calls-row missed" : "calls-row"} key={call.id}>
                  <span className="calls-row-avatar" style={{ background: color.bg, color: color.fg }}>
                    {others.length > 1 ? <IconUsers size={15} /> : initials(otherNames[0] ?? "?")}
                  </span>

                  <div className="calls-row-main">
                    <span className="calls-row-who">{headline}</span>
                    <span className="calls-row-meta">
                      <span className={outgoing ? "calls-dir out" : "calls-dir in"} aria-hidden="true">
                        {outgoing ? "↗" : "↙"}
                      </span>
                      {missed
                        ? outgoing
                          ? "No answer"
                          : "Missed"
                        : mine?.state === "declined"
                          ? "Declined"
                          : call.durationSeconds != null
                            ? formatDurationLabel(call.durationSeconds)
                            : "In progress"}
                      {call.kind === "video" && <span className="calls-row-tag">Video</span>}
                      {call.eventId != null && <span className="calls-row-tag">Scheduled</span>}
                    </span>
                  </div>

                  <span className="calls-row-when">{formatWhenLabel(call.startedAt)}</span>

                  <div className="calls-row-actions">
                    <button
                      className="calls-icon-btn"
                      type="button"
                      disabled={busy || others.length === 0}
                      aria-label={`Call ${headline} back`}
                      title="Call back"
                      onClick={() => void dial(others.map((p) => p.userId), "audio")}
                    >
                      <IconPhone size={15} />
                    </button>
                    <button
                      className="calls-icon-btn"
                      type="button"
                      disabled={busy || others.length === 0}
                      aria-label={`Start a video call with ${headline}`}
                      title="Video call"
                      onClick={() => void dial(others.map((p) => p.userId), "video")}
                    >
                      <IconVideo size={15} />
                    </button>
                  </div>
                </li>
              );
            })}

            {!loading && rows.length === 0 && (
              <li className="calls-empty">
                {tab === "missed"
                  ? "No missed calls. "
                  : tab === "incoming"
                    ? "Nobody has called you yet. "
                    : tab === "outgoing"
                      ? "You haven't called anyone yet. "
                      : "No calls yet. "}
                Pick someone from the list to start one.
              </li>
            )}
          </ul>
        </div>

        <aside className="calls-side">
          {upcoming.length > 0 && (
            <section className="calls-panel">
              <h2 className="calls-panel-head">Scheduled</h2>
              <ul className="calls-panel-list">
                {upcoming.map((event) => {
                  const guests = event.attendees
                    .map((a) => Number(a.userId))
                    .filter((id) => id !== myUserId);
                  return (
                    <li className="calls-upcoming" key={`${event.id}-${event.occurrenceDate}`}>
                      <div className="calls-upcoming-main">
                        <strong>{event.title}</strong>
                        <span>
                          {event.allDay ? "All day" : formatWhenLabel(event.startsAt)}
                          {guests.length > 0 && ` · ${guests.length} guest${guests.length > 1 ? "s" : ""}`}
                        </span>
                      </div>
                      <button
                        className="calls-join"
                        type="button"
                        // A meeting with no other attendees has nobody to ring.
                        disabled={busy || guests.length === 0}
                        title={guests.length === 0 ? "No other guests to call" : "Start this call"}
                        onClick={() => void dial(guests, "video", { eventId: String(event.id), title: event.title })}
                      >
                        Start
                      </button>
                    </li>
                  );
                })}
              </ul>
            </section>
          )}

          <section className="calls-panel">
            <h2 className="calls-panel-head">
              People to call
              <span className="calls-panel-hint">Frequent first</span>
            </h2>
            <ul className="calls-panel-list">
              {directory.map((entry) => {
                const color = avatarColor(String(entry.user.id));
                return (
                  <li className="calls-person" key={String(entry.user.id)}>
                    <span className="calls-person-avatar" style={{ background: color.bg, color: color.fg }}>
                      {initials(entry.user.name)}
                      <span
                        className={`calls-person-dot ${entry.status}`}
                        aria-label={entry.status === "active" ? "Online" : entry.status === "away" ? "Away" : "Offline"}
                      />
                    </span>
                    <div className="calls-person-main">
                      <span className="calls-person-name">{entry.user.name}</span>
                      <span className="calls-person-meta">
                        {entry.callCount > 0
                          ? `${entry.callCount} call${entry.callCount > 1 ? "s" : ""}${entry.lastCallAt ? ` · ${formatWhenLabel(entry.lastCallAt)}` : ""}`
                          : entry.status === "active"
                            ? "Online"
                            : entry.user.email}
                      </span>
                    </div>
                    <div className="calls-person-actions">
                      <button
                        className="calls-icon-btn"
                        type="button"
                        disabled={busy}
                        aria-label={`Call ${entry.user.name}`}
                        title="Call"
                        onClick={() => void dial([entry.user.id], "audio")}
                      >
                        <IconPhone size={15} />
                      </button>
                      <button
                        className="calls-icon-btn"
                        type="button"
                        disabled={busy}
                        aria-label={`Start a video call with ${entry.user.name}`}
                        title="Video call"
                        onClick={() => void dial([entry.user.id], "video")}
                      >
                        <IconVideo size={15} />
                      </button>
                    </div>
                  </li>
                );
              })}
              {directory.length === 0 && <li className="calls-empty">Invite a teammate to start calling.</li>}
            </ul>
          </section>
        </aside>
      </div>

      {pickerOpen && (
        <NewCallModal
          members={members.filter((row) => !row.member.deactivatedAt && String(row.user.id) !== String(myUserId))}
          presence={presence}
          busy={busy}
          onClose={() => setPickerOpen(false)}
          onSubmit={submitNewCall}
        />
      )}

      {activeCall && myUserId != null && (
        <CallOverlay
          workspaceId={workspaceId!}
          call={activeCall}
          myUserId={myUserId}
          nameById={nameById}
          avatarById={avatarById}
          onLeave={() => void leave()}
          onEnd={() => void endForAll()}
        />
      )}
    </AppShell>
  );
}

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