/*******************************************************************************
 * Copyright (c) 2025, Jan Koester jan.koester@gmx.net
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 * Neither the name of the <organization> nor the
 *      names of its contributors may be used to endorse or promote products
 *      derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *******************************************************************************/

#include <cstring>
#include <sstream>
#include <exception>
#include <vector>
#include <map>
#include <tuple>

#ifdef Windows
#include <windows.h>
#else
#include <unistd.h>
#include <fcntl.h>
#include <sys/file.h>
#include <sys/stat.h>
#endif

#include "file.h"

authdb::File::File(const char* path,size_t version,const char *domain) : _filePath(path) {
#ifdef Windows
    // Attempt to open the file.
    _FileDes = INVALID_HANDLE_VALUE;
	_LockDes = INVALID_HANDLE_VALUE;

    _FileDes = CreateFile(
        path,                  // File path
        GENERIC_READ | GENERIC_WRITE, // Desired access: read and write
        FILE_SHARE_READ | FILE_SHARE_WRITE,       // Share mode: allow other processes to read
        NULL,                  // Security attributes (use default)
        OPEN_EXISTING,         // Creation disposition: open existing file
        FILE_ATTRIBUTE_NORMAL, // File attributes
        NULL);                 // Template file

    // Check if opening the file succeeded.
    if (_FileDes == INVALID_HANDLE_VALUE) {
        // File does not exist, so try to create it.
        _FileDes = CreateFile(
            path,
            GENERIC_READ | GENERIC_WRITE,
            FILE_SHARE_READ,
            NULL,
            CREATE_NEW, // Create a new file, fails if it exists
            FILE_ATTRIBUTE_NORMAL,
            NULL);

        if (_FileDes == INVALID_HANDLE_VALUE) {
            // Failed to create the file.
            std::string err = "could not create file: ";
            err += path;
            err += "!";
            throw AuthBackendError(err.c_str());
        }

        // File was successfully created. Write the header.
        AuthHeader head;
        head.Version = version;
        head.Revesion = 0;
        head.start = 0xFE;
        memset(head.domain, 0, 255);
        _snprintf(head.domain, 255, "%s", domain); // Use _snprintf for safety

        DWORD bytesWritten;
        if (!WriteFile(_FileDes, &head, sizeof(head), &bytesWritten, NULL) || bytesWritten != sizeof(head)) {
            // Failed to write the header.
            std::string err = "could not write header to file: ";
            err += path;
            err += "!";
            CloseHandle(_FileDes); // Clean up the handle
            throw AuthBackendError(err.c_str());
        }
    }

    // Set up the lock file path (assuming _lockFile is a std::string).
    _lockFile = std::string(path) + ".lock";

    // A final check for the handle, though it should be valid here.
    if (_FileDes == INVALID_HANDLE_VALUE) {
        std::string err = "could not open file: ";
        err += path;
        err += "!";
        throw AuthBackendError(err.c_str());
    }
#else
    if( (_FileDes=open(path,O_RDWR)) <0){

        _FileDes=open(path,O_CREAT|O_RDWR,0660);

        AuthHeader head;

        head.Version=version;
        head.Revesion=0;
        head.start=0xFE;
        memset(head.domain,0,255);
        snprintf(head.domain,255,"%s",domain);

        if(_FileDes < 0 || ::write(_FileDes,&head,sizeof(head)) < 0){
            std::string err="could not create file: ";
            err+=path;
            err+="!";
            throw AuthBackendError(err.c_str());
        }

    }

    _lockFile = path;
    _lockFile += ".lock";

    if(_FileDes<0){
        std::string err="could not open file: ";
        err+=path;
        err+="!";
        throw AuthBackendError(err.c_str());
    }
#endif
}

authdb::File::~File(){
#ifdef Windows
    if(_FileDes != INVALID_HANDLE_VALUE)
        CloseHandle(_FileDes);
#else
    if(_FileDes >= 0)
        close(_FileDes);
#endif
}

void authdb::File::read(unsigned char* src, size_t srcsize){
#ifdef Windows
    DWORD bytesRead;
    size_t totalBytesRead = 0;

    // Use a loop to ensure we read all the requested bytes,
    // as ReadFile can return before reading all of them.
    while (totalBytesRead < srcsize) {
        // Read the remaining bytes.
        DWORD bytesToRead = (DWORD)(srcsize - totalBytesRead);

        if (!ReadFile(
            _FileDes,              // The file handle
            src + totalBytesRead,     // Pointer to the buffer
            bytesToRead,              // Number of bytes to read
            &bytesRead,               // Pointer to store bytes read
            NULL))                    // Not using overlapped I/O
        {
            // If ReadFile returns FALSE, an error occurred.
            // GetLastError() provides the specific error code.
            std::string err = "could not read file (error: " + std::to_string(GetLastError()) + ")!";
            throw AuthBackendError(err.c_str());
        }

        // Check if we reached the end of the file prematurely.
        if (bytesRead == 0 && bytesToRead > 0) {
            // This condition is usually met when the end of the file is reached
            // before the requested number of bytes could be read.
            throw AuthBackendError("unexpected end of file!");
        }

        // Update the total bytes read.
        totalBytesRead += bytesRead;
    }
#else
    int pos=0;
    do{
        int rd=::read(_FileDes,src+pos,srcsize);
        if(rd<0){
            throw AuthBackendError("could not read file!");
        }
        pos+=rd;
        srcsize-=rd;
    }while(srcsize!=0);
#endif
}

void authdb::File::write(const unsigned char* dest, size_t destsize){
#ifdef Windows
    DWORD bytesWritten;
    size_t bytesRemaining = destsize;
    size_t totalBytesWritten = 0;

    // Loop until all bytes have been written.
    while (bytesRemaining > 0) {
        // Cast the remaining size to DWORD, as required by WriteFile.
        DWORD bytesToWrite = (DWORD)bytesRemaining;

        // Write to the file.
        if (!WriteFile(
            _FileDes,              // The file handle.
            dest + totalBytesWritten, // Pointer to the current position in the buffer.
            bytesToWrite,             // Number of bytes to write in this call.
            &bytesWritten,            // A variable to receive the number of bytes written.
            NULL))                    // Not using overlapped I/O.
        {
            // If WriteFile returns FALSE, an error occurred.
            std::string err = "could not write file (error: " + std::to_string(GetLastError()) + ")!";
            throw AuthBackendError(err.c_str());
        }

        // Update our counters with the number of bytes actually written.
        totalBytesWritten += bytesWritten;
        bytesRemaining -= bytesWritten;
    }
#else
    int pos=0;
    do{
        int wr = ::write(_FileDes,dest+pos,destsize);
        if(wr<0){
            throw AuthBackendError("could not write file!");
        }
        pos+=wr;
        destsize-=wr;
    }while(destsize!=0);
#endif
}

void authdb::File::lock(){
#ifdef Windows
    for (int attempt=0;;++attempt) {
        // Step 1: Open or create the lock file.
        // We use a share mode of 0 to get an exclusive handle, which is a form of locking in itself.
        _LockDes = CreateFileA(
            _lockFile.c_str(),
            GENERIC_READ | GENERIC_WRITE,
            0,                     // dwShareMode: No sharing (exclusive access)
            NULL,                  // lpSecurityAttributes: Default security
            OPEN_ALWAYS,           // dwCreationDisposition: Open if exists, create if not
            FILE_ATTRIBUTE_NORMAL, // dwFlagsAndAttributes
            NULL);                 // hTemplateFile

        if (_LockDes == INVALID_HANDLE_VALUE) {
            if (attempt > 100) {
                throw AuthBackendError("couldn't open lockfile: too many retries");
            }
            Sleep(10);
            continue;
        }

        // Step 2: Attempt to acquire an exclusive lock on the file.
        OVERLAPPED overlapped = { 0 }; // LockFileEx requires an OVERLAPPED structure.

        if (!LockFileEx(
            _LockDes,
            LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY, // Exclusive and non-blocking
            0,                     // dwReserved
            1, 0,                  // nNumberOfBytesToLock: Lock a single byte
            &overlapped))
        {
            // Lock failed.
            DWORD error = GetLastError();
            CloseHandle(_LockDes); // IMPORTANT: Close the handle to avoid resource leaks.

            // If the lock failed because another process holds it, we continue the loop.
            // This is the equivalent of the Unix 'default: continue' switch case.
            if (error == ERROR_LOCK_VIOLATION) {
                continue;
            }
            else {
                // A different, unrecoverable error occurred. Throw an exception.
                std::string err = "couldn't lock the lockfile (error code: ";
                err += std::to_string(error);
                err += ")!";
                throw AuthBackendError(err.c_str());
            }
        }
        else {
            // Lock acquired successfully.
            return;
        }
    }
#else
    for(int attempt=0;;++attempt){
        mode_t m = umask( 0 );
        _LockDes = open( _lockFile.c_str(), O_RDWR|O_CREAT, 0666 );
        umask( m );

        if(_LockDes<0){
            if(attempt > 100){
                throw AuthBackendError("couldn't open lockfile: too many retries");
            }
            usleep(10000);
            continue;
        }

         if( flock( _LockDes, LOCK_EX)  != 0 ){
            int err = errno;
            close(_LockDes);
            _LockDes = -1;
            switch(err){
                case ENOLCK:
                case EINVAL:{
                    std::string msg = "couldn't lock the lockfile: ";
                    msg+=strerror(err);
                    throw AuthBackendError(msg.c_str());
                }
                default:
                    continue;
            }
        }else{
            return;
        }
    }
#endif
}

void authdb::File::unlock(){
#ifdef Windows
    OVERLAPPED overlapped = { 0 };
    UnlockFileEx(
        _LockDes,
        0,     // dwReserved
        1, 0,  // nNumberOfBytesToUnlock: Unlock a single byte
        &overlapped);
    CloseHandle(_LockDes);
    DeleteFileA(_lockFile.c_str());
#else
    flock(_LockDes,LOCK_UN);
    close(_LockDes);
    remove(_lockFile.c_str());
#endif
}

void authdb::File::setPos(size_t pos){
#ifdef Windows
    LARGE_INTEGER liPos;
    liPos.QuadPart = pos; 

    if (!SetFilePointerEx(_FileDes, liPos, NULL, FILE_BEGIN)) {
        DWORD errCode = GetLastError();
        std::stringstream ss;
        ss << "could not set to pos: " << pos << " on file. Windows error code: " << errCode;
        throw AuthBackendError(ss.str());
    }
#else
    if(lseek(_FileDes,pos,SEEK_SET)<0){
        std::string err = "could not set to pos: ";
        err += pos;
        err += " on file: ";
        err+= strerror(errno);
        throw AuthBackendError(err.c_str());
    }
#endif
}

size_t authdb::File::getPos(){
#ifdef Windows
    LARGE_INTEGER li;
    li.QuadPart = 0;
    LARGE_INTEGER newPos;
    if (!SetFilePointerEx(_FileDes, li, &newPos, FILE_CURRENT)) {
        throw AuthBackendError("could not get position!");
    }
    return static_cast<size_t>(newPos.QuadPart);
#else
    int pos = lseek(_FileDes,0,SEEK_CUR);
    if(pos<0){
        throw AuthBackendError("could not get position!");
    }
    return pos;
#endif
}

size_t authdb::File::end(){
#ifdef Windows
    LARGE_INTEGER liZero = { 0 };
    LARGE_INTEGER newPos;
    if (!SetFilePointerEx(_FileDes, liZero, &newPos, FILE_END)) {
        // Handle error: GetLastError() would provide more info
        throw AuthBackendError("could not find end!");
    }
    return newPos.QuadPart;
#else
    int pos = lseek(_FileDes,0,SEEK_END);
    if(pos<0){
        throw AuthBackendError("could not find end!");
    }
    return pos;
#endif
}

size_t authdb::File::getRevesion(){
#ifdef Windows
    LARGE_INTEGER cur;
    SetFilePointerEx(_FileDes, { 0 }, &cur, FILE_CURRENT);
    SetFilePointerEx(_FileDes, { 0 }, NULL, FILE_BEGIN);
    AuthHeader head;
    DWORD bytesRead;
    if (!ReadFile(_FileDes, &head, sizeof(head), &bytesRead, NULL) || bytesRead != sizeof(head)) {
        // Handle error and short reads
        throw AuthBackendError("Could not read auth header!");
    }
    SetFilePointerEx(_FileDes, cur, NULL, FILE_BEGIN);
    return head.Revesion;
#else
    size_t cur=getPos();

    setPos(0);

    AuthHeader head;
    if (::read(_FileDes,&head,sizeof(head)) != sizeof(head)) {
        setPos(cur);
        return 0;
    }

    setPos(cur);

    return head.Revesion;
#endif
}

void authdb::File::newRevesion(){
#ifdef Windows

#else
    size_t cur=getPos();
    setPos(0);
    AuthHeader head;
    if (::read(_FileDes,&head,sizeof(head)) != sizeof(head)) {
        setPos(cur);
        return;
    }
    ++head.Revesion;
    setPos(0);
    if (::write(_FileDes,&head,sizeof(head)) != sizeof(head)) {
        setPos(cur);
        return;
    }
    setPos(cur);
#endif
}

void authdb::File::vacuum(){
#ifdef Windows
    // 1. Read header
    LARGE_INTEGER liZero = { 0 };
    SetFilePointerEx(_FileDes, liZero, NULL, FILE_BEGIN);

    AuthHeader head;
    DWORD bytesRead;
    if (!ReadFile(_FileDes, &head, sizeof(head), &bytesRead, NULL) || bytesRead != sizeof(head)) {
        throw AuthBackendError("vacuum: could not read header!");
    }

    // 2. Collect all valid records
    struct RecordEntry {
        AuthData::Record rec;
        std::vector<unsigned char> data;
    };

    // Use a map keyed by (ruid_string, fieldname, type) to deduplicate
    // keeping the last occurrence
    std::map<std::tuple<std::string,std::string,int>, RecordEntry> dedup;
    std::vector<std::tuple<std::string,std::string,int>> insertOrder;

    LARGE_INTEGER liEnd;
    SetFilePointerEx(_FileDes, liZero, &liEnd, FILE_END);
    size_t fileEnd = static_cast<size_t>(liEnd.QuadPart);
    size_t rd = sizeof(AuthHeader);

    while (rd < fileEnd) {
        LARGE_INTEGER liPos;
        liPos.QuadPart = rd;
        SetFilePointerEx(_FileDes, liPos, NULL, FILE_BEGIN);

        RecordEntry entry;
        memset(&entry.rec, 0, sizeof(AuthData::Record));
        DWORD br;
        if (!ReadFile(_FileDes, &entry.rec, sizeof(AuthData::Record), &br, NULL) || br != sizeof(AuthData::Record)) {
            break;
        }

        size_t datasize = entry.rec.datasize;
        rd = rd + sizeof(AuthData::Record) + datasize;

        if (entry.rec.type == DataType::EmptyData) {
            continue;
        }

        entry.data.resize(datasize);
        if (datasize > 0) {
            DWORD dr;
            if (!ReadFile(_FileDes, entry.data.data(), (DWORD)datasize, &dr, NULL) || dr != datasize) {
                break;
            }
        }

        std::string ruid_str(reinterpret_cast<char*>(entry.rec.ruid), 16);
        std::string fname(entry.rec.fieldname);
        auto key = std::make_tuple(ruid_str, fname, entry.rec.type);

        if (dedup.find(key) == dedup.end()) {
            insertOrder.push_back(key);
        }
        dedup[key] = std::move(entry);
    }

    // 3. Rewrite: header + valid records
    SetFilePointerEx(_FileDes, liZero, NULL, FILE_BEGIN);

    DWORD bw;
    WriteFile(_FileDes, &head, sizeof(head), &bw, NULL);

    for (auto &key : insertOrder) {
        auto it = dedup.find(key);
        if (it == dedup.end()) continue;
        auto &entry = it->second;
        entry.rec.data = nullptr;
        WriteFile(_FileDes, &entry.rec, sizeof(AuthData::Record), &bw, NULL);
        if (!entry.data.empty()) {
            WriteFile(_FileDes, entry.data.data(), (DWORD)entry.data.size(), &bw, NULL);
        }
    }

    // 4. Truncate file at current position
    SetEndOfFile(_FileDes);

    ++head.Revesion;
    SetFilePointerEx(_FileDes, liZero, NULL, FILE_BEGIN);
    WriteFile(_FileDes, &head, sizeof(head), &bw, NULL);

#else
    // 1. Read header
    lseek(_FileDes, 0, SEEK_SET);

    AuthHeader head;
    if (::read(_FileDes, &head, sizeof(head)) != sizeof(head)) {
        throw AuthBackendError("vacuum: could not read header!");
    }

    // 2. Collect all valid records
    struct RecordEntry {
        AuthData::Record rec;
        std::vector<unsigned char> data;
    };

    // Use a map keyed by (ruid_string, fieldname, type) to deduplicate
    // keeping the last occurrence
    std::map<std::tuple<std::string,std::string,int>, RecordEntry> dedup;
    std::vector<std::tuple<std::string,std::string,int>> insertOrder;

    off_t fileEndOff = lseek(_FileDes, 0, SEEK_END);
    if (fileEndOff < 0) {
        throw AuthBackendError("vacuum: lseek failed");
    }
    size_t fileEnd = static_cast<size_t>(fileEndOff);
    size_t rd = sizeof(AuthHeader);

    while (rd < fileEnd) {
        lseek(_FileDes, rd, SEEK_SET);

        RecordEntry entry;
        entry.rec = AuthData::Record();
        ssize_t br = ::read(_FileDes, &entry.rec, sizeof(AuthData::Record));
        if (br != sizeof(AuthData::Record)) {
            break;
        }

        size_t datasize = entry.rec.datasize;
        rd = rd + sizeof(AuthData::Record) + datasize;

        // Skip deleted/empty records
        if (entry.rec.type == DataType::EmptyData) {
            continue;
        }

        entry.data.resize(datasize);
        if (datasize > 0) {
            ssize_t dr = ::read(_FileDes, entry.data.data(), datasize);
            if (dr != (ssize_t)datasize) {
                break;
            }
        }

        // Build dedup key from (ruid, fieldname, type)
        std::string ruid_str(reinterpret_cast<char*>(entry.rec.ruid), 16);
        std::string fname(entry.rec.fieldname);
        auto key = std::make_tuple(ruid_str, fname, entry.rec.type);

        if (dedup.find(key) == dedup.end()) {
            insertOrder.push_back(key);
        }
        dedup[key] = std::move(entry);
    }

    // 3. Rewrite: header + valid records
    lseek(_FileDes, 0, SEEK_SET);
    if (::write(_FileDes, &head, sizeof(head)) != sizeof(head)) {
        throw AuthBackendError("vacuum: failed to write header");
    }

    for (auto &key : insertOrder) {
        auto it = dedup.find(key);
        if (it == dedup.end()) continue;
        auto &entry = it->second;
        entry.rec.data = nullptr; // pointer not meaningful on disk
        if (::write(_FileDes, &entry.rec, sizeof(AuthData::Record)) != sizeof(AuthData::Record)) {
            throw AuthBackendError("vacuum: failed to write record");
        }
        if (!entry.data.empty()) {
            if (::write(_FileDes, entry.data.data(), entry.data.size()) != (ssize_t)entry.data.size()) {
                throw AuthBackendError("vacuum: failed to write record data");
            }
        }
    }

    // 4. Truncate file at current write position
    off_t newEnd = lseek(_FileDes, 0, SEEK_CUR);
    if (newEnd < 0 || ftruncate(_FileDes, newEnd) != 0) {
        throw AuthBackendError("vacuum: failed to truncate file");
    }

    // 5. Bump revision
    ++head.Revesion;
    lseek(_FileDes, 0, SEEK_SET);
    if (::write(_FileDes, &head, sizeof(head)) != sizeof(head)) {
        throw AuthBackendError("vacuum: failed to write updated header");
    }
#endif
}

void authdb::File::purge(){
#ifdef Windows
    if(_FileDes != INVALID_HANDLE_VALUE){
        CloseHandle(_FileDes);
        _FileDes = INVALID_HANDLE_VALUE;
    }
    DeleteFileA(_filePath.c_str());
    DeleteFileA(_lockFile.c_str());
#else
    if(_FileDes >= 0){
        close(_FileDes);
        _FileDes = -1;
    }
    unlink(_filePath.c_str());
    unlink(_lockFile.c_str());
#endif
}
