/*============================================================================

The Medical Imaging Interaction Toolkit (MITK)

Copyright (c) German Cancer Research Center (DKFZ)
All rights reserved.

Use of this source code is governed by a 3-clause BSD license that can be
found in the LICENSE file.

============================================================================*/

#include <mitkDataStorageBridge.h>
#include <mitkStringProperty.h>
#include <mitkProperties.h>
#include <mitkNodePredicateAnd.h>
#include <mitkNodePredicateNot.h>
#include <mitkNodePredicateDataType.h>
#include <mitkNodePredicateFunction.h>
#include <mitkLog.h>

#include <algorithm>
#include <regex>
#include <set>
#include <sstream>
#include <chrono>
#include <ctime>
#include <iomanip>


namespace
{
  /**
   * @brief Convert a JSON value to its string representation for filtering.
   *
   * Needed to ensure that strings are returned w/o '\"' escapes and null is converted into empty string.
   * - JSON string -> the string value
   * - JSON null -> empty string
   * - JSON boolean, number and object/array -> dump() representation
   */
  std::string JsonValueToFilterString(const nlohmann::json& json)
  {
    if (json.is_string())
    {
      return json.get<std::string>();
    }
    else if (json.is_null())
    {
      return "";
    }
    else
    {
      // For boolean, numbers and complex types (arrays, objects), use the JSON dump
      return json.dump();
    }
  }

  mitk::BaseProperty::ConstPointer GetConstProperty(const mitk::DataNode* node, const std::string& propertyKey,
    const std::optional<std::string>& context, const mitk::PropertyScope scope)
  {
    if (nullptr == node)
      mitkThrow() << "Wrong use of GetConstProperty. Node is nullptr.";

    std::string contextName = context.has_value() ? context.value() : "";

    mitk::BaseProperty::ConstPointer allResult = node->GetConstProperty(propertyKey, contextName, false);

    if (scope == mitk::PropertyScope::All)
    {
      return allResult;
    }

    // scope is not all, we have to check explicit because the node implementation has a automatic
    // fall trough...
    mitk::BaseProperty::ConstPointer dataResult;
    if (nullptr != node->GetData())
    {
      dataResult = node->GetData()->GetConstProperty(propertyKey, contextName, false);
    }

    if (scope == mitk::PropertyScope::Data)
    {
      return dataResult;
    }

    // scope is Node only. If dataResult and allResult are different pointer,
    // then there is a property defined at node level.
    if (allResult != dataResult)
    {
      return allResult;
    }

    return nullptr;
  }

  /**
   * @brief Get the correct set of property keys for a given scope.
   *
   * Uses IPropertyProvider::GetPropertyKeys() which returns only keys owned by
   * the respective provider (no fall-through from node to data or vice versa).
   *
   * @pre node must not be nullptr.
   */
  std::vector<std::string> GetPropertyKeysForScope(
    const mitk::DataNode* node,
    const std::string& contextName,
    mitk::PropertyScope scope)
  {
    // Get data-level keys (needed for Data and All scopes)
    std::vector<std::string> dataKeys;
    if (scope != mitk::PropertyScope::Node && node->GetData() != nullptr)
    {
      dataKeys = node->GetData()->GetPropertyKeys(contextName);
    }

    if (scope == mitk::PropertyScope::Data)
      return dataKeys;

    auto nodeKeys = node->GetPropertyKeys(contextName);

    if (scope == mitk::PropertyScope::Node)
      return nodeKeys;

    // Scope::All — union of node + data keys
    if (!dataKeys.empty())
    {
      std::set<std::string> allKeySet(nodeKeys.begin(), nodeKeys.end());
      allKeySet.insert(dataKeys.begin(), dataKeys.end());
      return std::vector<std::string>(allKeySet.begin(), allKeySet.end());
    }

    return nodeKeys;
  }

  std::string GenerateTimestampString()
  {
    auto now = std::chrono::system_clock::now();

    // Split into seconds + fractional part
    auto seconds = std::chrono::time_point_cast<std::chrono::seconds>(now);
    auto fraction = now - seconds;
    auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(fraction).count();

    std::time_t t = std::chrono::system_clock::to_time_t(seconds);

    std::tm tm{};
#if defined(_WIN32)
    gmtime_s(&tm, &t);   // Windows thread-safe
#else
    gmtime_r(&t, &tm);   // POSIX thread-safe
#endif

    std::ostringstream oss;
    oss << std::put_time(&tm, "%Y-%m-%dT%H:%M:%S");

    // Append fractional seconds
    oss << "." << std::setw(3) << std::setfill('0') << millis;

    // UTC marker
    oss << "Z";

    return oss.str();
  }

  /**
   * @brief Mark a node as modified via REST API.
   *
   * Sets the internal properties "restapi.modified" to the current timestamp and
   * "restapi.lastmodification" to the specified operation description.
   *
   * @param node The node to mark as modified. Must not be nullptr.
   * @param operation Description of the modification operation (e.g., "created", "property_set:name").
   */
  void MarkNodeAsModified(mitk::DataNode* node, const std::string& operation)
  {
    if (node == nullptr)
    {
      return;
    }

    node->SetStringProperty(mitk::DataStorageBridge::MODIFIED_PROPERTY_KEY, GenerateTimestampString().c_str());
    node->SetStringProperty(mitk::DataStorageBridge::LAST_MODIFICATION_PROPERTY_KEY, operation.c_str());
  }

  /**
   * @brief Convert a filter pattern with wildcards into a regex string.
   *
   * Escapes all regex-special characters in the literal segments and maps
   * wildcard characters to their regex equivalents:
   * - '*' -> '.*' (match zero or more characters)
   * - '?' -> '.'  (match exactly one character)
   *
   * The result is anchored with ^ and $ for a full-string match.
   *
   * @pre filterPattern must not be empty.
   * @param filterPattern The filter pattern (e.g., "CT*Scan", "Child?").
   * @return A regex string that matches the same semantics.
   */
  std::string ConvertFilterToRegex(const std::string& filterPattern)
  {
    std::string regex = "^";

    for (char c : filterPattern)
    {
      switch (c)
      {
        case '*': regex += ".*"; break;
        case '?': regex += "."; break;
        // Escape regex-special characters
        case '.': case '+': case '(': case ')':
        case '[': case ']': case '{': case '}':
        case '\\': case '^': case '$': case '|':
          regex += '\\';
          regex += c;
          break;
        default:
          regex += c;
          break;
      }
    }

    regex += "$";
    return regex;
  }

  /**
   * @brief Check if a filter pattern contains wildcard characters (* or ?).
   */
  bool ContainsWildcard(const std::string& filterValue)
  {
    return filterValue.find_first_of("*?") != std::string::npos;
  }

  /**
   * @brief Check if a property value matches a filter pattern.
   *
   * Supports glob-style wildcards anywhere in the pattern:
   * - '*' matches zero or more characters (e.g., "CT*", "*Scan", "CT*2024*Scan")
   * - '?' matches exactly one character (e.g., "Child?", "Node_0?")
   * - No wildcards: exact match
   *
   * @param propertyValue The actual property value, or std::nullopt if property doesn't exist.
   * @param filterValue The filter pattern.
   * @return true if the value matches the pattern.
   */
  bool MatchesPropertyFilter(
    const std::optional<std::string>& propertyValue,
    const std::string& filterValue)
  {
    if (!propertyValue.has_value())
    {
      return false;
    }

    if (!ContainsWildcard(filterValue))
    {
      return propertyValue.value() == filterValue;
    }

    try
    {
      std::regex pattern(ConvertFilterToRegex(filterValue));
      return std::regex_match(propertyValue.value(), pattern);
    }
    catch (const std::regex_error&)
    {
      return false;
    }
  }

  /**
   * @brief Check if a property key is internal (starts with INTERNAL_PROPERTY_PREFIX).
   *
   * Internal properties are REST API metadata and should not be exposed to clients.
   *
   * @param key The property key to check.
   * @return true if the property is internal.
   */
  bool IsInternalProperty(const std::string& key)
  {
    // Check if key starts with INTERNAL_PROPERTY_PREFIX ("restapi.")
    return key.rfind(mitk::DataStorageBridge::INTERNAL_PROPERTY_PREFIX, 0) == 0;
  }

}

namespace mitk
{

  DataStorageBridge::DataStorageBridge()
    : m_UidMapper(std::make_unique<NodeUidMapper>())
  {
  }

  DataStorageBridge::~DataStorageBridge() = default;

  void DataStorageBridge::SetDispatcher(StorageThreadDispatcherBase* dispatcher)
  {
    std::lock_guard<std::mutex> lock(m_Mutex);
    m_Dispatcher = dispatcher;
  }

  void DataStorageBridge::DispatchTask(std::function<void()> task) const
  {
    auto dispatcher = m_Dispatcher.Lock();

    if (dispatcher.IsNotNull() && !dispatcher->IsDispatchThread())
    {
      dispatcher->Execute(std::move(task));
    }
    else
    {
      task();
    }
  }

  template <typename R>
  R DataStorageBridge::DispatchTask(std::function<R()> task) const
  {
    auto dispatcher = m_Dispatcher.Lock();

    if (dispatcher.IsNotNull() && !dispatcher->IsDispatchThread())
    {
      return dispatcher->ExecuteWithResult<R>(std::move(task));
    }
    else
    {
      return task();
    }
  }

  void DataStorageBridge::SetDataStorage(DataStorage* dataStorage)
  {
    std::lock_guard<std::mutex> lock(m_Mutex);
    m_DataStorage = dataStorage;
    m_UidMapper->SetDataStorage(dataStorage);
  }

  DataStorage::Pointer DataStorageBridge::GetDataStorage() const
  {
    std::lock_guard<std::mutex> lock(m_Mutex);
    return m_DataStorage.Lock();
  }

  bool DataStorageBridge::HasDataStorage() const
  {
    std::lock_guard<std::mutex> lock(m_Mutex);
    return m_DataStorage.Lock().IsNotNull();
  }

  std::string DataStorageBridge::GetNodeUid(DataNode* node) const
  {
    return m_UidMapper->GetOrCreateUid(node);
  }

  NodePredicateBase::Pointer DataStorageBridge::BuildNodePredicate(const NodeQueryParams& params) const
  {
    auto dataStorage = m_DataStorage.Lock();
    if (dataStorage.IsNull())
      mitkThrow() << "BuildNodePredicate called without a valid DataStorage.";

    std::vector<NodePredicateBase::Pointer> predicates;

    // Hierarchy filter (toplevel = root nodes only)
    if (params.hierarchy == Hierarchy::Toplevel)
    {
      auto isRootPredicate = NodePredicateFunction::New(
        [dataStorage](const DataNode* node) -> bool {
          auto sources = dataStorage->GetSources(node);
          return sources->Size() == 0;
        });
      predicates.push_back(isRootPredicate.GetPointer());
    }

    // Path filter
    if (params.path.has_value())
    {
      const std::string targetPath = params.path.value();
      auto pathPredicate = NodePredicateFunction::New(
        [this, targetPath](const DataNode* node) -> bool {
          return this->BuildNodePath(node) == targetPath;
        });
      predicates.push_back(pathPredicate.GetPointer());
    }

    // Data type filter
    if (params.dataType.has_value())
    {
      std::string dataType = params.dataType.value();

      auto dataTypePredicate = NodePredicateDataType::New(dataType.c_str());
      predicates.push_back(dataTypePredicate.GetPointer());
    }

    // Parent UID filter
    if (params.parentUid.has_value())
    {
      const std::string targetParentUid = params.parentUid.value();
      auto parentPredicate = NodePredicateFunction::New(
        [this, dataStorage, targetParentUid](const DataNode* node) -> bool {
          auto sources = dataStorage->GetSources(node);

          if (targetParentUid == "null")
          {
            // Filter for root nodes (no parent)
            return sources->Size() == 0;
          }
          else
          {
            // Check if parent matches the target UID
            for (auto srcIt = sources->Begin(); srcIt != sources->End(); ++srcIt)
            {
              auto parentUid = m_UidMapper->GetUid(srcIt->Value().GetPointer());
              if (parentUid.has_value() && parentUid.value() == targetParentUid)
              {
                return true;
              }
            }
            return false;
          }
        });
      predicates.push_back(parentPredicate.GetPointer());
    }

    // Property filters
    for (const auto& propFilter : params.propertyFilters)
    {
      auto propertyPredicate = NodePredicateFunction::New(
        [params, propFilter](const DataNode* node) -> bool {
          std::optional<std::string> propValue;

          // Handle special fields that aren't stored as properties
          if (propFilter.key == "name")
          {
            propValue = node->GetName();
          }
          else
          {
            auto prop = GetConstProperty(node, propFilter.key, params.context, params.propertyScope);
            if (prop != nullptr)
            {
              // Use JSON representation for filtering to match what users see in API responses.
              // This ensures BoolProperty filters with "true"/"false" (not "1"/"0"),
              // making the filter behavior consistent with the REST API output.
              auto jsonValue = ConvertPropertyToSelfContainedJson(prop);
              propValue = JsonValueToFilterString(jsonValue);
            }
          }

          bool matches = MatchesPropertyFilter(propValue, propFilter.value);

          // Apply negation if needed (for != operator)
          return propFilter.negated ? !matches : matches;
        });
      predicates.push_back(propertyPredicate.GetPointer());
    }

    // Combine all predicates with AND
    if (predicates.empty())
    {
      // No filters - return a predicate that matches everything
      return NodePredicateFunction::New([](const DataNode*) { return true; });
    }
    else if (predicates.size() == 1)
    {
      return predicates[0];
    }

    // Build a chain of AND predicates
    auto combined = NodePredicateAnd::New();
    for (auto predicate : predicates)
    {
      combined->AddPredicate(predicate);
    }
    return combined.GetPointer();
  }

  DataStorageBridge::NodeQueryResult DataStorageBridge::GetNodes(const NodeQueryParams& params) const
  {
    std::lock_guard<std::mutex> lock(m_Mutex);
    return this->DispatchTask<NodeQueryResult>([this, params]()
    {
      NodeQueryResult result;
      result.nodes = Json::array();
      result.totalCount = 0;
      result.limit = params.limit;
      result.offset = params.offset;

      auto dataStorage = m_DataStorage.Lock();
      if (dataStorage.IsNull())
      {
        return result;
      }

      // Build predicate from query parameters
      auto predicate = this->BuildNodePredicate(params);

      // Get filtered nodes using the predicate
      auto filteredNodes = dataStorage->GetSubset(predicate);

      // Convert to vector for sorting and pagination
      std::vector<DataNode*> matchingNodes;
      matchingNodes.reserve(filteredNodes->Size());
      for (auto it = filteredNodes->Begin(); it != filteredNodes->End(); ++it)
      {
        matchingNodes.push_back(it->Value().GetPointer());
      }

      // Apply sorting if specified
      // Sorting is done by JSON result fields: uid, name, path, parent_uid, data_type, children_count, timestamp
      if (params.sort.has_value())
      {
        const auto& sortSpec = params.sort.value();
        std::sort(matchingNodes.begin(), matchingNodes.end(),
          [this, &sortSpec, &dataStorage](DataNode* a, DataNode* b) {
            const std::string& field = sortSpec.field;

            // Handle numeric fields (children_count, timestamp)
            if (field == "children_count")
            {
              int countA = this->GetChildrenCount(a);
              int countB = this->GetChildrenCount(b);
              return sortSpec.ascending ? (countA < countB) : (countA > countB);
            }
            else if (field == "timestamp")
            {
              unsigned long timeA = a->GetMTime();
              unsigned long timeB = b->GetMTime();
              return sortSpec.ascending ? (timeA < timeB) : (timeA > timeB);
            }

            // Handle string fields
            std::string valA, valB;

            if (field == "uid")
            {
              valA = m_UidMapper->GetOrCreateUid(a);
              valB = m_UidMapper->GetOrCreateUid(b);
            }
            else if (field == "name")
            {
              valA = a->GetName();
              valB = b->GetName();
            }
            else if (field == "path")
            {
              valA = this->BuildNodePath(a);
              valB = this->BuildNodePath(b);
            }
            else if (field == "parent_uid")
            {
              auto sourcesA = dataStorage->GetSources(a);
              auto sourcesB = dataStorage->GetSources(b);
              valA = (sourcesA->Size() > 0) ? m_UidMapper->GetOrCreateUid(sourcesA->ElementAt(0)) : "";
              valB = (sourcesB->Size() > 0) ? m_UidMapper->GetOrCreateUid(sourcesB->ElementAt(0)) : "";
            }
            else if (field == "data_type")
            {
              auto dataA = a->GetData();
              auto dataB = b->GetData();
              valA = dataA ? dataA->GetNameOfClass() : "";
              valB = dataB ? dataB->GetNameOfClass() : "";
            }
            else
            {
              // Unknown field - no sorting
              return false;
            }

            return sortSpec.ascending ? (valA < valB) : (valA > valB);
          });
      }

      // Store total count before pagination
      result.totalCount = static_cast<int>(matchingNodes.size());

      // Apply pagination
      int startIdx = std::min(params.offset, static_cast<int>(matchingNodes.size()));
      int endIdx = std::min(params.offset + params.limit, static_cast<int>(matchingNodes.size()));

      // Build result with optional field selection
      for (int i = startIdx; i < endIdx; ++i)
      {
        Json nodeJson = this->NodeToJson(matchingNodes[i]);

        if (!params.fields.empty())
        {
          Json filtered = Json::object();
          for (const auto& field : params.fields)
          {
            if (nodeJson.contains(field))
            {
              filtered[field] = nodeJson[field];
            }
          }
          result.nodes.push_back(filtered);
        }
        else
        {
          result.nodes.push_back(nodeJson);
        }
      }

      return result;
    });
  }

  std::optional<DataStorageBridge::Json> DataStorageBridge::GetNode(const std::string& uid) const
  {
    std::lock_guard<std::mutex> lock(m_Mutex);
    return this->DispatchTask<std::optional<Json>>([this, uid]()
    {
      auto dataStorage = m_DataStorage.Lock();
      if (dataStorage.IsNull())
      {
        return std::optional<Json>(std::nullopt);
      }

      auto node = m_UidMapper->FindNodeByUid(uid);
      if (node == nullptr)
      {
        return std::optional<Json>(std::nullopt);
      }

      return std::optional<Json>(this->NodeToJson(node));
    });
  }

  DataStorageBridge::CreateNodeResult DataStorageBridge::CreateNode(const Json& nodeData, const std::optional<std::string>& parentUid)
  {
    std::lock_guard<std::mutex> lock(m_Mutex);
    return this->DispatchTask<CreateNodeResult>([this, nodeData, parentUid]()
    {
      CreateNodeResult result;
      result.success = false;

      auto dataStorage = m_DataStorage.Lock();
      if (dataStorage.IsNull())
      {
        return result;
      }

      // Find parent node if specified
      DataNode::Pointer parentNode;
      if (parentUid.has_value())
      {
        parentNode = m_UidMapper->FindNodeByUid(parentUid.value());
        if (parentNode.IsNull())
        {
          return result;  // Parent not found
        }
      }

      auto node = DataNode::New();

      // Set name if provided
      if (nodeData.contains("name") && nodeData["name"].is_string())
      {
        node->SetName(nodeData["name"].get<std::string>());
      }

      // Apply properties if provided
      if (nodeData.contains("properties") && nodeData["properties"].is_object())
      {
        for (auto& [key, value] : nodeData["properties"].items())
        {
          try
          {
            auto prop = ConvertPropertyFromSelfContainedJson(value);
            if (prop.IsNotNull())
            {
              node->SetProperty(key, prop);
            }
            else
            {
              MITK_WARN << "REST API: Failed to deserialize property '" << key << "' - null result from deserialization";
              result.failedProperties.push_back(key);
            }
          }
          catch (const std::exception& e)
          {
            MITK_WARN << "REST API: Failed to deserialize property '" << key << "': " << e.what();
            result.failedProperties.push_back(key);
          }
        }
      }

      // Add to DataStorage (with or without parent)
      if (parentNode != nullptr)
      {
        dataStorage->Add(node, parentNode);
      }
      else
      {
        dataStorage->Add(node);
      }

      // Get/create UID for the new node
      result.uid = m_UidMapper->GetOrCreateUid(node);
      result.success = true;

      // Mark node as modified via REST API
      MarkNodeAsModified(node, "created");

      return result;
    });
  }

  bool DataStorageBridge::UpdateNode(const std::string& uid, const Json& updates)
  {
    std::lock_guard<std::mutex> lock(m_Mutex);
    return this->DispatchTask<bool>([this, uid, updates]()
    {
      auto dataStorage = m_DataStorage.Lock();
      if (dataStorage.IsNull())
      {
        return false;
      }

      mitk::DataNode::Pointer node = m_UidMapper->FindNodeByUid(uid);
      if (node == nullptr)
      {
        return false;
      }

      // Handle parent_uid for reparenting
      if (!updates.contains("parent_uid"))
      {
        return false;
      }

      DataNode::Pointer newParent;
      if (!updates["parent_uid"].is_null())
      {
        std::string newParentUid = updates["parent_uid"].get<std::string>();
        newParent = m_UidMapper->FindNodeByUid(newParentUid);
      }

      if (newParent == node)
      { // Prevent circular references: new parent cannot be the node itself or a descendant
        return false;
      }

      // Check if newParent is a descendant of node
      auto descendants = dataStorage->GetDerivations(node, nullptr, true);
      for (auto it = descendants->Begin(); it != descendants->End(); ++it)
      {
        if (it->Value() == newParent)
        {
          return false;  // Would create circular reference
        }
      }

      // Reparent: remove and re-add under new parent
      // This is the same approach used by QmitkDataStorageTreeModel
      //
      // Important: Preserve the UID across reparenting operation.
      // The NodeUidMapper clears mappings when a node is removed from DataStorage,
      // so we save the UID and restore the mapping after re-adding.
      const std::string preservedUid = uid;

      dataStorage->Remove(node);
      if (newParent != nullptr)
      {
        dataStorage->Add(node, newParent);
      }
      else
      {
        dataStorage->Add(node);  // Move to root level
      }

      m_UidMapper->RestoreUid(node, preservedUid);

      MarkNodeAsModified(node, "reparented");

      return true;
    });
  }

  DataStorageBridge::DeleteResult DataStorageBridge::DeleteNode(const std::string& uid, bool recursive)
  {
    std::lock_guard<std::mutex> lock(m_Mutex);
    return this->DispatchTask<DeleteResult>([this, uid, recursive]()
    {
      DeleteResult result;
      result.success = false;
      result.deletedUid = uid;
      result.childrenCount = 0;

      auto dataStorage = m_DataStorage.Lock();
      if (dataStorage.IsNull())
      {
        return result;
      }

      auto node = m_UidMapper->FindNodeByUid(uid);
      if (node == nullptr)
      {
        return result;
      }

      // Check for children
      auto derivatives = dataStorage->GetDerivations(node);
      result.childrenCount = static_cast<int>(derivatives->Size());

      if (result.childrenCount > 0 && !recursive)
      {
        // Has children but recursive not requested
        return result;
      }

      // If recursive, collect and delete all descendants first
      if (recursive && result.childrenCount > 0)
      {
        // Collect all descendants (depth-first)
        std::vector<DataNode*> toDelete;
        std::function<void(DataNode*)> collectDescendants = [&](DataNode* n) {
          auto children = dataStorage->GetDerivations(n);
          for (auto it = children->Begin(); it != children->End(); ++it)
          {
            collectDescendants(it->Value().GetPointer());
          }
          toDelete.push_back(n);
        };

        // Collect children (not the node itself yet)
        for (auto it = derivatives->Begin(); it != derivatives->End(); ++it)
        {
          collectDescendants(it->Value().GetPointer());
        }

        // Delete all descendants (children first, then grandchildren, etc.)
        for (auto descendant : toDelete)
        {
          // Use GetOrCreateUid to ensure all deleted children have UIDs for reporting
          result.deletedChildren.push_back(m_UidMapper->GetOrCreateUid(descendant));
          dataStorage->Remove(descendant);
        }
      }

      // Delete the node itself
      dataStorage->Remove(node);
      result.success = true;

      return result;
    });
  }

  DataStorageBridge::GetNodeDataResult DataStorageBridge::GetNodeData(const std::string& uid) const
  {
    std::lock_guard<std::mutex> lock(m_Mutex);
    return this->DispatchTask<GetNodeDataResult>([this, uid]()
    {
      GetNodeDataResult result;
      result.nodeFound = false;
      result.data = nullptr;

      auto dataStorage = m_DataStorage.Lock();
      if (dataStorage.IsNull())
      {
        return result;
      }

      auto node = m_UidMapper->FindNodeByUid(uid);
      if (node == nullptr)
      {
        return result;  // nodeFound = false
      }

      result.nodeFound = true;

      auto data = node->GetData();
      if (data == nullptr)
      {
        return result;  // nodeFound = true, data = nullptr
      }

      // Clone the data for thread-safe processing outside the lock
      // This allows the caller to serialize/process the data without
      // blocking other DataStorage operations
      result.data = dynamic_cast<BaseData*>(data->Clone().GetPointer());
      return result;
    });
  }

  DataNode::ConstPointer DataStorageBridge::FindDataNode(const std::string& uid) const
  {
    std::lock_guard<std::mutex> lock(m_Mutex);
    return this->DispatchTask<DataNode::ConstPointer>([this, uid]()
    {
      return DataNode::ConstPointer(m_UidMapper->FindNodeByUid(uid));
    });
  }

  DataStorageBridge::OperationStatus DataStorageBridge::SetNodeData(const std::string& uid, BaseData* data)
  {
    std::lock_guard<std::mutex> lock(m_Mutex);
    return this->DispatchTask<OperationStatus>([this, uid, data]()
    {
      auto dataStorage = m_DataStorage.Lock();
      if (dataStorage.IsNull())
      {
        return OperationStatus::InternalError;
      }

      auto node = m_UidMapper->FindNodeByUid(uid);
      if (node == nullptr)
      {
        return OperationStatus::NodeNotFound;
      }

      node->SetData(data);

      // Mark node as modified via REST API
      MarkNodeAsModified(node, "data_set");

      return OperationStatus::Success;
    });
  }

  std::optional<DataStorageBridge::Json> DataStorageBridge::GetNodeProperties(
    const std::string& uid,
    const PropertyQueryParams& params) const
  {
    std::lock_guard<std::mutex> lock(m_Mutex);
    return this->DispatchTask<std::optional<Json>>([this, uid, params]()
    {
      auto dataStorage = m_DataStorage.Lock();
      if (dataStorage.IsNull())
      {
        return std::optional<Json>(std::nullopt);
      }

      auto node = m_UidMapper->FindNodeByUid(uid);
      if (node == nullptr)
      {
        return std::optional<Json>(std::nullopt);
      }

      const std::string contextName = params.context.has_value() ? params.context.value() : "";

      // Get the correct set of property keys for the requested scope
      auto keys = GetPropertyKeysForScope(node, contextName, params.scope);
      DataNode::PropertyListKeyNames filterdKeys;
      for (const auto& key : keys)
      {
        // Skip internal properties (restapi.*)
        if (IsInternalProperty(key))
        {
          continue;
        }

        // Filter by names if specified
        if (params.names.empty() ||
          std::find(params.names.begin(), params.names.end(), key) != params.names.end())
        {
          filterdKeys.push_back(key);
        }
      }

      // Check if we should return content or just names
      if (!params.includeContent)
      {
        // Return just property names as array
        Json names = Json::array();
        for (const auto& key : filterdKeys)
        {
          names.push_back(key);
        }
        return std::optional<Json>(names);
      }

      Json result = Json::object();
      for (const auto& key : filterdKeys)
      {
        auto prop = GetConstProperty(node, key, params.context, params.scope);
        if (prop != nullptr)
        {
          result[key] = ConvertPropertyToSelfContainedJson(prop);
        }
      }

      return std::optional<Json>(result);
    });
  }

  std::optional<DataStorageBridge::Json> DataStorageBridge::GetNodeProperty(
    const std::string& uid,
    const std::string& key,
    const PropertyQueryParams& params) const
  {
    // Reject access to internal properties (no dispatch needed for this check)
    if (IsInternalProperty(key))
    {
      return std::nullopt;
    }

    std::lock_guard<std::mutex> lock(m_Mutex);
    return this->DispatchTask<std::optional<Json>>([this, uid, key, params]()
    {
      auto dataStorage = m_DataStorage.Lock();
      if (dataStorage.IsNull())
      {
        return std::optional<Json>(std::nullopt);
      }

      auto node = m_UidMapper->FindNodeByUid(uid);
      if (node == nullptr)
      {
        return std::optional<Json>(std::nullopt);
      }

      auto prop = GetConstProperty(node, key, params.context, params.scope);

      if (prop == nullptr)
      {
        return std::optional<Json>(std::nullopt);
      }

      Json result = Json::object();
      result[key] = ConvertPropertyToSelfContainedJson(prop);

      return std::optional<Json>(result);
    });
  }

  DataStorageBridge::OperationStatus DataStorageBridge::SetNodeProperty(
    const std::string& uid,
    const std::string& key,
    const Json& value,
    const PropertyQueryParams& params)
  {
    // Reject modification of internal properties (no dispatch needed for this check)
    if (IsInternalProperty(key))
    {
      return OperationStatus::InvalidInput;
    }

    // Mutation operations only support Node and Data scopes, not All
    if (params.scope == PropertyScope::All)
    {
      return OperationStatus::InvalidInput;
    }

    std::lock_guard<std::mutex> lock(m_Mutex);
    return this->DispatchTask<OperationStatus>([this, uid, key, value, params]()
    {
      auto dataStorage = m_DataStorage.Lock();
      if (dataStorage.IsNull())
      {
        return OperationStatus::InternalError;
      }

      auto node = m_UidMapper->FindNodeByUid(uid);
      if (node == nullptr)
      {
        return OperationStatus::NodeNotFound;
      }

      try
      {
        // Handle the case the also simple value forms are wrapped in {"value": ...}
        Json propValue = value;
        if (value.is_object() && value.contains("value") && !value.contains("type"))
        {
           propValue = value["value"];
        }
        auto prop = ConvertPropertyFromSelfContainedJson(propValue);
        if (prop.IsNotNull())
        {
          const std::string contextName = params.context.has_value() ? params.context.value() : "";

          if (params.scope == PropertyScope::Data)
          {
            auto data = node->GetData();
            if (data != nullptr)
            {
              data->SetProperty(key, prop, contextName);
              // Mark node as modified via REST API
              MarkNodeAsModified(node, "property_set:" + key);
              return OperationStatus::Success;
            }
            return OperationStatus::InvalidInput;
          }
          else
          {
            node->SetProperty(key, prop, contextName);
            // Mark node as modified via REST API
            MarkNodeAsModified(node, "property_set:" + key);
            return OperationStatus::Success;
          }
        }
      }
      catch (const std::exception&)
      {
        return OperationStatus::InvalidInput;
      }

      return OperationStatus::InvalidInput;
    });
  }

  DataStorageBridge::OperationStatus DataStorageBridge::DeleteNodeProperty(
    const std::string& uid,
    const std::string& key,
    const PropertyQueryParams& params)
  {
    // Reject deletion of internal properties (no dispatch needed for this check)
    if (IsInternalProperty(key))
    {
      return OperationStatus::InvalidInput;
    }

    // Mutation operations only support Node and Data scopes, not All
    if (params.scope == PropertyScope::All)
    {
      return OperationStatus::InvalidInput;
    }

    std::lock_guard<std::mutex> lock(m_Mutex);
    return this->DispatchTask<OperationStatus>([this, uid, key, params]()
    {
      auto dataStorage = m_DataStorage.Lock();
      if (dataStorage.IsNull())
      {
        return OperationStatus::InternalError;
      }

      auto node = m_UidMapper->FindNodeByUid(uid);
      if (node == nullptr)
      {
        return OperationStatus::NodeNotFound;
      }

      const std::string contextName = params.context.has_value() ? params.context.value() : "";
      PropertyList* propertyList = nullptr;

      if (params.scope == PropertyScope::Data)
      {
        auto data = node->GetData();
        if (data != nullptr)
        {
          propertyList = data->GetPropertyList();
        }
      }
      else
      {
        propertyList = node->GetPropertyList(contextName);
      }

      if (propertyList == nullptr)
      {
        return OperationStatus::InternalError;
      }

      // Verify the property actually exists in the targeted scope
      auto prop = GetConstProperty(node, key, params.context, params.scope);
      if (prop == nullptr)
      {
        return OperationStatus::PropertyNotFound;
      }

      propertyList->DeleteProperty(key);

      // Mark node as modified via REST API
      MarkNodeAsModified(node, "property_deleted:" + key);

      return OperationStatus::Success;
    });
  }

  DataStorageBridge::ReplacePropertiesResult DataStorageBridge::ReplaceNodeProperties(
    const std::string& uid,
    const Json& properties,
    const PropertyQueryParams& params)
  {
    // Mutation operations only support Node and Data scopes, not All
    if (params.scope == PropertyScope::All)
    {
      return {OperationStatus::InvalidInput};
    }

    std::lock_guard<std::mutex> lock(m_Mutex);
    return this->DispatchTask<ReplacePropertiesResult>([this, uid, properties, params]()
    {
      auto dataStorage = m_DataStorage.Lock();
      if (dataStorage.IsNull())
      {
        return ReplacePropertiesResult{OperationStatus::InternalError};
      }

      auto node = m_UidMapper->FindNodeByUid(uid);
      if (node == nullptr)
      {
        return ReplacePropertiesResult{OperationStatus::NodeNotFound};
      }

      if (!properties.is_object())
      {
        return ReplacePropertiesResult{OperationStatus::InvalidInput};
      }

      const std::string contextName = params.context.has_value() ? params.context.value() : "";

      // Get the correct property list for the target scope
      PropertyList* propertyList = nullptr;
      if (params.scope == PropertyScope::Data)
      {
        auto data = node->GetData();
        if (data != nullptr)
        {
          propertyList = data->GetPropertyList();
        }
      }
      else
      {
        propertyList = node->GetPropertyList(contextName);
      }

      if (propertyList == nullptr)
      {
        return ReplacePropertiesResult{OperationStatus::InvalidInput};
      }

      // Collect existing property keys using scope-aware helper
      const auto existingKeys = GetPropertyKeysForScope(node, contextName, params.scope);

      // Track what was replaced vs removed
      std::vector<std::string> replaced;
      std::vector<std::string> removed;

      // Set new properties on the correct target (skip internal properties - clients cannot set them)
      for (const auto& [key, value] : properties.items())
      {
        // Skip internal properties
        if (IsInternalProperty(key))
        {
          continue;
        }

        try
        {
          auto prop = ConvertPropertyFromSelfContainedJson(value);
          if (prop.IsNotNull())
          {
            if (params.scope == PropertyScope::Data)
            {
              node->GetData()->SetProperty(key, prop, contextName);
            }
            else
            {
              node->SetProperty(key, prop, contextName);
            }
            replaced.push_back(key);
          }
        }
        catch (const std::exception&)
        {
          // Skip invalid properties
        }
      }

      // Remove properties that weren't in the new set
      // (but protect system properties like "name" and internal "restapi.*" properties)
      for (const auto& existingName : existingKeys)
      {
        if (!properties.contains(existingName))
        {
          // Don't remove "name" property (it's protected)
          if (existingName == "name" && params.scope != PropertyScope::Data)
          {
            continue;
          }
          // Don't remove internal properties (restapi.*)
          if (IsInternalProperty(existingName))
          {
            continue;
          }
          propertyList->DeleteProperty(existingName);
          removed.push_back(existingName);
        }
      }

      // Mark node as modified via REST API
      MarkNodeAsModified(node, "properties_replaced");

      Json result;
      result["replaced"] = replaced;
      result["removed"] = removed;

      return ReplacePropertiesResult{OperationStatus::Success, result};
    });
  }

  std::optional<DataStorageBridge::Json> DataStorageBridge::GetNodeAvailableContexts(const std::string& uid) const
  {
    std::lock_guard<std::mutex> lock(m_Mutex);
    return this->DispatchTask<std::optional<Json>>([this, uid]()
    {
      auto dataStorage = m_DataStorage.Lock();
      if (dataStorage.IsNull())
      {
        return std::optional<Json>(std::nullopt);
      }

      auto node = m_UidMapper->FindNodeByUid(uid);
      if (node == nullptr)
      {
        return std::optional<Json>(std::nullopt);
      }

      Json contexts = Json::array();

      // Add null for default context (always available)
      contexts.push_back(nullptr);

      // Add named contexts from the node
      auto contextNames = node->GetPropertyContextNames();
      for (const auto& contextName : contextNames)
      {
        contexts.push_back(contextName);
      }

      return std::optional<Json>(contexts);
    });
  }

  DataStorageBridge::Json DataStorageBridge::NodeToJson(DataNode* node) const
  {
    // Note: caller must hold m_Mutex
    auto dataStorage = m_DataStorage.Lock();
    if (dataStorage.IsNull())
      mitkThrow() << "NodeToJson called without a valid DataStorage.";

    Json result;

    // Get or create UID
    result["uid"] = m_UidMapper->GetOrCreateUid(node);
    result["name"] = node->GetName();
    result["path"] = this->BuildNodePath(node);

    // Get parent UID if exists
    auto sources = dataStorage->GetSources(node);
    if (sources->Size() > 0)
    {
      auto parent = sources->ElementAt(0).GetPointer();
      result["parent_uid"] = m_UidMapper->GetOrCreateUid(parent);
    }
    else
    {
      result["parent_uid"] = nullptr;
    }

    // Get data type
    auto data = node->GetData();
    if (data != nullptr)
    {
      result["data_type"] = data->GetNameOfClass();
    }
    else
    {
      result["data_type"] = nullptr;
    }

    // Children count
    result["children_count"] = this->GetChildrenCount(node);

    // Timestamp: ITK modified time (ever-increasing integer, session-only, higher is newer)
    result["timestamp"] = static_cast<unsigned long>(node->GetMTime());

    return result;
  }

  std::string DataStorageBridge::BuildNodePath(const DataNode* node) const
  {
    // Note: caller must hold m_Mutex
    auto dataStorage = m_DataStorage.Lock();
    if (dataStorage.IsNull())
      mitkThrow() << "BuildNodePath called without a valid DataStorage.";

    constexpr int maxDepth = 256;

    std::string path = "/" + node->GetName();

    auto sources = dataStorage->GetSources(node);
    int depth = 0;
    while (sources->Size() > 0 && depth < maxDepth)
    {
      auto parent = sources->ElementAt(0).GetPointer();
      path = "/" + parent->GetName() + path;
      sources = dataStorage->GetSources(parent);
      ++depth;
    }

    return path;
  }

  int DataStorageBridge::GetChildrenCount(const DataNode* node) const
  {
    // Note: caller must hold m_Mutex
    auto dataStorage = m_DataStorage.Lock();
    if (dataStorage.IsNull())
      mitkThrow() << "GetChildrenCount called without a valid DataStorage.";

    auto derivatives = dataStorage->GetDerivations(node);
    return static_cast<int>(derivatives->Size());
  }

}
