import Foundation
import SwiftData
import os.log

private let logger = Logger(subsystem: "ro.xdev.healthprobe", category: "SnapshotLifecycleService")

enum SnapshotLifecycleService {
    struct DeletionPreview {
        let target: HealthSnapshot
        let affectedDeltas: [SnapshotDelta]
        let mergedDelta: SnapshotDelta?
        let willBreakChain: Bool
        let description: String
    }

    static func previewDeletion(of snapshot: HealthSnapshot, context: ModelContext) throws -> DeletionPreview {
        let incomingDelta = try fetchIncomingDelta(toSnapshotID: snapshot.id, context: context)
        let outgoingDelta = try fetchOutgoingDelta(fromSnapshotID: snapshot.id, context: context)

        var willBreakChain = false
        var description = ""

        let integrityResult = IntegrityService.validate(snapshot: snapshot, delta: incomingDelta)
        switch integrityResult {
        case .valid:
            break
        case .checksumMismatch(_, let expected, let actual):
            willBreakChain = true
            description = "Checksum mismatch: expected \(expected.prefix(8))…, got \(actual.prefix(8))…"
        case .missingDelta(let fromID, _):
            willBreakChain = true
            description = "Missing delta from \(fromID)"
        case .corrupted(_, let reason):
            willBreakChain = true
            description = reason
        }

        var affectedDeltas: [SnapshotDelta] = []
        if let d = incomingDelta { affectedDeltas.append(d) }
        if let d = outgoingDelta { affectedDeltas.append(d) }

        // For intermediate deletion, compute the merged delta preview
        var mergedDelta: SnapshotDelta? = nil
        if let d1 = incomingDelta, let d2 = outgoingDelta,
           let prevSnap = try fetchSnapshot(id: d1.fromSnapshotID, context: context),
           let nextSnap = try fetchSnapshot(id: d2.toSnapshotID, context: context) {
            mergedDelta = DeltaService.mergeDeltas(
                d1: d1, d2: d2,
                snapshotBefore: prevSnap,
                snapshotAfter: nextSnap
            )
        }

        return DeletionPreview(
            target: snapshot,
            affectedDeltas: affectedDeltas,
            mergedDelta: mergedDelta,
            willBreakChain: willBreakChain,
            description: description
        )
    }

    static func delete(_ snapshot: HealthSnapshot, context: ModelContext) throws {
        let incomingDelta = try fetchIncomingDelta(toSnapshotID: snapshot.id, context: context)
        let outgoingDelta = try fetchOutgoingDelta(fromSnapshotID: snapshot.id, context: context)

        let deviceID = snapshot.deviceID
        let version = Bundle.main.appBuildVersion

        let log = LocalOperationLog(
            operationType: "delete",
            summary: buildSummary(snapshot: snapshot, incoming: incomingDelta, outgoing: outgoingDelta),
            deviceID: deviceID,
            appBuildVersion: version
        )
        var completedLog = log
        completedLog.affectedSnapshotIDs = [snapshot.id.uuidString]

        if incomingDelta == nil && outgoingDelta == nil {
            // Standalone snapshot — just delete
            context.delete(snapshot)
        } else if incomingDelta == nil, let outgoing = outgoingDelta {
            // Oldest snapshot: delete it and outgoing delta, set next as chain start
            if let nextSnap = try fetchSnapshot(id: outgoing.toSnapshotID, context: context) {
                nextSnap.previousSnapshotID = nil
                nextSnap.isChainStart = true
                nextSnap.contentEquivalentSnapshotID = nil
                for typeCount in nextSnap.typeCounts ?? [] {
                    typeCount.contentEquivalentTypeCountID = nil
                }
                invalidateDetailCaches(for: nextSnap)
            }
            context.delete(outgoing)
            context.delete(snapshot)
        } else if outgoingDelta == nil, let incoming = incomingDelta {
            // Latest snapshot: delete it and incoming delta
            context.delete(incoming)
            context.delete(snapshot)
        } else if let d1 = incomingDelta, let d2 = outgoingDelta {
            // Intermediate snapshot: merge deltas and delete
            guard let prevSnap = try fetchSnapshot(id: d1.fromSnapshotID, context: context),
                  let nextSnap = try fetchSnapshot(id: d2.toSnapshotID, context: context) else {
                logger.error("SnapshotLifecycleService: failed to find surrounding snapshots for merge")
                throw LifecycleError.missingNeighbor
            }

            let merged = DeltaService.mergeDeltas(
                d1: d1, d2: d2,
                snapshotBefore: prevSnap,
                snapshotAfter: nextSnap
            )
            merged.deviceID = deviceID
            context.insert(merged)
            for td in merged.typeDeltas ?? [] { context.insert(td) }

            nextSnap.previousSnapshotID = prevSnap.id
            _ = refreshContentEquivalence(for: nextSnap, baseline: prevSnap)
            invalidateDetailCaches(for: nextSnap)
            context.delete(d1)
            context.delete(d2)
            context.delete(snapshot)
        }

        try context.save()
        LocalOperationLogStore.append(completedLog)
    }

    static func rebuildMissingDetailCaches(
        context: ModelContext,
        maxTypeCounts: Int
    ) throws -> Bool {
        guard maxTypeCounts > 0 else { return false }
        MemoryLog.log("snapshotLifecycle.rebuildMissingDetailCaches.begin", metadata: [
            "maxTypeCounts": "\(maxTypeCounts)"
        ])

        let snapshotIDs = try context.fetch(FetchDescriptor<HealthSnapshot>()).map { $0.id }
        MemoryLog.log("snapshotLifecycle.rebuildMissingDetailCaches.snapshotsFetched", metadata: [
            "snapshotCount": "\(snapshotIDs.count)"
        ])
        var rebuiltCount = 0
        var updatedAliases = 0

        // Alias pass: process in batches to avoid memory bloat
        let aliasBatchSize = 5
        for batchStart in stride(from: 0, to: snapshotIDs.count, by: aliasBatchSize) {
            let batchEnd = min(batchStart + aliasBatchSize, snapshotIDs.count)
            for id in snapshotIDs[batchStart..<batchEnd] {
                guard let snapshot = try fetchSnapshot(id: id, context: context),
                      let baselineID = snapshot.previousSnapshotID,
                      let baseline = try fetchSnapshot(id: baselineID, context: context) else {
                    continue
                }

                if refreshContentEquivalence(for: snapshot, baseline: baseline) {
                    updatedAliases += 1
                }
            }
            // Save batch to flush object cache and allow garbage collection
            if updatedAliases > 0 {
                try context.save()
            }
        }

        MemoryLog.log("snapshotLifecycle.rebuildMissingDetailCaches.aliasPassFinished", metadata: [
            "updatedAliases": "\(updatedAliases)"
        ])
        if MemoryLog.isFootprintAtOrAbove(MemoryLog.detailCacheBuildFootprintLimit) {
            MemoryLog.log("snapshotLifecycle.rebuildMissingDetailCaches.pausedMemoryPressure", metadata: [
                "limit": MemoryLog.format(MemoryLog.detailCacheBuildFootprintLimit),
                "phase": "afterAliasPass",
                "updatedAliases": "\(updatedAliases)"
            ])
            return false
        }

        // Detail cache pass
        for id in snapshotIDs {
            guard let snapshot = try fetchSnapshot(id: id, context: context),
                  let baselineID = snapshot.previousSnapshotID,
                  let baseline = try fetchSnapshot(id: baselineID, context: context) else {
                continue
            }

            let baselineByType = Dictionary(
                uniqueKeysWithValues: (baseline.typeCounts ?? []).map { ($0.typeIdentifier, $0) }
            )

            if snapshot.isContentAlias,
               snapshot.contentEquivalentSnapshotID == baseline.contentRepresentativeSnapshotID {
                continue
            }

            for typeCount in snapshot.typeCounts ?? [] {
                guard shouldBackfillDetailCache(
                    typeCount: typeCount,
                    baseline: baselineByType[typeCount.typeIdentifier],
                    baselineID: baseline.id
                ) else {
                    continue
                }

                if MemoryLog.isFootprintAtOrAbove(MemoryLog.detailCacheBuildFootprintLimit) {
                    MemoryLog.log("snapshotLifecycle.rebuildMissingDetailCaches.pausedMemoryPressure", metadata: [
                        "limit": MemoryLog.format(MemoryLog.detailCacheBuildFootprintLimit),
                        "phase": "beforeDetailCacheBuild",
                        "rebuiltCount": "\(rebuiltCount)",
                        "updatedAliases": "\(updatedAliases)"
                    ])
                    if rebuiltCount > 0 || updatedAliases > 0 {
                        try context.save()
                    }
                    return false
                }

                MemoryLog.log("snapshotLifecycle.detailCache.buildBegin", metadata: detailCacheMetadata(
                    current: typeCount,
                    previous: baselineByType[typeCount.typeIdentifier],
                    source: "backfill"
                ))
                typeCount.setDetailCache(
                    TypeCountDetailCacheBuilder.build(
                        current: typeCount,
                        previous: baselineByType[typeCount.typeIdentifier],
                        baselineSnapshotID: baseline.id
                    )
                )
                MemoryLog.log("snapshotLifecycle.detailCache.buildEnd", metadata: detailCacheMetadata(
                    current: typeCount,
                    previous: baselineByType[typeCount.typeIdentifier],
                    source: "backfill"
                ))
                rebuiltCount += 1

                if rebuiltCount >= maxTypeCounts {
                    MemoryLog.log("snapshotLifecycle.rebuildMissingDetailCaches.pausedAtLimit", metadata: [
                        "rebuiltCount": "\(rebuiltCount)",
                        "updatedAliases": "\(updatedAliases)"
                    ])
                    try context.save()
                    return false
                }
            }
        }

        if rebuiltCount > 0 || updatedAliases > 0 {
            try context.save()
        }
        MemoryLog.log("snapshotLifecycle.rebuildMissingDetailCaches.complete", metadata: [
            "rebuiltCount": "\(rebuiltCount)",
            "updatedAliases": "\(updatedAliases)"
        ])
        return true
    }

    // MARK: - Fetch helpers

    private static func fetchIncomingDelta(toSnapshotID snapshotID: UUID, context: ModelContext) throws -> SnapshotDelta? {
        let descriptor = FetchDescriptor<SnapshotDelta>(
            predicate: #Predicate<SnapshotDelta> { $0.toSnapshotID == snapshotID }
        )
        return try context.fetch(descriptor).first
    }

    private static func fetchOutgoingDelta(fromSnapshotID snapshotID: UUID, context: ModelContext) throws -> SnapshotDelta? {
        let descriptor = FetchDescriptor<SnapshotDelta>(
            predicate: #Predicate<SnapshotDelta> { $0.fromSnapshotID == snapshotID }
        )
        return try context.fetch(descriptor).first
    }

    private static func fetchSnapshot(id: UUID, context: ModelContext) throws -> HealthSnapshot? {
        let descriptor = FetchDescriptor<HealthSnapshot>(
            predicate: #Predicate<HealthSnapshot> { $0.id == id }
        )
        return try context.fetch(descriptor).first
    }

    private static func invalidateDetailCaches(for snapshot: HealthSnapshot) {
        for typeCount in snapshot.typeCounts ?? [] {
            typeCount.setDetailCache(nil)
        }
    }

    @discardableResult
    private static func refreshContentEquivalence(for snapshot: HealthSnapshot, baseline: HealthSnapshot) -> Bool {
        let previousSnapshotAliasID = snapshot.contentEquivalentSnapshotID
        let previousTypeAliasIDs = Dictionary(
            uniqueKeysWithValues: (snapshot.typeCounts ?? []).map { ($0.id, $0.contentEquivalentTypeCountID) }
        )

        snapshot.contentEquivalentSnapshotID = nil
        for typeCount in snapshot.typeCounts ?? [] {
            typeCount.contentEquivalentTypeCountID = nil
        }

        guard snapshot.monitoredTypeSetHash == baseline.monitoredTypeSetHash else {
            return contentEquivalenceDidChange(
                snapshot: snapshot,
                previousSnapshotAliasID: previousSnapshotAliasID,
                previousTypeAliasIDs: previousTypeAliasIDs
            )
        }

        let baselineByType = Dictionary(
            uniqueKeysWithValues: (baseline.typeCounts ?? []).map { ($0.typeIdentifier, $0) }
        )

        for typeCount in snapshot.typeCounts ?? [] {
            guard let baselineType = baselineByType[typeCount.typeIdentifier],
                  areTypeCountsContentEquivalent(typeCount, baselineType) else {
                continue
            }

            typeCount.contentEquivalentTypeCountID = baselineType.contentRepresentativeTypeCountID
            typeCount.setDetailCache(nil)
        }

        if areTypeCountsContentEquivalent(snapshot.typeCounts ?? [], baseline.typeCounts ?? []) {
            snapshot.contentEquivalentSnapshotID = baseline.contentRepresentativeSnapshotID
        }

        return contentEquivalenceDidChange(
            snapshot: snapshot,
            previousSnapshotAliasID: previousSnapshotAliasID,
            previousTypeAliasIDs: previousTypeAliasIDs
        )
    }

    private static func contentEquivalenceDidChange(
        snapshot: HealthSnapshot,
        previousSnapshotAliasID: UUID?,
        previousTypeAliasIDs: [UUID: UUID?]
    ) -> Bool {
        if snapshot.contentEquivalentSnapshotID != previousSnapshotAliasID {
            return true
        }

        for typeCount in snapshot.typeCounts ?? [] {
            if typeCount.contentEquivalentTypeCountID != (previousTypeAliasIDs[typeCount.id] ?? nil) {
                return true
            }
        }

        return false
    }

    private static func areTypeCountsContentEquivalent(_ lhs: [TypeCount], _ rhs: [TypeCount]) -> Bool {
        let lhsByType = Dictionary(uniqueKeysWithValues: lhs.map { ($0.typeIdentifier, $0) })
        let rhsByType = Dictionary(uniqueKeysWithValues: rhs.map { ($0.typeIdentifier, $0) })
        guard lhsByType.keys == rhsByType.keys else { return false }

        for typeIdentifier in lhsByType.keys {
            guard let lhsType = lhsByType[typeIdentifier],
                  let rhsType = rhsByType[typeIdentifier],
                  lhsType.count == rhsType.count,
                  lhsType.contentHash == rhsType.contentHash,
                  lhsType.quality == rhsType.quality,
                  lhsType.isUnsupported == rhsType.isUnsupported else {
                return false
            }
        }

        return true
    }

    private static func areTypeCountsContentEquivalent(_ lhs: TypeCount, _ rhs: TypeCount) -> Bool {
        lhs.count == rhs.count &&
        lhs.contentHash == rhs.contentHash &&
        lhs.quality == rhs.quality &&
        lhs.isUnsupported == rhs.isUnsupported
    }

    @MainActor private static func shouldBackfillDetailCache(
        typeCount: TypeCount,
        baseline: TypeCount?,
        baselineID: UUID
    ) -> Bool {
        if typeCount.isContentAlias {
            return false
        }

        if typeCount.detailCache?.matchesBaseline(baselineID) == true {
            return false
        }

        guard canBuildDetailCache(typeCount),
              baseline.map(canBuildDetailCache(_:)) ?? true else {
            return false
        }

        return true
    }

    @MainActor private static func canBuildDetailCache(_ typeCount: TypeCount) -> Bool {
        typeCount.count <= 0 || typeCount.recordArchiveData != nil
    }

    private static func detailCacheMetadata(current: TypeCount, previous: TypeCount?, source: String) -> [String: String] {
        [
            "source": source,
            "type": current.typeIdentifier,
            "currentCount": "\(current.count)",
            "previousCount": "\(previous?.count ?? 0)",
            "currentArchive": current.recordArchiveData.map { MemoryLog.format(UInt64($0.count)) } ?? "nil",
            "previousArchive": previous?.recordArchiveData.map { MemoryLog.format(UInt64($0.count)) } ?? "nil",
            "isAlias": "\(current.isContentAlias)"
        ]
    }

    private static func buildSummary(snapshot: HealthSnapshot, incoming: SnapshotDelta?, outgoing: SnapshotDelta?) -> String {
        let position: String
        if incoming == nil && outgoing == nil { position = "standalone" }
        else if incoming == nil { position = "oldest" }
        else if outgoing == nil { position = "latest" }
        else { position = "intermediate" }
        return "Deleted \(position) snapshot \(snapshot.id) at \(snapshot.timestamp)"
    }

    enum LifecycleError: Error {
        case missingNeighbor
    }
}

private extension Bundle {
    var appBuildVersion: String {
        let version = infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
        let build = infoDictionary?["CFBundleVersion"] as? String ?? ""
        return "\(version) (\(build))"
    }
}
