import { shallowEqual } from 'react-redux';
import type { FolderNodeId } from '../types/identifiers';
import type { Folder } from '../types/documents';
import { createRootNode } from '../app/workspaceUtils';

import type { FolderTreeNode } from '../lib/api/apiTypes';
import { getFolderTree } from '../lib/api/apiClient';
import { flattenFolderTree } from '../app/workspaceUtils';

type ManagedFolder = Folder;

type FetchFolder = (id: FolderNodeId) => Promise<unknown>;


class FoldersManager<T extends ManagedFolder = ManagedFolder> {
    private byId: Map<FolderNodeId, T>;
    private fetcher?: FetchFolder;
    private inflight: Map<FolderNodeId, Promise<T | null>>;
    private treePromise: Promise<FolderTreeNode[]> | null = null;
    private treeSnapshot: FolderTreeNode[] = [];
    private listeners: Set<() => void>;
    private emitScheduled: boolean;


    constructor(
        fetchFolder?: FetchFolder,
    ) {
        this.byId = new Map();
        this.fetcher = fetchFolder;
        this.inflight = new Map();
        this.listeners = new Set();
        this.emitScheduled = false;
    }

    private emit() {
        if (this.emitScheduled) {
            return;
        }
        this.emitScheduled = true;
        setTimeout(() => {
            this.emitScheduled = false;
            this.listeners.forEach((fn) => fn());
        }, 0);
    }

    subscribe(listener: () => void) {
        this.listeners.add(listener);
        return () => this.listeners.delete(listener);
    }

    setFetcher(fetchFolder?: FetchFolder) {
        this.fetcher = fetchFolder;
    }

    ingest(rawFolders: unknown[] = []): { canonical: T[]; changed: boolean } {
        const result = this.ingestInternal(rawFolders);
        if (result.changed) {
            this.emit();
        }
        return result;
    }

    private ingestInternal(rawFolders: unknown[] = []): { canonical: T[]; changed: boolean } {
        const folders = rawFolders.map((f) => f as T).filter(Boolean);
        let changed = false;
        let nextById = this.byId;
        const canonical: T[] = [];

        folders.forEach((folder) => {
            const id = folder?.id;
            if (id == null) {
                canonical.push(folder);
                return;
            }

            const existing = nextById.get(id as FolderNodeId);
            const merged = existing ? ({ ...existing, ...folder } as T) : ({ ...(folder as T) } as T);
            const useExisting = existing && shallowEqual(existing, merged);
            const nextFolder = useExisting ? (existing as T) : merged;

            if (!useExisting) {
                if (!changed) {
                    nextById = new Map(this.byId);
                }
                nextById.set(id as FolderNodeId, nextFolder);
                changed = true;
            }
            canonical.push(nextFolder);
        });

        if (changed) {
            this.byId = nextById;
        }

        return { canonical, changed };
    }

    async ensure(id: FolderNodeId, fetcherOverride?: FetchFolder): Promise<T | null> {
        if (id == null) {
            return null;
        }

        const cached = this.byId.get(id);
        if (cached) {
            return cached;
        }

        const fetcher = fetcherOverride || this.fetcher;
        if (!fetcher) {
            return null;
        }

        const inflight = this.inflight.get(id);
        if (inflight) {
            return inflight;
        }

        const request = (async () => {
            try {
                const fetched = await fetcher(id);
                const { canonical } = this.ingest([fetched as unknown]);
                return canonical[0] ?? null;
            } finally {
                this.inflight.delete(id);
            }
        })();

        this.inflight.set(id, request);
        return request;
    }

    map(mapper: (folder: T) => T | undefined): boolean {
        if (!this.byId.size) {
            return false;
        }

        let changed = false;
        const next = new Map<FolderNodeId, T>();
        this.byId.forEach((folder, key) => {
            const updated = mapper(folder);
            const nextFolder = updated === undefined ? folder : updated;
            if (nextFolder !== folder) {
                changed = true;
            }
            next.set(key, nextFolder ?? folder);
        });

        if (changed) {
            this.byId = next;
            this.emit();
        }

        return changed;
    }

    remove(ids: Array<FolderNodeId>): boolean {
        if (!Array.isArray(ids) || ids.length === 0) {
            return false;
        }
        let changed = false;
        let next = this.byId;
        ids.forEach((id) => {
            if (next.has(id)) {
                if (!changed) {
                    next = new Map(this.byId);
                }
                next.delete(id);
                changed = true;
            }
        });
        if (changed) {
            this.byId = next;
            this.emit();
        }
        return changed;
    }

    getById(id: FolderNodeId): T | null {
        return this.byId.get(id) ?? null;
    }

    getMany(ids: Array<FolderNodeId> = []): T[] {
        return ids
            .map((id) => this.byId.get(id) || null)
            .filter((folder): folder is T => Boolean(folder));
    }

    getSnapshot(): Map<FolderNodeId, T> {
        return this.byId;
    }

    getTreeSnapshot(): FolderTreeNode[] {
        return this.treeSnapshot;
    }

    async ensureTree(): Promise<FolderTreeNode[]> {
        if (this.treeSnapshot.length > 0) {
            return this.treeSnapshot;
        }

        if (this.treePromise) {
            return this.treePromise;
        }

        this.treePromise = (async () => {
            try {
                const raw = await getFolderTree();
                const flattened = flattenFolderTree(raw);
                this.ingest(flattened);
                const rootsPromises = raw as FolderTreeNode[];
                const rootNode = createRootNode() as FolderTreeNode;

                rootNode.children = rootsPromises;
                rootNode.hasChildren = rootsPromises.length > 0;
                rootNode.loaded = true;

                this.treeSnapshot = [rootNode];
                this.emit();
                return [rootNode];
            } catch (error) {
                console.warn('Failed to fetch folder tree', error);
                return [];
            } finally {
                this.treePromise = null;
            }
        })();

        return this.treePromise;
    }
}

export default FoldersManager;
