import Foundation import EventKit private let SAME_TIME_TOL_MIN: Double = 5 struct MirrorRecord: Hashable, Codable { var targetCalendarID: String var sourceCalendarID: String var sourceStableID: String var occurrenceTimestamp: TimeInterval? var targetEventIdentifier: String? var lastKnownStartTimestamp: TimeInterval var lastKnownEndTimestamp: TimeInterval var updatedAt: Date = Date() var sourceKey: String { sourceOccurrenceKey( sourceCalID: sourceCalendarID, sourceStableID: sourceStableID, occurrence: occurrenceTimestamp.map { Date(timeIntervalSince1970: $0) } ) } var timeKey: String { "\(lastKnownStartTimestamp)|\(lastKnownEndTimestamp)" } } @MainActor final class MirrorEngine { private let log: (String) -> Void private let mirrorIndexDefaultsKey = "mirror-index.v1" init(log: @escaping (String) -> Void) { self.log = log } private func loadMirrorIndex() -> [String: MirrorRecord] { guard let data = UserDefaults.standard.data(forKey: mirrorIndexDefaultsKey) else { return [:] } do { return try JSONDecoder().decode([String: MirrorRecord].self, from: data) } catch { log("✗ Failed to load mirror index: \(error.localizedDescription)") return [:] } } private func saveMirrorIndex(_ index: [String: MirrorRecord]) { do { let data = try JSONEncoder().encode(index) UserDefaults.standard.set(data, forKey: mirrorIndexDefaultsKey) } catch { log("✗ Failed to save mirror index: \(error.localizedDescription)") } } func runMirror( store: EKEventStore, config: MirrorConfig, sourceCalendar: EKCalendar, targetCalendars: [EKCalendar], sessionGuard: inout Set, isMultiRouteRun: Bool ) async { let srcCal = sourceCalendar let srcName = calLabel(srcCal) let targets = targetCalendars.filter { $0.calendarIdentifier != srcCal.calendarIdentifier } if targets.isEmpty { log("No target calendars selected. Choose at least one target or add a route with valid targets.") return } let cal = Calendar.current let todayStart = cal.startOfDay(for: Date()) let windowStart = cal.date(byAdding: .day, value: -config.daysBack, to: todayStart)! let windowEnd = cal.date(byAdding: .day, value: config.daysForward, to: todayStart)! log("=== BusyMirror ===") log("Source: \(srcName) Targets: \(targets.map { calLabel($0) }.joined(separator: ", "))") log("Window: \(windowStart) -> \(windowEnd)") log("WRITE: \(config.writeEnabled) \(config.writeEnabled ? "" : "(DRY-RUN)") mode: \(config.overlapMode.rawValue) mergeGapMin: \(config.mergeGapMin) allDay: \(config.mirrorAllDay)") log("Route: \(srcName) → {\(targets.map { calLabel($0) }.joined(separator: ", "))}") // Source events (recurrences expanded by EventKit) let srcPred = store.predicateForEvents(withStart: windowStart, end: windowEnd, calendars: [srcCal]) var srcEvents = store.events(matching: srcPred) let srcFetched = srcEvents.count srcEvents = srcEvents.filter { $0.calendar.calendarIdentifier == srcCal.calendarIdentifier } let srcKept = srcEvents.count if srcKept != srcFetched { log("- WARN: filtered \(srcFetched - srcKept) stray source event(s) not in \(srcName)") } srcEvents.sort { ($0.startDate ?? .distantPast) < ($1.startDate ?? .distantPast) } var srcBlocks: [Block] = [] var skippedMirrors = 0 let titleFilters = config.excludedTitleFilterTerms let organizerFilters = config.excludedOrganizerFilterTerms let enforceWorkHours = config.filterByWorkHours && config.workHoursEnd > config.workHoursStart let allowedStartMinutes = config.workHoursStart * 60 let allowedEndMinutes = config.workHoursEnd * 60 var skippedWorkHours = 0 var skippedTitles = 0 var skippedOrganizers = 0 var skippedStatus = 0 for ev in srcEvents { if Task.isCancelled { break } if config.mirrorAcceptedOnly, ev.hasAttendees { let attendees = ev.attendees ?? [] if let me = attendees.first(where: { $0.isCurrentUser }) { if me.participantStatus != .accepted { skippedStatus += 1 continue } } else { skippedStatus += 1 continue } } if enforceWorkHours, !ev.isAllDay, let start = ev.startDate, isOutsideWorkHours(start, calendar: cal, startMinutes: allowedStartMinutes, endMinutes: allowedEndMinutes) { skippedWorkHours += 1 continue } if shouldSkip(title: ev.title, filters: titleFilters, titlePrefix: config.titlePrefix) { skippedTitles += 1 continue } if shouldSkipOrganizer(organizerValues: organizerStrings(for: ev), filters: organizerFilters) { skippedOrganizers += 1 continue } if !config.mirrorAllDay && ev.isAllDay { continue } if isMirrorEvent(ev, prefix: config.titlePrefix, placeholder: config.placeholderTitle) { skippedMirrors += 1 continue } guard let s = ev.startDate, let e = ev.endDate, e > s else { continue } guard ev.calendar.calendarIdentifier == srcCal.calendarIdentifier else { continue } let srcID = stableSourceIdentifier(for: ev) srcBlocks.append(Block(start: s, end: e, srcStableID: srcID, label: ev.title, notes: ev.notes, occurrence: ev.occurrenceDate)) } if skippedMirrors > 0 { log("- SKIP mirrored-on-source: \(skippedMirrors) instance(s)") } if skippedWorkHours > 0 { log("- SKIP outside work hours: \(skippedWorkHours) event(s)") } if skippedTitles > 0 { log("- SKIP title filter: \(skippedTitles) event(s)") } if skippedOrganizers > 0 { log("- SKIP organizer filter: \(skippedOrganizers) event(s)") } if skippedStatus > 0 { log("- SKIP non-accepted status: \(skippedStatus) event(s)") } srcBlocks = uniqueBlocks(srcBlocks, trackByID: config.mergeGapMin == 0) let baseBlocks = (config.mergeGapMin > 0) ? mergeBlocks(srcBlocks, gapMinutes: config.mergeGapMin) : srcBlocks let trackByID = (config.mergeGapMin == 0) var mirrorIndex = loadMirrorIndex() var mirrorIndexChanged = false func sourceKey(for blk: Block) -> String? { guard trackByID, let sid = blk.srcStableID else { return nil } return sourceOccurrenceKey(sourceCalID: srcCal.calendarIdentifier, sourceStableID: sid, occurrence: blk.occurrence) } // Cache target events across routes when possible var targetEventCache: [String: [EKEvent]] = [:] for tgt in targets { if Task.isCancelled { break } let tgtName = calLabel(tgt) log(">>> Target: \(tgtName)") if tgt.calendarIdentifier == srcCal.calendarIdentifier { log("- SKIP target is same as source: \(tgtName)") continue } let tgtEvents: [EKEvent] if let cached = targetEventCache[tgt.calendarIdentifier] { tgtEvents = cached } else { let tgtPred = store.predicateForEvents(withStart: windowStart, end: windowEnd, calendars: [tgt]) var evs = store.events(matching: tgtPred) let tgtFetched = evs.count evs = evs.filter { $0.calendar.calendarIdentifier == tgt.calendarIdentifier } if tgtFetched != evs.count { log("- WARN: filtered \(tgtFetched - evs.count) stray target event(s) not in \(tgtName)") } targetEventCache[tgt.calendarIdentifier] = evs tgtEvents = evs } var placeholderSet = Set() var occupied: [Block] = [] var placeholdersBySourceKey: [String: EKEvent] = [:] var placeholdersByTime: [String: EKEvent] = [:] var targetEventsByIdentifier: [String: EKEvent] = [:] for tv in tgtEvents { guard tv.calendar.calendarIdentifier == tgt.calendarIdentifier else { continue } if let eid = tv.eventIdentifier { targetEventsByIdentifier[eid] = tv } if let ts = tv.startDate, let te = tv.endDate { let timeKey = mirrorTimeKey(start: ts, end: te) if isMirrorEvent(tv, prefix: config.titlePrefix, placeholder: config.placeholderTitle) { placeholderSet.insert(timeKey) placeholdersByTime[timeKey] = tv let parsed = parseMirrorURL(tv.url) if let sourceCalID = parsed.sourceCalID, let sourceStableID = parsed.sourceStableID, !sourceCalID.isEmpty, !sourceStableID.isEmpty { let key = sourceOccurrenceKey(sourceCalID: sourceCalID, sourceStableID: sourceStableID, occurrence: parsed.occ) placeholdersBySourceKey[key] = tv let recordKey = mirrorRecordKey(targetCalID: tgt.calendarIdentifier, sourceKey: key) let record = MirrorRecord( targetCalendarID: tgt.calendarIdentifier, sourceCalendarID: sourceCalID, sourceStableID: sourceStableID, occurrenceTimestamp: parsed.occ?.timeIntervalSince1970, targetEventIdentifier: tv.eventIdentifier, lastKnownStartTimestamp: ts.timeIntervalSince1970, lastKnownEndTimestamp: te.timeIntervalSince1970 ) if mirrorIndex[recordKey] != record { mirrorIndex[recordKey] = record mirrorIndexChanged = true } } } occupied.append(Block(start: ts, end: te, srcStableID: nil, label: nil, notes: nil, occurrence: nil)) } } occupied = coalesce(occupied) var created = 0 var skipped = 0 var updated = 0 func guardKey(for blk: Block, targetID: String) -> String { if let key = sourceKey(for: blk) { return "\(key)|\(targetID)" } return "\(srcCal.calendarIdentifier)|\(blk.start.timeIntervalSince1970)|\(blk.end.timeIntervalSince1970)|\(targetID)" } func desiredNotes(for blk: Block) -> String? { (!config.hideDetails && config.copyDescription) ? blk.notes : nil } func upsertMirrorRecord(for blk: Block, event: EKEvent) { guard let sid = blk.srcStableID, let key = sourceKey(for: blk), let startDate = event.startDate, let endDate = event.endDate else { return } let recordKey = mirrorRecordKey(targetCalID: tgt.calendarIdentifier, sourceKey: key) let record = MirrorRecord( targetCalendarID: tgt.calendarIdentifier, sourceCalendarID: srcCal.calendarIdentifier, sourceStableID: sid, occurrenceTimestamp: blk.occurrence?.timeIntervalSince1970, targetEventIdentifier: event.eventIdentifier, lastKnownStartTimestamp: startDate.timeIntervalSince1970, lastKnownEndTimestamp: endDate.timeIntervalSince1970 ) if mirrorIndex[recordKey] != record { mirrorIndex[recordKey] = record mirrorIndexChanged = true } } func removeMirrorRecord(for key: String) { let recordKey = mirrorRecordKey(targetCalID: tgt.calendarIdentifier, sourceKey: key) if mirrorIndex.removeValue(forKey: recordKey) != nil { mirrorIndexChanged = true } } func resolveMappedEvent(for record: MirrorRecord) -> EKEvent? { if let eid = record.targetEventIdentifier, let event = targetEventsByIdentifier[eid], event.calendar.calendarIdentifier == tgt.calendarIdentifier { return event } return placeholdersByTime[record.timeKey] } func rememberMirrorEvent(_ event: EKEvent, for blk: Block) { if let startDate = event.startDate, let endDate = event.endDate { let timeKey = mirrorTimeKey(start: startDate, end: endDate) placeholderSet.insert(timeKey) placeholdersByTime[timeKey] = event } if let key = sourceKey(for: blk) { placeholdersBySourceKey[key] = event } if let eid = event.eventIdentifier { targetEventsByIdentifier[eid] = event } upsertMirrorRecord(for: blk, event: event) } func needsUpdate(existing: EKEvent, blk: Block, displayTitle: String, desiredNotes: String?, desiredURL: URL?) -> Bool { let curS = existing.startDate ?? blk.start let curE = existing.endDate ?? blk.end if abs(curS.timeIntervalSince(blk.start)) > SAME_TIME_TOL_MIN * 60 { return true } if abs(curE.timeIntervalSince(blk.end)) > SAME_TIME_TOL_MIN * 60 { return true } if (existing.title ?? "") != displayTitle { return true } if (existing.notes ?? "") != (desiredNotes ?? "") { return true } if existing.isAllDay { return true } if (existing.url?.absoluteString ?? "") != (desiredURL?.absoluteString ?? "") { return true } return false } func createOrUpdateIfNeeded(_ blk: Block) async { let gKey = guardKey(for: blk, targetID: tgt.calendarIdentifier) if sessionGuard.contains(gKey) { skipped += 1 log("- SKIP loop-guard [\(srcName) -> \(tgtName)] \(blk.start) -> \(blk.end)") return } let baseSourceTitle = stripPrefix(blk.label, prefix: config.titlePrefix) let effectiveTitle = config.hideDetails ? config.placeholderTitle : (baseSourceTitle.isEmpty ? config.placeholderTitle : baseSourceTitle) let titleSuffix = config.hideDetails ? "" : (baseSourceTitle.isEmpty ? "" : " — \(baseSourceTitle)") let displayTitle = (config.titlePrefix.isEmpty ? "" : config.titlePrefix) + effectiveTitle let notes = desiredNotes(for: blk) let desiredURL = buildMirrorURL( targetCalID: tgt.calendarIdentifier, sourceCalID: srcCal.calendarIdentifier, sourceStableID: blk.srcStableID, occurrence: blk.occurrence, start: blk.start, end: blk.end ) let exactTimeKey = mirrorTimeKey(start: blk.start, end: blk.end) let blkSourceKey = sourceKey(for: blk) func updateExisting(_ existing: EKEvent, byTime: Bool) async { let curS = existing.startDate ?? blk.start let curE = existing.endDate ?? blk.end rememberMirrorEvent(existing, for: blk) if !needsUpdate(existing: existing, blk: blk, displayTitle: displayTitle, desiredNotes: notes, desiredURL: desiredURL) { sessionGuard.insert(gKey) skipped += 1 return } let byTimeSuffix = byTime ? " (by time)" : "" if !config.writeEnabled { sessionGuard.insert(gKey) log("~ WOULD UPDATE [\(srcName) -> \(tgtName)]\(byTimeSuffix) \(curS) -> \(curE) TO \(blk.start) -> \(blk.end)\(titleSuffix) [title: \(displayTitle)]") updated += 1 return } existing.title = displayTitle existing.startDate = blk.start existing.endDate = blk.end existing.isAllDay = false existing.notes = notes existing.url = desiredURL do { try await MainActor.run { try store.save(existing, span: .thisEvent, commit: true) } log("✓ UPDATED [\(srcName) -> \(tgtName)]\(byTimeSuffix) \(blk.start) -> \(blk.end)") rememberMirrorEvent(existing, for: blk) occupied = coalesce(occupied + [Block(start: blk.start, end: blk.end, srcStableID: nil, label: nil, notes: nil, occurrence: nil)]) sessionGuard.insert(gKey) updated += 1 } catch { log("Update failed: \(error.localizedDescription)") } } if let blkSourceKey, let record = mirrorIndex[mirrorRecordKey(targetCalID: tgt.calendarIdentifier, sourceKey: blkSourceKey)], let existing = resolveMappedEvent(for: record) { await updateExisting(existing, byTime: false) return } if let blkSourceKey, let existing = placeholdersBySourceKey[blkSourceKey] { await updateExisting(existing, byTime: false) return } if let existingByTime = placeholdersByTime[exactTimeKey] { await updateExisting(existingByTime, byTime: true) return } if placeholderSet.contains(exactTimeKey) { skipped += 1 return } if !config.writeEnabled { sessionGuard.insert(gKey) log("+ WOULD CREATE [\(srcName) -> \(tgtName)] \(blk.start) -> \(blk.end)\(titleSuffix) [title: \(displayTitle)]") return } guard tgt.calendarIdentifier != srcCal.calendarIdentifier else { skipped += 1 log("- SKIP invariant: target is source [\(srcName)]") return } let newEv = EKEvent(eventStore: store) newEv.calendar = tgt newEv.title = displayTitle newEv.startDate = blk.start newEv.endDate = blk.end newEv.isAllDay = false newEv.notes = notes newEv.url = desiredURL newEv.availability = .busy do { try await MainActor.run { try store.save(newEv, span: .thisEvent, commit: true) } created += 1 log("✓ CREATED [\(srcName) -> \(tgtName)] \(blk.start) -> \(blk.end)") rememberMirrorEvent(newEv, for: blk) occupied = coalesce(occupied + [Block(start: blk.start, end: blk.end, srcStableID: nil, label: nil, notes: nil, occurrence: nil)]) sessionGuard.insert(gKey) } catch { log("Save failed: \(error.localizedDescription)") } } for b in baseBlocks { if Task.isCancelled { break } switch config.overlapMode { case .allow: await createOrUpdateIfNeeded(b) case .skipCovered: if fullyCovered(occupied, block: b, tolMin: SAME_TIME_TOL_MIN) { log("- SKIP covered [\(srcName) -> \(tgtName)] \(b.start) -> \(b.end)") skipped += 1 } else { await createOrUpdateIfNeeded(b) } case .fillGaps: let gaps = gapsWithin(occupied, in: b) if gaps.isEmpty { log("- SKIP no gaps [\(srcName) -> \(tgtName)] \(b.start) -> \(b.end)") skipped += 1 } else { for g in gaps { await createOrUpdateIfNeeded(g) } } } } log("[Summary → \(tgtName)] created=\(created), updated=\(updated), skipped=\(skipped)") if config.autoDeleteMissing { let activeSourceKeys = Set(baseBlocks.compactMap { sourceKey(for: $0) }) let validTimeKeys = Set(baseBlocks.map { mirrorTimeKey(start: $0.start, end: $0.end) }) var byID: [String: EKEvent] = [:] for tv in placeholdersByTime.values { guard tv.calendar.calendarIdentifier == tgt.calendarIdentifier else { continue } if let eid = tv.eventIdentifier { byID[eid] = tv } } var removed = 0 var skippedOtherSource = 0 var skippedLegacyNoURL = 0 var handledEventIDs = Set() let staleMirrorRecords = mirrorIndex.filter { $0.value.targetCalendarID == tgt.calendarIdentifier && $0.value.sourceCalendarID == srcCal.calendarIdentifier && !activeSourceKeys.contains($0.value.sourceKey) } for (recordKey, record) in staleMirrorRecords { if Task.isCancelled { break } let candidate = resolveMappedEvent(for: record) if let candidate { if !config.writeEnabled { log("~ WOULD DELETE (missing source) [\(srcName) -> \(tgtName)] \(candidate.startDate ?? windowStart) -> \(candidate.endDate ?? windowEnd)") } else { do { try await MainActor.run { try store.remove(candidate, span: .thisEvent, commit: true) } removed += 1 } catch { log("Delete failed: \(error.localizedDescription)") } } if let eid = candidate.eventIdentifier { handledEventIDs.insert(eid) } } if config.writeEnabled || candidate == nil { if mirrorIndex.removeValue(forKey: recordKey) != nil { mirrorIndexChanged = true } } } for ev in byID.values { if Task.isCancelled { break } if let eid = ev.eventIdentifier, handledEventIDs.contains(eid) { continue } let parsed = parseMirrorURL(ev.url) var shouldDelete = false var parsedSourceKey: String? = nil if let sourceCalID = parsed.sourceCalID, !sourceCalID.isEmpty { if sourceCalID != srcCal.calendarIdentifier { skippedOtherSource += 1 continue } if let sourceStableID = parsed.sourceStableID, !sourceStableID.isEmpty { let key = sourceOccurrenceKey(sourceCalID: sourceCalID, sourceStableID: sourceStableID, occurrence: parsed.occ) parsedSourceKey = key if !activeSourceKeys.contains(key) { shouldDelete = true } } else if trackByID, let s = ev.startDate, let e = ev.endDate, !validTimeKeys.contains(mirrorTimeKey(start: s, end: e)) { shouldDelete = true } } else if trackByID && !isMultiRouteRun { if let s = ev.startDate, let e = ev.endDate, !validTimeKeys.contains(mirrorTimeKey(start: s, end: e)) { shouldDelete = true } } else if trackByID && isMultiRouteRun { let hasMapping = mirrorIndex.values.contains { $0.targetCalendarID == tgt.calendarIdentifier && $0.sourceCalendarID == srcCal.calendarIdentifier && $0.targetEventIdentifier == ev.eventIdentifier } if !hasMapping { skippedLegacyNoURL += 1 } continue } if shouldDelete { if !config.writeEnabled { log("~ WOULD DELETE (missing source) [\(srcName) -> \(tgtName)] \(ev.startDate ?? windowStart) -> \(ev.endDate ?? windowEnd)") } else { do { try await MainActor.run { try store.remove(ev, span: .thisEvent, commit: true) } removed += 1 } catch { log("Delete failed: \(error.localizedDescription)") } } if let key = parsedSourceKey { removeMirrorRecord(for: key) } } } if removed > 0 { log("[Cleanup missing for \(tgtName)] deleted=\(removed)") } if skippedOtherSource > 0 { log("- INFO cleanup skipped \(skippedOtherSource) placeholders from other source routes on \(tgtName)") } if skippedLegacyNoURL > 0 { log("- INFO cleanup skipped \(skippedLegacyNoURL) unmanaged legacy placeholders without source metadata on \(tgtName)") } } } if mirrorIndexChanged { saveMirrorIndex(mirrorIndex) } } func runCleanup( store: EKEventStore, daysBack: Int, daysForward: Int, sourceCalendar: EKCalendar, targetCalendars: [EKCalendar], titlePrefix: String, placeholderTitle: String, writeEnabled: Bool ) async { let cal = Calendar.current let todayStart = cal.startOfDay(for: Date()) let windowStart = cal.date(byAdding: .day, value: -daysBack, to: todayStart)! let windowEnd = cal.date(byAdding: .day, value: daysForward, to: todayStart)! log("=== Cleanup Busy placeholders in window ===") log("(Cleanup is SAFE: mirrored events detected by url prefix or title prefix ‘\(titlePrefix)’)") log("Window: \(windowStart) -> \(windowEnd)") for tgt in targetCalendars { let tgtPred = store.predicateForEvents(withStart: windowStart, end: windowEnd, calendars: [tgt]) let tgtEvents = store.events(matching: tgtPred) var delCount = 0 for ev in tgtEvents { guard isMirrorEvent(ev, prefix: titlePrefix, placeholder: placeholderTitle) else { continue } if !writeEnabled { log("~ WOULD DELETE [\(tgt.title)] \(ev.startDate ?? todayStart) -> \(ev.endDate ?? todayStart)") } else { do { try await MainActor.run { try store.remove(ev, span: .thisEvent, commit: true) } delCount += 1 } catch { log("Delete failed: \(error.localizedDescription)") } } } log("[Cleanup \(tgt.title)] deleted=\(delCount)") } } }