import {
  Dispatch,
  SetStateAction,
  useCallback,
  useEffect,
  useRef,
  useState,
} from 'react';
import DocumentsManager from '../../documents/DocumentsManager';
import type { DocumentId } from '../../types/identifiers';
import type { Document } from '../../types/documents';

interface FolderContentsEntry {
  documents?: Document[];
  [key: string]: unknown;
}

interface UseDocumentsOptions {
  setFolderContents: Dispatch<SetStateAction<Map<string, FolderContentsEntry>>>;
  fetchDocumentById?: (id: DocumentId) => Promise<Document | null>;
}

const useDocuments = ({
  setFolderContents,
  fetchDocumentById,
}: UseDocumentsOptions) => {
  const managerRef = useRef(
    new DocumentsManager<Document>(fetchDocumentById),
  );
  const [documents, setDocumentsState] = useState<Document[]>([]);

  useEffect(() => {
    managerRef.current.setFetcher(fetchDocumentById);
  }, [fetchDocumentById]);

  const setDocuments = useCallback(
    (value: Document[] | ((prev: Document[]) => Document[])) => {
      setDocumentsState((prev) => {
        const resolved = typeof value === 'function' ? value(prev) : value;
        if (!Array.isArray(resolved)) {
          return resolved;
        }
        const { canonical } = managerRef.current.ingest(resolved);
        return canonical;
      });
    },
    [],
  );

  const mapDocumentCaches = useCallback(
    (mapper: (doc: Document) => Document | undefined) => {
      managerRef.current.map(mapper);
      const lookupSnapshot = managerRef.current.getSnapshot();

      setDocumentsState((prev) => {
        if (!Array.isArray(prev) || prev.length === 0) {
          return prev;
        }
        let changed = false;
        const next = prev.map((doc) => {
          const id = doc?.id;
          if (id != null && lookupSnapshot.has(id as DocumentId)) {
            const canonical = lookupSnapshot.get(id as DocumentId) as Document;
            if (canonical !== doc) {
              changed = true;
            }
            return canonical;
          }
          const updated = mapper(doc);
          const nextDoc = updated === undefined ? doc : updated;
          if (nextDoc !== doc) {
            changed = true;
          }
          return nextDoc;
        });
        return changed ? next : prev;
      });
      setFolderContents((prev) => {
        if (!prev.size) {
          return prev;
        }
        let changed = false;
        const next = new Map();
        prev.forEach((contents, key) => {
          const docs = Array.isArray(contents?.documents) ? contents.documents : null;
          if (!docs || docs.length === 0) {
            next.set(key, contents);
            return;
          }
          let docsChanged = false;
          const updatedDocs = docs.map((doc) => {
            const id = doc?.id;
            if (id != null && lookupSnapshot.has(id as DocumentId)) {
              const canonical = lookupSnapshot.get(id as DocumentId) as Document;
              if (canonical !== doc) {
                docsChanged = true;
              }
              return canonical;
            }
            const updated = mapper(doc);
            const nextDoc = updated === undefined ? doc : updated;
            if (nextDoc !== doc) {
              docsChanged = true;
            }
            return nextDoc;
          });
          if (docsChanged) {
            changed = true;
            next.set(key, { ...contents, documents: updatedDocs });
          } else {
            next.set(key, contents);
          }
        });
        return changed ? next : prev;
      });
    },
    [setFolderContents],
  );

  const updateDocumentCaches = useCallback(
    (documentId, updater) => {
      if (!documentId) {
        return;
      }

      mapDocumentCaches((doc) => {
        if (!doc || doc.id !== documentId) {
          return doc;
        }
        const updated = updater(doc);
        return updated === undefined ? doc : updated;
      });
    },
    [mapDocumentCaches],
  );

  const removeDocumentsFromLookup = useCallback(
    (documentIds: Array<DocumentId>) => {
      if (!Array.isArray(documentIds) || !documentIds.length) {
        return;
      }
      managerRef.current.remove(documentIds);
    },
    [],
  );

  return {
    documents,
    setDocuments,
    removeDocumentsFromLookup,
    mapDocumentCaches,
    updateDocumentCaches,
    documentsManager: managerRef.current,
  };
};

export default useDocuments;
