/**
 * @file raw_trajectory_store.cpp
 * @brief RM75 厂商原始拖动示教轨迹的不可覆盖存储和 manifest 固化。
 *
 * @author 唐永康
 * @date 2026-07-10
 */
#include "rm_control/modules/trajectory/raw_trajectory_store.h"

#include "rm_control/modules/trajectory/sha256_file_checksum.h"

#include <algorithm>
#include <chrono>
#include <ctime>
#include <fstream>
#include <iomanip>
#include <iterator>
#include <sstream>
#include <system_error>

namespace rm_control::modules::trajectory {
namespace {

constexpr std::size_t kMaximumSanitizedSessionNameLength = 64U;
constexpr int kMaximumUniqueDirectoryAttempts = 1000;
constexpr int kCollisionSuffixWidth = 3;
constexpr const char *kSessionsDirectoryName = "sessions";
constexpr const char *kRawTrajectoryFileName = "raw_trajectory.txt";
constexpr const char *kSynchronizedSamplesFileName = "synchronized_samples.csv";
constexpr const char *kManifestFileName = "manifest.json";
constexpr const char *kManifestTemporaryFileName = "manifest.json.tmp";

struct UtcTimestamp {
    std::string compact_with_milliseconds;
    std::string iso8601_with_milliseconds;
};

std::string PathForError(const std::filesystem::path &path) {
    return path.string();
}

bool ConvertToUtc(std::time_t time, std::tm *utc) {
    if (utc == nullptr) {
        return false;
    }
#if defined(_WIN32)
    return ::gmtime_s(utc, &time) == 0;
#else
    return ::gmtime_r(&time, utc) != nullptr;
#endif
}

common::Result<UtcTimestamp> CurrentUtcTimestamp() {
    const auto now = std::chrono::system_clock::now();
    const auto milliseconds_since_epoch =
        std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch());
    const auto whole_seconds =
        std::chrono::duration_cast<std::chrono::seconds>(milliseconds_since_epoch);
    const auto fractional_milliseconds = milliseconds_since_epoch - whole_seconds;
    const std::time_t time = std::chrono::system_clock::to_time_t(now);

    std::tm utc{};
    if (!ConvertToUtc(time, &utc)) {
        return common::Result<UtcTimestamp>::failure("failed to convert current time to UTC");
    }

    std::ostringstream compact;
    compact << std::put_time(&utc, "%Y%m%dT%H%M%S") << '_'
            << std::setw(3) << std::setfill('0') << fractional_milliseconds.count() << 'Z';

    std::ostringstream iso8601;
    iso8601 << std::put_time(&utc, "%Y-%m-%dT%H:%M:%S") << '.'
            << std::setw(3) << std::setfill('0') << fractional_milliseconds.count() << 'Z';
    return common::Result<UtcTimestamp>::success(
        UtcTimestamp{compact.str(), iso8601.str()});
}

char ToUpperAscii(char value) {
    if (value >= 'a' && value <= 'z') {
        return static_cast<char>(value - 'a' + 'A');
    }
    return value;
}

bool IsWindowsReservedFileName(const std::string &name) {
    std::string uppercase;
    uppercase.reserve(name.size());
    std::transform(name.begin(), name.end(), std::back_inserter(uppercase), ToUpperAscii);

    if (uppercase == "CON" || uppercase == "PRN" || uppercase == "AUX" ||
        uppercase == "NUL") {
        return true;
    }
    if (uppercase.size() != 4U) {
        return false;
    }
    const bool has_reserved_prefix = uppercase.compare(0, 3, "COM") == 0 ||
                                     uppercase.compare(0, 3, "LPT") == 0;
    return has_reserved_prefix && uppercase[3] >= '1' && uppercase[3] <= '9';
}

void RemoveLeadingAndTrailingUnderscores(std::string *value) {
    if (value == nullptr) {
        return;
    }
    const auto first_character = value->find_first_not_of('_');
    if (first_character == std::string::npos) {
        value->clear();
        return;
    }
    const auto last_character = value->find_last_not_of('_');
    *value = value->substr(first_character, last_character - first_character + 1U);
}

std::string FormatCollisionSuffix(int attempt) {
    if (attempt == 0) {
        return "";
    }
    std::ostringstream suffix;
    suffix << '_' << std::setw(kCollisionSuffixWidth) << std::setfill('0') << attempt;
    return suffix.str();
}

RawTrajectorySessionPaths MakeSessionPaths(const std::filesystem::path &directory,
                                           const std::string &session_id,
                                           const std::string &saved_at_utc) {
    RawTrajectorySessionPaths paths;
    paths.directory = directory;
    paths.raw_trajectory = directory / kRawTrajectoryFileName;
    paths.synchronized_csv = directory / kSynchronizedSamplesFileName;
    paths.manifest = directory / kManifestFileName;
    paths.session_id = session_id;
    paths.saved_at_utc = saved_at_utc;
    return paths;
}

common::Result<void> ValidateSessionPaths(const RawTrajectorySessionPaths &paths) {
    if (paths.directory.empty() || paths.session_id.empty() || paths.saved_at_utc.empty()) {
        return common::Result<void>::failure(
            "raw trajectory session directory, session ID, and saved UTC time must not be empty");
    }

    const std::filesystem::path normalized_directory = paths.directory.lexically_normal();
    if (normalized_directory.filename() != paths.session_id ||
        paths.raw_trajectory.lexically_normal() != normalized_directory / kRawTrajectoryFileName ||
        paths.synchronized_csv.lexically_normal() !=
            normalized_directory / kSynchronizedSamplesFileName ||
        paths.manifest.lexically_normal() != normalized_directory / kManifestFileName) {
        return common::Result<void>::failure(
            "raw trajectory artifact paths do not match the controlled session layout: " +
            PathForError(paths.directory));
    }

    std::error_code filesystem_error;
    const std::filesystem::file_status directory_status =
        std::filesystem::symlink_status(normalized_directory, filesystem_error);
    if (filesystem_error) {
        return common::Result<void>::failure(
            "failed to inspect raw trajectory session directory " +
            PathForError(normalized_directory) + ": " + filesystem_error.message());
    }
    if (!std::filesystem::is_directory(directory_status)) {
        return common::Result<void>::failure(
            "raw trajectory session path is not a real directory: " +
            PathForError(normalized_directory));
    }
    return common::Result<void>::success();
}

std::string EscapeJsonString(const std::string &value) {
    std::ostringstream escaped;
    escaped << std::hex << std::setfill('0');
    for (const unsigned char character : value) {
        switch (character) {
        case '"':
            escaped << "\\\"";
            break;
        case '\\':
            escaped << "\\\\";
            break;
        case '\b':
            escaped << "\\b";
            break;
        case '\f':
            escaped << "\\f";
            break;
        case '\n':
            escaped << "\\n";
            break;
        case '\r':
            escaped << "\\r";
            break;
        case '\t':
            escaped << "\\t";
            break;
        default:
            if (character < 0x20U) {
                escaped << "\\u" << std::setw(4) << static_cast<unsigned int>(character);
            } else {
                escaped << static_cast<char>(character);
            }
            break;
        }
    }
    return escaped.str();
}

std::string BuildManifestJson(const RawTrajectoryArtifact &artifact) {
    std::ostringstream manifest;
    manifest << "{\n"
             << "  \"schema_version\": 1,\n"
             << "  \"session_id\": \"" << EscapeJsonString(artifact.paths.session_id) << "\",\n"
             << "  \"robot_model\": \"" << EscapeJsonString(artifact.metadata.robot_model) << "\",\n"
             << "  \"sdk_version\": \"" << EscapeJsonString(artifact.metadata.sdk_version) << "\",\n"
             << "  \"firmware_version\": \""
             << EscapeJsonString(artifact.metadata.firmware_version) << "\",\n"
             << "  \"saved_at_utc\": \"" << EscapeJsonString(artifact.paths.saved_at_utc) << "\",\n"
             << "  \"manifest_written_at_utc\": \""
             << EscapeJsonString(artifact.manifest_written_at_utc) << "\",\n"
             << "  \"trajectory_point_count\": "
             << artifact.metadata.trajectory_point_count << ",\n"
             << "  \"recorded_sample_count\": "
             << artifact.metadata.recorded_sample_count << ",\n"
             << "  \"synchronized_timestamp_unit\": \"millisecond\",\n"
             << "  \"synchronized_monotonic_clock\": \"std::chrono::steady_clock\",\n"
             << "  \"synchronized_timestamp_is_derived\": false,\n"
             << "  \"vendor_raw_timestamp_semantics\": \"vendor_defined_unmodified\",\n"
             << "  \"raw_trajectory_file\": \"" << kRawTrajectoryFileName << "\",\n"
             << "  \"synchronized_samples_file\": \""
             << kSynchronizedSamplesFileName << "\",\n"
             << "  \"synchronized_csv_write_succeeded\": "
             << (artifact.metadata.synchronized_csv_write_succeeded ? "true" : "false")
             << ",\n"
             << "  \"raw_file_present\": "
             << (artifact.raw_file_present ? "true" : "false") << ",\n"
             << "  \"raw_file_size_bytes\": " << artifact.raw_file_size_bytes << ",\n"
             << "  \"sha256\": \"" << artifact.raw_file_sha256 << "\",\n"
             << "  \"save_interface_called\": "
             << (artifact.metadata.vendor_save_api_called ? "true" : "false") << ",\n"
             << "  \"save_interface_return_code\": "
             << artifact.metadata.vendor_save_return_code << ",\n"
             << "  \"save_interface_error\": \""
             << EscapeJsonString(artifact.metadata.vendor_save_error_message) << "\"\n"
             << "}\n";
    return manifest.str();
}

common::Result<void> WriteManifestAtomically(const RawTrajectoryArtifact &artifact) {
    std::error_code filesystem_error;
    if (std::filesystem::exists(artifact.paths.manifest, filesystem_error)) {
        return common::Result<void>::failure(
            "refusing to overwrite existing raw trajectory manifest: " +
            PathForError(artifact.paths.manifest));
    }
    if (filesystem_error) {
        return common::Result<void>::failure(
            "failed to inspect raw trajectory manifest " +
            PathForError(artifact.paths.manifest) + ": " + filesystem_error.message());
    }

    const std::filesystem::path temporary_manifest =
        artifact.paths.directory / kManifestTemporaryFileName;
    if (std::filesystem::exists(temporary_manifest, filesystem_error)) {
        return common::Result<void>::failure(
            "refusing to overwrite existing temporary manifest: " +
            PathForError(temporary_manifest));
    }
    if (filesystem_error) {
        return common::Result<void>::failure(
            "failed to inspect temporary manifest " + PathForError(temporary_manifest) + ": " +
            filesystem_error.message());
    }

    std::ofstream output(temporary_manifest, std::ios::out | std::ios::trunc);
    if (!output) {
        return common::Result<void>::failure(
            "failed to create temporary manifest: " + PathForError(temporary_manifest));
    }
    output << BuildManifestJson(artifact);
    output.flush();
    if (!output) {
        return common::Result<void>::failure(
            "failed to write temporary manifest: " + PathForError(temporary_manifest));
    }
    output.close();
    if (!output) {
        return common::Result<void>::failure(
            "failed to close temporary manifest: " + PathForError(temporary_manifest));
    }

    std::filesystem::create_hard_link(
        temporary_manifest, artifact.paths.manifest, filesystem_error);
    if (filesystem_error) {
        return common::Result<void>::failure(
            "failed to atomically publish manifest " + PathForError(artifact.paths.manifest) +
            ": " + filesystem_error.message());
    }
    std::filesystem::remove(temporary_manifest, filesystem_error);
    if (filesystem_error) {
        return common::Result<void>::failure(
            "manifest was published but its temporary hard link could not be removed " +
            PathForError(temporary_manifest) + ": " + filesystem_error.message());
    }
    return common::Result<void>::success();
}

common::Result<void> InspectAndProtectRawFile(RawTrajectoryArtifact *artifact) {
    if (artifact == nullptr) {
        return common::Result<void>::failure("raw trajectory artifact output must not be null");
    }

    std::error_code filesystem_error;
    const bool raw_file_exists =
        std::filesystem::exists(artifact->paths.raw_trajectory, filesystem_error);
    if (filesystem_error) {
        return common::Result<void>::failure(
            "failed to inspect raw trajectory file " +
            PathForError(artifact->paths.raw_trajectory) + ": " + filesystem_error.message());
    }
    if (!raw_file_exists) {
        return common::Result<void>::success();
    }

    const std::filesystem::file_status raw_status =
        std::filesystem::symlink_status(artifact->paths.raw_trajectory, filesystem_error);
    if (filesystem_error) {
        return common::Result<void>::failure(
            "failed to inspect raw trajectory file type " +
            PathForError(artifact->paths.raw_trajectory) + ": " + filesystem_error.message());
    }
    if (!std::filesystem::is_regular_file(raw_status)) {
        return common::Result<void>::failure(
            "raw trajectory artifact is not a regular file: " +
            PathForError(artifact->paths.raw_trajectory));
    }

    artifact->raw_file_present = true;
    artifact->raw_file_size_bytes =
        std::filesystem::file_size(artifact->paths.raw_trajectory, filesystem_error);
    if (filesystem_error) {
        return common::Result<void>::failure(
            "failed to read raw trajectory file size " +
            PathForError(artifact->paths.raw_trajectory) + ": " + filesystem_error.message());
    }

    constexpr std::filesystem::perms kAllWritePermissions =
        std::filesystem::perms::owner_write | std::filesystem::perms::group_write |
        std::filesystem::perms::others_write;
    std::filesystem::permissions(artifact->paths.raw_trajectory, kAllWritePermissions,
                                 std::filesystem::perm_options::remove, filesystem_error);
    if (filesystem_error) {
        return common::Result<void>::failure(
            "failed to make raw trajectory read-only " +
            PathForError(artifact->paths.raw_trajectory) + ": " + filesystem_error.message());
    }
    const auto modification_time_before_checksum =
        std::filesystem::last_write_time(artifact->paths.raw_trajectory, filesystem_error);
    if (filesystem_error) {
        return common::Result<void>::failure(
            "failed to inspect raw trajectory modification time " +
            PathForError(artifact->paths.raw_trajectory) + ": " + filesystem_error.message());
    }

    auto checksum_result = CalculateFileSha256(artifact->paths.raw_trajectory);
    if (!checksum_result.ok) {
        return common::Result<void>::failure(checksum_result.error_message);
    }
    artifact->raw_file_sha256 = checksum_result.value;

    const std::uintmax_t size_after_checksum =
        std::filesystem::file_size(artifact->paths.raw_trajectory, filesystem_error);
    if (filesystem_error) {
        return common::Result<void>::failure(
            "failed to verify raw trajectory file size " +
            PathForError(artifact->paths.raw_trajectory) + ": " + filesystem_error.message());
    }
    if (size_after_checksum != artifact->raw_file_size_bytes) {
        return common::Result<void>::failure(
            "raw trajectory file changed while SHA-256 was being calculated: " +
            PathForError(artifact->paths.raw_trajectory));
    }
    const auto modification_time_after_checksum =
        std::filesystem::last_write_time(artifact->paths.raw_trajectory, filesystem_error);
    if (filesystem_error) {
        return common::Result<void>::failure(
            "failed to verify raw trajectory modification time " +
            PathForError(artifact->paths.raw_trajectory) + ": " + filesystem_error.message());
    }
    if (modification_time_after_checksum != modification_time_before_checksum) {
        return common::Result<void>::failure(
            "raw trajectory modification time changed during SHA-256 calculation: " +
            PathForError(artifact->paths.raw_trajectory));
    }
    return common::Result<void>::success();
}

common::Result<void> RemoveWritePermissions(const std::filesystem::path &path) {
    constexpr std::filesystem::perms kAllWritePermissions =
        std::filesystem::perms::owner_write | std::filesystem::perms::group_write |
        std::filesystem::perms::others_write;
    std::error_code filesystem_error;
    std::filesystem::permissions(path, kAllWritePermissions,
                                 std::filesystem::perm_options::remove, filesystem_error);
    if (filesystem_error) {
        return common::Result<void>::failure(
            "failed to remove write permissions from " + PathForError(path) + ": " +
            filesystem_error.message());
    }
    return common::Result<void>::success();
}

common::Result<void> ProtectCompletedSession(
    const RawTrajectorySessionPaths &paths) {
    std::error_code filesystem_error;
    const bool synchronized_csv_exists =
        std::filesystem::exists(paths.synchronized_csv, filesystem_error);
    if (filesystem_error) {
        return common::Result<void>::failure(
            "failed to inspect synchronized trajectory CSV permissions: " +
            filesystem_error.message());
    }
    if (synchronized_csv_exists) {
        const auto csv_result = RemoveWritePermissions(paths.synchronized_csv);
        if (!csv_result.ok) {
            return csv_result;
        }
    }
    const auto manifest_result = RemoveWritePermissions(paths.manifest);
    if (!manifest_result.ok) {
        return manifest_result;
    }
    return RemoveWritePermissions(paths.directory);
}

} // namespace

std::string SanitizeRawTrajectorySessionName(const std::string &requested_session_name) {
    std::string sanitized;
    sanitized.reserve(std::min(requested_session_name.size(),
                               kMaximumSanitizedSessionNameLength));
    bool previous_character_was_invalid = false;
    for (const unsigned char character : requested_session_name) {
        const bool is_ascii_letter = (character >= 'A' && character <= 'Z') ||
                                     (character >= 'a' && character <= 'z');
        const bool is_ascii_digit = character >= '0' && character <= '9';
        const bool is_allowed = is_ascii_letter || is_ascii_digit || character == '_' ||
                                character == '-';

        if (is_allowed) {
            sanitized.push_back(static_cast<char>(character));
            previous_character_was_invalid = false;
        } else if (!sanitized.empty() && !previous_character_was_invalid) {
            sanitized.push_back('_');
            previous_character_was_invalid = true;
        }
        if (sanitized.size() >= kMaximumSanitizedSessionNameLength) {
            break;
        }
    }

    RemoveLeadingAndTrailingUnderscores(&sanitized);
    if (sanitized.empty()) {
        sanitized = "trajectory";
    }
    if (IsWindowsReservedFileName(sanitized)) {
        sanitized = "trajectory_" + sanitized;
    }
    if (sanitized.size() > kMaximumSanitizedSessionNameLength) {
        sanitized.resize(kMaximumSanitizedSessionNameLength);
        RemoveLeadingAndTrailingUnderscores(&sanitized);
    }
    return sanitized;
}

common::Result<RawTrajectorySessionPaths>
CreateUniqueRawTrajectorySession(const std::filesystem::path &storage_root,
                                 const std::string &requested_session_name) {
    if (storage_root.empty()) {
        return common::Result<RawTrajectorySessionPaths>::failure(
            "raw trajectory storage root must not be empty");
    }

    const std::filesystem::path sessions_root = storage_root / kSessionsDirectoryName;
    std::error_code filesystem_error;
    std::filesystem::create_directories(sessions_root, filesystem_error);
    if (filesystem_error) {
        return common::Result<RawTrajectorySessionPaths>::failure(
            "failed to create raw trajectory sessions root " + PathForError(sessions_root) +
            ": " + filesystem_error.message());
    }

    const std::filesystem::file_status sessions_status =
        std::filesystem::symlink_status(sessions_root, filesystem_error);
    if (filesystem_error || !std::filesystem::is_directory(sessions_status)) {
        const std::string detail = filesystem_error ? filesystem_error.message() :
                                                      "path is not a real directory";
        return common::Result<RawTrajectorySessionPaths>::failure(
            "invalid raw trajectory sessions root " + PathForError(sessions_root) + ": " +
            detail);
    }

    auto timestamp_result = CurrentUtcTimestamp();
    if (!timestamp_result.ok) {
        return common::Result<RawTrajectorySessionPaths>::failure(timestamp_result.error_message);
    }
    const std::string sanitized_name =
        SanitizeRawTrajectorySessionName(requested_session_name);
    const std::string base_session_id =
        sanitized_name + "_" + timestamp_result.value.compact_with_milliseconds;

    for (int attempt = 0; attempt < kMaximumUniqueDirectoryAttempts; ++attempt) {
        const std::string session_id = base_session_id + FormatCollisionSuffix(attempt);
        const std::filesystem::path session_directory = sessions_root / session_id;
        filesystem_error.clear();
        const bool directory_created =
            std::filesystem::create_directory(session_directory, filesystem_error);
        if (directory_created) {
            return common::Result<RawTrajectorySessionPaths>::success(
                MakeSessionPaths(session_directory, session_id,
                                 timestamp_result.value.iso8601_with_milliseconds));
        }
        if (filesystem_error) {
            return common::Result<RawTrajectorySessionPaths>::failure(
                "failed to create unique raw trajectory session " +
                PathForError(session_directory) + ": " + filesystem_error.message());
        }
    }

    return common::Result<RawTrajectorySessionPaths>::failure(
        "failed to reserve a unique raw trajectory session after " +
        std::to_string(kMaximumUniqueDirectoryAttempts) + " attempts under " +
        PathForError(sessions_root));
}

common::Result<RawTrajectoryArtifact>
FinalizeRawTrajectorySession(const RawTrajectorySessionPaths &paths,
                             const RawTrajectoryManifestMetadata &metadata) {
    auto validation_result = ValidateSessionPaths(paths);
    if (!validation_result.ok) {
        return common::Result<RawTrajectoryArtifact>::failure(validation_result.error_message);
    }

    RawTrajectoryArtifact artifact;
    artifact.paths = paths;
    artifact.metadata = metadata;

    auto timestamp_result = CurrentUtcTimestamp();
    if (!timestamp_result.ok) {
        return common::Result<RawTrajectoryArtifact>::failure(timestamp_result.error_message);
    }
    artifact.manifest_written_at_utc = timestamp_result.value.iso8601_with_milliseconds;

    auto raw_file_result = InspectAndProtectRawFile(&artifact);
    if (!raw_file_result.ok) {
        return common::Result<RawTrajectoryArtifact>::failure(raw_file_result.error_message);
    }

    auto manifest_result = WriteManifestAtomically(artifact);
    if (!manifest_result.ok) {
        return common::Result<RawTrajectoryArtifact>::failure(manifest_result.error_message);
    }
    const auto protection_result = ProtectCompletedSession(paths);
    if (!protection_result.ok) {
        return common::Result<RawTrajectoryArtifact>::failure(
            protection_result.error_message);
    }
    return common::Result<RawTrajectoryArtifact>::success(artifact);
}

} // namespace rm_control::modules::trajectory
