182 lines
7.3 KiB
Swift
182 lines
7.3 KiB
Swift
import Foundation
|
|
|
|
struct SyncMerger {
|
|
private static let logTag = "SyncMerger"
|
|
|
|
static func mergeDataSafely(
|
|
localBackup: ClimbDataBackup,
|
|
serverBackup: ClimbDataBackup,
|
|
dataManager: ClimbingDataManager,
|
|
imagePathMapping: [String: String]
|
|
) throws -> (gyms: [Gym], problems: [Problem], sessions: [ClimbSession], attempts: [Attempt], uniqueDeletions: [DeletedItem]) {
|
|
|
|
// Merge deletion lists first to prevent resurrection of deleted items
|
|
let localDeletions = dataManager.getDeletedItems()
|
|
let allDeletions = localDeletions + serverBackup.deletedItems
|
|
let uniqueDeletions = Array(Set(allDeletions))
|
|
|
|
AppLogger.info("Merging gyms...", tag: logTag)
|
|
let mergedGyms = mergeGyms(
|
|
local: dataManager.gyms,
|
|
server: serverBackup.gyms,
|
|
deletedItems: uniqueDeletions)
|
|
|
|
AppLogger.info("Merging problems...", tag: logTag)
|
|
let mergedProblems = try mergeProblems(
|
|
local: dataManager.problems,
|
|
server: serverBackup.problems,
|
|
imagePathMapping: imagePathMapping,
|
|
deletedItems: uniqueDeletions)
|
|
|
|
AppLogger.info("Merging sessions...", tag: logTag)
|
|
let mergedSessions = try mergeSessions(
|
|
local: dataManager.sessions,
|
|
server: serverBackup.sessions,
|
|
deletedItems: uniqueDeletions)
|
|
|
|
AppLogger.info("Merging attempts...", tag: logTag)
|
|
let mergedAttempts = try mergeAttempts(
|
|
local: dataManager.attempts,
|
|
server: serverBackup.attempts,
|
|
deletedItems: uniqueDeletions)
|
|
|
|
return (mergedGyms, mergedProblems, mergedSessions, mergedAttempts, uniqueDeletions)
|
|
}
|
|
|
|
private static func mergeGyms(local: [Gym], server: [BackupGym], deletedItems: [DeletedItem]) -> [Gym] {
|
|
var merged = local
|
|
let deletedGymIds = Set(deletedItems.filter { $0.type == "gym" }.map { $0.id })
|
|
let localGymIds = Set(local.map { $0.id.uuidString })
|
|
|
|
merged.removeAll { deletedGymIds.contains($0.id.uuidString) }
|
|
|
|
// Add new items from server (excluding deleted ones)
|
|
for serverGym in server {
|
|
if let serverGymConverted = try? serverGym.toGym() {
|
|
let localHasGym = localGymIds.contains(serverGym.id)
|
|
let isDeleted = deletedGymIds.contains(serverGym.id)
|
|
|
|
if !localHasGym && !isDeleted {
|
|
merged.append(serverGymConverted)
|
|
}
|
|
}
|
|
}
|
|
|
|
return merged
|
|
}
|
|
|
|
private static func mergeProblems(
|
|
local: [Problem],
|
|
server: [BackupProblem],
|
|
imagePathMapping: [String: String],
|
|
deletedItems: [DeletedItem]
|
|
) throws -> [Problem] {
|
|
var merged = local
|
|
let deletedProblemIds = Set(deletedItems.filter { $0.type == "problem" }.map { $0.id })
|
|
let localProblemIds = Set(local.map { $0.id.uuidString })
|
|
|
|
merged.removeAll { deletedProblemIds.contains($0.id.uuidString) }
|
|
|
|
for serverProblem in server {
|
|
let localHasProblem = localProblemIds.contains(serverProblem.id)
|
|
let isDeleted = deletedProblemIds.contains(serverProblem.id)
|
|
|
|
if !localHasProblem && !isDeleted {
|
|
var problemToAdd = serverProblem
|
|
|
|
if !imagePathMapping.isEmpty, let imagePaths = serverProblem.imagePaths, !imagePaths.isEmpty {
|
|
let updatedImagePaths = imagePaths.compactMap { oldPath in
|
|
imagePathMapping[oldPath] ?? oldPath
|
|
}
|
|
if updatedImagePaths != imagePaths {
|
|
problemToAdd = BackupProblem(
|
|
id: serverProblem.id,
|
|
gymId: serverProblem.gymId,
|
|
name: serverProblem.name,
|
|
description: serverProblem.description,
|
|
climbType: serverProblem.climbType,
|
|
difficulty: serverProblem.difficulty,
|
|
tags: serverProblem.tags,
|
|
location: serverProblem.location,
|
|
imagePaths: updatedImagePaths,
|
|
isActive: serverProblem.isActive,
|
|
dateSet: serverProblem.dateSet,
|
|
notes: serverProblem.notes,
|
|
createdAt: serverProblem.createdAt,
|
|
updatedAt: serverProblem.updatedAt
|
|
)
|
|
}
|
|
}
|
|
|
|
if let serverProblemConverted = try? problemToAdd.toProblem() {
|
|
merged.append(serverProblemConverted)
|
|
}
|
|
}
|
|
}
|
|
|
|
return merged
|
|
}
|
|
|
|
private static func mergeSessions(
|
|
local: [ClimbSession], server: [BackupClimbSession], deletedItems: [DeletedItem]
|
|
) throws -> [ClimbSession] {
|
|
var merged = local
|
|
let deletedSessionIds = Set(deletedItems.filter { $0.type == "session" }.map { $0.id })
|
|
let localSessionIds = Set(local.map { $0.id.uuidString })
|
|
|
|
merged.removeAll { session in
|
|
deletedSessionIds.contains(session.id.uuidString) && session.status != .active
|
|
}
|
|
|
|
for serverSession in server {
|
|
let localHasSession = localSessionIds.contains(serverSession.id)
|
|
let isDeleted = deletedSessionIds.contains(serverSession.id)
|
|
|
|
if !localHasSession && !isDeleted {
|
|
if let serverSessionConverted = try? serverSession.toClimbSession() {
|
|
merged.append(serverSessionConverted)
|
|
}
|
|
}
|
|
}
|
|
|
|
return merged
|
|
}
|
|
|
|
private static func mergeAttempts(
|
|
local: [Attempt], server: [BackupAttempt], deletedItems: [DeletedItem]
|
|
) throws -> [Attempt] {
|
|
var merged = local
|
|
let deletedAttemptIds = Set(deletedItems.filter { $0.type == "attempt" }.map { $0.id })
|
|
let localAttemptIds = Set(local.map { $0.id.uuidString })
|
|
|
|
// Get active session IDs to protect their attempts
|
|
let activeSessionIds = Set(
|
|
local.compactMap { attempt in
|
|
return attempt.sessionId
|
|
}.filter { sessionId in
|
|
// Check if this session ID belongs to an active session
|
|
// For now, we'll be conservative and not delete attempts during merge
|
|
return true
|
|
})
|
|
|
|
// Remove items that were deleted on other devices (but be conservative with attempts)
|
|
merged.removeAll { attempt in
|
|
deletedAttemptIds.contains(attempt.id.uuidString)
|
|
&& !activeSessionIds.contains(attempt.sessionId)
|
|
}
|
|
|
|
for serverAttempt in server {
|
|
let localHasAttempt = localAttemptIds.contains(serverAttempt.id)
|
|
let isDeleted = deletedAttemptIds.contains(serverAttempt.id)
|
|
|
|
if !localHasAttempt && !isDeleted {
|
|
if let serverAttemptConverted = try? serverAttempt.toAttempt() {
|
|
merged.append(serverAttemptConverted)
|
|
}
|
|
}
|
|
}
|
|
|
|
return merged
|
|
}
|
|
}
|