import Foundation

enum AnomalyDetector {
    struct DetectionResult {
        var records: [AnomalyRecord]
        var consumedPostRestoreSuppressionDeltaID: UUID? = nil
    }

    // AnomalyDetector is pure — must not mutate SwiftData models, must not call context.save().
    // currentTypeCounts and previousTypeCounts are pre-built maps provided by the caller.
    // detect() sets record.deltaID = delta.id on EVERY created AnomalyRecord internally —
    // the caller must not set deltaID externally. All returned records have deltaID == delta.id.
    static func detect(
        delta: SnapshotDelta,
        current: HealthSnapshot,
        previous: HealthSnapshot,
        currentTypeCounts: [String: TypeCount],
        previousTypeCounts: [String: TypeCount]
    ) -> DetectionResult {
                // Quality gate: current snapshot must be complete.
                // Previous may be partial due known unavailable metrics; per-type quality guards
                // below still prevent analysis on impaired comparisons.
                guard current.snapshotQuality == SnapshotQuality.complete else {
            return DetectionResult(records: [])
        }

        var records: [AnomalyRecord] = []
        var syncAnomalyCount = 0
        var consumedDeltaID: UUID? = nil

        let typeDeltas = delta.typeDeltas ?? []

        for typeDelta in typeDeltas {
            // Registry gate — suppress appeared/disappeared anomalies from non-normal reasons
            guard typeDelta.reason == TypeDeltaReason.normal else { continue }

            // count = -1 guard: skip any TypeDelta where either quality is not complete
            let prevQuality = typeDelta.qualityBefore
            let currQuality = typeDelta.qualityAfter
            if prevQuality != nil && prevQuality != SnapshotQuality.complete { continue }
            if currQuality != nil && currQuality != SnapshotQuality.complete { continue }

            guard typeDelta.transition == .changed else { continue }

            let typeID = typeDelta.typeIdentifier
            let prevTC = previousTypeCounts[typeID]
            let currTC = currentTypeCounts[typeID]
            let countDelta = typeDelta.countDelta

            // historicalInsertion
            if countDelta > 0 {
                let currEarliest = currTC?.earliestDate
                let prevEarliest = prevTC?.earliestDate
                let currLatest   = currTC?.latestDate
                let prevLatest   = prevTC?.latestDate

                let isHistorical: Bool
                if let ce = currEarliest, let pe = prevEarliest {
                    isHistorical = ce < pe
                } else if let cl = currLatest, let pl = prevLatest {
                    let dayDiff = abs(cl.timeIntervalSince(pl))
                    isHistorical = dayDiff < 86_400 // within 1 day
                } else {
                    isHistorical = false
                }

                if isHistorical {
                    let record = makeRecord(
                        delta: delta,
                        snapshotID: current.id,
                        deviceID: current.deviceID,
                        type: .historicalInsertion,
                        severity: .warning,
                        typeID: typeID,
                        message: "Historical insertion detected for \(typeDelta.displayName): +\(countDelta) records"
                    )
                    records.append(record)
                }
            }

            // deletion
            if countDelta < 0 {
                let prevCount = prevTC?.count ?? 0
                let severity: Severity
                if prevCount > 0 {
                    let ratio = Double(abs(countDelta)) / Double(prevCount)
                    severity = ratio >= 0.05 ? .critical : .warning
                } else {
                    severity = .warning
                }
                let record = makeRecord(
                    delta: delta,
                    snapshotID: current.id,
                    deviceID: current.deviceID,
                    type: .deletion,
                    severity: severity,
                    typeID: typeID,
                    message: "Records disappeared for \(typeDelta.displayName): \(countDelta) records. Verify Health read authorization first; if access was intentionally revoked, confirm that decision. Otherwise, treat this as possible data deletion."
                )
                records.append(record)
            }

            // duplication
            if countDelta > 0, let prevCount = prevTC?.count, prevCount > 0 {
                let ratio = Double(countDelta) / Double(prevCount)
                if ratio > 0.5 {
                    let currEarliest = currTC?.earliestDate
                    let prevEarliest = prevTC?.earliestDate
                    let currLatest   = currTC?.latestDate
                    let prevLatest   = prevTC?.latestDate

                    let earliestClose = zip(currEarliest, prevEarliest)
                        .map { abs($0.timeIntervalSince($1)) < 86_400 } ?? true
                    let latestClose = zip(currLatest, prevLatest)
                        .map { abs($0.timeIntervalSince($1)) < 86_400 } ?? true

                    if earliestClose && latestClose {
                        let record = makeRecord(
                            delta: delta,
                            snapshotID: current.id,
                            deviceID: current.deviceID,
                            type: .duplication,
                            severity: .warning,
                            typeID: typeID,
                            message: "Duplication detected for \(typeDelta.displayName): +\(countDelta) records (\(Int(ratio * 100))% increase)"
                        )
                        records.append(record)
                    }
                }
            }

            // silentReplacement
            if countDelta == 0 && typeDelta.hashBefore != typeDelta.hashAfter {
                let record = makeRecord(
                    delta: delta,
                    snapshotID: current.id,
                    deviceID: current.deviceID,
                    type: .silentReplacement,
                    severity: .info,
                    typeID: typeID,
                    message: "Silent replacement detected for \(typeDelta.displayName): count unchanged but hash differs"
                )
                records.append(record)
            }

            // Count types contributing to syncAnomaly
            if abs(countDelta) > 0, let prevCount = prevTC?.count, prevCount > 0 {
                let ratio = Double(abs(countDelta)) / Double(prevCount)
                if ratio > 0.10 { syncAnomalyCount += 1 }
            }
        }

        // syncAnomaly — ≥ 4 types simultaneously changed by >10%
        if syncAnomalyCount >= 4 {
            // isPostRestore suppression: suppress syncAnomaly if previous snapshot has the flag
            // and the suppression token hasn't been consumed yet.
            // Suppression is only consumed when current.snapshotQuality == .complete (enforced
            // by the quality gate at the top — if we get here, both are complete).
            if previous.isPostRestore && previous.isPostRestoreSuppressedDeltaID == nil {
                // Suppression consumed — return the delta ID for the caller to persist
                consumedDeltaID = delta.id
                // Do not emit syncAnomaly
            } else {
                let record = makeRecord(
                    delta: delta,
                    snapshotID: current.id,
                    deviceID: current.deviceID,
                    type: .syncAnomaly,
                    severity: .critical,
                    typeID: nil,
                    message: "Sync anomaly detected: \(syncAnomalyCount) types changed by >10% simultaneously"
                )
                records.append(record)
            }
        }

        // Set anomalyFlags on current snapshot (non-persisted here — caller does context.save())
        let flagValues = Set(records.map { $0.anomalyType.rawValue })
        current.anomalyFlags = Array(flagValues)

        return DetectionResult(
            records: records,
            consumedPostRestoreSuppressionDeltaID: consumedDeltaID
        )
    }

    private static func makeRecord(
        delta: SnapshotDelta,
        snapshotID: UUID,
        deviceID: String,
        type anomalyType: AnomalyType,
        severity: Severity,
        typeID: String?,
        message: String
    ) -> AnomalyRecord {
        let record = AnomalyRecord(
            snapshotID: snapshotID,
            deviceID: deviceID,
            anomalyType: anomalyType,
            severity: severity
        )
        record.deltaID = delta.id  // set structurally inside detect(), never by caller
        record.typeIdentifier = typeID
        record.message = message
        return record
    }
}

// Optional zip for two optionals (avoids force-unwrapping in comparisons)
private func zip<A, B>(_ a: A?, _ b: B?) -> (A, B)? {
    guard let a, let b else { return nil }
    return (a, b)
}
