#pragma once

#include <fcntl.h>
#include <unistd.h>

#include <filesystem>
#include <fstream>
#include <optional>
#include <cstring>
#include <memory>

#include "aqsi-commons-global.h"

#include "AqsiError.h"
#include "AqsiException.h"

namespace aqsi {
/** Попытка создать приложение с одним экземпляром
\throw aqsi::AqsiException - При возникновении ошибок работы с файлами
Подробнее можно посмотреть:
- https://man7.org/linux/man-pages/man2/open.2.html
- https://man7.org/linux/man-pages/man2/flock.2.html
\details В случае когда приложение работает как сервис systemd и для этого сервиса настроен
сторожевой таймер (watchdog) рекомендуется перед завершением работы приложения (если предусмотрено логикой приложения
при неудачном вызове функции) оповестить systemd об успешном статусе загрузки приложения (void
WatchdogNotifier::notifyReady)
\retval true - В случае успеха запуска первого экземпляра приложения
\retval false - В случае неудачи, когда уже запущено приложение
 */
[[nodiscard]] AQSICOMMONS_EXPORT bool tryRunSingleInstanceApp();

namespace file {

struct AQSICOMMONS_EXPORT Permissions {
    char const* group;
    int mode;
};

struct AQSICOMMONS_EXPORT Options {
    char const* path;
    std::optional<Permissions> permissions;
    std::optional<size_t> max_size;
};

/** Возвращает путь к временному файлу
\param [in] path Путь к исходному файлу
*/
AQSICOMMONS_EXPORT std::filesystem::path getTmpFilePath(std::filesystem::path const& path);

/** Создаёт файл с заданными правами доступа
\param [in] options Параметры файла
\throw std::runtime_error Ошибка при создании файла
*/
AQSICOMMONS_EXPORT void create(Options const& options);

/** Удаляет файл
\param [in] options Параметры файла
\throw std::filesystem::filesystem_error Ошибка при удалении файла
*/
AQSICOMMONS_EXPORT void remove(Options const& options);

/** Устанавливает имя владельца файла
\param [in] path Путь к файлу
\param [in] owner Имя владельца файла
\throw std::invalid_argument Не получилось найти UID для имени пользователя owner
\throw std::filesystem::filesystem_error Не получилось установить имя владельца файла
*/
AQSICOMMONS_EXPORT void setOwner(std::filesystem::path const& path, std::string const& owner);

/** Устанавливает имя группы владельца файла
\param [in] path Путь к файлу
\param [in] group Имя группы владельца файла
\throw std::invalid_argument Не получилось найти GID для имени группы group
\throw std::filesystem::filesystem_error Не получилось установить имя группы владельца файла
*/
AQSICOMMONS_EXPORT void setGroup(std::filesystem::path const& path, std::string const& group);

/** Возвращает строку с описанием ошибки
\param [in] num Номер ошибки
\return Строка с описанием ошибки
*/
AQSICOMMONS_EXPORT std::string getErrorString(int num);

/** Читает данные из файла
\param [in] options Параметры файла
\param [out] container Контейнер для данных
\throw std::runtime_error Ошибка при чтении файла
*/
template <class T>
AQSICOMMONS_EXPORT void
read(Options const& options, T& container)
{
    using namespace std::literals;
    auto const error_str = "read file "s + options.path + ": "s;
    if (not std::filesystem::exists(options.path)) {
        throw std::runtime_error(error_str + "not exist"s);
    }

    std::ifstream f(options.path, std::ios_base::in | std::ios_base::binary);
    if (not f.is_open()) {
        throw std::runtime_error(error_str + "unable to open"s);
    }

    f.seekg(0, std::ios_base::end);
    auto file_size = f.tellg();
    f.seekg(0);

    if (file_size <= 0) {
        throw std::runtime_error(error_str + "no data"s);
    }

    if (options.max_size.has_value() and *options.max_size < file_size) {
        file_size = *options.max_size;
    }

    try {
        container.resize(file_size);
        f.read(container.data(), file_size);
    } catch (std::exception const& e) {
        throw std::runtime_error(error_str + e.what());
    } catch (...) {
        throw std::runtime_error(error_str + "undefined error"s);
    }
}

/** Перезаписывает данные в файле, устанавливает группу и меняет права доступа к файлу
\param [in] options Параметры файла
\param [out] container Контейнер с данными
\throw aqsi::AqsiException При возникновении ошибок при перезаписи
*/
template <typename T>
AQSICOMMONS_EXPORT void
rewrite(aqsi::file::Options const& options, T const& container)
{
    using namespace std::string_literals;

    auto const tmp_path = aqsi::file::getTmpFilePath(options.path);

    auto fd = ::open(
        tmp_path.c_str(), O_WRONLY | O_TRUNC | O_CREAT | O_SYNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH);

    if (fd < 0) {
        throw AqsiException(MakeAqsiErrorFromErrnoWithDescription(
            errno, "Не удалось открыть для записи или создать файл '"s + options.path + "' ."));
    }

    std::optional<int> errno_close;

    {
        auto l_closeFile = [&errno_close](int* fd_pointer) {
            auto const close_result = ::close(*fd_pointer);
            if (close_result != 0) {
                errno_close = errno;
            }
        };

        std::unique_ptr<int, decltype(l_closeFile)> close_file_guard(&fd, l_closeFile);

        if (container.size() > 0) {
            auto bytes_written = ::write(fd, container.data(), container.size());

            auto const l_writeUntilGetAnError = [fd, &container, &bytes_written, &options]() {
                auto bytes_written_total             = bytes_written;
                auto steel_need_to_write_bytes_count = container.size() - bytes_written;

                do {
                    bytes_written =
                        ::write(fd, container.data() + bytes_written_total, steel_need_to_write_bytes_count);
                    steel_need_to_write_bytes_count -= bytes_written;
                    bytes_written_total += bytes_written;
                } while (bytes_written_total < container.size() and bytes_written >= 0);

                if (bytes_written < 0) {
                    throw AqsiException(MakeAqsiErrorFromErrnoWithDescription(
                        errno, "Не удалось записать данные ("s + std::to_string(container.size()) + " байт) в файл '"s +
                                   options.path + "' ."));
                }
            };

            if (bytes_written >= 0 and bytes_written < container.size()) {
                l_writeUntilGetAnError();
            } else if (bytes_written < 0) {
                if (errno != EINTR) {
                    throw AqsiException(MakeAqsiErrorFromErrnoWithDescription(
                        errno, "Не удалось записать данные ("s + std::to_string(container.size()) + " байт) в файл '"s +
                                   options.path + "' ."));
                } else {
                    // https://man7.org/linux/man-pages/man2/lseek.2.html
                    auto const lseek_result = ::lseek(fd, 0, SEEK_SET);
                    if (lseek_result == (off_t)-1) {
                        throw AqsiException(MakeAqsiErrorFromErrnoWithDescription(
                            errno,
                            "Не удалось установить смещение для (при повторной попытке записи, прерванной получением сигнала)  записи данных ("s +
                                std::to_string(container.size()) + " байт) в файл '"s + options.path + "' ."));
                    }
                    bytes_written = 0;
                    l_writeUntilGetAnError();
                }
            }
        }

        auto const fsync_result = ::fsync(fd);

        if (fsync_result != 0) {
            throw AqsiException(MakeAqsiErrorFromErrnoWithDescription(
                errno, "Не удалось синхронизировать содержимое файла '"s + options.path +
                           "' с ПЗУ после записи в него данных ("s + std::to_string(container.size()) + " байт)."));
        }
    }

    auto const unknown_error_message = "Неизвестная ошибка.";

    {
        auto error_message =
            "Не удалось переименовать файл '"s + tmp_path.c_str() + "' на '" + options.path + "'. Ошибка:";
        try {
            std::filesystem::rename(tmp_path, options.path);

            if (options.permissions.has_value()) {
                error_message = "Не удалось установить группу '"s + options.permissions->group + "' для файла '"s +
                                options.path + "'. Ошибка: ";
                aqsi::file::setGroup(options.path, options.permissions->group);

                error_message = "Не удалось установить права доступа '"s + std::to_string(options.permissions->mode) +
                                "' для файл по пути '" + options.path + "'. Ошибка: ";
                std::filesystem::permissions(options.path, std::filesystem::perms(options.permissions->mode));
            }

        } catch (std::bad_alloc const& bad_alloc_exception) {
            throw AqsiException(
                MakeAqsiError(AqsiErrorCode::kResourceError, error_message + bad_alloc_exception.what()));
        } catch (std::filesystem::filesystem_error const& filesystem_error) {
            throw AqsiException(
                MakeAqsiError(AqsiErrorCode::kChangeValueError, error_message + filesystem_error.what()));
        } catch (...) {
            throw AqsiException(MakeAqsiError(AqsiErrorCode::kUnknown, error_message + unknown_error_message));
        }
    }

    if (errno_close.has_value()) {
        throw AqsiException(MakeAqsiErrorFromErrnoWithDescription(
            errno_close.value(), "Не удалось закрыть файл '"s + options.path + "' после записи данных ("s +
                                     std::to_string(container.size()) + " байт)."));
    }
}

}  // namespace file
}  // namespace aqsi
