#include "file_watcher.h"

#include "core/file_poll_diff.h"

#include <cstring>
#include <unordered_map>
#include <vector>
#include <string>

#if defined(__linux__)
  #include <sys/inotify.h>
  #include <unistd.h>
  #include <fcntl.h>
  #include <errno.h>
  #include <poll.h>
#elif defined(_WIN32)
  #ifndef WIN32_LEAN_AND_MEAN
    #define WIN32_LEAN_AND_MEAN
  #endif
  #include <windows.h>
#endif

namespace fn {

#if defined(__linux__)

struct FileWatcher {
    int                                       fd = -1;
    std::unordered_map<int, std::string>      wd_to_path;  // inotify wd -> path
    std::string                               last_err;
};

FileWatcher* file_watcher_create() {
    auto* w = new FileWatcher();
    w->fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
    if (w->fd < 0) {
        w->last_err = std::string("inotify_init1: ") + std::strerror(errno);
    }
    return w;
}

void file_watcher_destroy(FileWatcher* w) {
    if (!w) return;
    if (w->fd >= 0) ::close(w->fd);
    delete w;
}

bool file_watcher_add(FileWatcher* w, const char* path) {
    if (!w || w->fd < 0 || !path) return false;
    const uint32_t mask = IN_MODIFY | IN_CREATE | IN_DELETE
                        | IN_CLOSE_WRITE | IN_MOVED_FROM | IN_MOVED_TO
                        | IN_DELETE_SELF | IN_MOVE_SELF;
    int wd = inotify_add_watch(w->fd, path, mask);
    if (wd < 0) {
        w->last_err = std::string("inotify_add_watch(") + path + "): " + std::strerror(errno);
        return false;
    }
    w->wd_to_path[wd] = path;
    w->last_err.clear();
    return true;
}

std::vector<FileEvent> file_watcher_poll(FileWatcher* w) {
    std::vector<FileEvent> out;
    if (!w || w->fd < 0) return out;

    // Drain todos los eventos pendientes sin bloquear.
    char buf[4096] __attribute__((aligned(__alignof__(struct inotify_event))));
    while (true) {
        ssize_t n = ::read(w->fd, buf, sizeof(buf));
        if (n <= 0) {
            if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
                w->last_err = std::string("inotify read: ") + std::strerror(errno);
            }
            break;
        }
        for (char* p = buf; p < buf + n; ) {
            auto* ev = reinterpret_cast<struct inotify_event*>(p);
            FileEvent fe;
            auto it = w->wd_to_path.find(ev->wd);
            std::string base = (it != w->wd_to_path.end()) ? it->second : std::string();
            if (ev->len > 0) {
                if (!base.empty() && base.back() != '/') base += '/';
                base += ev->name;
            }
            fe.path = base;

            if (ev->mask & (IN_CREATE | IN_MOVED_TO)) {
                fe.kind = FileEvent::Created;
                out.push_back(fe);
            }
            if (ev->mask & (IN_DELETE | IN_DELETE_SELF | IN_MOVED_FROM | IN_MOVE_SELF)) {
                fe.kind = FileEvent::Deleted;
                out.push_back(fe);
            }
            if (ev->mask & (IN_MODIFY | IN_CLOSE_WRITE)) {
                fe.kind = FileEvent::Modified;
                out.push_back(fe);
            }
            p += sizeof(struct inotify_event) + ev->len;
        }
    }
    return out;
}

const char* file_watcher_last_error(const FileWatcher* w) {
    return w ? w->last_err.c_str() : "";
}

#elif defined(_WIN32)

// Una entry por directorio vigilado: un OVERLAPPED + buffer + handle.
// Para "vigilar un archivo" en Windows registramos su directorio padre y
// filtramos por nombre en el poll().
struct WinWatch {
    HANDLE     dir       = INVALID_HANDLE_VALUE;
    OVERLAPPED ovl       = {};
    std::vector<uint8_t> buf;
    std::string dir_path;     // directorio absoluto vigilado
    std::string filter_name;  // si !empty: solo emitir eventos cuyo path == dir_path/filter_name
    bool       pending   = false;
};

struct FileWatcher {
    std::vector<WinWatch*> watches;
    std::string            last_err;
};

static void start_read(WinWatch* ww) {
    if (ww->dir == INVALID_HANDLE_VALUE) return;
    DWORD bytes = 0;
    BOOL ok = ::ReadDirectoryChangesW(
        ww->dir,
        ww->buf.data(),
        (DWORD)ww->buf.size(),
        FALSE, // bWatchSubtree
        FILE_NOTIFY_CHANGE_FILE_NAME |
        FILE_NOTIFY_CHANGE_DIR_NAME  |
        FILE_NOTIFY_CHANGE_LAST_WRITE|
        FILE_NOTIFY_CHANGE_SIZE      |
        FILE_NOTIFY_CHANGE_CREATION,
        &bytes,
        &ww->ovl,
        NULL);
    ww->pending = (ok != 0);
}

FileWatcher* file_watcher_create() {
    return new FileWatcher();
}

void file_watcher_destroy(FileWatcher* w) {
    if (!w) return;
    for (auto* ww : w->watches) {
        if (ww->dir != INVALID_HANDLE_VALUE) ::CloseHandle(ww->dir);
        if (ww->ovl.hEvent) ::CloseHandle(ww->ovl.hEvent);
        delete ww;
    }
    delete w;
}

static std::string dirname_of(const std::string& path) {
    size_t pos = path.find_last_of("\\/");
    if (pos == std::string::npos) return ".";
    return path.substr(0, pos);
}
static std::string basename_of(const std::string& path) {
    size_t pos = path.find_last_of("\\/");
    if (pos == std::string::npos) return path;
    return path.substr(pos + 1);
}

bool file_watcher_add(FileWatcher* w, const char* path) {
    if (!w || !path) return false;
    DWORD attrs = ::GetFileAttributesA(path);
    if (attrs == INVALID_FILE_ATTRIBUTES) {
        w->last_err = std::string("GetFileAttributes(") + path + "): not found";
        return false;
    }
    std::string dir, filter;
    if (attrs & FILE_ATTRIBUTE_DIRECTORY) {
        dir = path;
    } else {
        dir = dirname_of(path);
        filter = basename_of(path);
    }
    HANDLE h = ::CreateFileA(
        dir.c_str(),
        FILE_LIST_DIRECTORY,
        FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
        NULL,
        OPEN_EXISTING,
        FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
        NULL);
    if (h == INVALID_HANDLE_VALUE) {
        w->last_err = std::string("CreateFileA(") + dir + "): " + std::to_string(::GetLastError());
        return false;
    }
    auto* ww = new WinWatch();
    ww->dir = h;
    ww->buf.resize(8192);
    ww->ovl.hEvent = ::CreateEventA(NULL, TRUE, FALSE, NULL);
    ww->dir_path = dir;
    ww->filter_name = filter;
    start_read(ww);
    w->watches.push_back(ww);
    w->last_err.clear();
    return true;
}

static std::string narrow_w(const wchar_t* wstr, size_t wlen) {
    if (wlen == 0) return {};
    int n = ::WideCharToMultiByte(CP_UTF8, 0, wstr, (int)wlen, NULL, 0, NULL, NULL);
    std::string out(n, 0);
    ::WideCharToMultiByte(CP_UTF8, 0, wstr, (int)wlen, out.data(), n, NULL, NULL);
    return out;
}

std::vector<FileEvent> file_watcher_poll(FileWatcher* w) {
    std::vector<FileEvent> out;
    if (!w) return out;
    for (auto* ww : w->watches) {
        if (!ww->pending) { start_read(ww); continue; }
        DWORD bytes = 0;
        BOOL ok = ::GetOverlappedResult(ww->dir, &ww->ovl, &bytes, FALSE);
        if (!ok) {
            // ERROR_IO_INCOMPLETE => sin novedades; ignorar
            DWORD err = ::GetLastError();
            if (err != ERROR_IO_INCOMPLETE) {
                w->last_err = std::string("GetOverlappedResult: ") + std::to_string(err);
                ww->pending = false;
            }
            continue;
        }
        ww->pending = false;
        ::ResetEvent(ww->ovl.hEvent);

        const uint8_t* p = ww->buf.data();
        const uint8_t* end = p + bytes;
        while (p < end) {
            auto* fni = reinterpret_cast<const FILE_NOTIFY_INFORMATION*>(p);
            std::string name = narrow_w(fni->FileName, fni->FileNameLength / sizeof(wchar_t));
            std::string full = ww->dir_path + "\\" + name;

            bool match = ww->filter_name.empty() || ww->filter_name == name;
            if (match) {
                FileEvent fe;
                fe.path = full;
                switch (fni->Action) {
                    case FILE_ACTION_ADDED:
                    case FILE_ACTION_RENAMED_NEW_NAME:
                        fe.kind = FileEvent::Created; out.push_back(fe); break;
                    case FILE_ACTION_REMOVED:
                    case FILE_ACTION_RENAMED_OLD_NAME:
                        fe.kind = FileEvent::Deleted; out.push_back(fe); break;
                    case FILE_ACTION_MODIFIED:
                        fe.kind = FileEvent::Modified; out.push_back(fe); break;
                    default: break;
                }
            }
            if (fni->NextEntryOffset == 0) break;
            p += fni->NextEntryOffset;
        }
        start_read(ww);
    }
    return out;
}

const char* file_watcher_last_error(const FileWatcher* w) {
    return w ? w->last_err.c_str() : "";
}

#else  // Other platforms — stub

struct FileWatcher { std::string last_err = "file_watcher: platform not supported"; };

FileWatcher* file_watcher_create()                 { return new FileWatcher(); }
void         file_watcher_destroy(FileWatcher* w)  { delete w; }
bool         file_watcher_add(FileWatcher*, const char*) { return false; }
std::vector<FileEvent> file_watcher_poll(FileWatcher*)   { return {}; }
const char*  file_watcher_last_error(const FileWatcher* w) { return w ? w->last_err.c_str() : ""; }

#endif

// ─────────────────────────────────────────────────────────────────────────────
// Helper portable basado en file_poll_diff (puro). Comun a todas las plataformas.
// ─────────────────────────────────────────────────────────────────────────────

std::vector<FileEvent> file_watcher_events_from_diff(
    const std::vector<FileSnapshotEntry>& before,
    const std::vector<FileSnapshotEntry>& after) {

    // Convertir a fn_ui::FileEntry para llamar a la funcion pura.
    std::vector<fn_ui::FileEntry> a, b;
    a.reserve(before.size());
    b.reserve(after.size());
    for (const auto& e : before) a.push_back({e.path, e.size, e.mtime});
    for (const auto& e : after)  b.push_back({e.path, e.size, e.mtime});

    fn_ui::FileDiff diff = fn_ui::file_poll_diff(a, b);

    std::vector<FileEvent> out;
    out.reserve(diff.added.size() + diff.modified.size() + diff.removed.size());
    for (const auto& p : diff.added)    out.push_back({p, FileEvent::Created});
    for (const auto& p : diff.modified) out.push_back({p, FileEvent::Modified});
    for (const auto& p : diff.removed)  out.push_back({p, FileEvent::Deleted});
    return out;
}

} // namespace fn
