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()

    // updatedAt is intentionally excluded from equality and hashing: it is a
    // bookkeeping timestamp that changes on every write and should not cause
    // the mirror index to be marked dirty when the meaningful fields are equal.
    static func == (lhs: MirrorRecord, rhs: MirrorRecord) -> Bool {
        lhs.targetCalendarID == rhs.targetCalendarID &&
        lhs.sourceCalendarID == rhs.sourceCalendarID &&
        lhs.sourceStableID == rhs.sourceStableID &&
        lhs.occurrenceTimestamp == rhs.occurrenceTimestamp &&
        lhs.targetEventIdentifier == rhs.targetEventIdentifier &&
        lhs.lastKnownStartTimestamp == rhs.lastKnownStartTimestamp &&
        lhs.lastKnownEndTimestamp == rhs.lastKnownEndTimestamp
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(targetCalendarID)
        hasher.combine(sourceCalendarID)
        hasher.combine(sourceStableID)
        hasher.combine(occurrenceTimestamp)
        hasher.combine(targetEventIdentifier)
        hasher.combine(lastKnownStartTimestamp)
        hasher.combine(lastKnownEndTimestamp)
    }

    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<String>,
        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<String>()
            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.span(start: ts, end: te))
                }
            }
            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 store.save(existing, span: .thisEvent, commit: true)
                        log("✓ UPDATED [\(srcName) -> \(tgtName)]\(byTimeSuffix) \(blk.start) -> \(blk.end)")
                        rememberMirrorEvent(existing, for: blk)
                        occupied = coalesce(occupied + [Block.span(start: blk.start, end: blk.end)])
                        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 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.span(start: blk.start, end: blk.end)])
                    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<String>()

                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 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 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 store.remove(ev, span: .thisEvent, commit: true)
                        delCount += 1
                    } catch {
                        log("Delete failed: \(error.localizedDescription)")
                    }
                }
            }
            log("[Cleanup \(tgt.title)] deleted=\(delCount)")
        }
    }
}
