import { useCallback } from 'react';
import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
import { DEFAULT_FOLDER_NAME, getRowId, isDocumentRowKey } from '../../app/appLayoutUtils';

type DocumentId = string | number;
type FolderId = DocumentId | 'root';
type NullableFolderId = FolderId | null;

type StatusLevel = 'success' | 'error' | 'info' | string;

type DocumentCacheMapper = (
  doc: DocumentLike | null | undefined,
) => DocumentLike | null | undefined;

type MapDocumentCaches = (mapper: DocumentCacheMapper) => void;

type UpdateDocumentCaches = (
  documentId: DocumentId,
  updater: DocumentCacheMapper,
) => void;

type EnsureFolderData = (
  folderId: FolderId,
  options?: { force?: boolean; includeDocuments?: boolean; prefetchDepth?: number },
) => Promise<FolderContents>;

type ApplySelectedFolder = (folderId: FolderId, contents?: FolderContents | null) => void;

type RemoveDocumentsFromCaches = (documentIds: DocumentId[]) => void;

type CloseDocumentPreview = () => void;

type NotifyApiError = (error: unknown, fallbackMessage?: string) => void;

type SetStatusMessage = (message: string, level?: StatusLevel) => void;

interface ApiClient {
  post<T = unknown>(url: string, data?: unknown, config?: Record<string, unknown>): Promise<{ data: T }>;
  patch<T = unknown>(url: string, data?: unknown, config?: Record<string, unknown>): Promise<{ data: T }>;
  delete<T = unknown>(url: string, config?: Record<string, unknown>): Promise<{ data: T }>;
}

interface Tag {
  id: DocumentId;
  label: string;
  color?: string | null;
  [key: string]: unknown;
}

interface DocumentLike {
  id?: DocumentId;
  folder_id?: NullableFolderId;
  folder_path?: string | null;
  folder_name?: string | null;
  issued_at?: number | null;
  title?: string;
  tags?: Tag[];
  [key: string]: unknown;
}

interface FolderContents {
  documents?: DocumentLike[];
  subfolders?: Array<{ id?: FolderId; [key: string]: unknown }>;
  [key: string]: unknown;
}

interface FolderNode {
  id: FolderId;
  parentId?: FolderId;
  children: FolderId[];
  hasChildren?: boolean;
  [key: string]: unknown;
}

interface TagManager {
  normalizeLabel: (label: string) => string;
  buildPayload: (args: { label: string }) => Record<string, unknown>;
}

interface DocumentTagExtras {
  option?: Tag | null;
  input?: { value?: string } | null;
}

interface DeleteOptions {
  showMessage?: boolean;
  manageLoading?: boolean;
}

interface TagAttachArgs {
  documentId?: DocumentId;
  tagId?: DocumentId;
  tag?: Tag | null;
}

interface TagRemoveOptions {
  refreshTagList?: boolean;
  showMessage?: boolean;
}

interface FolderDeleteOptions {
  showMessage?: boolean;
  manageLoading?: boolean;
}

interface UseDocumentMutationsArgs {
  api: ApiClient;
  token?: string | null;
  documentLookup: Map<DocumentId, DocumentLike>;
  folderLabelMap: Map<FolderId, string>;
  ensureFolderData: EnsureFolderData;
  selectedFolder: FolderId;
  setSelectedFolder: Dispatch<SetStateAction<FolderId>>;
  setDocuments: Dispatch<SetStateAction<DocumentLike[]>>;
  setFolderContents: Dispatch<SetStateAction<Map<FolderId, FolderContents>>>;
  setSearchResults: Dispatch<SetStateAction<DocumentLike[] | null>>;
  setSelectedEntries: Dispatch<SetStateAction<string[]>>;
  setSelectionOrder: Dispatch<SetStateAction<string[]>>;
  selectionOrderRef: MutableRefObject<string[] | null>;
  selectionAnchorRef: MutableRefObject<string | null>;
  setFocusedDocumentId: Dispatch<SetStateAction<DocumentId | null>>;
  focusedDocumentId: DocumentId | null;
  setFocusedRowKey: Dispatch<SetStateAction<string | null>>;
  focusedRowKey: string | null;
  notifyApiError: NotifyApiError;
  setStatusMessage: SetStatusMessage;
  setLoading: (next: boolean) => void;
  mapDocumentCaches: MapDocumentCaches;
  applySelectedFolder: ApplySelectedFolder;
  folderNodes: Map<FolderId, FolderNode>;
  setFolderNodes: Dispatch<SetStateAction<Map<FolderId, FolderNode>>>;
  removeDocumentsFromCaches: RemoveDocumentsFromCaches;
  closeDocumentPreview: CloseDocumentPreview;
  previewDocumentId?: DocumentId | null;
  refreshCurrentFolder: () => Promise<void>;
  documentsViewMode?: string;
  updateDocumentCaches: UpdateDocumentCaches;
  tagLookupById: Map<DocumentId, Tag>;
  tags: Tag[];
  refreshTags: () => Promise<void>;
  tagManager: TagManager;
  extractDocumentFromResponse?: (payload: unknown) => DocumentLike | null | undefined;
}

interface UseDocumentMutationsResult {
  moveDocumentsToFolder: (
    documentIds: Array<DocumentId | DocumentLike>,
    targetFolderId?: NullableFolderId,
  ) => Promise<void>;
  handleThumbnailRegeneration: (documentId: DocumentId) => Promise<void>;
  handleDocumentsDelete: (
    documentIds: DocumentId[],
    options?: DeleteOptions,
  ) => Promise<boolean>;
  handleDocumentTagAdd: (
    document: DocumentLike,
    label: string,
    extras?: DocumentTagExtras | null,
  ) => Promise<void>;
  handleDocumentTagAttach: (args: TagAttachArgs) => Promise<boolean>;
  handleDocumentTitleUpdate: (documentId: DocumentId, nextTitle: string) => Promise<boolean>;
  handleDocumentIssuedUpdate: (
    documentId: DocumentId,
    nextIssuedDate: number | null,
  ) => Promise<boolean>;
  handleTagRemove: (
    documentId?: DocumentId,
    tagId?: DocumentId,
    options?: TagRemoveOptions,
  ) => Promise<boolean>;
  handleFolderDelete: (folderId?: FolderId, options?: FolderDeleteOptions) => Promise<boolean>;
}

const normalizeDocumentId = (value: unknown): DocumentId | null => {
  if (!value) return null;
  if (typeof value === 'object' && value !== null && 'id' in value && value.id != null) {
    return value.id as DocumentId;
  }
  return value as DocumentId;
};

const useDocumentMutations = ({
  api,
  token,
  documentLookup,
  folderLabelMap,
  ensureFolderData,
  selectedFolder,
  setSelectedFolder,
  setDocuments,
  setFolderContents,
  setSearchResults,
  setSelectedEntries,
  setSelectionOrder,
  selectionOrderRef,
  selectionAnchorRef,
  setFocusedDocumentId,
  focusedDocumentId,
  setFocusedRowKey,
  focusedRowKey,
  notifyApiError,
  setStatusMessage,
  setLoading,
  mapDocumentCaches,
  applySelectedFolder,
  folderNodes,
  setFolderNodes,
  removeDocumentsFromCaches,
  closeDocumentPreview,
  previewDocumentId,
  refreshCurrentFolder,
  documentsViewMode,
  updateDocumentCaches,
  tagLookupById,
  tags,
  refreshTags,
  tagManager,
  extractDocumentFromResponse,
}: UseDocumentMutationsArgs): UseDocumentMutationsResult => {
  const moveDocumentsToFolder = useCallback(
    async (documentIds: Array<DocumentId | DocumentLike>, targetFolderId?: NullableFolderId) => {
      const uniqueIds = Array.from(
        new Set((documentIds || []).map((value) => normalizeDocumentId(value)).filter(Boolean) as DocumentId[]),
      );
      if (!uniqueIds.length) return;

      const uniqueIdSet = new Set(uniqueIds);
      const target = targetFolderId === 'root' ? null : targetFolderId ?? null;
      const targetLabel =
        target === null ? DEFAULT_FOLDER_NAME : folderLabelMap.get(targetFolderId as FolderId) || 'target folder';

      const movedDocs = uniqueIds
        .map((id) => {
          const doc = documentLookup.get(id) || null;
          if (!doc) {
            return null;
          }
          return {
            id,
            sourceFolderId: (doc.folder_id ?? null) as NullableFolderId,
            document: doc,
          };
        })
        .filter(Boolean) as Array<{ id: DocumentId; sourceFolderId: NullableFolderId; document: DocumentLike }>;

      const updatedDocsMap = new Map<DocumentId, DocumentLike>();
      const resolveTargetName = () => {
        if (!targetLabel) {
          return null;
        }
        const segments = String(targetLabel).split('/');
        return segments[segments.length - 1] || targetLabel;
      };
      const targetName = resolveTargetName();

      movedDocs.forEach(({ id, document }) => {
        if (!document) {
          return;
        }
        const updated: DocumentLike = {
          ...document,
          folder_id: target,
        };
        if (targetLabel) {
          updated.folder_path = targetLabel;
          if (targetName) {
            updated.folder_name = targetName;
          }
        } else if (target === null) {
          updated.folder_path = DEFAULT_FOLDER_NAME;
          updated.folder_name = DEFAULT_FOLDER_NAME;
        }
        updatedDocsMap.set(id, updated);
      });

      const pruneRow = (collection: string[]): string[] =>
        collection.filter((key) => {
          if (!isDocumentRowKey(key)) {
            return true;
          }
          const id = getRowId(key);
          return id ? !uniqueIdSet.has(id as DocumentId) : true;
        });

      setLoading(true);
      try {
        if (uniqueIds.length === 1) {
          await api.patch(`/documents/${uniqueIds[0]}/folder`, { folder_id: target });
        } else {
          await api.post('/documents/bulk/move', {
            document_ids: uniqueIds,
            folder_id: target,
          });
        }

        const count = uniqueIds.length;
        const suffix = count === 1 ? '' : 's';
        setStatusMessage(`Moved ${count} document${suffix} to ${targetLabel}.`, 'success');

        if (updatedDocsMap.size) {
          mapDocumentCaches((doc) => {
            if (!doc || !uniqueIdSet.has(doc.id as DocumentId)) {
              return doc;
            }
            const updated = updatedDocsMap.get(doc.id as DocumentId);
            if (updated) {
              return updated;
            }
            return { ...doc, folder_id: target };
          });
        } else {
          mapDocumentCaches((doc) => {
            if (!doc || !uniqueIdSet.has(doc.id as DocumentId)) {
              return doc;
            }
            return { ...doc, folder_id: target };
          });
        }

        if (uniqueIdSet.size) {
          setSearchResults((prev) => {
            if (!Array.isArray(prev) || !prev.length) {
              return prev;
            }
            const filtered = prev.filter((doc) => doc && !uniqueIdSet.has(doc.id as DocumentId));
            return filtered.length === prev.length ? prev : filtered;
          });
          setDocuments((prev) => prev.filter((doc) => !uniqueIdSet.has(doc.id as DocumentId)));
          setFolderContents((prev: Map<FolderId, FolderContents>) => {
            if (!prev.size) {
              return prev;
            }
            let changed = false;
            const next = new Map<FolderId, FolderContents>(prev);
            movedDocs.forEach(({ id, sourceFolderId }) => {
              const sourceKey = (sourceFolderId || 'root') as FolderId;
              const entry = next.get(sourceKey);
              if (!entry?.documents?.length) {
                return;
              }
              const filteredDocs = entry.documents.filter((doc) => doc.id !== id);
              if (filteredDocs.length !== entry.documents.length) {
                changed = true;
                next.set(sourceKey, { ...entry, documents: filteredDocs });
              }
            });
            return changed ? next : prev;
          });

          setSelectedEntries((prev) => pruneRow(prev));
          setSelectionOrder((prev) => pruneRow(prev));
          const nextSelectionOrder = pruneRow(selectionOrderRef.current || []);
          selectionOrderRef.current = nextSelectionOrder;
          if (
            selectionAnchorRef.current &&
            isDocumentRowKey(selectionAnchorRef.current) &&
            uniqueIdSet.has(getRowId(selectionAnchorRef.current) as DocumentId)
          ) {
            selectionAnchorRef.current = null;
          }
          if (focusedDocumentId && uniqueIdSet.has(focusedDocumentId)) {
            setFocusedDocumentId(null);
          }
          if (
            focusedRowKey &&
            isDocumentRowKey(focusedRowKey) &&
            uniqueIdSet.has(getRowId(focusedRowKey) as DocumentId)
          ) {
            setFocusedRowKey(null);
          }
        }

        if (targetFolderId && targetFolderId !== selectedFolder) {
          await ensureFolderData(targetFolderId as FolderId, { force: true, prefetchDepth: 1 });
        }
      } catch (error) {
        const message = (error as Record<string, any>)?.response?.data?.error || 'Failed to move documents.';
        notifyApiError(error, message);
      } finally {
        setLoading(false);
      }
    },
    [
      api,
      documentLookup,
      folderLabelMap,
      ensureFolderData,
      selectedFolder,
      setSearchResults,
      setDocuments,
      setFolderContents,
      setSelectedEntries,
      setSelectionOrder,
      selectionOrderRef,
      selectionAnchorRef,
      setFocusedDocumentId,
      focusedDocumentId,
      setFocusedRowKey,
      focusedRowKey,
      notifyApiError,
      setStatusMessage,
      setLoading,
      mapDocumentCaches,
    ],
  );

  const handleThumbnailRegeneration = useCallback(
    async (documentId: DocumentId) => {
      if (!token) {
        setStatusMessage('Log in to manage assets.', 'error');
        return;
      }
      setLoading(true);
      try {
        await api.post(`/documents/${documentId}/assets`, null, {
          params: { force: true },
        });
        setStatusMessage('Document re-analysis queued.', 'info');
        await refreshCurrentFolder();
      } catch (error) {
        const message = (error as Record<string, any>)?.response?.data?.error || 'Failed to request thumbnail generation.';
        notifyApiError(error, message);
      } finally {
        setLoading(false);
      }
    },
    [api, token, refreshCurrentFolder, notifyApiError, setStatusMessage, setLoading],
  );

  const handleDocumentsDelete = useCallback(
    async (documentIds: DocumentId[], { showMessage = true, manageLoading = true }: DeleteOptions = {}) => {
      if (!documentIds || documentIds.length === 0) {
        return false;
      }

      if (!token) {
        setStatusMessage('Log in to manage documents.', 'error');
        return false;
      }

      if (manageLoading) {
        setLoading(true);
      }

      try {
        await Promise.all(documentIds.map((documentId) => api.post(`/documents/${documentId}/trash`)));

        removeDocumentsFromCaches(documentIds);

        if (previewDocumentId && documentIds.includes(previewDocumentId)) {
          closeDocumentPreview();
        }

        if (showMessage) {
          const message = documentIds.length === 1 ? 'Document deleted.' : 'Documents deleted.';
          setStatusMessage(message, 'success');
        }
        return true;
      } catch (error) {
        const message = (error as Record<string, any>)?.response?.data?.error || 'Failed to delete documents.';
        notifyApiError(error, message);
        return false;
      } finally {
        if (manageLoading) {
          setLoading(false);
        }
      }
    },
    [
      api,
      token,
      removeDocumentsFromCaches,
      previewDocumentId,
      closeDocumentPreview,
      notifyApiError,
      setStatusMessage,
      setLoading,
    ],
  );

  const handleDocumentTitleUpdate = useCallback(
    async (documentId: DocumentId, nextTitle: string) => {
      const trimmed = nextTitle?.trim?.() || '';
      if (!trimmed) {
        setStatusMessage('Document title cannot be empty.', 'error');
        return false;
      }

      setLoading(true);
      try {
        const { data } = await api.patch(`/documents/${documentId}`, { title: trimmed });
        const updatedDocument = extractDocumentFromResponse?.(data);

        updateDocumentCaches(documentId, (doc) => {
          if (updatedDocument) {
            return { ...doc, ...updatedDocument };
          }
          return { ...doc, title: trimmed };
        });

        setStatusMessage('Document title updated.', 'success');
        return true;
      } catch (error) {
        const message = (error as Record<string, any>)?.response?.data?.error || 'Failed to update document title.';
        notifyApiError(error, message);
        return false;
      } finally {
        setLoading(false);
      }
    },
    [api, extractDocumentFromResponse, notifyApiError, setLoading, setStatusMessage, updateDocumentCaches],
  );

  const handleDocumentIssuedUpdate = useCallback(
    async (documentId: DocumentId, nextIssuedDate: number | null) => {
      setLoading(true);
      const payload = { issued_at: nextIssuedDate || null };
      try {
        const { data } = await api.patch(`/documents/${documentId}`, payload);
        const updatedDocument = extractDocumentFromResponse?.(data);

        updateDocumentCaches(documentId, (doc) => {
          if (updatedDocument) {
            return { ...doc, ...updatedDocument };
          }
          return { ...doc, issued_at: payload.issued_at };
        });

        const message = payload.issued_at ? 'Issued date updated.' : 'Issued date cleared.';
        setStatusMessage(message, 'success');
        return true;
      } catch (error) {
        const message = (error as Record<string, any>)?.response?.data?.error || 'Failed to update issued date.';
        notifyApiError(error, message);
        return false;
      } finally {
        setLoading(false);
      }
    },
    [api, extractDocumentFromResponse, notifyApiError, setLoading, setStatusMessage, updateDocumentCaches],
  );

  const handleDocumentTagAdd = useCallback(
    async (document: DocumentLike, label: string, extras: DocumentTagExtras | null = null) => {
      const normalizedLabel = tagManager.normalizeLabel(label);
      const optionCandidate = extras?.option ?? null;
      const input = extras?.input ?? null;

      let tag: Tag | null = null;
      if (optionCandidate && optionCandidate.id) {
        tag = tags.find((item) => item.id === optionCandidate.id) || optionCandidate;
      }
      if (!tag) {
        tag = tags.find((item) => item.label.toLowerCase() === normalizedLabel.toLowerCase()) || null;
      }
      try {
        if (!tag) {
          const payload = tagManager.buildPayload({ label: normalizedLabel });
          const { data } = await api.post('/tags', payload);
          tag = data as Tag;
          await refreshTags();
        }
        await api.post(`/documents/${document.id}/tags`, { tag_ids: [tag.id] });
        setStatusMessage('Tag assigned.', 'success');
        if (input && typeof input === 'object') {
          input.value = '';
        }
        await refreshCurrentFolder();
      } catch (error) {
        notifyApiError(error, 'Failed to assign tag.');
      }
    },
    [api, tags, refreshTags, refreshCurrentFolder, notifyApiError, setStatusMessage, tagManager],
  );

  const handleDocumentTagAttach = useCallback(
    async ({ documentId, tagId, tag: tagData = null }: TagAttachArgs) => {
      if (!documentId || !tagId) {
        return false;
      }

      const resolveTagForCache = (): Tag | null => {
        const lookupTag = tagLookupById.get(tagId);
        const source = lookupTag ?? tagData;
        if (!source || source.id == null || typeof source.label?.trim !== 'function') {
          return null;
        }
        return {
          id: source.id,
          label: source.label,
          color: Object.prototype.hasOwnProperty.call(source, 'color') ? (source as Tag).color ?? null : null,
        };
      };

      try {
        await api.post(`/documents/${documentId}/tags`, { tag_ids: [tagId] });
        updateDocumentCaches(documentId, (doc) => {
          if (!doc) {
            return doc;
          }
          const currentTags = Array.isArray(doc.tags) ? doc.tags : [];
          if (currentTags.some((existing) => existing?.id === tagId)) {
            return doc;
          }
          const resolvedTag = resolveTagForCache();
          if (!resolvedTag) {
            return doc;
          }
          return { ...doc, tags: [...currentTags, resolvedTag] };
        });
        setStatusMessage('Tag assigned.', 'success');
        if (documentsViewMode !== 'desk') {
          await refreshCurrentFolder();
        }
        return true;
      } catch (error) {
        const message = (error as Record<string, any>)?.response?.data?.error || 'Failed to assign tag.';
        notifyApiError(error, message);
        return false;
      }
    },
    [
      api,
      refreshCurrentFolder,
      documentsViewMode,
      notifyApiError,
      setStatusMessage,
      updateDocumentCaches,
      tagLookupById,
    ],
  );

  const applyTagRemovalToCaches = useCallback(
    (documentId?: DocumentId, tagId?: DocumentId) => {
      if (!documentId || !tagId) {
        return;
      }

      updateDocumentCaches(documentId, (doc) => {
        if (!doc || !Array.isArray(doc.tags)) {
          return doc;
        }
        const nextTags = doc.tags.filter((tagEntry) => tagEntry.id !== tagId);
        if (nextTags.length === doc.tags.length) {
          return doc;
        }
        return { ...doc, tags: nextTags };
      });
    },
    [updateDocumentCaches],
  );

  const handleTagRemove = useCallback(
    async (
      documentId?: DocumentId,
      tagId?: DocumentId,
      { refreshTagList = true, showMessage = true }: TagRemoveOptions = {},
    ) => {
      if (!documentId || !tagId) {
        return false;
      }

      try {
        await api.delete(`/documents/${documentId}/tags/${tagId}`);
        applyTagRemovalToCaches(documentId, tagId);
        if (refreshTagList) {
          await refreshTags();
        }
        if (showMessage) {
          setStatusMessage('Tag removed.', 'success');
        }
        return true;
      } catch (error) {
        const message = (error as Record<string, any>)?.response?.data?.error || 'Failed to remove tag.';
        notifyApiError(error, message);
        return false;
      }
    },
    [api, applyTagRemovalToCaches, notifyApiError, refreshTags, setStatusMessage],
  );

  const handleFolderDelete = useCallback(
    async (folderId?: FolderId, { showMessage = true, manageLoading = true }: FolderDeleteOptions = {}) => {
      if (!token) {
        if (showMessage) {
          setStatusMessage('Log in to manage folders.', 'error');
        }
        return false;
      }
      if (!folderId || folderId === 'root') {
        if (showMessage) {
          setStatusMessage('The root folder cannot be removed.', 'error');
        }
        return false;
      }

      if (manageLoading) {
        setLoading(true);
      }

      try {
        const contents = await ensureFolderData(folderId, {
          force: true,
          prefetchDepth: 1,
        });
        const hasChildren = (contents.subfolders || []).length > 0;
        const hasDocs = (contents.documents || []).length > 0;
        if (hasChildren || hasDocs) {
          if (showMessage) {
            setStatusMessage('Folder must be empty before it can be deleted.', 'error');
          }
          return false;
        }

        await api.delete(`/folders/${folderId}`);

        setFolderNodes((prev: Map<FolderId, FolderNode>) => {
          const next = new Map<FolderId, FolderNode>(prev);
          const node = next.get(folderId);
          next.delete(folderId);
          if (node) {
            const parentId = node.parentId || 'root';
            const parentNode = next.get(parentId);
            if (parentNode) {
              const remaining = parentNode.children.filter((id) => id !== folderId);
              next.set(parentId, {
                ...parentNode,
                children: remaining,
                hasChildren: remaining.length > 0,
              });
            }
          }
          return next;
        });

        setFolderContents((prev: Map<FolderId, FolderContents>) => {
          const next = new Map<FolderId, FolderContents>(prev);
          next.delete(folderId);
          return next;
        });

        if (selectedFolder === folderId) {
          const node = folderNodes.get(folderId);
          const parentId = node?.parentId || 'root';
          setSelectedFolder(parentId);
          const parentContents = await ensureFolderData(parentId, {
            force: true,
            prefetchDepth: 1,
          });
          applySelectedFolder(parentId, parentContents);
        } else if (selectedFolder !== 'root') {
          await ensureFolderData(selectedFolder, { force: true, prefetchDepth: 1 });
        }

        if (showMessage) {
          setStatusMessage('Folder deleted.', 'success');
        }
        return true;
      } catch (error) {
        const message = (error as Record<string, any>)?.response?.data?.error || 'Failed to delete folder.';
        notifyApiError(error, message);
        if (showMessage) {
          setStatusMessage(message, 'error');
        }
        return false;
      } finally {
        if (manageLoading) {
          setLoading(false);
        }
      }
    },
    [
      api,
      token,
      ensureFolderData,
      selectedFolder,
      folderNodes,
      setSelectedFolder,
      applySelectedFolder,
      setFolderNodes,
      setFolderContents,
      notifyApiError,
      setStatusMessage,
      setLoading,
    ],
  );

  return {
    moveDocumentsToFolder,
    handleThumbnailRegeneration,
    handleDocumentsDelete,
    handleDocumentTagAdd,
    handleDocumentTagAttach,
    handleDocumentTitleUpdate,
    handleDocumentIssuedUpdate,
    handleTagRemove,
    handleFolderDelete,
  };
};

export default useDocumentMutations;
