import Foundation

// Interface updated 2026-05-17 — see AGENTS.md
struct TypeCountDailyChangeBin: Codable, Equatable, Sendable {
    let dayStart: Date
    let added: Int
    let disappeared: Int
    let unchanged: Int
}

// Interface updated 2026-05-17 — see AGENTS.md
struct TypeCountDetailCache: Codable, Equatable, Sendable {
    static let previewLimit = 1_000

    let baselineSnapshotID: UUID?
    let addedCount: Int
    let disappearedCount: Int
    let addedPreviewRecords: [HealthRecordValue]
    let disappearedPreviewRecords: [HealthRecordValue]
    let dailyChangeBins: [TypeCountDailyChangeBin]
    let earliestRecordDate: Date?
    let latestRecordDate: Date?

    var isPreviewLimited: Bool {
        addedCount > Self.previewLimit || disappearedCount > Self.previewLimit
    }

    func matchesBaseline(_ snapshotID: UUID?) -> Bool {
        baselineSnapshotID == snapshotID
    }
}

enum TypeCountDetailCacheArchive {
    static func encode(_ cache: TypeCountDetailCache) -> Data? {
        try? PropertyListEncoder().encode(cache)
    }

    static func decode(_ data: Data) -> TypeCountDetailCache? {
        try? PropertyListDecoder().decode(TypeCountDetailCache.self, from: data)
    }
}

enum TypeCountDetailCacheBuilder {
    static func build(
        current: TypeCount,
        previous: TypeCount?,
        baselineSnapshotID: UUID?
    ) -> TypeCountDetailCache? {
        guard let currentFingerprints = HealthRecordArchive.fingerprintSet(from: current.recordArchiveData),
              let previousFingerprints = HealthRecordArchive.fingerprintSet(from: previous?.recordArchiveData) else {
            return nil
        }

        if current.count > 0, current.recordArchiveData == nil { return nil }
        if let previous, previous.count > 0, previous.recordArchiveData == nil { return nil }

        var accumulator = DetailCacheAccumulator()

        if let previousArchive = previous?.recordArchiveData {
            guard HealthRecordArchive.forEachRecord(in: previousArchive, { record in
                if !currentFingerprints.contains(record.recordFingerprint) {
                    accumulator.add(record, as: .disappeared)
                }
            }) else {
                return nil
            }
        }

        if let currentArchive = current.recordArchiveData {
            guard HealthRecordArchive.forEachRecord(in: currentArchive, { record in
                if previousFingerprints.contains(record.recordFingerprint) {
                    accumulator.add(record, as: .unchanged)
                } else {
                    accumulator.add(record, as: .added)
                }
            }) else {
                return nil
            }
        }

        return TypeCountDetailCache(
            baselineSnapshotID: baselineSnapshotID,
            addedCount: accumulator.addedCount,
            disappearedCount: accumulator.disappearedCount,
            addedPreviewRecords: accumulator.addedPreview.records,
            disappearedPreviewRecords: accumulator.disappearedPreview.records,
            dailyChangeBins: accumulator.dailyBins,
            earliestRecordDate: accumulator.earliestRecordDate,
            latestRecordDate: accumulator.latestRecordDate
        )
    }
}

private enum TypeCountDetailCacheRecordKind {
    case added
    case disappeared
    case unchanged
}

private struct DetailCacheAccumulator {
    private let calendar = Calendar.current
    private var bins: [Date: (added: Int, disappeared: Int, unchanged: Int)] = [:]

    var addedCount = 0
    var disappearedCount = 0
    var addedPreview = NewestRecordBuffer(limit: TypeCountDetailCache.previewLimit)
    var disappearedPreview = NewestRecordBuffer(limit: TypeCountDetailCache.previewLimit)
    var earliestRecordDate: Date?
    var latestRecordDate: Date?

    var dailyBins: [TypeCountDailyChangeBin] {
        bins
            .map { dayStart, counts in
                TypeCountDailyChangeBin(
                    dayStart: dayStart,
                    added: counts.added,
                    disappeared: counts.disappeared,
                    unchanged: counts.unchanged
                )
            }
            .sorted { $0.dayStart < $1.dayStart }
    }

    mutating func add(_ record: HealthRecordValue, as kind: TypeCountDetailCacheRecordKind) {
        updateDateRange(with: record.startDate)

        let dayStart = calendar.startOfDay(for: record.startDate)
        var bin = bins[dayStart] ?? (added: 0, disappeared: 0, unchanged: 0)

        switch kind {
        case .added:
            addedCount += 1
            addedPreview.append(record)
            bin.added += 1
        case .disappeared:
            disappearedCount += 1
            disappearedPreview.append(record)
            bin.disappeared += 1
        case .unchanged:
            bin.unchanged += 1
        }

        bins[dayStart] = bin
    }

    private mutating func updateDateRange(with date: Date) {
        if earliestRecordDate.map({ date < $0 }) ?? true {
            earliestRecordDate = date
        }
        if latestRecordDate.map({ date > $0 }) ?? true {
            latestRecordDate = date
        }
    }
}

private struct NewestRecordBuffer {
    let limit: Int
    private var storage: [HealthRecordValue] = []
    private var oldestPreviewIndex: Int?

    init(limit: Int) {
        self.limit = limit
    }

    var records: [HealthRecordValue] {
        storage.sorted { $0.startDate > $1.startDate }
    }

    mutating func append(_ record: HealthRecordValue) {
        guard limit > 0 else { return }

        if storage.count < limit {
            storage.append(record)
            refreshOldestPreviewIndex()
            return
        }

        guard let oldestPreviewIndex,
              record.startDate > storage[oldestPreviewIndex].startDate else {
            return
        }

        storage[oldestPreviewIndex] = record
        refreshOldestPreviewIndex()
    }

    private mutating func refreshOldestPreviewIndex() {
        oldestPreviewIndex = storage.indices.min { lhs, rhs in
            storage[lhs].startDate < storage[rhs].startDate
        }
    }
}
