import { useCallback, useEffect, useReducer, useRef, useState } from "react";
import { archiveChat, createChat, deleteArchivedChat, hideProject as hideProjectRequest, listChats, listProjectChats, openChat, renameChat, sendInitialChatMessage, setChatPinned, uploadChatAttachment } from "./chats-api.js";
import { assertSessionWorkspace, chatOperationError, initialChatsState, reduceChatsState } from "./chats-state.js";
import { setupErrorMessage } from "../setup/setup-errors.js";
import { createNewChatMessagePreview, reconcileNewChatMessagePreview } from "./new-chat-transition.js";
import { createNewChatAttempt } from "../conversation/idempotent-action.js";
import { collectProjectHistory } from "./project-history.js";

export function useChats({ accountId, workspaceId, onSessionOpened, onTransitionStarted, onTransitionFailed }) {
  const [state, dispatch] = useReducer(reduceChatsState, initialChatsState);
  const [searchQuery, setSearchQuery] = useState("");
  const [activeSearch, setActiveSearch] = useState("");
  const [archived, setArchived] = useState(false);
  const requestIdRef = useRef(0);
  const projectRequestIdRef = useRef(0);
  const archivedCacheRef = useRef(null);
  const loadProjects = useCallback(async (seed) => {
    const requestId = ++projectRequestIdRef.current;
    try {
      const result = await collectProjectHistory(seed, listProjectChats, {
        isCurrent: () => requestId === projectRequestIdRef.current
      });
      if (!result) return;
      dispatch({ type: "projects.loaded", ...result });
    } catch (error) {
      if (requestId === projectRequestIdRef.current) {
        dispatch({ type: "projects.failed", error: chatOperationError(error, "chats_unavailable") });
      }
    }
  }, []);
  const load = useCallback(async (cursor, search = "", archivedMode = archived, { silent = false, signal } = {}) => {
    const requestId = ++requestIdRef.current;
    const queryScope = archivedMode ? "archived" : "recent";
    dispatch({ type: "page.loading", queryScope, silent });
    try {
      const result = await listChats(cursor, search, archivedMode, { signal });
      if (requestId !== requestIdRef.current) return;
      if (archivedMode && !cursor && !search) archivedCacheRef.current = result;
      dispatch({ type: "page.loaded", threads: result.threads ?? [], projectThreads: result.projectThreads, nextCursor: result.nextCursor, errors: result.errors, append: Boolean(cursor), queryScope, searching: Boolean(search) });
      if (!archivedMode && !cursor && !search) void loadProjects(result);
    } catch (error) {
      if (signal?.aborted) return;
      if (requestId === requestIdRef.current) dispatch({ type: "page.failed", error: chatOperationError(error, "chats_unavailable") });
    }
  }, [archived, loadProjects]);
  useEffect(() => {
    const timer = setTimeout(() => setActiveSearch(searchQuery.trim()), 300);
    return () => clearTimeout(timer);
  }, [searchQuery]);
  useEffect(() => {
    const controller = new AbortController();
    void load(undefined, activeSearch, archived, { signal: controller.signal });
    return () => controller.abort();
  }, [load, activeSearch, archived]);
  useEffect(() => () => { projectRequestIdRef.current += 1; }, []);
  function changeArchived(nextArchived) {
    if (nextArchived && !activeSearch && archivedCacheRef.current) {
      dispatch({ type: "page.loaded", ...archivedCacheRef.current, queryScope: "archived" });
    }
    setArchived(nextArchived);
  }
  async function open(thread) {
    const requestId = onTransitionStarted(thread.name || thread.preview || "Chat", () => open(thread), transitionTarget(thread, accountId, workspaceId));
    try {
      const result = await openChat(thread, thread.workspaceId);
      onSessionOpened(result, requestId);
    } catch (error) {
      onTransitionFailed(requestId, setupErrorMessage(error, "chat_open_failed"));
    }
  }
  async function create(selectedWorkspaceId = workspaceId, composerOptions, existingRequestId, clientSessionId = crypto.randomUUID()) {
    const requestId = existingRequestId ?? onTransitionStarted("New chat", () => create(selectedWorkspaceId, composerOptions, undefined, clientSessionId), transitionTarget({}, accountId, selectedWorkspaceId));
    try {
      const result = await createChat(accountId, selectedWorkspaceId, clientSessionId);
      assertSessionWorkspace(result, selectedWorkspaceId);
      onSessionOpened({ ...result, events: [] }, requestId, composerOptions);
      void load(undefined, activeSearch);
      if (activeSearch) void loadProjects();
      return true;
    } catch (error) {
      onTransitionFailed(requestId, setupErrorMessage(error, "chat_create_failed"));
      return false;
    }
  }
  async function createWithMessage(selectedWorkspaceId, text, payload = { text: text.trim() }, files = [], existingAttempt) {
    const attempt = existingAttempt ?? createNewChatAttempt();
    const optimisticEvent = createNewChatMessagePreview(text, files);
    const requestId = onTransitionStarted(text.trim() || "New chat", () => createWithMessage(selectedWorkspaceId, text, payload, files, attempt), transitionTarget({}, accountId, selectedWorkspaceId, [optimisticEvent]));
    try {
      const created = attempt.created ?? await createChat(accountId, selectedWorkspaceId, attempt.clientSessionId);
      attempt.created = created;
      assertSessionWorkspace(created, selectedWorkspaceId);
      let uploadedAttachments = attempt.uploadedAttachments;
      let uploadFailureCount = attempt.uploadFailureCount ?? 0;
      if (!uploadedAttachments) {
        const uploadResults = await Promise.allSettled(files.map(async (file) => {
          const dataBase64 = await fileToBase64(file);
          return uploadChatAttachment(created.session.id, {
            name: file.name,
            mimeType: file.type,
            dataBase64
          });
        }));
        uploadedAttachments = uploadResults
          .filter((result) => result.status === "fulfilled")
          .map((result) => result.value.attachment);
        uploadFailureCount = uploadResults.filter((result) => result.status === "rejected").length;
        attempt.uploadedAttachments = uploadedAttachments;
        attempt.uploadFailureCount = uploadFailureCount;
      }
      if (!text.trim() && files.length > 0 && uploadedAttachments.length === 0) throw new Error("upload_failed");
      const acceptedOptimisticEvent = reconcileNewChatMessagePreview(optimisticEvent, uploadedAttachments.length);
      const sent = await sendInitialChatMessage(created.session.id, {
        ...payload,
        clientMessageId: attempt.clientMessageId,
        attachments: uploadedAttachments.map((item) => ({ id: item.id }))
      });
      assertSessionWorkspace({ session: sent.session ?? created.session }, selectedWorkspaceId);
      const openedSession = sent.session ?? created.session;
      onSessionOpened({
        session: uploadFailureCount > 0 ? { ...openedSession, attachmentUploadFailureCount: uploadFailureCount } : openedSession,
        events: [acceptedOptimisticEvent]
      }, requestId, payload.options);
      void load(undefined, activeSearch);
      if (activeSearch) void loadProjects();
    } catch (error) {
      onTransitionFailed(requestId, setupErrorMessage(error, "chat_message_failed"));
    }
  }
  async function archive(thread) {
    dispatch({ type: "archive.started", thread });
    try {
      await archiveChat(thread);
      archivedCacheRef.current = null;
      void load(undefined, "", false);
      return true;
    } catch (error) {
      dispatch({ type: "archive.failed", thread, error: chatOperationError(error, "chat_archive_failed") });
      return false;
    }
  }
  async function deleteArchived(thread) {
    dispatch({ type: "delete.started", thread });
    try {
      await deleteArchivedChat(thread);
      if (archivedCacheRef.current) {
        archivedCacheRef.current = {
          ...archivedCacheRef.current,
          threads: (archivedCacheRef.current.threads ?? []).filter((item) => !(
            item.ownerId === thread.ownerId && item.id === thread.id
          ))
        };
      }
      return true;
    } catch (error) {
      dispatch({ type: "delete.failed", thread, error: chatOperationError(error, "chat_delete_failed") });
      return false;
    }
  }
  async function rename(thread, name) {
    try {
      const result = await renameChat(thread, name);
      dispatch({ type: "thread.renamed", ...result });
    } catch (error) {
      dispatch({ type: "page.failed", error: chatOperationError(error, "chat_rename_failed") });
    }
  }
  async function pin(thread) {
    try {
      const result = await setChatPinned(thread, !thread.pinned);
      dispatch({ type: "thread.pinned", ...result });
    } catch (error) {
      dispatch({ type: "page.failed", error: chatOperationError(error, "chat_pin_failed") });
    }
  }
  async function hideProject(project) {
    try {
      const result = await hideProjectRequest(project.key);
      dispatch({ type: "projects.hidden", hiddenProject: result.hiddenProject });
      return true;
    } catch (error) {
      dispatch({ type: "projects.failed", error: chatOperationError(error, "chats_unavailable") });
      return false;
    }
  }
  return {
    ...state,
    searchQuery,
    setSearchQuery,
    archived,
    setArchived: changeArchived,
    loadMore: () => load(state.nextCursor, activeSearch, archived),
    open,
    create,
    createWithMessage,
    archive,
    deleteArchived,
    rename,
    pin,
    hideProject,
    reload: (options = {}) => load(undefined, activeSearch, archived, options)
  };
}

function transitionTarget(thread, accountId, workspaceId, optimisticEvents = []) {
  const targetWorkspaceId = thread.id ? (thread.workspaceId || "") : (workspaceId || "");
  return {
    accountId: thread.ownerId || accountId || "",
    workspaceId: targetWorkspaceId,
    projectLabel: thread.projectLabel || "",
    projectless: Boolean(thread.projectless || !targetWorkspaceId),
    codexThreadId: thread.id || "",
    ...(optimisticEvents.length ? { optimisticEvents } : {})
  };
}

function fileToBase64(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(String(reader.result).split(",")[1] ?? "");
    reader.onerror = () => reject(reader.error);
    reader.readAsDataURL(file);
  });
}
