/**
 * ProjFS N-API Bridge
 * 
 * Cross-platform FUSE3 API bridge for Windows ProjFS
 * Uses ThreadSafeFunction for safe JavaScript callbacks from native code
 */

#include <napi.h>
#include <windows.h>
#include <projectedfslib.h>
#pragma comment(lib, "ProjectedFSLib.lib")

#include <string>
#include <memory>
#include <mutex>

// Helper functions
std::string WideToUtf8(const std::wstring& wide) {
    if (wide.empty()) return "";
    int size = WideCharToMultiByte(CP_UTF8, 0, wide.c_str(), -1, nullptr, 0, nullptr, nullptr);
    std::string result(size - 1, 0);
    WideCharToMultiByte(CP_UTF8, 0, wide.c_str(), -1, result.data(), size, nullptr, nullptr);
    return result;
}

std::wstring Utf8ToWide(const std::string& utf8) {
    if (utf8.empty()) return L"";
    int size = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, nullptr, 0);
    std::wstring result(size - 1, 0);
    MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, result.data(), size);
    return result;
}

// Context structure to hold callback and state
struct BridgeContext {
    Napi::ThreadSafeFunction readdirCallback;
    PRJ_NAMESPACE_VIRTUALIZATION_CONTEXT projfsContext = nullptr;
    std::mutex mutex;
    bool isRunning = false;
};

// ProjFS callbacks using ThreadSafeFunction (safer than global references)
HRESULT GetPlaceholderInfoCallback(
    const PRJ_CALLBACK_DATA* callbackData
) {
    // Minimal implementation - just return success
    return S_OK;
}

HRESULT StartDirectoryEnumerationCallback(
    const PRJ_CALLBACK_DATA* callbackData,
    const GUID* enumerationId
) {
    return S_OK;
}

HRESULT GetDirectoryEnumerationCallback(
    const PRJ_CALLBACK_DATA* callbackData,
    const GUID* enumerationId,
    PCWSTR searchExpression,  
    PRJ_DIR_ENTRY_BUFFER_HANDLE dirEntryBufferHandle
) {
    auto context = static_cast<BridgeContext*>(callbackData->InstanceContext);
    
    if (!context) {
        return HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND);
    }
    
    try {
        std::string relativePath = WideToUtf8(callbackData->FilePathName);
        std::string fusePath = "/" + relativePath;
        
        HRESULT result = S_OK;
        auto callback = [&](Napi::Env env, Napi::Function jsCallback) {
            try {
                Napi::Value jsResult = jsCallback.Call({ Napi::String::New(env, fusePath) });
                
                if (jsResult.IsArray()) {
                    Napi::Array entries = jsResult.As<Napi::Array>();
                    
                    for (uint32_t i = 0; i < entries.Length(); i++) {
                        Napi::Value entry = entries.Get(i);
                        std::string name;
                        
                        if (entry.IsString()) {
                            name = entry.As<Napi::String>().Utf8Value();
                        } else if (entry.IsObject()) {
                            Napi::Object obj = entry.As<Napi::Object>();
                            if (obj.Has("name")) {
                                name = obj.Get("name").As<Napi::String>().Utf8Value();
                            }
                        }
                        
                        if (name.empty() || name == "." || name == "..") continue;
                        
                        // Create file info
                        PRJ_FILE_BASIC_INFO fileInfo = {};
                        fileInfo.IsDirectory = true; // Assume directory for simplicity
                        fileInfo.FileSize = 0;
                        
                        FILETIME ft;
                        GetSystemTimeAsFileTime(&ft);
                        fileInfo.CreationTime.LowPart = ft.dwLowDateTime;
                        fileInfo.CreationTime.HighPart = ft.dwHighDateTime;
                        fileInfo.LastWriteTime = fileInfo.CreationTime;
                        fileInfo.LastAccessTime = fileInfo.CreationTime;
                        fileInfo.ChangeTime = fileInfo.CreationTime;
                        
                        fileInfo.FileAttributes = FILE_ATTRIBUTE_DIRECTORY;
                        
                        std::wstring wname = Utf8ToWide(name);
                        if (PrjFillDirEntryBuffer(wname.c_str(), &fileInfo, dirEntryBufferHandle) != S_OK) {
                            break;
                        }
                    }
                }
            } catch (...) {
                result = HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND);
            }
        };
        
        // Use BlockingCall to safely invoke JavaScript from C++
        context->readdirCallback.BlockingCall(callback);
        return result;
        
    } catch (...) {
        return HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND);
    }
}

HRESULT EndDirectoryEnumerationCallback(
    const PRJ_CALLBACK_DATA* callbackData,
    const GUID* enumerationId
) {
    return S_OK;
}

// Mount class using ThreadSafeFunction
class ProjFSMount : public Napi::ObjectWrap<ProjFSMount> {
private:
    std::wstring rootPath;
    std::unique_ptr<BridgeContext> context;
    
public:
    static Napi::Object Init(Napi::Env env, Napi::Object exports) {
        Napi::Function func = DefineClass(env, "ProjFSMount", {
            InstanceMethod("mount", &ProjFSMount::Mount),
            InstanceMethod("unmount", &ProjFSMount::Unmount),
            InstanceMethod("isMounted", &ProjFSMount::IsMounted),
        });
        
        exports.Set("ProjFSMount", func);
        return exports;
    }
    
    ProjFSMount(const Napi::CallbackInfo& info) : Napi::ObjectWrap<ProjFSMount>(info) {
        if (info.Length() < 1 || !info[0].IsString()) {
            Napi::TypeError::New(info.Env(), "Expected mountPath").ThrowAsJavaScriptException();
            return;
        }
        
        std::string mountPath = info[0].As<Napi::String>().Utf8Value();
        rootPath = Utf8ToWide(mountPath);
        context = std::make_unique<BridgeContext>();
    }
    
    Napi::Value Mount(const Napi::CallbackInfo& info) {
        std::lock_guard<std::mutex> lock(context->mutex);
        
        if (context->isRunning) {
            return info.Env().Undefined();
        }
        
        if (info.Length() < 1 || !info[0].IsObject()) {
            Napi::TypeError::New(info.Env(), "Expected operations object").ThrowAsJavaScriptException();
            return info.Env().Undefined();
        }
        
        Napi::Object ops = info[0].As<Napi::Object>();
        if (!ops.Has("readdir")) {
            Napi::TypeError::New(info.Env(), "Operations must have readdir function").ThrowAsJavaScriptException();
            return info.Env().Undefined();
        }
        
        // Create ThreadSafeFunction for readdir (this is the key fix)
        context->readdirCallback = Napi::ThreadSafeFunction::New(
            info.Env(),
            ops.Get("readdir").As<Napi::Function>(),
            "readdir",
            0,  // max queue size (0 = unlimited)
            1   // initial thread count
        );
        
        // Mark as placeholder
        GUID instanceId;
        HRESULT hr = PrjMarkDirectoryAsPlaceholder(rootPath.c_str(), nullptr, nullptr, &instanceId);
        if (FAILED(hr) && hr != HRESULT_FROM_WIN32(ERROR_REPARSE_POINT_ENCOUNTERED)) {
            context->readdirCallback.Release();
            Napi::Error::New(info.Env(), "Failed to mark placeholder").ThrowAsJavaScriptException();
            return info.Env().Undefined();
        }
        
        // Set up callbacks
        PRJ_CALLBACKS callbacks = {};
        callbacks.GetPlaceholderInfoCallback = GetPlaceholderInfoCallback;
        callbacks.StartDirectoryEnumerationCallback = StartDirectoryEnumerationCallback;
        callbacks.GetDirectoryEnumerationCallback = GetDirectoryEnumerationCallback;
        callbacks.EndDirectoryEnumerationCallback = EndDirectoryEnumerationCallback;
        
        // Start virtualization with context
        hr = PrjStartVirtualizing(rootPath.c_str(), &callbacks, context.get(), nullptr, &context->projfsContext);
        if (FAILED(hr)) {
            context->readdirCallback.Release();
            Napi::Error::New(info.Env(), "Failed to start virtualization").ThrowAsJavaScriptException();
            return info.Env().Undefined();
        }
        
        context->isRunning = true;
        return info.Env().Undefined();
    }
    
    Napi::Value Unmount(const Napi::CallbackInfo& info) {
        std::lock_guard<std::mutex> lock(context->mutex);
        
        if (!context->isRunning) {
            return info.Env().Undefined();
        }
        
        if (context->projfsContext) {
            PrjStopVirtualizing(context->projfsContext);
            context->projfsContext = nullptr;
        }
        
        context->readdirCallback.Release();
        
        context->isRunning = false;
        return info.Env().Undefined();
    }
    
    Napi::Value IsMounted(const Napi::CallbackInfo& info) {
        return Napi::Boolean::New(info.Env(), context->isRunning);
    }
};

Napi::Object Init(Napi::Env env, Napi::Object exports) {
    return ProjFSMount::Init(env, exports);
}

NODE_API_MODULE(projfs_fuse, Init)