"use client";

import { useCallback, useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import type { Call } from "@slackwsh/contracts";
import { api } from "../lib/api";
import { avatarColor, initials } from "../lib/avatar";
import { startIncomingRingtone, stopIncomingRingtone } from "../lib/call-ringtone";
import { IconPhone, IconPhoneOff, IconVideo } from "./icons";

/**
 * App-wide ringer.
 *
 * Mounted by AppShell rather than by the calls page, because being rung is not
 * something that only happens while you are looking at your call history —
 * every workspace screen has to be able to interrupt you — including /channel
 * and /workspace, which do not use AppShell.
 *
 * Answering navigates to /calls with the call id, which is where the media
 * surface lives. That keeps exactly one place in the app capable of holding a
 * live WebRTC session, so a stray second one can't fight it for the microphone.
 */
export function IncomingCallWatcher({
  workspaceId,
  myUserId,
  nameById,
}: {
  workspaceId: string;
  myUserId: number | null;
  nameById: Map<string, string>;
}) {
  const router = useRouter();
  const [ringing, setRinging] = useState<Call[]>([]);
  const [busyId, setBusyId] = useState<number | null>(null);

  const isRingingMe = useCallback(
    (call: Call) =>
      (call.status === "ringing" || call.status === "active") &&
      call.participants.some((p) => Number(p.userId) === myUserId && p.state === "ringing"),
    [myUserId],
  );

  const refresh = useCallback(() => {
    if (!workspaceId || myUserId == null) return;
    api
      .listCalls(workspaceId, { active: true })
      .then((res) => setRinging(res.calls.filter(isRingingMe)))
      .catch(() => undefined);
  }, [workspaceId, myUserId, isRingingMe]);

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

  useEffect(() => {
    if (!workspaceId || myUserId == null) return;
    const timer = window.setInterval(() => {
      void refresh();
    }, 5_000);
    const onFocus = () => {
      void refresh();
    };
    const onVisibility = () => {
      if (document.visibilityState === "visible") void refresh();
    };
    window.addEventListener("focus", onFocus);
    document.addEventListener("visibilitychange", onVisibility);
    return () => {
      window.clearInterval(timer);
      window.removeEventListener("focus", onFocus);
      document.removeEventListener("visibilitychange", onVisibility);
    };
  }, [workspaceId, myUserId, refresh]);

  // Merge from the event rather than refetching: the payload carries the whole
  // call, and a refetch would race the fanout that triggered it.
  useEffect(() => {
    function onCallEvent(e: Event) {
      const detail = (e as CustomEvent).detail as { type?: string; call?: Call } | undefined;
      const call = detail?.call;
      if (!call || String(call.workspaceId) !== String(workspaceId)) return;
      setRinging((current) => {
        const rest = current.filter((c) => Number(c.id) !== Number(call.id));
        return isRingingMe(call) ? [...rest, call] : rest;
      });
    }
    window.addEventListener("slackwsh:call", onCallEvent);
    return () => window.removeEventListener("slackwsh:call", onCallEvent);
  }, [workspaceId, isRingingMe]);

  // Loop ringtone while any call is ringing me; stop when the stack clears.
  useEffect(() => {
    if (ringing.length > 0) startIncomingRingtone();
    else stopIncomingRingtone();
    return () => stopIncomingRingtone();
  }, [ringing]);

  async function accept(call: Call) {
    setBusyId(Number(call.id));
    try {
      await api.joinCall(workspaceId, String(call.id));
      setRinging((current) => current.filter((c) => Number(c.id) !== Number(call.id)));
      router.push(`/calls?workspaceId=${workspaceId}&callId=${call.id}`);
    } catch {
      // Usually "this call has ended" — the caller gave up while it rang.
      refresh();
    } finally {
      setBusyId(null);
    }
  }

  async function decline(call: Call) {
    setBusyId(Number(call.id));
    setRinging((current) => current.filter((c) => Number(c.id) !== Number(call.id)));
    try {
      await api.declineCall(workspaceId, String(call.id));
    } catch {
      refresh();
    } finally {
      setBusyId(null);
    }
  }

  if (myUserId == null || ringing.length === 0) return null;

  return (
    <div className="ring-stack" role="region" aria-label="Incoming calls">
      {ringing.map((call) => {
        const fromId = String(call.startedBy);
        const fromName = nameById.get(fromId) ?? "Someone";
        const color = avatarColor(fromId);
        const others = call.participants.length - 2;
        const busy = busyId === Number(call.id);
        return (
          <div className="ring-card" key={call.id} role="alertdialog" aria-label={`${fromName} is calling`}>
            <span className="ring-avatar" style={{ background: color.bg, color: color.fg }}>
              {initials(fromName)}
            </span>
            <div className="ring-body">
              <strong className="ring-who">{fromName}</strong>
              <span className="ring-what">
                {call.kind === "video" ? "Incoming video call" : "Incoming call"}
                {others > 0 && ` · +${others} other${others > 1 ? "s" : ""}`}
              </span>
            </div>
            <div className="ring-actions">
              <button
                className="ring-btn decline"
                type="button"
                onClick={() => void decline(call)}
                disabled={busy}
                aria-label={`Decline call from ${fromName}`}
              >
                <IconPhoneOff size={15} />
              </button>
              <button
                className="ring-btn accept"
                type="button"
                onClick={() => void accept(call)}
                disabled={busy}
                aria-label={`Answer call from ${fromName}`}
              >
                {call.kind === "video" ? <IconVideo size={15} /> : <IconPhone size={15} />}
              </button>
            </div>
          </div>
        );
      })}
    </div>
  );
}
