/*
 * Copyright (c) 2014-2026 Enrico M. Crisostomo
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 3, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program.  If not, see <http://www.gnu.org/licenses/>.
 */
#ifndef _GNU_SOURCE
#  define _GNU_SOURCE
#endif

#include <libfswatch/libfswatch_config.h>

#include "libfswatch/gettext_defs.h"
#include "inotify_monitor.hpp"
#include <algorithm>
#include <array>
#include <cerrno>
#include <cstdint>
#include <unordered_map>
#include <unordered_set>
#include <limits.h>
#ifdef __sun
#  define NAME_MAX         255    /* # chars in a file name */
#endif
#include <unistd.h>
#include <stdio.h>
#include <sstream>
#include <ctime>
#include <cmath>
#include <sys/epoll.h>
#include <sys/eventfd.h>
#include "libfswatch_exception.hpp"
#include "../c/libfswatch_log.h"
#include "path_utils.hpp"

namespace fsw
{

  struct inotify_monitor_impl
  {
    int inotify_monitor_handle = -1;
    int epoll_handle = -1;
    int wake_handle = -1;
    std::vector<event> events;
    /*
     * A map of file names by descriptor is kept in sync because the name field
     * of the inotify_event structure is present only when it identifies a
     * child of a watched directory.  In all the other cases, we store the path
     * for easy retrieval.
     */
    std::unordered_set<int> watched_descriptors;
    std::unordered_map<std::string, int> path_to_wd;
    /*
     * Since the inotify API maintains only works with watch
     * descriptors a cache maintaining a relationship between a watch
     * descriptor and the path used to get it is required to be able to map an
     * event to the path it refers to.  From man inotify:
     *
     *   The inotify API identifies events via watch descriptors.  It is the
     *   application's responsibility to cache a mapping (if one is needed)
     *   between watch descriptors and pathnames.  Be aware that directory
     *   renamings may affect multiple cached pathnames.
     */
    std::unordered_map<int, std::string> wd_to_path;
    std::unordered_set<int> descriptors_to_remove;
    std::unordered_set<int> watches_to_remove;
    std::vector<std::string> paths_to_rescan;
    std::vector<std::string> paths_to_fire_create;
    time_t curr_time;
  };

  static const unsigned int BUFFER_SIZE = (10 * ((sizeof(struct inotify_event)) + NAME_MAX + 1));
  static const int EPOLL_EVENT_COUNT = 2;

  struct scoped_fd
  {
    int fd = -1;

    scoped_fd() = default;
    explicit scoped_fd(int fd) : fd(fd) {}
    scoped_fd(const scoped_fd&) = delete;
    scoped_fd& operator=(const scoped_fd&) = delete;

    ~scoped_fd()
    {
      if (fd >= 0) close(fd);
    }

    int get() const
    {
      return fd;
    }

    int release()
    {
      const int released = fd;
      fd = -1;
      return released;
    }
  };

  static int latency_to_timeout_ms(double latency)
  {
    if (latency <= 0) return 0;
    if (latency >= INT_MAX / 1000.0) return INT_MAX;

    return static_cast<int>(std::ceil(latency * 1000.0));
  }

  static void add_epoll_interest(int epoll_fd, int fd)
  {
    struct epoll_event event {};
    event.events = EPOLLIN;
    event.data.fd = fd;

    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event) != 0)
    {
      fsw_log_perror("epoll_ctl");
      throw libfsw_exception(_("Cannot initialize inotify."));
    }
  }

  static void drain_eventfd(int fd)
  {
    for (;;)
    {
      uint64_t value = 0;
      ssize_t read_count = read(fd, &value, sizeof(value));

      if (read_count == static_cast<ssize_t>(sizeof(value))) continue;
      if (read_count == -1 && errno == EINTR) continue;
      if (read_count == -1 && errno == EAGAIN) break;
      if (read_count == -1)
      {
        fsw_log_perror("read");
        break;
      }

      break;
    }
  }

  inotify_monitor::inotify_monitor(std::vector<std::string> paths_to_monitor,
                                   FSW_EVENT_CALLBACK *callback,
                                   void *context) :
    monitor(paths_to_monitor, callback, context),
    impl(std::make_unique<inotify_monitor_impl>())
  {
    scoped_fd inotify_fd(inotify_init1(IN_CLOEXEC | IN_NONBLOCK));

    if (inotify_fd.get() == -1)
    {
      perror("inotify_init1");
      throw libfsw_exception(_("Cannot initialize inotify."));
    }

    scoped_fd wake_fd(eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK));
    if (wake_fd.get() == -1)
    {
      fsw_log_perror("eventfd");
      throw libfsw_exception(_("Cannot initialize inotify."));
    }

    scoped_fd epoll_fd(epoll_create1(EPOLL_CLOEXEC));
    if (epoll_fd.get() == -1)
    {
      fsw_log_perror("epoll_create1");
      throw libfsw_exception(_("Cannot initialize inotify."));
    }

    add_epoll_interest(epoll_fd.get(), inotify_fd.get());
    add_epoll_interest(epoll_fd.get(), wake_fd.get());

    impl->inotify_monitor_handle = inotify_fd.release();
    impl->wake_handle = wake_fd.release();
    impl->epoll_handle = epoll_fd.release();
  }

  inotify_monitor::~inotify_monitor()
  {
    // log removal of inotify watchers (automatically removed by close below)
    for (auto inotify_desc_pair : impl->watched_descriptors)
    {
      std::ostringstream log;
      log << _("Removing: ") << inotify_desc_pair << "\n";
      FSW_ELOG(log.str().c_str());
    }

    // close inotify (removes watches)
    if (impl->inotify_monitor_handle >= 0)
    {
      close(impl->inotify_monitor_handle);
    }

    if (impl->epoll_handle >= 0)
    {
      close(impl->epoll_handle);
    }

    if (impl->wake_handle >= 0)
    {
      close(impl->wake_handle);
    }
  }

  bool inotify_monitor::add_watch(const std::string& path)
  {
    // TODO: Consider optionally adding the IN_EXCL_UNLINK flag.
    auto event_mask = IN_ALL_EVENTS;
    if (!watch_access)
    {
      event_mask &= ~(IN_ACCESS | IN_CLOSE_NOWRITE | IN_OPEN);
    }

    int inotify_desc = inotify_add_watch(impl->inotify_monitor_handle,
                                         path.c_str(),
                                         event_mask);

    if (inotify_desc == -1)
    {
      perror("inotify_add_watch");
    }
    else
    {
      impl->watched_descriptors.insert(inotify_desc);
      impl->wd_to_path[inotify_desc] = path;
      impl->path_to_wd[path] = inotify_desc;

      std::ostringstream log;
      log << _("Added: ") << path << "\n";
      FSW_ELOG(log.str().c_str());
    }

    return (inotify_desc != -1);
  }

  void inotify_monitor::scan(const std::filesystem::path& path, const bool is_root_path)
  {
    try 
    {
      auto status = std::filesystem::symlink_status(path);

      if (!std::filesystem::exists(status))
      {
        return;
      }

      // Check if the path is a symbolic link
      if (follow_symlinks && std::filesystem::is_symlink(status))
      {
        auto link_path = std::filesystem::read_symlink(path);
        scan(link_path, is_root_path);
        return;
      }

      const bool is_dir = std::filesystem::is_directory(status);
      if (should_prune_path(path.string(), is_dir, is_root_path)) return;

      /*
      * When watching a directory the inotify API will return change events of
      * first-level children.  Therefore, we do not need to manually add a watch
      * for a child unless it is a directory.  Directories and root paths must
      * remain watchable so output filters do not prevent discovery of matching
      * children.
      *
      * For the same reason, the directory_only flag is ignored and treated as if
      * it were always set to true.
      */
      if (!is_dir && !is_root_path) return;
      if (!is_dir && directory_only) return;
      if (!add_watch(path)) return;
      if (!recursive || !is_dir) return;

      // TODO: Consider using std::filesystem::recursive_directory_iterator
      const auto entries = get_subdirectories(path);

      for (const auto& entry : entries)
        // Scan children but only watch directories.
        scan(entry, false);
    }
    catch (const std::filesystem::filesystem_error& e) 
    {
        // Handle errors, such as permission issues or non-existent paths
        FSW_ELOGF(_("Filesystem error: %s"), e.what());
    }
  }

  bool inotify_monitor::is_watched(const std::string& path) const
  {
    return (impl->path_to_wd.find(path) != impl->path_to_wd.end());
  }

  void inotify_monitor::scan_root_paths()
  {
    for (const std::string& path : paths)
    {
      if (!is_watched(path)) scan(path, true);
    }
  }

  void inotify_monitor::preprocess_dir_event(const struct inotify_event *event)
  {
    std::vector<fsw_event_flag> flags;

    if (event->mask & IN_ISDIR) flags.push_back(fsw_event_flag::IsDir);
    if (event->mask & IN_MOVE_SELF) flags.push_back(fsw_event_flag::Updated);
    if (event->mask & IN_MOVED_FROM)
    {
      flags.push_back(fsw_event_flag::Removed);
      flags.push_back(fsw_event_flag::MovedFrom);
    }
    if (event->mask & IN_MOVED_TO)
    {
      flags.push_back(fsw_event_flag::Created);
      flags.push_back(fsw_event_flag::MovedTo);
    }
    if (event->mask & IN_UNMOUNT) flags.push_back(fsw_event_flag::PlatformSpecific);

    if (!flags.empty())
    {
      impl->events.emplace_back(impl->wd_to_path[event->wd], impl->curr_time, flags, event->cookie);
    }

  }

  void inotify_monitor::preprocess_node_event(const struct inotify_event *event)
  {
    std::vector<fsw_event_flag> flags;

    if (event->mask & IN_ACCESS) flags.push_back(fsw_event_flag::PlatformSpecific);
    if (event->mask & IN_ATTRIB) flags.push_back(fsw_event_flag::AttributeModified);
    if (event->mask & IN_CLOSE_NOWRITE) flags.push_back(fsw_event_flag::PlatformSpecific);
    if (event->mask & IN_CLOSE_WRITE) flags.push_back(fsw_event_flag::CloseWrite);
    if (event->mask & IN_CREATE) flags.push_back(fsw_event_flag::Created);
    if (event->mask & IN_DELETE) flags.push_back(fsw_event_flag::Removed);
    if (event->mask & IN_DELETE_SELF) flags.push_back(fsw_event_flag::Removed);
    if (event->mask & IN_MODIFY) flags.push_back(fsw_event_flag::Updated);
    if (event->mask & IN_OPEN) flags.push_back(fsw_event_flag::PlatformSpecific);

    // Build the file name.
    std::ostringstream filename_stream;
    filename_stream << impl->wd_to_path[event->wd];

    if (event->len > 1)
    {
      filename_stream << "/";
      filename_stream << event->name;
    }

    if ((event->mask & IN_ISDIR) && (event->mask & IN_CREATE))
    {
      const std::string created_dir_path = filename_stream.str();
      impl->paths_to_rescan.push_back(created_dir_path);
      impl->paths_to_fire_create.push_back(created_dir_path);
    }

    if (!flags.empty())
    {
      impl->events.emplace_back(filename_stream.str(), impl->curr_time, flags, event->cookie);
    }

    {
      std::ostringstream log;
      log << _("Generic event: ") << event->wd << "::" << filename_stream.str() << "\n";
      FSW_ELOG(log.str().c_str());
    }

    /*
     * inotify automatically removes the watch of a watched item that has been
     * removed and posts an IN_IGNORED event after an IN_DELETE_SELF.
     */
    if (event->mask & IN_IGNORED)
    {
      std::ostringstream log;
      log << "IN_IGNORED: " << event->wd << "::" << filename_stream.str() << "\n";
      FSW_ELOG(log.str().c_str());

      impl->descriptors_to_remove.insert(event->wd);
    }

    /*
     * inotify sends an IN_MOVE_SELF event when a watched object is moved into
     * the same filesystem and keeps watching it.  Since its path has changed,
     * we remove the watch so that recreation is attempted at the next
     * iteration.
     *
     * Beware that a race condition exists which may result in events go
     * unnoticed when a watched file x is removed and a new file named x is
     * created thereafter.  In this case, fswatch could be blocked on read and
     * it would not have any chance to create a new watch descriptor for x until
     * an event is received and read unblocks.
     */
    if (event->mask & IN_MOVE_SELF)
    {
      std::ostringstream log;
      log << "IN_MOVE_SELF: " << event->wd << "::" << filename_stream.str() << "\n";
      FSW_ELOG(log.str().c_str());

      impl->watches_to_remove.insert(event->wd);
      impl->descriptors_to_remove.insert(event->wd);
    }

    /*
     * A file could be moved to a path which is being observed.  The clobbered
     * file is handled by the corresponding IN_DELETE_SELF event.
     */

    /*
     * inotify automatically removes the watch of the object the IN_DELETE_SELF
     * event is related to.
     */
    if (event->mask & IN_DELETE_SELF)
    {
      std::ostringstream log;
      log << "IN_DELETE_SELF: " << event->wd << "::" << filename_stream.str() << "\n";
      FSW_ELOG(log.str().c_str());

      impl->descriptors_to_remove.insert(event->wd);
    }
  }

  void inotify_monitor::preprocess_event(const struct inotify_event *event)
  {
    if (event->mask & IN_Q_OVERFLOW)
    {
      notify_overflow(impl->wd_to_path[event->wd]);
    }

    preprocess_dir_event(event);
    preprocess_node_event(event);
  }

  void inotify_monitor::remove_watch(int wd)
  {
    /*
     * No need to remove the inotify watch because it is removed automatically
     * when a watched element is deleted.
     */
    impl->wd_to_path.erase(wd);
  }

  void inotify_monitor::process_pending_events()
  {
    // Remove watches.
    auto wtd = impl->watches_to_remove.begin();

    while (wtd != impl->watches_to_remove.end())
    {
      if (inotify_rm_watch(impl->inotify_monitor_handle, *wtd) != 0)
      {
        perror("inotify_rm_watch");
      }
      else
      {
        std::ostringstream log;
        log << _("Removed: ") << *wtd << "\n";
        FSW_ELOG(log.str().c_str());
      }

      impl->watches_to_remove.erase(wtd++);
    }

    // Clean up descriptors.
    auto fd = impl->descriptors_to_remove.begin();

    while (fd != impl->descriptors_to_remove.end())
    {
      const std::string& curr_path = impl->wd_to_path[*fd];
      impl->path_to_wd.erase(curr_path);
      impl->wd_to_path.erase(*fd);
      impl->watched_descriptors.erase(*fd);

      impl->descriptors_to_remove.erase(fd++);
    }

    // Process paths to be rescanned
    std::for_each(impl->paths_to_rescan.begin(),
		  impl->paths_to_rescan.end(),
		  [this] (const std::string& p)
		  {
		    this->scan(p);
		  }
		  );

    impl->paths_to_rescan.clear();
  }

  void inotify_monitor::process_synthetic_events()
  {
    for (const std::string& path : impl->paths_to_fire_create)
    {
      FSW_ELOGF(_("Synthetic event: processing directory: %s\n"), path.c_str());

      const auto entries = get_directory_entries(path);

      for (const auto& entry : entries)
      {
        std::vector<fsw_event_flag> flags{fsw_event_flag::Created};

        try
        {
          if (entry.is_directory()) flags.push_back(fsw_event_flag::IsDir);
        }
        catch (const std::filesystem::filesystem_error& e)
        {
          FSW_ELOGF(_("Filesystem error: %s"), e.what());
        }

        impl->events.emplace_back(entry.path().string(), impl->curr_time, flags);
      }
    }

    impl->paths_to_fire_create.clear();
  }

  void inotify_monitor::on_stop()
  {
    if (!impl || impl->wake_handle < 0) return;

    uint64_t value = 1;
    for (;;)
    {
      ssize_t write_count = write(impl->wake_handle, &value, sizeof(value));

      if (write_count == static_cast<ssize_t>(sizeof(value))) break;
      if (write_count == -1 && errno == EINTR) continue;
      if (write_count == -1 && errno == EAGAIN) break;

      fsw_log_perror("write");
      break;
    }
  }

  void inotify_monitor::run()
  {
    std::array<char, BUFFER_SIZE> buffer{};
    std::array<struct epoll_event, EPOLL_EVENT_COUNT> epoll_events{};
    const int timeout_ms = latency_to_timeout_ms(this->latency);

    for(;;)
    {
      std::unique_lock<std::mutex> run_guard(run_mutex);
      if (should_stop) break;
      run_guard.unlock();

      process_pending_events();

      scan_root_paths();

      // Use epoll to timeout on file descriptor read the amount specified by
      // the monitor latency.  This way, the monitor has a chance to update its
      // watches with at least the periodicity expected by the user.
      int rv = epoll_wait(impl->epoll_handle,
                          epoll_events.data(),
                          epoll_events.size(),
                          timeout_ms);

      if (rv == -1)
      {
        if (errno == EINTR) continue;
        fsw_log_perror("epoll_wait");
        continue;
      }

      // In case of wait timeout just repeat the loop.
      if (rv == 0) continue;

      bool wake_requested = false;

      for (int i = 0; i < rv; ++i)
      {
        if (epoll_events[i].data.fd == impl->wake_handle)
        {
          drain_eventfd(impl->wake_handle);
          wake_requested = true;
          continue;
        }

        if (epoll_events[i].data.fd != impl->inotify_monitor_handle)
        {
          continue;
        }

        for (;;)
        {
          ssize_t record_num = read(impl->inotify_monitor_handle,
                                    buffer.data(),
                                    buffer.size());

          {
            std::ostringstream log;
            log << _("Number of records: ") << record_num << "\n";
            FSW_ELOG(log.str().c_str());
          }

          if (!record_num)
          {
            throw libfsw_exception(_("read() on inotify descriptor read 0 records."));
          }

          if (record_num == -1)
          {
            if (errno == EAGAIN) break;
            if (errno == EINTR) continue;
            perror("read()");
            throw libfsw_exception(_("read() on inotify descriptor returned -1."));
          }

          time(&impl->curr_time);

          for (char *p = buffer.data(); p < buffer.data() + record_num;)
          {
            struct inotify_event const *event = reinterpret_cast<struct inotify_event *> (p);

            preprocess_event(event);

            p += (sizeof(struct inotify_event)) + event->len;
          }
        }
      }

      if (!impl->events.empty())
      {
        notify_events(impl->events);
        impl->events.clear();
      }

      process_pending_events();
      process_synthetic_events();

      if (!impl->events.empty())
      {
        notify_events(impl->events);
        impl->events.clear();
      }
      if (wake_requested) break;
    }
  }
}
