#include "plugin/linux_native_files.h"

#include "common/method_channel_utils.h"
#include "plugin/coreframe_web_view_host_plugin_private.h"

#include <fcntl.h>
#include <glib/gstdio.h>
#include <sys/stat.h>
#include <unistd.h>

#include <algorithm>
#include <cctype>
#include <cerrno>
#include <cstring>
#include <limits>
#include <memory>
#include <sstream>
#include <utility>

namespace {

constexpr const char *kChannelName = "coreframe/native_files";
constexpr int64_t kStaleAfterSeconds = 24LL * 60 * 60;

struct PickRequest {
  std::vector<std::string> accept;
  bool multiple;
};

struct StageTaskData {
  StageTaskData(CoreframeWebViewHostPlugin *plugin, std::string directory,
                std::vector<LinuxNativeFileSelection> picked)
      : staging_directory(std::move(directory)), selections(std::move(picked)) {
    g_weak_ref_init(&owner, G_OBJECT(plugin));
  }

  ~StageTaskData() { g_weak_ref_clear(&owner); }

  GWeakRef owner;
  std::string staging_directory;
  std::vector<LinuxNativeFileSelection> selections;
};

bool IsFilterCharacter(char value) {
  return std::isalnum(static_cast<unsigned char>(value)) != 0 || value == '!' ||
         value == '#' || value == '$' || value == '&' || value == '^' ||
         value == '_' || value == '.' || value == '+' || value == '-';
}

bool IsValidFilter(const std::string &value) {
  if (value.size() >= 2 && value.size() <= 33 && value.front() == '.') {
    if (std::isalnum(static_cast<unsigned char>(value[1])) == 0) {
      return false;
    }
    return std::all_of(value.begin() + 2, value.end(), [](char character) {
      return std::isalnum(static_cast<unsigned char>(character)) != 0 ||
             character == '.' || character == '_' || character == '+' ||
             character == '-';
    });
  }
  const auto slash = value.find('/');
  if (slash == std::string::npos || slash == 0 || slash == value.size() - 1 ||
      value.find('/', slash + 1) != std::string::npos) {
    return false;
  }
  const bool valid_type =
      std::all_of(value.begin(), value.begin() + slash, IsFilterCharacter);
  const auto subtype = value.substr(slash + 1);
  return valid_type &&
         (subtype == "*" ||
          std::all_of(subtype.begin(), subtype.end(), IsFilterCharacter));
}

std::optional<PickRequest> ParsePickRequest(FlValue *arguments) {
  if (arguments == nullptr ||
      fl_value_get_type(arguments) != FL_VALUE_TYPE_MAP ||
      fl_value_get_length(arguments) != 2) {
    return std::nullopt;
  }
  FlValue *accept = map_lookup(arguments, "accept");
  FlValue *multiple = map_lookup(arguments, "multiple");
  if (accept == nullptr || fl_value_get_type(accept) != FL_VALUE_TYPE_LIST ||
      multiple == nullptr ||
      fl_value_get_type(multiple) != FL_VALUE_TYPE_BOOL ||
      fl_value_get_length(accept) > LinuxNativeFileStager::kMaximumFiles) {
    return std::nullopt;
  }
  PickRequest request{{}, fl_value_get_bool(multiple)};
  request.accept.reserve(fl_value_get_length(accept));
  for (size_t index = 0; index < fl_value_get_length(accept); ++index) {
    FlValue *value = fl_value_get_list_value(accept, index);
    if (fl_value_get_type(value) != FL_VALUE_TYPE_STRING) {
      return std::nullopt;
    }
    const std::string filter = fl_value_get_string(value);
    if (!IsValidFilter(filter)) {
      return std::nullopt;
    }
    request.accept.push_back(filter);
  }
  return request;
}

std::string SafeName(const std::string &name) {
  gchar *utf8 = nullptr;
  if (g_utf8_validate(name.c_str(), name.size(), nullptr)) {
    utf8 = g_strdup(name.c_str());
  } else {
    utf8 = g_filename_to_utf8(name.c_str(), name.size(), nullptr, nullptr,
                              nullptr);
  }
  if (utf8 == nullptr || *utf8 == '\0') {
    g_free(utf8);
    return "file";
  }
  const glong characters = g_utf8_strlen(utf8, -1);
  gchar *end =
      g_utf8_offset_to_pointer(utf8, std::min<glong>(characters, 1024));
  std::string result(utf8, end);
  g_free(utf8);
  return result;
}

std::optional<std::string> MediaTypeForPath(const std::string &path) {
  gboolean uncertain = FALSE;
  gchar *content_type =
      g_content_type_guess(path.c_str(), nullptr, 0, &uncertain);
  if (content_type == nullptr) {
    return std::nullopt;
  }
  gchar *mime_type = g_content_type_get_mime_type(content_type);
  g_free(content_type);
  if (mime_type == nullptr || *mime_type == '\0') {
    g_free(mime_type);
    return std::nullopt;
  }
  std::string result(mime_type);
  g_free(mime_type);
  return result;
}

std::string UniqueDestination(const std::string &directory) {
  gchar *uuid = g_uuid_string_random();
  gchar *path = g_build_filename(directory.c_str(), uuid, nullptr);
  std::string result(path);
  g_free(path);
  g_free(uuid);
  return result;
}

bool CopyBounded(int source, int destination, int64_t maximum_bytes,
                 int64_t *copied_bytes) {
  char buffer[64 * 1024];
  int64_t total = 0;
  while (true) {
    const ssize_t read_bytes = read(source, buffer, sizeof(buffer));
    if (read_bytes < 0) {
      if (errno == EINTR)
        continue;
      return false;
    }
    if (read_bytes == 0) {
      *copied_bytes = total;
      return true;
    }
    if (total > maximum_bytes - read_bytes) {
      errno = EFBIG;
      return false;
    }
    ssize_t written = 0;
    while (written < read_bytes) {
      const ssize_t next =
          write(destination, buffer + written, read_bytes - written);
      if (next < 0 && errno == EINTR)
        continue;
      if (next <= 0)
        return false;
      written += next;
    }
    total += read_bytes;
  }
}

LinuxNativeFileStagingResult
StagingFailure(LinuxNativeFileStagingFailure failure,
               std::vector<LinuxStagedNativeFile> files = {}) {
  LinuxNativeFileStager::Remove(files);
  return {{}, failure};
}

std::string ApplicationIdentity() {
  auto *application = g_application_get_default();
  const gchar *identifier = nullptr;
  if (application != nullptr) {
    identifier = g_application_get_application_id(application);
  }
  if (identifier != nullptr && *identifier != '\0') {
    return identifier;
  }
  gchar *executable = g_file_read_link("/proc/self/exe", nullptr);
  const std::string path = executable != nullptr ? executable : "application";
  g_free(executable);
  uint64_t hash = 14695981039346656037ULL;
  for (const unsigned char byte : path) {
    hash ^= byte;
    hash *= 1099511628211ULL;
  }
  std::ostringstream value;
  value << "executable-" << std::hex << hash;
  return value.str();
}

std::string StorageNamespace() {
  std::string application = ApplicationIdentity();
  for (char &character : application) {
    if (std::isalnum(static_cast<unsigned char>(character)) == 0 &&
        character != '-' && character != '_' && character != '.') {
      character = '_';
    }
  }
  if (application.empty() || application == "." || application == "..") {
    application = "application";
  }
  application.resize(std::min<size_t>(application.size(), 128));
  gchar *path = g_build_filename(g_get_user_cache_dir(), "coreframe",
                                 "native-files", application.c_str(), nullptr);
  std::string result(path);
  g_free(path);
  return result;
}

std::string StorageDirectory(const std::string &storage, const char *name) {
  gchar *path = g_build_filename(storage.c_str(), name, nullptr);
  std::string result(path);
  g_free(path);
  return result;
}

void AddFilters(GtkFileChooser *chooser,
                const std::vector<std::string> &accept) {
  if (accept.empty())
    return;
  GtkFileFilter *filter = gtk_file_filter_new();
  gtk_file_filter_set_name(filter, "Accepted files");
  for (const auto &value : accept) {
    std::string normalized = value;
    std::transform(normalized.begin(), normalized.end(), normalized.begin(),
                   [](unsigned char character) {
                     return static_cast<char>(std::tolower(character));
                   });
    if (normalized.front() == '.') {
      const std::string pattern = "*" + normalized;
      gtk_file_filter_add_pattern(filter, pattern.c_str());
    } else {
      gtk_file_filter_add_mime_type(filter, normalized.c_str());
    }
  }
  gtk_file_chooser_add_filter(chooser, filter);
}

void RespondAndClear(CoreframeWebViewHostPlugin *self,
                     FlMethodResponse *response) {
  if (self->native_files_call != nullptr) {
    respond(self->native_files_call, response);
    g_clear_object(&self->native_files_call);
  } else {
    g_object_unref(response);
  }
  self->native_files_busy = FALSE;
}

void StageThread(GTask *task, gpointer, gpointer task_data, GCancellable *) {
  auto *data = static_cast<StageTaskData *>(task_data);
  auto *result = new LinuxNativeFileStagingResult(
      LinuxNativeFileStager(data->staging_directory).Stage(data->selections));
  g_task_return_pointer(task, result, [](gpointer value) {
    delete static_cast<LinuxNativeFileStagingResult *>(value);
  });
}

void StageFinished(GObject *, GAsyncResult *async_result, gpointer) {
  auto *data =
      static_cast<StageTaskData *>(g_task_get_task_data(G_TASK(async_result)));
  auto *self = reinterpret_cast<CoreframeWebViewHostPlugin *>(
      g_weak_ref_get(&data->owner));
  auto *result = static_cast<LinuxNativeFileStagingResult *>(
      g_task_propagate_pointer(G_TASK(async_result), nullptr));
  std::unique_ptr<LinuxNativeFileStagingResult> owned(result);
  if (self == nullptr) {
    if (result != nullptr)
      LinuxNativeFileStager::Remove(result->files);
    return;
  }
  if (result == nullptr) {
    RespondAndClear(
        self, error_response("internal", "Native file selection failed."));
    g_object_unref(self);
    return;
  }
  if (self->disposing || self->native_files_call == nullptr) {
    LinuxNativeFileStager::Remove(result->files);
    self->native_files_busy = FALSE;
    g_object_unref(self);
    return;
  }
  if (result->failure.has_value()) {
    const bool capacity =
        *result->failure == LinuxNativeFileStagingFailure::kCapacityExceeded;
    RespondAndClear(
        self,
        error_response(capacity ? "capacity_exceeded" : "unreadable_selection",
                       capacity ? "The native file staging limit was exceeded."
                                : "A selected file could not be read."));
    g_object_unref(self);
    return;
  }
  FlValue *files = fl_value_new_list();
  for (const auto &file : result->files) {
    fl_value_append_take(files, file.ToChannelValue());
  }
  // Sending the channel response transfers ownership of every staged path to
  // Dart. All earlier failure and teardown paths remain native-owned.
  RespondAndClear(self, success_response(files));
  g_object_unref(self);
}

void PickerResponse(GtkNativeDialog *dialog, gint response_id,
                    gpointer user_data) {
  auto *self = static_cast<CoreframeWebViewHostPlugin *>(user_data);
  g_signal_handlers_disconnect_by_func(dialog, G_CALLBACK(PickerResponse),
                                       self);
  self->native_file_chooser = nullptr;

  if (self->disposing || self->native_files_call == nullptr) {
    gtk_native_dialog_destroy(dialog);
    g_object_unref(dialog);
    return;
  }
  if (response_id == GTK_RESPONSE_CANCEL ||
      response_id == GTK_RESPONSE_DELETE_EVENT ||
      response_id == GTK_RESPONSE_CLOSE) {
    gtk_native_dialog_destroy(dialog);
    g_object_unref(dialog);
    RespondAndClear(self, success_response(fl_value_new_list()));
    return;
  }
  if (response_id != GTK_RESPONSE_ACCEPT) {
    gtk_native_dialog_destroy(dialog);
    g_object_unref(dialog);
    RespondAndClear(
        self, error_response("internal", "Native file selection failed."));
    return;
  }

  GSList *filenames = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(dialog));
  gtk_native_dialog_destroy(dialog);
  g_object_unref(dialog);
  std::vector<LinuxNativeFileSelection> selections;
  for (GSList *item = filenames; item != nullptr; item = item->next) {
    const char *path = static_cast<const char *>(item->data);
    gchar *basename = g_path_get_basename(path);
    selections.push_back({path, basename});
    g_free(basename);
  }
  g_slist_free_full(filenames, g_free);
  if (selections.empty()) {
    RespondAndClear(self, error_response("unreadable_selection",
                                         "A selected file could not be read."));
    return;
  }
  if (selections.size() > LinuxNativeFileStager::kMaximumFiles) {
    RespondAndClear(self, error_response("capacity_exceeded",
                                         "Too many files were selected."));
    return;
  }

  auto *data = new StageTaskData(self, self->native_files_staging_directory,
                                 std::move(selections));
  GTask *task = g_task_new(nullptr, nullptr, StageFinished, nullptr);
  g_task_set_task_data(task, data, [](gpointer value) {
    delete static_cast<StageTaskData *>(value);
  });
  g_task_run_in_thread(task, StageThread);
  g_object_unref(task);
}

void NativeFilesMethodCall(FlMethodChannel *, FlMethodCall *method_call,
                           gpointer user_data) {
  auto *self = static_cast<CoreframeWebViewHostPlugin *>(user_data);
  const char *method = fl_method_call_get_name(method_call);
  if (strcmp(method, "isAvailable") == 0) {
    respond(method_call,
            success_response(fl_value_new_bool(
                !self->disposing && self->flutter_view != nullptr &&
                self->native_files_staging_directory != nullptr)));
    return;
  }
  if (strcmp(method, "getResourceStorageRoot") == 0) {
    const auto *directory = self->native_files_resource_directory;
    gchar *storage =
        directory == nullptr ? nullptr : g_path_get_dirname(directory);
    if (directory != nullptr && g_mkdir_with_parents(directory, 0700) == 0 &&
        g_chmod(storage, 0700) == 0 && g_chmod(directory, 0700) == 0) {
      respond(method_call,
              success_response(fl_value_new_string(directory)));
    } else {
      respond(method_call, success_response(fl_value_new_null()));
    }
    g_free(storage);
    return;
  }
  if (strcmp(method, "pick") != 0) {
    respond(method_call,
            FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()));
    return;
  }
  if (self->native_files_busy) {
    respond(method_call,
            error_response("picker_busy",
                           "A native file picker is already active."));
    return;
  }
  if (self->disposing || self->flutter_view == nullptr ||
      self->native_files_staging_directory == nullptr) {
    respond(
        method_call,
        error_response("unsupported_platform",
                       "Native file selection is unavailable on this host."));
    return;
  }
  const auto request = ParsePickRequest(fl_method_call_get_args(method_call));
  if (!request.has_value()) {
    respond(method_call,
            error_response("internal", "The native file request is invalid."));
    return;
  }

  GtkWidget *toplevel = gtk_widget_get_toplevel(self->flutter_view);
  GtkWindow *parent = GTK_IS_WINDOW(toplevel) ? GTK_WINDOW(toplevel) : nullptr;
  GtkFileChooserNative *chooser = gtk_file_chooser_native_new(
      "Select files", parent, GTK_FILE_CHOOSER_ACTION_OPEN, "Open", "Cancel");
  gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(chooser),
                                       request->multiple);
  gtk_file_chooser_set_local_only(GTK_FILE_CHOOSER(chooser), FALSE);
  AddFilters(GTK_FILE_CHOOSER(chooser), request->accept);
  self->native_files_busy = TRUE;
  self->native_files_call = FL_METHOD_CALL(g_object_ref(method_call));
  self->native_file_chooser = GTK_NATIVE_DIALOG(chooser);
  g_signal_connect(chooser, "response", G_CALLBACK(PickerResponse), self);
  gtk_native_dialog_show(GTK_NATIVE_DIALOG(chooser));
}

void SweepThread(GTask *task, gpointer, gpointer task_data, GCancellable *) {
  LinuxNativeFileStager::SweepStale(static_cast<const char *>(task_data));
  g_task_return_boolean(task, TRUE);
}

} // namespace

FlValue *LinuxStagedNativeFile::ToChannelValue() const {
  FlValue *value = fl_value_new_map();
  fl_value_set_string_take(value, "name", fl_value_new_string(name.c_str()));
  fl_value_set_string_take(value, "size", fl_value_new_int(size));
  fl_value_set_string_take(value, "stagedPath",
                           fl_value_new_string(staged_path.c_str()));
  fl_value_set_string_take(value, "lastModifiedMilliseconds",
                           last_modified_milliseconds.has_value()
                               ? fl_value_new_int(*last_modified_milliseconds)
                               : fl_value_new_null());
  fl_value_set_string_take(value, "mediaType",
                           media_type.has_value()
                               ? fl_value_new_string(media_type->c_str())
                               : fl_value_new_null());
  return value;
}

LinuxNativeFileStager::LinuxNativeFileStager(std::string staging_directory,
                                             size_t maximum_files,
                                             int64_t maximum_file_bytes,
                                             int64_t maximum_total_bytes)
    : staging_directory_(std::move(staging_directory)),
      maximum_files_(maximum_files), maximum_file_bytes_(maximum_file_bytes),
      maximum_total_bytes_(maximum_total_bytes) {}

LinuxNativeFileStagingResult LinuxNativeFileStager::Stage(
    const std::vector<LinuxNativeFileSelection> &selections) const {
  if (selections.size() > maximum_files_) {
    return StagingFailure(LinuxNativeFileStagingFailure::kCapacityExceeded);
  }
  if (g_mkdir_with_parents(staging_directory_.c_str(), 0700) != 0 ||
      g_chmod(staging_directory_.c_str(), 0700) != 0) {
    return StagingFailure(LinuxNativeFileStagingFailure::kUnreadable);
  }

  int64_t total_bytes = 0;
  std::vector<LinuxStagedNativeFile> staged;
  staged.reserve(selections.size());
  for (const auto &selection : selections) {
    const int source =
        open(selection.source_path.c_str(), O_RDONLY | O_CLOEXEC);
    struct stat metadata = {};
    if (source < 0 || fstat(source, &metadata) != 0 ||
        !S_ISREG(metadata.st_mode) || metadata.st_size < 0) {
      if (source >= 0)
        close(source);
      return StagingFailure(LinuxNativeFileStagingFailure::kUnreadable,
                            std::move(staged));
    }
    if (metadata.st_size > maximum_file_bytes_ ||
        metadata.st_size > maximum_total_bytes_ - total_bytes) {
      close(source);
      return StagingFailure(LinuxNativeFileStagingFailure::kCapacityExceeded,
                            std::move(staged));
    }

    const std::string destination_path = UniqueDestination(staging_directory_);
    const int destination = open(destination_path.c_str(),
                                 O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0600);
    if (destination < 0) {
      close(source);
      g_unlink(destination_path.c_str());
      return StagingFailure(LinuxNativeFileStagingFailure::kUnreadable,
                            std::move(staged));
    }
    const int64_t maximum_bytes =
        std::min(maximum_file_bytes_, maximum_total_bytes_ - total_bytes);
    int64_t copied_bytes = 0;
    const bool copied =
        CopyBounded(source, destination, maximum_bytes, &copied_bytes);
    const int copy_error = errno;
    close(source);
    const bool closed = close(destination) == 0;
    if (!copied || !closed || copied_bytes != metadata.st_size) {
      g_unlink(destination_path.c_str());
      return StagingFailure(
          !copied && copy_error == EFBIG
              ? LinuxNativeFileStagingFailure::kCapacityExceeded
              : LinuxNativeFileStagingFailure::kUnreadable,
          std::move(staged));
    }
    total_bytes += copied_bytes;
    const int64_t modified = metadata.st_mtim.tv_sec;
    const auto modified_milliseconds =
        modified >= 0 && modified <= std::numeric_limits<int64_t>::max() / 1000
            ? std::optional<int64_t>(modified * 1000 +
                                     metadata.st_mtim.tv_nsec / 1000000)
            : std::nullopt;
    staged.push_back({SafeName(selection.name), copied_bytes, destination_path,
                      modified_milliseconds,
                      MediaTypeForPath(selection.source_path)});
  }
  return {std::move(staged), std::nullopt};
}

void LinuxNativeFileStager::Remove(
    const std::vector<LinuxStagedNativeFile> &files) {
  for (const auto &file : files) {
    g_unlink(file.staged_path.c_str());
  }
}

void LinuxNativeFileStager::SweepStale(const std::string &staging_directory) {
  GDir *directory = g_dir_open(staging_directory.c_str(), 0, nullptr);
  if (directory == nullptr)
    return;
  const int64_t cutoff =
      g_get_real_time() / G_USEC_PER_SEC - kStaleAfterSeconds;
  while (const gchar *name = g_dir_read_name(directory)) {
    gchar *path = g_build_filename(staging_directory.c_str(), name, nullptr);
    struct stat metadata = {};
    if (lstat(path, &metadata) == 0 && S_ISREG(metadata.st_mode) &&
        metadata.st_mtime <= cutoff) {
      g_unlink(path);
    }
    g_free(path);
  }
  g_dir_close(directory);
}

void initialize_linux_native_files(CoreframeWebViewHostPlugin *self) {
  const auto storage = StorageNamespace();
  self->native_files_staging_directory =
      g_strdup(StorageDirectory(storage, "staging").c_str());
  self->native_files_resource_directory =
      g_strdup(StorageDirectory(storage, "resources").c_str());
  self->native_files_channel =
      fl_method_channel_new(fl_plugin_registrar_get_messenger(self->registrar),
                            kChannelName, method_codec());
  fl_method_channel_set_method_call_handler(self->native_files_channel,
                                            NativeFilesMethodCall,
                                            g_object_ref(self), g_object_unref);

  GTask *sweep = g_task_new(nullptr, nullptr, nullptr, nullptr);
  g_task_set_task_data(sweep, g_strdup(self->native_files_staging_directory),
                       g_free);
  g_task_run_in_thread(sweep, SweepThread);
  g_object_unref(sweep);
}

void dispose_linux_native_files(CoreframeWebViewHostPlugin *self) {
  if (self->native_files_channel != nullptr) {
    fl_method_channel_set_method_call_handler(self->native_files_channel,
                                              nullptr, nullptr, nullptr);
  }
  if (self->native_file_chooser != nullptr) {
    g_signal_handlers_disconnect_by_func(self->native_file_chooser,
                                         G_CALLBACK(PickerResponse), self);
    gtk_native_dialog_destroy(self->native_file_chooser);
    g_clear_object(&self->native_file_chooser);
  }
  if (self->native_files_call != nullptr) {
    respond(
        self->native_files_call,
        error_response("internal", "Native file selection was interrupted."));
    g_clear_object(&self->native_files_call);
  }
  self->native_files_busy = FALSE;
  g_clear_object(&self->native_files_channel);
  g_clear_pointer(&self->native_files_staging_directory, g_free);
  g_clear_pointer(&self->native_files_resource_directory, g_free);
}
