import {IFile, IStorageProvider} from "../types.js"
import {createHash} from "crypto";
import micromatch from 'micromatch';
import path from "node:path";

export class VirtualFile implements IFile {
    path: string
    name: string
    extension: string = ""
    fullPath: string // path + name + extension
    data: Buffer

    constructor(fullPath: string, data: Buffer) {
        this.fullPath = fullPath
        const parts = fullPath.split(VirtualStorageProvider.separator)
        const fileName = parts.pop()
        if (!fileName) {
            throw new Error("Unable to parse file name")
        }
        this.name = fileName
        if (fileName.split(".").length > 1) {
            this.name = fileName.split(".").slice(0, -1).join(".")
            this.extension = fileName.split(".").slice(-1).join("")
        }
        this.path = parts.join(VirtualStorageProvider.separator)
        this.data = data
    }

    async readData(): Promise<Buffer> {
        try {
            return this.data
        } catch (err: any) {
            throw new Error(`Failed to read file content: ${err.message}`)
        }
    }

    async writeData(data: Buffer): Promise<void> {
        try {
            this.data = data
        } catch (err: any) {
            throw new Error(`Failed to write file content: ${err.message}`)
        }
    }

    async getDataHash(algo: string = "sha256"): Promise<string> {
        const hash = createHash(algo)
        hash.update(this.data)
        return hash.digest("hex")
    }
}

export type VirtualDirectoryMockCallbackArgs =
    ["has", boolean, string] |
    ["get", VirtualMockedEntry | undefined, string] |
    ["set", VirtualMockedEntry, string, VirtualMockedEntry] |
    ["delete", boolean, string] |
    ["clear", void] |
    ["keys", IterableIterator<string>] |
    ["values", IterableIterator<VirtualMockedEntry>] |
    ["entries", IterableIterator<[string, VirtualMockedEntry]>] |
    ["forEach", void, (value: VirtualMockedEntry, path: string, map: VirtualMockedDirectory) => void, any]

export class VirtualDirectoryMock {
    content: VirtualDirectory = new Map<string, VirtualMockedEntry>()

    constructor(
        public callback: (...args: VirtualDirectoryMockCallbackArgs) => any
    ) {
        this.initializeProxies()
    }

    initializeProxies() {
        const methodsToMock = ["has", "get", "set", "delete", "clear", "keys", "values", "entries", "forEach"] as const
        const chainableMethods = ["set"]
        for (const methodName of methodsToMock) {
            const originalMethod = this.content[methodName as keyof VirtualDirectory] as any;
            (this as any)[methodName] = (...args: any) => {
                const result = originalMethod.apply(this.content, args);
                this.callback(...[methodName, result, ...args] as VirtualDirectoryMockCallbackArgs);
                if (chainableMethods.includes(methodName)) {
                    return this;
                }
                return result;
            };
        }
    }

    get size() {
        return this.content.size
    }

    has!: (path: string) => boolean;
    get!: (path: string) => VirtualMockedEntry | undefined;
    set!: (path: string, value: VirtualMockedEntry) => this;
    delete!: (path: string) => boolean;
    clear!: () => void;
    keys!: () => IterableIterator<string>;
    values!: () => IterableIterator<VirtualMockedEntry>;
    entries!: () => IterableIterator<[string, VirtualMockedEntry]>;
    forEach!: (callback: (value: VirtualMockedEntry, path: string, map: VirtualDirectoryMock) => void, thisArg?: any) => void;
}

export class VirtualDirectoryOverrideMock extends VirtualDirectoryMock {
    initializeProxies() {
        const methodsToMock = ["has", "get", "set", "delete", "clear", "keys", "values", "entries", "forEach"] as const
        const chainableMethods = ["set"]
        for (const methodName of methodsToMock) {
            const originalMethod = this.content[methodName as keyof VirtualDirectory] as any;
            (this as any)[methodName] = (...args: any) => {
                const originalResult = originalMethod.apply(this.content, args);
                const result = this.callback(...[methodName, originalResult, ...args] as VirtualDirectoryMockCallbackArgs);
                if (chainableMethods.includes(methodName)) {
                    return this;
                }
                return result;
            };
        }
    }
}

export type VirtualEntry = VirtualFile | VirtualDirectory
export type VirtualMockedEntry = VirtualEntry | VirtualDirectoryMock
export type VirtualMockedDirectory = VirtualDirectory | VirtualDirectoryMock
export type VirtualDirectory = Map<string, VirtualMockedEntry>

export class VirtualStorageProvider implements IStorageProvider {
    static separator: string = "/"
    files: VirtualMockedDirectory = new Map<string, VirtualMockedEntry>()

    constructor() {
    }

    normalizePath(targetPath: string): string {
        const resolvedPath = path.resolve(targetPath)
        return resolvedPath.replaceAll(/\\/g, VirtualStorageProvider.separator)
    }

    relativePath(targetPath: string, basePath = ".") {
        return path.relative(basePath, targetPath)
    }

    splitPath(path: string): [string, string] {
        const normalizedPath = this.normalizePath(path);
        const lastSeparatorIndex = normalizedPath.lastIndexOf(VirtualStorageProvider.separator);

        if (lastSeparatorIndex === -1) {
            return ["", normalizedPath];
        }

        const dirPath = normalizedPath.substring(0, lastSeparatorIndex);
        const entryName = normalizedPath.substring(lastSeparatorIndex + 1);

        return [dirPath, entryName];
    }

    mkDir(path: string): VirtualMockedDirectory {
        const normalizedPath = this.normalizePath(path);

        if (normalizedPath === "") {
            return this.files;
        }

        const parts = normalizedPath.split(VirtualStorageProvider.separator).filter(p => p !== "");
        let currentDir = this.files;

        if (parts.length > 0 && /^[A-Za-z]:$/.test(parts[0]!)) {
            const diskRoot = parts[0]!;
            let diskDir = currentDir.get(diskRoot);

            if (!diskDir) {
                diskDir = new Map<string, VirtualMockedEntry>();
                currentDir.set(diskRoot, diskDir);
            } else if (diskDir instanceof VirtualFile) {
                throw new Error(`File ${diskRoot} already exists`);
            }

            currentDir = diskDir;
            parts.shift();
        }

        for (const part of parts) {
            let nextDir = currentDir.get(part);

            if (nextDir instanceof VirtualFile) {
                throw new Error(`File ${part} already exists`);
            }
            if (!nextDir) {
                nextDir = new Map<string, VirtualMockedEntry>();
                currentDir.set(part, nextDir);
            }

            currentDir = nextDir;
        }

        return currentDir;
    }

    rm(path: string) {
        const normalizedPath = this.normalizePath(path);
        const [parentPath, entryName] = this.splitPath(normalizedPath);
        if (!entryName) {
            throw new Error("Invalid path");
        }

        const parentDir = parentPath === "" ? this.files : this.getEntry(parentPath);
        if (!parentDir) {
            throw new Error(`Directory ${parentPath} does not exist`);
        }
        if (parentDir instanceof VirtualFile) {
            throw new Error(`'${parentPath}' is a file but treated as a directory`);
        }

        parentDir.delete(entryName);
    }

    getEntry(path: string): VirtualMockedEntry | undefined {
        const normalizedPath = this.normalizePath(path);

        if (normalizedPath === "") {
            return this.files;
        }

        const parts = normalizedPath.split(VirtualStorageProvider.separator).filter(p => p !== "");
        let currentDir = this.files;

        const entry = parts.pop()!
        for (const part of parts) {
            const currentEntry = currentDir.get(part);

            if (!currentEntry) {
                throw new Error(`${normalizedPath} does not exist`);
            }
            if (currentEntry instanceof VirtualFile) {
                throw new Error(`'${part}' in ${normalizedPath} is a file but treated as a directory`);
            }

            currentDir = currentEntry;
        }
        return currentDir.get(entry)
    }

    async exists(path: string) {
        try {
            return this.getEntry(path) !== undefined;
        } catch {
            return false;
        }
    }

    async isFile(path: string) {
        try {
            return this.getEntry(path) instanceof VirtualFile;
        } catch {
            return false;
        }
    }

    async readFile(filePath: string): Promise<VirtualFile> {
        const entry = this.getEntry(filePath);
        if (entry instanceof VirtualFile) {
            return entry;
        }
        const message = entry ? "is not a file" : "does not exist";
        throw new Error(`${filePath} ${message}`);
    }

    async createFile(filePath: string, content: Buffer): Promise<VirtualFile> {
        const normalizedPath = this.normalizePath(filePath);
        const [dirPath, fileName] = this.splitPath(normalizedPath);

        if (!fileName) {
            throw new Error("Invalid path");
        }

        const dir = this.mkDir(dirPath);
        const file = new VirtualFile(normalizedPath, content);
        dir.set(fileName, file);
        return file;
    }

    async moveFile(sourcePath: string, targetPath: string): Promise<VirtualFile> {
        const targetFile = await this.copyFile(sourcePath, targetPath);
        await this.deleteFileOrDir(sourcePath);
        return targetFile;
    }

    async copyFile(sourcePath: string, targetPath: string): Promise<VirtualFile> {
        const sourceFile = this.getEntry(sourcePath);
        if (!sourceFile) {
            throw new Error(`${sourcePath} does not exist`);
        }
        if (!(sourceFile instanceof VirtualFile)) {
            throw new Error(`${sourcePath} is not a file`);
        }
        return await this.createFile(targetPath, sourceFile.data);
    }

    async isDir(path: string) {
        try {
            const entry = this.getEntry(path);
            return !!entry && !(entry instanceof VirtualFile);
        } catch {
            return false;
        }
    }

    async readDir(dirPath: string, ignore: string[] = []): Promise<string[]> {
        const normalizedPath = this.normalizePath(dirPath);
        const entry = this.getEntry(normalizedPath);

        if (!entry) {
            throw new Error(`'${dirPath}' does not exist`);
        }

        if (entry instanceof VirtualFile) {
            throw new Error(`'${dirPath}' is a file but treated as a directory`);
        }

        const separator = normalizedPath ? VirtualStorageProvider.separator : "";
        const prefix = normalizedPath + separator;
        let paths = Array.from(entry.keys()).map(p => this.relativePath(`${prefix}${p}`));

        if (ignore.length > 0) {
            paths = micromatch.not(paths, ignore, {
                dot: true,
                basename: false,
                cwd: "",
                windows: VirtualStorageProvider.separator === "/"
            });
        }

        return paths;
    }

    async readDirDeep(dirPath: string, ignore: string[] = []): Promise<string[]> {
        const normalizedPath = this.normalizePath(dirPath);
        const entry = this.getEntry(normalizedPath);

        if (!entry) {
            throw new Error(`'${dirPath}' does not exist`);
        }

        if (entry instanceof VirtualFile) {
            throw new Error(`'${dirPath}' is a file but treated as a directory`);
        }

        const separator = normalizedPath ? VirtualStorageProvider.separator : "";
        const prefix = normalizedPath + separator;
        let paths = Array.from(entry.keys()).map(p => this.relativePath(`${prefix}${p}`))
        if (ignore.length > 0) {
            paths = micromatch.not(paths, ignore, {
                dot: true,
                basename: false,
                cwd: "",
                windows: VirtualStorageProvider.separator === "/"
            });
        }

        const result = [...paths];
        for (const path of paths) {
            if (await this.isDir(path)) {
                result.push(...await this.readDirDeep(path, ignore));
            }
        }

        return result;
    }

    async createDir(dirPath: string): Promise<string> {
        const normalizedPath = this.normalizePath(dirPath);
        this.mkDir(normalizedPath);
        return this.relativePath(normalizedPath);
    }

    async mockDir(dirPath: string, mock: VirtualMockedDirectory): Promise<string> {
        const normalizedPath = this.normalizePath(dirPath);
        const [parentDirPath, dirName] = this.splitPath(normalizedPath);
        this.mkDir(parentDirPath).set(dirName, mock);
        return this.relativePath(normalizedPath);
    }

    async deleteFileOrDir(path: string): Promise<void> {
        this.rm(path);
    }
}