import { useEffect, useRef } from 'react';
import type { LocalTask } from '../api';
import {
  NotificationDebouncer,
  buildNotificationOptions,
  createNotification,
  mapStatusToEvent,
  shouldNotify,
  type NotifyEventSettings,
} from '../lib/notifications';

interface UseTaskNotificationsArgs {
  /** useLocalTaskList の data。undefined / null の間は何もしない。 */
  tasks: LocalTask[] | undefined;
  /** ログイン中ユーザーの ID。null の時は何もしない。 */
  currentUserId: string | null;
  /** マスター ON/OFF。false なら何もしない。 */
  enabled: boolean;
  /** 各イベント種別の ON/OFF。 */
  events: NotifyEventSettings;
  /** 通知クリック時のコールバック (taskId を受け取る)。 */
  onNotificationClick: (taskId: number) => void;
  /** デバウンス長 (ms)。テストやチューニング用。デフォルト 4000。 */
  debounceMs?: number;
}

/**
 * 自分が owner のタスクの状態遷移を検知してブラウザ通知を表示する hook。
 * 初回マウント時は snapshot のみ取得し、通知は発火しない。
 */
export function useTaskNotifications({
  tasks,
  currentUserId,
  enabled,
  events,
  onNotificationClick,
  debounceMs = 4000,
}: UseTaskNotificationsArgs): void {
  // 各タスクの前回観測 status。初回マウント時の snapshot 用に first-pass フラグを別管理。
  const prevStatusMap = useRef<Map<number, string>>(new Map());
  const initialized = useRef(false);
  const activeNotifications = useRef<Set<Notification>>(new Set());

  // onNotificationClick が再生成されても debouncer を作り直さないよう ref に逃がす。
  const onClickRef = useRef(onNotificationClick);
  useEffect(() => {
    onClickRef.current = onNotificationClick;
  }, [onNotificationClick]);

  // tasks / events も ref に逃がす (debouncer 内部から最新を読むため)。
  const latestTasksRef = useRef<LocalTask[] | undefined>(tasks);
  const eventsRef = useRef<NotifyEventSettings>(events);
  useEffect(() => {
    latestTasksRef.current = tasks;
  }, [tasks]);
  useEffect(() => {
    eventsRef.current = events;
  }, [events]);

  // debouncer は hook の生存期間中 1 つだけ持つ。
  const debouncerRef = useRef<NotificationDebouncer | null>(null);
  if (debouncerRef.current === null) {
    debouncerRef.current = new NotificationDebouncer(debounceMs, (taskId, status) => {
      // デバウンス満了時の最終評価。tasks / settings はクロージャに乗らないので
      // ref から最新を読む。デバウンス中に status が非通知状態に戻ったり、
      // ユーザーが設定を OFF にしている可能性があるため再チェックする。
      const task = latestTasksRef.current?.find(t => t.id === taskId);
      if (!task) return;
      const event = mapStatusToEvent(status);
      if (event === null) return;
      const settings = eventsRef.current;
      if (!settings[event]) return;
      const opts = buildNotificationOptions(task, event);
      const n = createNotification(opts, tid => onClickRef.current(tid));
      if (n) activeNotifications.current.add(n);
    });
  }

  // 主処理: tasks 更新ごとに diff を取って debouncer に投入。
  useEffect(() => {
    if (!enabled || !currentUserId || !tasks) return;

    const debouncer = debouncerRef.current!;
    const map = prevStatusMap.current;
    const seen = new Set<number>();

    for (const task of tasks) {
      if (task.ownerId !== currentUserId) continue;
      const currStatus = task.latestJob?.status ?? '';
      seen.add(task.id);

      if (!initialized.current) {
        // 初回マウント: snapshot のみ、通知しない
        map.set(task.id, currStatus);
        continue;
      }

      const prev = map.get(task.id);
      const event = shouldNotify(prev, currStatus, events);
      // prevStatusMap は遷移を検知したかどうかに関わらず常に最新化する。
      map.set(task.id, currStatus);
      if (event !== null) {
        debouncer.schedule(task.id, currStatus);
      }
    }

    // 消えたタスクの prev エントリは掃除 (メモリリーク防止)
    for (const id of map.keys()) {
      if (!seen.has(id)) {
        map.delete(id);
        debouncer.cancel(id);
      }
    }

    initialized.current = true;
  }, [tasks, enabled, currentUserId, events]);

  // unmount 時: 全 timer / 全 notification をクリーンアップ
  useEffect(() => {
    return () => {
      debouncerRef.current?.clearAll();
      activeNotifications.current.forEach(n => n.close());
      activeNotifications.current.clear();
    };
  }, []);

  // enabled が false に切り替わった時もデバウンス中をキャンセル
  useEffect(() => {
    if (!enabled) debouncerRef.current?.clearAll();
  }, [enabled]);

  // V2 dedup ACK: when the SW broadcasts a push, ACK if this page is
  // currently visible — that suppresses the SW's OS notification because
  // V1 (this hook) will show it (or already has via the polling diff).
  // Hidden tabs do NOT ACK, so the SW falls through to OS notification.
  useEffect(() => {
    if (!('serviceWorker' in navigator)) return;
    const handler = (e: MessageEvent) => {
      const data = e.data;
      if (!data || typeof data !== 'object') return;
      if (data.type === 'notify-request' && typeof data.tag === 'string') {
        if (document.visibilityState === 'visible' && enabled) {
          // Tell the SW we've got it. Use the same client postMessage flow
          // SW listeners pick up.
          navigator.serviceWorker.controller?.postMessage({
            type: 'notification-handled',
            tag: data.tag,
          });
        }
      }
    };
    navigator.serviceWorker.addEventListener('message', handler);
    return () => navigator.serviceWorker.removeEventListener('message', handler);
  }, [enabled]);
}
