#include "handle_duplicator.h"
#include <winternl.h>
#include <RestartManager.h>
#include <stdio.h>

#pragma comment(lib, "Rstrtmgr.lib")

typedef NTSTATUS(NTAPI* NtQueryObject_t)(
    HANDLE Handle,
    ULONG ObjectInformationClass,
    PVOID ObjectInformation,
    ULONG ObjectInformationLength,
    PULONG ReturnLength
    );

#define ObjectNameInformation 1
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)

typedef struct _OBJECT_NAME_INFORMATION {
    UNICODE_STRING Name;
    WCHAR NameBuffer[1];
} OBJECT_NAME_INFORMATION, * POBJECT_NAME_INFORMATION;

static bool get_processes_using_file(const wchar_t* file_path, DWORD* pids, DWORD* pid_count, DWORD max_pids) {
    DWORD session;
    WCHAR session_key[CCH_RM_SESSION_KEY + 1] = { 0 };
    DWORD error;

    error = RmStartSession(&session, 0, session_key);
    if (error != ERROR_SUCCESS) {
        return false;
    }

    LPCWSTR file_paths[1] = { file_path };
    error = RmRegisterResources(session, 1, file_paths, 0, NULL, 0, NULL);

    if (error != ERROR_SUCCESS) {
        RmEndSession(session);
        return false;
    }

    DWORD reason;
    UINT proc_info_needed;
    UINT proc_info_count = 10;
    RM_PROCESS_INFO* proc_info = (RM_PROCESS_INFO*)malloc(sizeof(RM_PROCESS_INFO) * proc_info_count);

    if (!proc_info) {
        RmEndSession(session);
        return false;
    }

    error = RmGetList(session, &proc_info_needed, &proc_info_count, proc_info, &reason);

    if (error == ERROR_MORE_DATA) {
        proc_info_count = proc_info_needed;
        free(proc_info);
        proc_info = (RM_PROCESS_INFO*)malloc(sizeof(RM_PROCESS_INFO) * proc_info_count);

        if (!proc_info) {
            RmEndSession(session);
            return false;
        }

        error = RmGetList(session, &proc_info_needed, &proc_info_count, proc_info, &reason);
    }

    bool found = false;
    if (error == ERROR_SUCCESS && proc_info_count > 0) {
        DWORD count = proc_info_count < max_pids ? proc_info_count : max_pids;
        for (UINT i = 0; i < count; i++) {
            pids[i] = proc_info[i].Process.dwProcessId;
        }
        *pid_count = count;
        found = true;
    }

    free(proc_info);
    RmEndSession(session);
    return found;
}

static bool get_file_handle_from_process(DWORD pid, const wchar_t* file_path, HANDLE* out_handle) {
    HANDLE process = OpenProcess(PROCESS_DUP_HANDLE, FALSE, pid);
    if (!process) {
        return false;
    }

    HMODULE ntdll = GetModuleHandleW(L"ntdll.dll");
    if (!ntdll) {
        CloseHandle(process);
        return false;
    }

    NtQueryObject_t pNtQueryObject = (NtQueryObject_t)GetProcAddress(ntdll, "NtQueryObject");
    if (!pNtQueryObject) {
        CloseHandle(process);
        return false;
    }

    wchar_t normalized_path[MAX_PATH];
    if (!GetFullPathNameW(file_path, MAX_PATH, normalized_path, NULL)) {
        CloseHandle(process);
        return false;
    }

    for (wchar_t* p = normalized_path; *p; p++) {
        *p = towlower(*p);
    }

    bool found = false;
    HANDLE current_process = GetCurrentProcess();

    for (DWORD handle_value = 4; handle_value < 0x10000; handle_value += 4) {
        HANDLE dup_handle = NULL;

        if (!DuplicateHandle(process, (HANDLE)(ULONG_PTR)handle_value, current_process,
            &dup_handle, 0, FALSE, DUPLICATE_SAME_ACCESS)) {
            continue;
        }

        DWORD file_type = GetFileType(dup_handle);
        if (file_type != FILE_TYPE_DISK) {
            CloseHandle(dup_handle);
            continue;
        }

        wchar_t handle_path[MAX_PATH] = { 0 };
        DWORD path_len = GetFinalPathNameByHandleW(dup_handle, handle_path, MAX_PATH, FILE_NAME_NORMALIZED);

        if (path_len > 0 && path_len < MAX_PATH) {
            wchar_t* actual_path = handle_path;
            if (wcsncmp(handle_path, L"\\\\?\\", 4) == 0) {
                actual_path = handle_path + 4;
            }

            wchar_t handle_normalized[MAX_PATH];
            if (GetFullPathNameW(actual_path, MAX_PATH, handle_normalized, NULL)) {
                for (wchar_t* p = handle_normalized; *p; p++) {
                    *p = towlower(*p);
                }

                if (wcscmp(handle_normalized, normalized_path) == 0) {
                    *out_handle = dup_handle;
                    found = true;
                    break;
                }
            }
        }

        CloseHandle(dup_handle);
    }

    CloseHandle(process);
    return found;
}

bool copy_locked_file(const wchar_t* source_path, const wchar_t* dest_path) {
    DWORD pids[32];
    DWORD pid_count = 0;

    if (!get_processes_using_file(source_path, pids, &pid_count, 32)) {
        if (CopyFileW(source_path, dest_path, FALSE)) {
            return true;
        }
        return false;
    }

    HANDLE file_handle = NULL;
    for (DWORD i = 0; i < pid_count; i++) {
        if (get_file_handle_from_process(pids[i], source_path, &file_handle)) {
            break;
        }
    }

    if (!file_handle) {
        return false;
    }

    LARGE_INTEGER file_size;
    if (!GetFileSizeEx(file_handle, &file_size)) {
        CloseHandle(file_handle);
        return false;
    }

    BYTE* buffer = (BYTE*)malloc((size_t)file_size.QuadPart);
    if (!buffer) {
        CloseHandle(file_handle);
        return false;
    }

    SetFilePointer(file_handle, 0, NULL, FILE_BEGIN);

    DWORD bytes_read = 0;
    BOOL read_result = ReadFile(file_handle, buffer, (DWORD)file_size.QuadPart, &bytes_read, NULL);

    CloseHandle(file_handle);

    if (!read_result || bytes_read != file_size.QuadPart) {
        free(buffer);
        return false;
    }

    HANDLE dest_file = CreateFileW(dest_path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if (dest_file == INVALID_HANDLE_VALUE) {
        free(buffer);
        return false;
    }

    DWORD bytes_written = 0;
    BOOL write_result = WriteFile(dest_file, buffer, bytes_read, &bytes_written, NULL);

    CloseHandle(dest_file);
    free(buffer);

    if (!write_result || bytes_written != bytes_read) {
        return false;
    }

    return true;
}