import { type ConversationSummary, listConversations } from '@chat-app/shared/chat';
import {
  createContext,
  type ReactNode,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';

import { playNotificationTone } from '../lib/notificationSound';
import { notify } from '../lib/osNotify';
import { supabase } from '../lib/supabase';
import { useAuth } from './AuthContext';

const LAST_READ_STORAGE_KEY = 'chatapp.conv_last_read';
const EPOCH = new Date(0).toISOString();

type LastReadMap = Record<string, string>;

function loadLastReadMap(): LastReadMap {
  try {
    const raw = window.localStorage.getItem(LAST_READ_STORAGE_KEY);
    if (!raw) return {};
    const parsed = JSON.parse(raw) as unknown;
    return parsed && typeof parsed === 'object' ? (parsed as LastReadMap) : {};
  } catch {
    return {};
  }
}

function saveLastReadMap(map: LastReadMap): void {
  try {
    window.localStorage.setItem(LAST_READ_STORAGE_KEY, JSON.stringify(map));
  } catch {
    /* quota / private mode */
  }
}

interface ConversationsContextValue {
  conversations: ConversationSummary[];
  loading: boolean;
  error: string | null;
  unread: Record<string, number>;
  totalUnread: number;
  refresh: () => Promise<void>;
  markRead: (conversationId: string) => void;
  setActiveConversation: (conversationId: string | null) => void;
}

const ConversationsContext = createContext<ConversationsContextValue | null>(null);

export function ConversationsProvider({ children }: { children: ReactNode }) {
  const { session, profile } = useAuth();
  const myId = session?.user.id;

  const [conversations, setConversations] = useState<ConversationSummary[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [unread, setUnread] = useState<Record<string, number>>({});

  const lastReadRef = useRef<LastReadMap>(loadLastReadMap());
  const activeConvIdRef = useRef<string | null>(null);
  const presenceRef = useRef(profile?.presenceState ?? 'offline');
  const conversationsRef = useRef<ConversationSummary[]>([]);
  conversationsRef.current = conversations;

  useEffect(() => {
    presenceRef.current = profile?.presenceState ?? 'offline';
  }, [profile?.presenceState]);

  const computeUnreadForConv = useCallback(
    async (convId: string): Promise<number> => {
      if (!myId) return 0;
      const since = lastReadRef.current[convId] ?? EPOCH;
      const { count, error: cErr } = await supabase
        .from('messages')
        .select('id', { count: 'exact', head: true })
        .eq('conversation_id', convId)
        .gt('created_at', since)
        .neq('sender_id', myId);
      if (cErr) return 0;
      return count ?? 0;
    },
    [myId],
  );

  const refresh = useCallback(async () => {
    if (!myId) {
      setConversations([]);
      setUnread({});
      setLoading(false);
      return;
    }
    try {
      setLoading(true);
      const convs = await listConversations(supabase);
      setConversations(convs);
      const entries = await Promise.all(
        convs.map(async (c) => [c.id, await computeUnreadForConv(c.id)] as const),
      );
      const next: Record<string, number> = {};
      for (const [id, count] of entries) {
        // Active conversation is always read.
        next[id] = id === activeConvIdRef.current ? 0 : count;
      }
      setUnread(next);
      setError(null);
    } catch (err: unknown) {
      setError(err instanceof Error ? err.message : 'failed to load conversations');
    } finally {
      setLoading(false);
    }
  }, [myId, computeUnreadForConv]);

  const markRead = useCallback((convId: string) => {
    const now = new Date().toISOString();
    lastReadRef.current[convId] = now;
    saveLastReadMap(lastReadRef.current);
    setUnread((prev) => (prev[convId] ? { ...prev, [convId]: 0 } : prev));
  }, []);

  const setActiveConversation = useCallback(
    (convId: string | null) => {
      activeConvIdRef.current = convId;
      if (convId) markRead(convId);
    },
    [markRead],
  );

  useEffect(() => {
    if (!myId) {
      setConversations([]);
      setUnread({});
      setLoading(false);
      return;
    }
    void refresh();

    const channel = supabase
      .channel('conv-ctx:' + myId)
      .on(
        'postgres_changes',
        { event: '*', schema: 'public', table: 'conversation_members' },
        () => {
          void refresh();
        },
      )
      .on('postgres_changes', { event: '*', schema: 'public', table: 'conversations' }, () => {
        void refresh();
      })
      .on(
        'postgres_changes',
        { event: 'INSERT', schema: 'public', table: 'messages' },
        (payload: { new: Record<string, unknown> }) => {
          const row = payload.new as unknown as { conversation_id: string; sender_id: string };
          const fromSelf = row.sender_id === myId;
          const active = row.conversation_id === activeConvIdRef.current;

          if (!fromSelf) {
            if (active) {
              // Viewing this conversation — implicit read.
              markRead(row.conversation_id);
            } else {
              setUnread((prev) => ({
                ...prev,
                [row.conversation_id]: (prev[row.conversation_id] ?? 0) + 1,
              }));
              // Notification sound + OS notification — respect DND. Body stays
              // empty because message content is E2E-encrypted and only
              // decryptable in the conversation view (not at this hook level).
              if (presenceRef.current !== 'dnd') {
                playNotificationTone();
                const conv = conversationsRef.current.find(
                  (c) => c.id === row.conversation_id,
                );
                const sender = conv?.members.find(
                  (m) => m.userId === row.sender_id,
                )?.profile;
                const senderName = sender?.displayName ?? '…';
                const title =
                  conv?.type === 'group'
                    ? (conv.name ?? 'Neue Nachricht') + ' · ' + senderName
                    : senderName;
                void notify({ title, body: 'Neue Nachricht' });
              }
            }
          }

          // Re-pull conversations to update lastMessageAt ordering.
          void refresh();
        },
      )
      .subscribe();

    return () => {
      void supabase.removeChannel(channel);
    };
  }, [myId, refresh, markRead]);

  const totalUnread = useMemo(() => {
    let s = 0;
    for (const v of Object.values(unread)) s += v;
    return s;
  }, [unread]);

  const value = useMemo<ConversationsContextValue>(
    () => ({
      conversations,
      loading,
      error,
      unread,
      totalUnread,
      refresh,
      markRead,
      setActiveConversation,
    }),
    [
      conversations,
      loading,
      error,
      unread,
      totalUnread,
      refresh,
      markRead,
      setActiveConversation,
    ],
  );

  return <ConversationsContext.Provider value={value}>{children}</ConversationsContext.Provider>;
}

export function useConversationsContext(): ConversationsContextValue {
  const ctx = useContext(ConversationsContext);
  if (!ctx) throw new Error('useConversationsContext must be used inside <ConversationsProvider>');
  return ctx;
}
