"use client";

import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { createClient, getBrowserUser } from "@/lib/supabase/client";
import { containsBlacklistedWord } from "@/lib/content-filter";
import { useWorkspaceStore } from "@/stores/workspace-store";
import type {
  Attachment,
  ChatContact,
  ChatContactRequest,
  ChatDirectoryProfile,
  ChatMessage,
  ChatMessageLink,
  ChatThread,
  ChatThreadRole,
  ChatThreadMember,
  Profile,
} from "@/types";

function defaultNoteContent(text: string) {
  return {
    type: "doc",
    content: [
      {
        type: "paragraph",
        content: text
          ? [{ type: "text", text }]
          : [{ type: "text", text: "Imported from chat." }],
      },
    ],
  };
}

function buildMessageTitle(body: string, fallback: string) {
  const firstLine = body
    .split("\n")
    .map((line) => line.trim())
    .find(Boolean);

  if (!firstLine) return fallback;
  return firstLine.length > 72 ? `${firstLine.slice(0, 69)}...` : firstLine;
}

function buildAttachmentBody(body: string, files: File[]) {
  const trimmed = body.trim();
  if (trimmed) return trimmed;

  if (files.length === 1) {
    return `Shared file: ${files[0].name}`;
  }

  return `Shared ${files.length} files`;
}

function getActionErrorMessage(error: unknown, fallback: string) {
  if (error instanceof Error && error.message) return error.message;

  if (error && typeof error === "object") {
    const message =
      "message" in error && typeof error.message === "string"
        ? error.message
        : null;
    const details =
      "details" in error && typeof error.details === "string"
        ? error.details
        : null;
    const hint =
      "hint" in error && typeof error.hint === "string"
        ? error.hint
        : null;

    return [message, details, hint].filter(Boolean).join(" · ") || fallback;
  }

  return fallback;
}

function canManageTargetRole(
  actorRole: ChatThreadRole | null | undefined,
  targetRole: ChatThreadRole | null | undefined,
  nextRole?: ChatThreadRole
) {
  if (!actorRole || !targetRole) return false;
  if (actorRole === "owner") return true;
  if (actorRole !== "admin") return false;
  if (targetRole === "owner") return false;
  if (nextRole === "owner") return false;
  return true;
}

function canRemoveTargetMember(
  actorRole: ChatThreadRole | null | undefined,
  targetRole: ChatThreadRole | null | undefined
) {
  if (!actorRole || !targetRole) return false;
  if (actorRole === "owner") return true;
  if (actorRole !== "admin") return false;
  return targetRole !== "owner";
}

export interface ChatComposerInput {
  body: string;
  parentMessageId?: string | null;
}

export interface SendContactRequestResult {
  status: "sent" | "pending" | "already_connected" | "auto_accepted";
}

function mapChatMessages(
  rawMessages: ChatMessage[],
  attachments: Attachment[] = []
) {
  const attachmentsByMessageId = new Map<string, Attachment[]>();

  attachments.forEach((attachment) => {
    const list = attachmentsByMessageId.get(attachment.entity_id) || [];
    list.push(attachment);
    attachmentsByMessageId.set(attachment.entity_id, list);
  });

  return rawMessages.map((message) => ({
    ...message,
    reactions: message.reactions || [],
    links: (message.links || []) as ChatMessageLink[],
    attachments: attachmentsByMessageId.get(message.id) || [],
  }));
}

export function useChat() {
  const supabase = createClient();
  const activeTeamId = useWorkspaceStore((state) => state.activeTeamId);
  const loadedThreadIdsRef = useRef(new Set<string>());
  const realtimeRefreshTimeoutRef = useRef<number | null>(null);

  const [loading, setLoading] = useState(true);
  const [user, setUser] = useState<Profile | null>(null);
  const [contacts, setContacts] = useState<ChatContact[]>([]);
  const [incomingRequests, setIncomingRequests] = useState<ChatContactRequest[]>([]);
  const [outgoingRequests, setOutgoingRequests] = useState<ChatContactRequest[]>([]);
  const [threads, setThreads] = useState<ChatThread[]>([]);
  const [messagesByThread, setMessagesByThread] = useState<Record<string, ChatMessage[]>>({});
  const [directoryResults, setDirectoryResults] = useState<ChatDirectoryProfile[]>([]);

  const getThreadParticipantContext = useCallback(
    (threadId: string, actorUserId: string, targetUserId: string) => {
      const thread = threads.find((entry) => entry.id === threadId) || null;
      const actor =
        thread?.participants?.find((participant) => participant.user_id === actorUserId) || null;
      const target =
        thread?.participants?.find((participant) => participant.user_id === targetUserId) || null;

      return { actor, target };
    },
    [threads]
  );

  const fetchProfilesByIds = useCallback(
    async (profileIds: string[]) => {
      const uniqueProfileIds = Array.from(new Set(profileIds.filter(Boolean)));

      if (uniqueProfileIds.length === 0) {
        return new Map<string, Profile>();
      }

      const rpcRes = await supabase.rpc("get_accessible_profiles", {
        profile_ids: uniqueProfileIds,
      });

      const shouldFallback =
        !!rpcRes.error &&
        getActionErrorMessage(rpcRes.error, "").match(
          /(get_accessible_profiles|Could not find the function|schema cache|does not exist)/i
        );

      const directRes =
        shouldFallback
          ? await supabase
              .from("profiles")
              .select("id, email, full_name, avatar_url, username, created_at, updated_at")
              .in("id", uniqueProfileIds)
          : null;

      const data = (rpcRes.data || directRes?.data || []) as Profile[];
      const error = rpcRes.error && !shouldFallback ? rpcRes.error : directRes?.error || null;

      if (error) {
        console.error("Failed to load chat profiles", error);
        return new Map<string, Profile>();
      }

      return new Map(
        data.map((profile) => [profile.id, profile])
      );
    },
    [supabase]
  );

  useEffect(() => {
    void getBrowserUser()
      .then((currentUser) => {
        if (!currentUser) {
          setUser(null);
          return;
        }

        setUser({
          id: currentUser.id,
          email: currentUser.email || "",
          full_name: currentUser.user_metadata?.full_name || "",
          avatar_url: currentUser.user_metadata?.avatar_url || null,
          username: currentUser.user_metadata?.username || null,
          created_at: new Date().toISOString(),
          updated_at: new Date().toISOString(),
        });
      })
      .catch(() => setUser(null));
  }, []);

  const requireCurrentUser = useCallback(async () => {
    const currentUser = await getBrowserUser({ force: true });
    if (!currentUser) throw new Error("Not authenticated");

    return {
      id: currentUser.id,
      email: currentUser.email || "",
      full_name: currentUser.user_metadata?.full_name || "",
      avatar_url: currentUser.user_metadata?.avatar_url || null,
      username: currentUser.user_metadata?.username || null,
      created_at: new Date().toISOString(),
      updated_at: new Date().toISOString(),
    } satisfies Profile;
  }, []);

  const fetchChatData = useCallback(async (options?: { silent?: boolean }) => {
    const currentUser = await requireCurrentUser();
    const shouldShowLoading = !(options?.silent ?? false);

    if (shouldShowLoading) {
      setLoading(true);
    }

    try {
      const [contactsRes, requestsRes, myMembershipsRes] = await Promise.all([
        supabase.from("chat_contacts").select("*").eq("user_id", currentUser.id),
        supabase
          .from("chat_contact_requests")
          .select("*")
          .or(`sender_id.eq.${currentUser.id},recipient_id.eq.${currentUser.id}`)
          .order("created_at", { ascending: false }),
        supabase
          .from("chat_thread_members")
          .select("*")
          .eq("user_id", currentUser.id)
          .order("joined_at", { ascending: false }),
      ]);

      if (contactsRes.error) throw contactsRes.error;
      if (requestsRes.error) throw requestsRes.error;
      if (myMembershipsRes.error) throw myMembershipsRes.error;

      const rawContacts = (contactsRes.data || []) as ChatContact[];
      const rawRequests = (requestsRes.data || []) as ChatContactRequest[];
      const memberships = (myMembershipsRes.data || []) as ChatThreadMember[];
      const threadIds = memberships.map((membership) => membership.thread_id);

      const [threadsRes, participantsRes, lastMessagesRes] = await Promise.all([
        threadIds.length > 0
          ? supabase
              .from("chat_threads")
              .select("*")
              .in("id", threadIds)
              .order("last_message_at", { ascending: false, nullsFirst: false })
          : Promise.resolve({ data: [], error: null }),
        threadIds.length > 0
          ? supabase.from("chat_thread_members").select("*").in("thread_id", threadIds)
          : Promise.resolve({ data: [], error: null }),
        threadIds.length > 0
          ? supabase
              .from("chat_messages")
              .select("*")
              .in("thread_id", threadIds)
              .order("created_at", { ascending: false })
              .limit(Math.max(threadIds.length * 6, 30))
          : Promise.resolve({ data: [], error: null }),
      ]);

      if (threadsRes.error) throw threadsRes.error;
      if (participantsRes.error) throw participantsRes.error;
      if (lastMessagesRes.error) throw lastMessagesRes.error;

      const rawThreads = (threadsRes.data || []) as ChatThread[];
      const rawParticipants = (participantsRes.data || []) as ChatThreadMember[];
      const rawLastMessages = (lastMessagesRes.data || []) as ChatMessage[];

      const profileIds = Array.from(
        new Set([
          currentUser.id,
          ...rawContacts.map((item) => item.contact_user_id),
          ...rawRequests.map((item) => item.sender_id),
          ...rawRequests.map((item) => item.recipient_id),
          ...rawParticipants.map((item) => item.user_id),
          ...rawLastMessages.map((item) => item.sender_id),
        ])
      );

      const profileMap = await fetchProfilesByIds(profileIds);

      const nextContacts = rawContacts.map((item) => ({
        ...item,
        contact: profileMap.get(item.contact_user_id) || null,
      }));

      const nextIncoming = rawRequests
        .filter((item) => item.recipient_id === currentUser.id && item.status === "pending")
        .map((item) => ({
          ...item,
          sender: profileMap.get(item.sender_id) || null,
          recipient: profileMap.get(item.recipient_id) || null,
        }));

      const nextOutgoing = rawRequests
        .filter((item) => item.sender_id === currentUser.id && item.status === "pending")
        .map((item) => ({
          ...item,
          sender: profileMap.get(item.sender_id) || null,
          recipient: profileMap.get(item.recipient_id) || null,
        }));

      const membershipMap = new Map(memberships.map((item) => [item.thread_id, item]));
      const participantsByThread = new Map<string, ChatThreadMember[]>();
      rawParticipants.forEach((participant) => {
        const list = participantsByThread.get(participant.thread_id) || [];
        list.push({
          ...participant,
          user: profileMap.get(participant.user_id) || null,
        });
        participantsByThread.set(participant.thread_id, list);
      });

      const lastMessageByThread = new Map<string, ChatMessage>();
      rawLastMessages.forEach((message) => {
        if (lastMessageByThread.has(message.thread_id)) return;
        lastMessageByThread.set(message.thread_id, {
          ...message,
          sender: profileMap.get(message.sender_id) || null,
        });
      });

      const nextThreads = rawThreads.map((thread) => {
        const membership = membershipMap.get(thread.id);
        const lastMessage = lastMessageByThread.get(thread.id) || null;
        const participants = participantsByThread.get(thread.id) || [];
        const unread =
          !!thread.last_message_at &&
          !!membership &&
          (!!lastMessage && lastMessage.sender_id !== currentUser.id) &&
          (!membership.last_read_at ||
            new Date(thread.last_message_at).getTime() >
              new Date(membership.last_read_at).getTime());

        return {
          ...thread,
          participants,
          last_message: lastMessage,
          unread,
        };
      });

      nextThreads.sort((a, b) => {
        const aTime = a.last_message_at || a.updated_at;
        const bTime = b.last_message_at || b.updated_at;
        return new Date(bTime).getTime() - new Date(aTime).getTime();
      });

      setUser(profileMap.get(currentUser.id) || currentUser);
      setContacts(nextContacts);
      setIncomingRequests(nextIncoming);
      setOutgoingRequests(nextOutgoing);
      setThreads(nextThreads);
    } finally {
      if (shouldShowLoading) {
        setLoading(false);
      }
    }
  }, [fetchProfilesByIds, requireCurrentUser, supabase]);

  useEffect(() => {
    const timer = window.setTimeout(() => {
      void fetchChatData().catch(() => setLoading(false));
    }, 0);

    return () => window.clearTimeout(timer);
  }, [fetchChatData]);

  const activeTeamThreads = useMemo(
    () =>
      threads.filter(
        (thread) => thread.team_id === activeTeamId && thread.thread_type === "channel"
      ),
    [activeTeamId, threads]
  );

  const fetchThreadMessages = useCallback(async (threadId: string) => {
    const baseRes = await supabase
      .from("chat_messages")
      .select("*")
      .eq("thread_id", threadId)
      .order("created_at", { ascending: true });

    if (baseRes.error) throw baseRes.error;

    const baseMessages = (baseRes.data || []) as ChatMessage[];
    const messageIds = baseMessages.map((message) => message.id);
    const senderIds = baseMessages.map((message) => message.sender_id);

    const [profileMap, reactionsRes, linksRes, attachmentsRes] = await Promise.all([
      fetchProfilesByIds(senderIds),
      messageIds.length > 0
        ? supabase.from("chat_message_reactions").select("*").in("message_id", messageIds)
        : Promise.resolve({ data: [], error: null }),
      messageIds.length > 0
        ? supabase.from("chat_message_links").select("*").in("message_id", messageIds)
        : Promise.resolve({ data: [], error: null }),
      messageIds.length > 0
        ? supabase
            .from("attachments")
            .select("*")
            .eq("entity_type", "chat_message")
            .in("entity_id", messageIds)
            .order("created_at", { ascending: true })
        : Promise.resolve({ data: [], error: null }),
    ]);

    if (reactionsRes.error) {
      console.error("Failed to load chat reactions", reactionsRes.error);
    }
    if (linksRes.error) {
      console.error("Failed to load chat links", linksRes.error);
    }

    if (attachmentsRes.error) throw attachmentsRes.error;

    const reactionsByMessageId = new Map<string, ChatMessage["reactions"]>();
    ((reactionsRes.data || []) as NonNullable<ChatMessage["reactions"]>).forEach((reaction) => {
      const list = reactionsByMessageId.get(reaction.message_id) || [];
      list.push(reaction);
      reactionsByMessageId.set(reaction.message_id, list);
    });

    const linksByMessageId = new Map<string, ChatMessageLink[]>();
    ((linksRes.data || []) as ChatMessageLink[]).forEach((link) => {
      const list = linksByMessageId.get(link.message_id) || [];
      list.push(link);
      linksByMessageId.set(link.message_id, list);
    });

    const rawMessages = baseMessages.map((message) => ({
      ...message,
      sender: profileMap.get(message.sender_id) || null,
      reactions: reactionsByMessageId.get(message.id) || [],
      links: linksByMessageId.get(message.id) || [],
    }));

    const messages = mapChatMessages(rawMessages, (attachmentsRes.data || []) as Attachment[]);

    loadedThreadIdsRef.current.add(threadId);
    setMessagesByThread((prev) => ({ ...prev, [threadId]: messages }));
    return messages;
  }, [fetchProfilesByIds, supabase]);

  const markThreadRead = useCallback(async (threadId: string) => {
    const currentUser = await requireCurrentUser();
    const nowIso = new Date().toISOString();
    const { error } = await supabase
      .from("chat_thread_members")
      .update({ last_read_at: nowIso })
      .eq("thread_id", threadId)
      .eq("user_id", currentUser.id);

    if (error) throw error;

    setThreads((prev) =>
      prev.map((thread) =>
        thread.id === threadId
          ? {
              ...thread,
              unread: false,
              participants: (thread.participants || []).map((participant) =>
                participant.user_id === currentUser.id
                  ? { ...participant, last_read_at: nowIso }
                  : participant
              ),
            }
          : thread
      )
    );
  }, [requireCurrentUser, supabase]);

  const searchDirectory = useCallback(async (query: string) => {
    const trimmed = query.trim();

    if (trimmed.length < 2) {
      setDirectoryResults([]);
      return [];
    }

    const { data, error } = await supabase.rpc("search_chat_profiles", {
      search_term: trimmed,
    });

    if (error) throw error;

    const nextResults = (data || []) as ChatDirectoryProfile[];
    setDirectoryResults(nextResults);
    return nextResults;
  }, [supabase]);

  const scheduleRealtimeRefresh = useCallback(
    (options?: { refreshThreads?: boolean; refreshLoadedMessages?: boolean }) => {
      if (realtimeRefreshTimeoutRef.current) {
        window.clearTimeout(realtimeRefreshTimeoutRef.current);
      }

      realtimeRefreshTimeoutRef.current = window.setTimeout(() => {
        realtimeRefreshTimeoutRef.current = null;

        void (async () => {
          if (options?.refreshThreads ?? true) {
            await fetchChatData({ silent: true });
          }

          if (options?.refreshLoadedMessages) {
            const loadedThreadIds = Array.from(loadedThreadIdsRef.current);
            await Promise.all(
              loadedThreadIds.map((threadId) =>
                fetchThreadMessages(threadId).catch(() => undefined)
              )
            );
          }
        })().catch(() => undefined);
      }, 180);
    },
    [fetchChatData, fetchThreadMessages]
  );

  useEffect(() => {
    if (!user?.id) return;

    const refreshThreads = () => scheduleRealtimeRefresh({ refreshThreads: true });
    const refreshThreadsAndMessages = () =>
      scheduleRealtimeRefresh({
        refreshThreads: true,
        refreshLoadedMessages: true,
      });
    const refreshLoadedMessages = () =>
      scheduleRealtimeRefresh({
        refreshThreads: false,
        refreshLoadedMessages: true,
      });

    const channel = supabase
      .channel(`chat-workspace:${user.id}`)
      .on(
        "postgres_changes",
        {
          event: "*",
          schema: "public",
          table: "chat_contact_requests",
          filter: `sender_id=eq.${user.id}`,
        },
        refreshThreads
      )
      .on(
        "postgres_changes",
        {
          event: "*",
          schema: "public",
          table: "chat_contact_requests",
          filter: `recipient_id=eq.${user.id}`,
        },
        refreshThreads
      )
      .on(
        "postgres_changes",
        {
          event: "*",
          schema: "public",
          table: "chat_contacts",
          filter: `user_id=eq.${user.id}`,
        },
        refreshThreads
      )
      .on(
        "postgres_changes",
        {
          event: "*",
          schema: "public",
          table: "chat_thread_members",
          filter: `user_id=eq.${user.id}`,
        },
        refreshThreadsAndMessages
      )
      .on(
        "postgres_changes",
        {
          event: "*",
          schema: "public",
          table: "chat_threads",
        },
        refreshThreadsAndMessages
      )
      .on(
        "postgres_changes",
        {
          event: "*",
          schema: "public",
          table: "chat_messages",
        },
        refreshThreadsAndMessages
      )
      .on(
        "postgres_changes",
        {
          event: "*",
          schema: "public",
          table: "chat_message_reactions",
        },
        refreshLoadedMessages
      )
      .on(
        "postgres_changes",
        {
          event: "*",
          schema: "public",
          table: "chat_message_links",
        },
        refreshLoadedMessages
      )
      .subscribe();

    return () => {
      if (realtimeRefreshTimeoutRef.current) {
        window.clearTimeout(realtimeRefreshTimeoutRef.current);
        realtimeRefreshTimeoutRef.current = null;
      }

      void supabase.removeChannel(channel);
    };
  }, [scheduleRealtimeRefresh, supabase, user?.id]);

  useEffect(() => {
    return () => {
      if (realtimeRefreshTimeoutRef.current) {
        window.clearTimeout(realtimeRefreshTimeoutRef.current);
      }
    };
  }, []);

  const sendContactRequestFallback = useCallback(
    async (targetUserId: string, message = "") => {
      const currentUser = await requireCurrentUser();

      if (contacts.some((contact) => contact.contact_user_id === targetUserId)) {
        return { status: "already_connected" } satisfies SendContactRequestResult;
      }

      const incomingRequest = incomingRequests.find(
        (request) => request.sender_id === targetUserId && request.status === "pending"
      );

      if (incomingRequest) {
        const { error } = await supabase.rpc("respond_chat_contact_request", {
          request_uuid: incomingRequest.id,
          next_status: "accepted",
        });

        if (error) {
          throw new Error(
            getActionErrorMessage(error, "Kontaktanfrage konnte nicht beantwortet werden.")
          );
        }

        await fetchChatData();
        return { status: "auto_accepted" } satisfies SendContactRequestResult;
      }

      const outgoingRequest = outgoingRequests.find(
        (request) => request.recipient_id === targetUserId && request.status === "pending"
      );

      if (outgoingRequest) {
        return { status: "pending" } satisfies SendContactRequestResult;
      }

      const { error } = await supabase.from("chat_contact_requests").insert({
        sender_id: currentUser.id,
        recipient_id: targetUserId,
        message: message.trim(),
      });

      if (error) {
        if (error.code === "23505") {
          await fetchChatData();
          return { status: "pending" } satisfies SendContactRequestResult;
        }

        throw new Error(
          getActionErrorMessage(error, "Kontaktanfrage konnte nicht erstellt werden.")
        );
      }

      await fetchChatData();
      return { status: "sent" } satisfies SendContactRequestResult;
    },
    [
      contacts,
      fetchChatData,
      incomingRequests,
      outgoingRequests,
      requireCurrentUser,
      supabase,
    ]
  );

  async function sendContactRequest(targetUserId: string, message = "") {
    const currentUser = await requireCurrentUser();

    if (targetUserId === currentUser.id) {
      throw new Error("Du kannst dich nicht selbst hinzufügen.");
    }

    const { data, error } = await supabase.rpc("request_chat_contact", {
      target_user_id: targetUserId,
      initial_message: message.trim(),
    });

    if (error) {
      const rawMessage = getActionErrorMessage(error, "");
      const shouldFallback =
        rawMessage.includes("request_chat_contact") ||
        rawMessage.includes("Could not find the function") ||
        rawMessage.includes("schema cache") ||
        rawMessage.includes("does not exist");

      if (shouldFallback) {
        return sendContactRequestFallback(targetUserId, message);
      }

      throw new Error(
        getActionErrorMessage(error, "Kontaktanfrage konnte nicht erstellt werden.")
      );
    }

    await fetchChatData();
    return {
      status: (data as SendContactRequestResult["status"]) || "sent",
    } satisfies SendContactRequestResult;
  }

  const respondToContactRequest = useCallback(
    async (requestId: string, nextStatus: "accepted" | "declined" | "cancelled") => {
      const { error } = await supabase.rpc("respond_chat_contact_request", {
        request_uuid: requestId,
        next_status: nextStatus,
      });

      if (error) throw error;
      await fetchChatData();
    },
    [fetchChatData, supabase]
  );

  async function openDirectThread(targetUserId: string) {
    const { data, error } = await supabase.rpc("get_or_create_direct_chat", {
      other_user_id: targetUserId,
    });

    if (error) throw error;
    await fetchChatData();
    return data as string;
  }

  async function createGroupThread(title: string, memberIds: string[]) {
    const uniqueMemberIds = Array.from(new Set(memberIds.filter(Boolean)));
    if (uniqueMemberIds.length === 0) {
      throw new Error("Wähle mindestens eine Person für die Gruppe aus.");
    }

    const { data, error } = await supabase.rpc("create_group_chat", {
      group_title: title.trim(),
      member_ids: uniqueMemberIds,
    });

    if (error) throw error;
    await fetchChatData();
    return data as string;
  }

  async function createTeamChannel(input: {
    teamId?: string;
    title: string;
    description?: string;
    postingPermission?: "members" | "admins";
    announcementOnly?: boolean;
    isPrivate?: boolean;
    memberIds?: string[];
  }) {
    const resolvedTeamId = input.teamId || activeTeamId;
    if (!resolvedTeamId) throw new Error("Select a team first");

    const rpcRes = await supabase.rpc("create_team_channel_chat", {
      team_uuid: resolvedTeamId,
      channel_title: input.title.trim(),
      channel_description: input.description?.trim() || "",
      channel_posting_permission: input.postingPermission || "members",
      announcement_only: input.announcementOnly || false,
      channel_is_private: input.isPrivate || false,
      member_ids: Array.from(new Set((input.memberIds || []).filter(Boolean))),
    });

    const shouldFallback =
      !!rpcRes.error &&
      /(create_team_channel_chat|Could not find the function|schema cache|does not exist|channel_is_private|member_ids)/i.test(
        getActionErrorMessage(rpcRes.error, "")
      );

    const legacyRes =
      shouldFallback && !input.isPrivate && (!input.memberIds || input.memberIds.length === 0)
        ? await supabase.rpc("create_team_channel_chat", {
            team_uuid: resolvedTeamId,
            channel_title: input.title.trim(),
            channel_description: input.description?.trim() || "",
            channel_posting_permission: input.postingPermission || "members",
            announcement_only: input.announcementOnly || false,
          })
        : null;

    const data = (rpcRes.data ?? legacyRes?.data) as string | null;
    const error =
      rpcRes.error && !shouldFallback
        ? rpcRes.error
        : legacyRes?.error || (rpcRes.error && !legacyRes ? rpcRes.error : null);

    if (error) {
      throw new Error(getActionErrorMessage(error, "Channel creation failed."));
    }

    await fetchChatData();
    return data as string;
  }

  async function updateThread(
    threadId: string,
    input: {
      title: string;
      description?: string;
      postingPermission?: "members" | "admins";
      announcementOnly?: boolean;
      isPrivate?: boolean;
    }
  ) {
    const rpcRes = await supabase.rpc("update_chat_thread_settings", {
      thread_uuid: threadId,
      next_title: input.title.trim(),
      next_description: input.description?.trim() || "",
      next_posting_permission: input.postingPermission || "members",
      next_announcement_only:
        typeof input.announcementOnly === "boolean" ? input.announcementOnly : null,
      next_is_private: typeof input.isPrivate === "boolean" ? input.isPrivate : null,
    });

    const shouldFallback =
      !!rpcRes.error &&
      /(update_chat_thread_settings|Could not find the function|schema cache|does not exist)/i.test(
        getActionErrorMessage(rpcRes.error, "")
      );

    const directRes =
      shouldFallback
        ? await supabase
            .from("chat_threads")
            .update({
              title: input.title.trim(),
              description: input.description?.trim() || "",
              posting_permission: input.postingPermission || "members",
              is_announcement:
                typeof input.announcementOnly === "boolean" ? input.announcementOnly : false,
              ...(typeof input.isPrivate === "boolean"
                ? { is_private: input.isPrivate }
                : {}),
            })
            .eq("id", threadId)
            .select("*")
            .single()
        : null;

    const data = (rpcRes.data || directRes?.data) as ChatThread | null;
    const error =
      rpcRes.error && !shouldFallback ? rpcRes.error : directRes?.error || null;

    if (error) {
      throw new Error(getActionErrorMessage(error, "Chat thread update failed."));
    }

    await fetchChatData();
    await fetchThreadMessages(threadId).catch(() => undefined);
    return data as ChatThread;
  }

  async function deleteThread(threadId: string) {
    const { error } = await supabase.from("chat_threads").delete().eq("id", threadId);
    if (error) throw error;

    loadedThreadIdsRef.current.delete(threadId);
    setMessagesByThread((prev) => {
      const next = { ...prev };
      delete next[threadId];
      return next;
    });

    await fetchChatData();
  }

  async function addThreadMember(
    threadId: string,
    userId: string,
    role: "owner" | "admin" | "member" | "viewer" = "member"
  ) {
    const rpcRes = await supabase.rpc("add_chat_thread_member", {
      thread_uuid: threadId,
      member_uuid: userId,
      member_role: role,
    });

    const shouldFallback =
      !!rpcRes.error &&
      /(add_chat_thread_member|Could not find the function|schema cache|does not exist)/i.test(
        getActionErrorMessage(rpcRes.error, "")
      );

    const directRes =
      shouldFallback
        ? await supabase
            .from("chat_thread_members")
            .insert({
              thread_id: threadId,
              user_id: userId,
              role,
            })
            .select("*")
            .single()
        : null;

    const error =
      rpcRes.error && !shouldFallback ? rpcRes.error : directRes?.error || null;

    if (error) {
      throw new Error(getActionErrorMessage(error, "Member could not be added."));
    }
    await fetchChatData();
    await fetchThreadMessages(threadId).catch(() => undefined);
  }

  async function updateThreadMemberRole(
    threadId: string,
    userId: string,
    role: "owner" | "admin" | "member" | "viewer"
  ) {
    const currentUser = await requireCurrentUser();
    if (currentUser.id === userId) {
      throw new Error("Du kannst deinen eigenen Rang nicht ändern.");
    }

    const { actor, target } = getThreadParticipantContext(threadId, currentUser.id, userId);
    if (actor && target && !canManageTargetRole(actor.role, target.role, role)) {
      throw new Error("Admins können keine Owner-Rollen bearbeiten oder vergeben.");
    }

    const rpcRes = await supabase.rpc("update_chat_thread_member_role", {
      thread_uuid: threadId,
      member_uuid: userId,
      member_role: role,
    });

    const shouldFallback =
      !!rpcRes.error &&
      /(update_chat_thread_member_role|Could not find the function|schema cache|does not exist)/i.test(
        getActionErrorMessage(rpcRes.error, "")
      );

    const directRes =
      shouldFallback
        ? await supabase
            .from("chat_thread_members")
            .update({ role })
            .eq("thread_id", threadId)
            .eq("user_id", userId)
            .select("*")
            .single()
        : null;

    const error =
      rpcRes.error && !shouldFallback ? rpcRes.error : directRes?.error || null;

    if (error) {
      throw new Error(getActionErrorMessage(error, "Member role could not be updated."));
    }
    await fetchChatData();
    await fetchThreadMessages(threadId).catch(() => undefined);
  }

  async function removeThreadMember(threadId: string, userId: string) {
    const currentUser = await requireCurrentUser();
    const { actor, target } = getThreadParticipantContext(threadId, currentUser.id, userId);
    if (actor && target && !canRemoveTargetMember(actor.role, target.role)) {
      throw new Error("Admins können keinen Owner entfernen.");
    }

    const rpcRes = await supabase.rpc("remove_chat_thread_member", {
      thread_uuid: threadId,
      member_uuid: userId,
    });

    const shouldFallback =
      !!rpcRes.error &&
      /(remove_chat_thread_member|Could not find the function|schema cache|does not exist)/i.test(
        getActionErrorMessage(rpcRes.error, "")
      );

    const directRes =
      shouldFallback
        ? await supabase
            .from("chat_thread_members")
            .delete()
            .eq("thread_id", threadId)
            .eq("user_id", userId)
        : null;

    const error =
      rpcRes.error && !shouldFallback ? rpcRes.error : directRes?.error || null;

    if (error) {
      throw new Error(getActionErrorMessage(error, "Member could not be removed."));
    }
    await fetchChatData();
    await fetchThreadMessages(threadId).catch(() => undefined);
  }

  async function sendMessage(threadId: string, input: ChatComposerInput) {
    const currentUser = await requireCurrentUser();
    const body = input.body.trim();
    if (!body) throw new Error("Message is empty");
    if (containsBlacklistedWord(body)) {
      throw new Error("Deine Nachricht enthält unangemessene Wörter und wurde nicht gesendet.");
    }

    const { data, error } = await supabase
      .from("chat_messages")
      .insert({
        thread_id: threadId,
        sender_id: currentUser.id,
        body,
        parent_message_id: input.parentMessageId || null,
      })
      .select("*")
      .single();

    if (error) throw error;

    const message = {
      ...(data as ChatMessage),
      sender: currentUser,
      reactions: [],
      links: [],
      attachments: [],
    };
    setMessagesByThread((prev) => ({
      ...prev,
      [threadId]: [...(prev[threadId] || []), message],
    }));
    setThreads((prev) =>
      prev
        .map((thread) =>
          thread.id === threadId
            ? {
                ...thread,
                last_message_at: message.created_at,
                last_message: message,
                unread: false,
              }
            : thread
        )
        .sort((a, b) => {
          const aTime = a.last_message_at || a.updated_at;
          const bTime = b.last_message_at || b.updated_at;
          return new Date(bTime).getTime() - new Date(aTime).getTime();
        })
    );
    return message;
  }

  async function updateMessage(messageId: string, body: string) {
    const trimmed = body.trim();
    if (!trimmed) throw new Error("Message is empty");
    if (containsBlacklistedWord(trimmed)) {
      throw new Error("Deine Nachricht enthält unangemessene Wörter und wurde nicht gespeichert.");
    }

    const { data, error } = await supabase
      .from("chat_messages")
      .update({
        body: trimmed,
        edited_at: new Date().toISOString(),
      })
      .eq("id", messageId)
      .select("*")
      .single();

    if (error) throw error;

    const nextMessage = data as ChatMessage;
    setMessagesByThread((prev) => {
      const nextEntries = Object.entries(prev).map(([threadId, messages]) => [
        threadId,
        messages.map((message) =>
          message.id === messageId
            ? {
                ...nextMessage,
                sender: message.sender || null,
                reactions: nextMessage.reactions || message.reactions || [],
                links: (nextMessage.links as ChatMessageLink[] | undefined) || message.links || [],
                attachments: message.attachments || [],
              }
            : message
        ),
      ]);
      return Object.fromEntries(nextEntries);
    });
    return nextMessage;
  }

  async function deleteMessage(messageId: string) {
    const { data, error } = await supabase
      .from("chat_messages")
      .update({
        body: "This message was removed.",
        deleted_at: new Date().toISOString(),
      })
      .eq("id", messageId)
      .select("*")
      .single();

    if (error) throw error;

    const nextMessage = data as ChatMessage;
    setMessagesByThread((prev) => {
      const nextEntries = Object.entries(prev).map(([threadId, messages]) => [
        threadId,
        messages.map((message) =>
          message.id === messageId
            ? {
                ...nextMessage,
                sender: message.sender || null,
                reactions: nextMessage.reactions || message.reactions || [],
                links: (nextMessage.links as ChatMessageLink[] | undefined) || message.links || [],
                attachments: message.attachments || [],
              }
            : message
        ),
      ]);
      return Object.fromEntries(nextEntries);
    });
    return nextMessage;
  }

  async function sendMessageWithAttachments(
    thread: ChatThread,
    input: ChatComposerInput,
    files: FileList | File[]
  ) {
    const fileList = Array.from(files);

    if (fileList.length === 0) {
      return sendMessage(thread.id, input);
    }

    const message = await sendMessage(thread.id, {
      ...input,
      body: buildAttachmentBody(input.body, fileList),
    });

    const uploadedAttachments: Attachment[] = [];

    try {
      for (const file of fileList) {
        const formData = new FormData();
        formData.append("file", file, file.name);
        formData.append("entityType", "chat_message");
        formData.append("entityId", message.id);

        const res = await fetch("/api/attachments", {
          method: "POST",
          body: formData,
        });

        const data = await res.json();

        if (!res.ok) {
          throw new Error(
            typeof data?.error === "string" ? data.error : "Chat attachment upload failed."
          );
        }

        if (data.attachment) {
          uploadedAttachments.push(data.attachment as Attachment);
        }
      }

      // Update the message in state with the uploaded attachments directly,
      // because RLS may prevent the regular client from reading them back.
      if (uploadedAttachments.length > 0) {
        setMessagesByThread((prev) => {
          const threadMessages = prev[thread.id] || [];
          return {
            ...prev,
            [thread.id]: threadMessages.map((m) =>
              m.id === message.id
                ? { ...m, attachments: [...(m.attachments || []), ...uploadedAttachments] }
                : m
            ),
          };
        });
      }

      // Also try fetching to pick up any other updates
      await fetchThreadMessages(thread.id).catch(() => undefined);
      return message;
    } catch (error) {
      // Still apply any attachments that were uploaded before the error
      if (uploadedAttachments.length > 0) {
        setMessagesByThread((prev) => {
          const threadMessages = prev[thread.id] || [];
          return {
            ...prev,
            [thread.id]: threadMessages.map((m) =>
              m.id === message.id
                ? { ...m, attachments: [...(m.attachments || []), ...uploadedAttachments] }
                : m
            ),
          };
        });
      }
      await fetchThreadMessages(thread.id).catch(() => undefined);
      throw error;
    }
  }

  async function toggleReaction(threadId: string, messageId: string, emoji: string) {
    const currentUser = await requireCurrentUser();
    const messages = messagesByThread[threadId] || [];
    const message = messages.find((item) => item.id === messageId);
    const existingReaction = message?.reactions?.find(
      (reaction) => reaction.user_id === currentUser.id && reaction.emoji === emoji
    );

    if (existingReaction) {
      const { error } = await supabase
        .from("chat_message_reactions")
        .delete()
        .eq("message_id", messageId)
        .eq("user_id", currentUser.id)
        .eq("emoji", emoji);
      if (error) throw error;
    } else {
      const { error } = await supabase.from("chat_message_reactions").insert({
        message_id: messageId,
        user_id: currentUser.id,
        emoji,
      });
      if (error) throw error;
    }

    await fetchThreadMessages(threadId);
  }

  async function toggleImportant(threadId: string, message: ChatMessage) {
    const { error } = await supabase
      .from("chat_messages")
      .update({ is_important: !message.is_important })
      .eq("id", message.id);

    if (error) throw error;
    await fetchThreadMessages(threadId);
  }

  async function createTaskFromMessage(thread: ChatThread, message: ChatMessage) {
    const currentUser = await requireCurrentUser();
    const title = buildMessageTitle(message.body, "Follow-up from chat");
    const { data: task, error: taskError } = await supabase
      .from("tasks")
      .insert({
        user_id: currentUser.id,
        team_id: thread.team_id,
        project_id: thread.context_type === "project" ? thread.context_id : null,
        title,
        description: message.body,
        status: "todo",
        priority: "none",
      })
      .select()
      .single();

    if (taskError) throw taskError;

    const { error: linkError } = await supabase.from("chat_message_links").insert({
      message_id: message.id,
      actor_id: currentUser.id,
      entity_type: "task",
      entity_id: task.id,
    });

    if (linkError) throw linkError;
    await fetchThreadMessages(thread.id);
    return task;
  }

  async function createNoteFromMessage(thread: ChatThread, message: ChatMessage) {
    const currentUser = await requireCurrentUser();
    const title = buildMessageTitle(message.body, "Chat note");
    const { data: note, error: noteError } = await supabase
      .from("notes")
      .insert({
        user_id: currentUser.id,
        team_id: thread.team_id,
        project_id: thread.context_type === "project" ? thread.context_id : null,
        title,
        content: defaultNoteContent(message.body),
      })
      .select()
      .single();

    if (noteError) throw noteError;

    const { error: linkError } = await supabase.from("chat_message_links").insert({
      message_id: message.id,
      actor_id: currentUser.id,
      entity_type: "note",
      entity_id: note.id,
    });

    if (linkError) throw linkError;
    await fetchThreadMessages(thread.id);
    return note;
  }

  return {
    loading,
    user,
    contacts,
    incomingRequests,
    outgoingRequests,
    threads,
    activeTeamThreads,
    messagesByThread,
    directoryResults,
    fetchChatData,
    fetchThreadMessages,
    markThreadRead,
    searchDirectory,
    sendContactRequest,
    respondToContactRequest,
    openDirectThread,
    createGroupThread,
    createTeamChannel,
    updateThread,
    deleteThread,
    addThreadMember,
    updateThreadMemberRole,
    removeThreadMember,
    sendMessage,
    sendMessageWithAttachments,
    updateMessage,
    deleteMessage,
    toggleReaction,
    toggleImportant,
    createTaskFromMessage,
    createNoteFromMessage,
  };
}
