/*
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2.1
 * of the License, 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 Lesser General Public License for more details.
 */

#include "./shtype.hpp"

#include <pwd.h>
#include <sys/stat.h>
#include <unistd.h>

#include <regex>
#include <sstream>
#include <string>

#include "../infotree/json-serializer.hpp"

namespace sh4lt {

ShType::ShType(std::string media,
               std::string label,
               std::string group_label,
               std::string description)
    : media_(std::move(media)),
      label_(std::move(label)),
      group_(std::move(group_label)),
      description_(std::move(description)) {}

ShType::ShType(std::string media, std::string label, std::string group_label)
    : media_(std::move(media)), label_(std::move(label)), group_(std::move(group_label)) {}

ShType::ShType(std::string media, std::string label)
    : ShType(std::move(media), std::move(label), default_group()) {}

auto ShType::media() const -> std::string { return media_; }

auto ShType::label() const -> std::string { return label_; }

auto ShType::group() const -> std::string { return group_; }

auto ShType::description() const -> std::string { return description_; }

auto ShType::get_prop(const std::string& key) const -> Any {
  auto found = properties_.find(key);
  if (properties_.end() == found) return {};
  return found->second;
}

auto ShType::get_custom_type(const std::string& key) const -> std::string {
  auto found = custom_types_.find(key);
  if (custom_types_.end() == found) return {};
  return found->second;
}

void ShType::set_prop(const std::string& key, const char* value) {
  properties_[key] = std::string(value);
}

void ShType::set_prop(const std::string& key, const std::string& value) {
  properties_[key] = std::string(value);
}

void ShType::set_media(const std::string& media) { media_ = media; }

void ShType::set_label(const std::string& label) { label_ = label; }

void ShType::set_group(const std::string& group_label) { group_ = group_label; }

void ShType::set_description(const std::string& description) { description_ = description; }

void ShType::set_custom_prop(const std::string& key,
                             const std::string& custom_type,
                             const std::string& value) {
  properties_[key] = value;
  custom_types_[key] = custom_type;
}

void ShType::set_custom_type(const std::string& key, const std::string& custom_type) {
  custom_types_[key] = custom_type;
}

auto ShType::get_properties() const -> std::map<std::string, Any> { return properties_; }

auto ShType::get_custom_types() const -> std::map<std::string, std::string> {
  return custom_types_;
}

auto ShType::as_info_tree() const -> InfoTree::ptr {
  InfoTree::ptr tree = InfoTree::make();
  tree->vgraft(".media", media_);
  tree->vgraft(".label", label_);
  tree->vgraft(".group", group_);
  tree->vgraft(".description", description_);
  if (properties_.empty()) {
    return tree;
  }
  InfoTree::ptr prop = InfoTree::make();
  for (const auto& it : properties_) {
    prop->vgraft(it.first, it.second);
  }
  tree->graft("property", prop);
  if (custom_types_.empty()) {
    return tree;
  }
  InfoTree::ptr type = InfoTree::make();
  for (const auto& it : custom_types_) {
    // property type
    type->vgraft(it.first, it.second);
  }
  tree->graft("type", type);
  return tree;
}
auto ShType::serialize(const ShType& shtype) -> std::string {
  if (!shtype) return {};
  return infotree::json::serialize(shtype.as_info_tree().get());
}

auto ShType::deserialize(const std::string& serialized_shtype) -> ShType {
  auto tree = infotree::json::deserialize(serialized_shtype);
  if (!tree) {
    auto res = ShType();
    // add parsing errors to the shtype log
    auto valid = infotree::json::is_valid(serialized_shtype);
    res.add_log(valid.msg());
    return res;
  }
  auto media = tree->branch_get_value("media");
  auto label = tree->branch_get_value("label");
  auto group = tree->branch_get_value("group");
  auto description = tree->branch_get_value("description");
  if (media.empty() || label.empty() || group.empty()) {
    auto res = ShType();
    res.add_log("missing media, or label or group");
    return res;
  }

  auto res = ShType(media.as<std::string>(),
                    label.as<std::string>(),
                    group.as<std::string>(),
                    description.as<std::string>());
  // parse "property"
  auto prop = InfoTree::get_subtree(tree.get(), "property");
  if (!prop) {
    return res;
  }
  for (const auto& it : prop->get_child_keys()) {
    res.set_prop(it, prop->branch_get_value(it));
  }
  // parse "type"
  auto types = InfoTree::get_subtree(tree.get(), "type");
  if (!types) {
    return res;
  }
  for (const auto& it : types->get_child_keys()) {
    res.set_custom_type(it, types->branch_get_value(it));
  }
  return res;
}

auto ShType::is_valid() const -> bool {
  return !media_.empty() && !label_.empty() && !group_.empty();
};

auto ShType::default_group() -> std::string {
  static const std::string group("Default");
  return group;
}

auto ShType::path() const -> fs::path { return get_path(label_, group_); }

struct Sh4ltDir {
  fs::path prefix{};
  std::string issue_log{};

  Sh4ltDir() {
    // get uid and username
    auto uid = geteuid();
    auto* pw = getpwuid(uid);
    std::string username{pw ? pw->pw_name : std::to_string(uid)};

    // check if dir exist and try to create it if necessary
    auto dir = fs::path(std::string("/tmp/sh4lt-") + username);
    if (!fs::directory_entry(dir).is_directory()) {
      std::error_code ec{};
      if (!fs::create_directories(dir, ec)) {
        issue_log.append(ec.message() + '\n');
        return;
      }
    }

    // check owner
    struct stat info {};
    stat(dir.string().c_str(), &info);
    if (info.st_uid != uid) {
      issue_log.append("Directory" + dir.string() + "must be owned by user " + username);
      return;
    }

    // check write permissions
    auto perms = fs::status(dir).permissions();
    if (fs::perms::none == (perms & fs::perms::owner_read) ||
        fs::perms::none == (perms & fs::perms::owner_read) ||
        fs::perms::none == (perms & fs::perms::owner_read)) {
      issue_log.append(
          std::string("missing owner read or write or exec permission for directory ") +
          dir.string());
      return;
    }

    prefix = dir;
  }
};

auto ShType::get_path(const std::string& label, const std::string& group_label) -> fs::path {
  static Sh4ltDir dir = Sh4ltDir();
  if (dir.prefix.empty()) return {};
  auto res =
      fs::path(dir.prefix / std::to_string(std::hash<std::string>{}(group_label + '/' + label)));
  return res;
}

auto ShType::id() const -> std::size_t { return std::hash<std::string>{}(group_ + '/' + label_); }

auto ShType::get_socket_dir() -> fs::path { return Sh4ltDir().prefix; }

auto ShType::get_log_for_path_issue() -> std::string {
  Sh4ltDir dir = Sh4ltDir();
  return dir.issue_log;
}

auto ShType::get_property_keys() const -> std::vector<std::string> {
  std::vector<std::string> res;
  for (const auto& prop : properties_) {
    res.push_back(prop.first);
  }
  return res;
}

}  // namespace sh4lt

