import SwiftUI

struct RecordChangeEvolutionSnapshot: Identifiable, Equatable, Sendable {
    let id: UUID
    let timestamp: Date
    let localSequenceNumber: Int
    let previousSnapshotID: UUID?
    let archiveObservationID: Int64?
    let count: Int
    let fallbackAdded: Int
    let fallbackDisappeared: Int
    let fallbackIsExact: Bool

    init(
        id: UUID,
        timestamp: Date,
        localSequenceNumber: Int = 0,
        previousSnapshotID: UUID? = nil,
        archiveObservationID: Int64? = nil,
        count: Int,
        fallbackAdded: Int = 0,
        fallbackDisappeared: Int = 0,
        fallbackIsExact: Bool = false
    ) {
        self.id = id
        self.timestamp = timestamp
        self.localSequenceNumber = localSequenceNumber
        self.previousSnapshotID = previousSnapshotID
        self.archiveObservationID = archiveObservationID
        self.count = count
        self.fallbackAdded = fallbackAdded
        self.fallbackDisappeared = fallbackDisappeared
        self.fallbackIsExact = fallbackIsExact
    }

    nonisolated fileprivate static func timelineSort(_ lhs: RecordChangeEvolutionSnapshot, _ rhs: RecordChangeEvolutionSnapshot) -> Bool {
        if lhs.timestamp != rhs.timestamp {
            return lhs.timestamp < rhs.timestamp
        }
        if lhs.localSequenceNumber != rhs.localSequenceNumber {
            return lhs.localSequenceNumber < rhs.localSequenceNumber
        }
        return lhs.id.uuidString < rhs.id.uuidString
    }

    fileprivate var fallbackDiff: RecordChangeDiff {
        RecordChangeDiff(
            added: fallbackAdded,
            disappeared: fallbackDisappeared,
            isExact: fallbackIsExact
        )
    }
}

struct RecordChangeEvolutionChart: View {
    let snapshots: [RecordChangeEvolutionSnapshot]
    let currentSnapshotID: UUID
    let typeIdentifier: String
    let displayName: String

    @State private var cachedCountsByObservationID: [Int64: Int] = [:]
    @State private var cachedDiffsByPair: [ArchiveDiffKey: RecordChangeDiff]?

    private var sortedSnapshots: [RecordChangeEvolutionSnapshot] {
        snapshots.sorted(by: RecordChangeEvolutionSnapshot.timelineSort)
    }

    private var currentIndex: Int? {
        sortedSnapshots.firstIndex { $0.id == currentSnapshotID }
    }

    private var contextSnapshots: [RecordChangeEvolutionSnapshot] {
        guard let idx = currentIndex else { return [] }
        let start = max(0, idx - 3)
        let end = min(sortedSnapshots.count, idx + 4)
        return Array(sortedSnapshots[start..<end])
    }

    private var diffTaskID: String {
        (
            [typeIdentifier, currentSnapshotID.uuidString]
            + contextSnapshots.map { "\($0.id.uuidString):\($0.archiveObservationID ?? -1)" }
        )
        .joined(separator: "|")
    }

    private var maxCount: Int {
        let counts = contextSnapshots.map(countForSnapshot)
        return counts.max() ?? 1
    }

    private var chartPoints: [ChartPoint] {
        contextSnapshots.map { snapshot in
            let previousSnapshot = previousSnapshot(for: snapshot)
            return ChartPoint(
                snapshot: snapshot,
                count: max(countForSnapshot(snapshot), 0),
                diff: recordDiff(current: snapshot, previous: previousSnapshot)
            )
        }
    }

    private var maxChangeCount: Int {
        let maxChange = chartPoints
            .map { max($0.diff.added, $0.diff.disappeared) }
            .max() ?? 0
        return max(maxChange, 1)
    }

    private var hasAnyRecordChanges: Bool {
        chartPoints.contains { $0.diff.added > 0 || $0.diff.disappeared > 0 }
    }

    private func recordDiff(
        current: RecordChangeEvolutionSnapshot,
        previous: RecordChangeEvolutionSnapshot?
    ) -> RecordChangeDiff {
        guard let previous = previous else { return RecordChangeDiff() }

        if let currentObservationID = current.archiveObservationID,
           let previousObservationID = previous.archiveObservationID,
           let cachedDiffsByPair {
            let key = ArchiveDiffKey(fromObservationID: previousObservationID, toObservationID: currentObservationID)
            return cachedDiffsByPair[key] ?? RecordChangeDiff(isExact: true)
        }

        return current.fallbackDiff
    }

    var body: some View {
        if contextSnapshots.count < 2 {
            emptyState
        } else {
            evolutionView
                .task(id: diffTaskID) {
                    await loadArchiveEvolutionCache()
                }
        }
    }

    private var emptyState: some View {
        VStack(spacing: 8) {
            Image(systemName: "chart.line.uptrend.xyaxis")
                .font(.system(size: 24, weight: .semibold))
                .foregroundStyle(.secondary)

            Text("Not enough snapshots")
                .font(.caption)
                .foregroundStyle(.secondary)
        }
        .frame(maxWidth: .infinity)
        .padding(24)
        .background(Color(.systemBackground).opacity(0.5), in: RoundedRectangle(cornerRadius: 8))
    }

    private var evolutionView: some View {
        VStack(alignment: .leading, spacing: 12) {
            HStack {
                Text("Evolution")
                    .font(.headline.weight(.semibold))
                    .foregroundStyle(.primary)

                Spacer()

                Text("\(contextSnapshots.count) snapshots")
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }

            evolutionChart

            if hasAnyRecordChanges {
                changeLegend
            }

            HStack(spacing: 12) {
                VStack(alignment: .leading, spacing: 4) {
                    Text("Min")
                        .font(.caption)
                        .foregroundStyle(.secondary)
                    Text("\(contextSnapshots.map(countForSnapshot).min() ?? 0)")
                        .font(.caption.weight(.semibold))
                        .monospacedDigit()
                }

                Spacer()

                VStack(alignment: .center, spacing: 4) {
                    Text("Current")
                        .font(.caption)
                        .foregroundStyle(.secondary)
                    if let current = contextSnapshots.first(where: { $0.id == currentSnapshotID }) {
                        Text("\(countForSnapshot(current))")
                            .font(.caption.weight(.semibold))
                            .monospacedDigit()
                            .foregroundStyle(Color.accentColor)
                    }
                }

                Spacer()

                VStack(alignment: .trailing, spacing: 4) {
                    Text("Max")
                        .font(.caption)
                        .foregroundStyle(.secondary)
                    Text("\(maxCount)")
                        .font(.caption.weight(.semibold))
                        .monospacedDigit()
                }
            }
            .padding(8)
            .background(Color(.systemBackground).opacity(0.3), in: RoundedRectangle(cornerRadius: 6))
        }
        .padding(12)
        .background(Color(.systemBackground).opacity(0.5), in: RoundedRectangle(cornerRadius: 8))
    }

    @ViewBuilder
    private var evolutionChart: some View {
        HStack(alignment: .bottom, spacing: 4) {
            ForEach(Array(chartPoints.enumerated()), id: \.element.snapshot.id) { idx, point in
                chartBar(at: idx, point: point)
            }
        }
        .frame(height: 132)
        .padding(12)
        .background(Color(.systemBackground).opacity(0.3), in: RoundedRectangle(cornerRadius: 8))
    }

    @ViewBuilder
    private func chartBar(at index: Int, point: ChartPoint) -> some View {
        let isCurrent = point.snapshot.id == currentSnapshotID
        let totalBarHeight = CGFloat(point.count) / CGFloat(max(maxCount, 1)) * 54
        let addedHeight = changeHeight(for: point.diff.added)
        let disappearedHeight = changeHeight(for: point.diff.disappeared)

        VStack(spacing: 0) {
            if isCurrent {
                Image(systemName: "star.fill")
                    .font(.system(size: 12, weight: .semibold))
                    .foregroundStyle(Color.accentColor)
                    .padding(.bottom, 4)
            }

            VStack(spacing: 3) {
                changeSegment(
                    count: point.diff.added,
                    height: addedHeight,
                    color: Color.healthyGreen,
                    pinsToBottom: true
                )

                RoundedRectangle(cornerRadius: 2)
                    .fill(Color.neutralGray.opacity(0.6))
                    .frame(height: max(4, totalBarHeight))
                    .frame(maxWidth: .infinity)

                changeSegment(
                    count: point.diff.disappeared,
                    height: disappearedHeight,
                    color: Color.criticalRed,
                    pinsToBottom: false
                )
            }
            .frame(height: 96, alignment: .bottom)
            .overlay(alignment: .bottom) {
                if point.diff.isFallback, point.diff.disappeared > 0 || point.diff.added > 0 {
                    Image(systemName: "tilde")
                        .font(.system(size: 9, weight: .bold))
                        .foregroundStyle(.secondary)
                        .offset(y: 12)
                }
            }

            Text(point.snapshot.timestamp.formatted(.dateTime.month().day()))
                .font(.caption2)
                .foregroundStyle(.secondary)
                .lineLimit(1)
                .padding(.top, 4)
        }
        .frame(maxWidth: .infinity)
    }

    private func changeHeight(for count: Int) -> CGFloat {
        guard count > 0 else { return 0 }
        let scaled = CGFloat(count) / CGFloat(maxChangeCount) * 18
        return max(4, scaled)
    }

    @ViewBuilder
    private func changeSegment(
        count: Int,
        height: CGFloat,
        color: Color,
        pinsToBottom: Bool
    ) -> some View {
        VStack {
            if pinsToBottom {
                Spacer(minLength: 0)
            }

            if count > 0 {
                RoundedRectangle(cornerRadius: 2)
                    .fill(color)
                    .frame(height: height)
                    .frame(maxWidth: .infinity)
                    .accessibilityLabel("\(count) records")
            } else {
                Color.clear.frame(height: 0)
            }

            if !pinsToBottom {
                Spacer(minLength: 0)
            }
        }
        .frame(height: 18)
    }

    private var changeLegend: some View {
        HStack(spacing: 10) {
            legendItem(color: Color.healthyGreen, label: "New")
            legendItem(color: Color.criticalRed, label: "Missing")
            Spacer(minLength: 0)
        }
        .font(.caption2)
        .foregroundStyle(.secondary)
    }

    private func legendItem(color: Color, label: String) -> some View {
        HStack(spacing: 4) {
            RoundedRectangle(cornerRadius: 2)
                .fill(color)
                .frame(width: 12, height: 4)
            Text(label)
        }
    }

    private func countForSnapshot(_ snapshot: RecordChangeEvolutionSnapshot) -> Int {
        if let observationID = snapshot.archiveObservationID,
           let cachedCount = cachedCountsByObservationID[observationID] {
            return cachedCount
        }

        return snapshot.count
    }

    private func previousSnapshot(for snapshot: RecordChangeEvolutionSnapshot) -> RecordChangeEvolutionSnapshot? {
        if let previousSnapshotID = snapshot.previousSnapshotID,
           let previous = sortedSnapshots.first(where: { $0.id == previousSnapshotID }) {
            return previous
        }

        guard let index = sortedSnapshots.firstIndex(where: { $0.id == snapshot.id }),
              index > 0 else {
            return nil
        }
        return sortedSnapshots[index - 1]
    }

    @MainActor
    private func loadArchiveEvolutionCache() async {
        let archiveSnapshots = contextSnapshots.filter { $0.archiveObservationID != nil }
        guard !archiveSnapshots.isEmpty else {
            cachedCountsByObservationID = [:]
            cachedDiffsByPair = nil
            return
        }

        do {
            let cache = try CoreDataArchiveCacheStore()
            var counts: [Int64: Int] = [:]
            var diffs: [ArchiveDiffKey: RecordChangeDiff] = [:]

            for snapshot in archiveSnapshots {
                guard let observationID = snapshot.archiveObservationID else { continue }
                let summary = try cache.typeSummaries(observationID: observationID)
                    .first { $0.sampleTypeIdentifier == typeIdentifier }
                counts[observationID] = summary?.visibleRecordCount ?? 0

                guard let previous = previousSnapshot(for: snapshot),
                      let previousObservationID = previous.archiveObservationID else {
                    continue
                }

                if let diff = try cache.diffSummary(
                    fromObservationID: previousObservationID,
                    toObservationID: observationID,
                    sampleTypeIdentifier: typeIdentifier
                ) {
                    diffs[ArchiveDiffKey(fromObservationID: previousObservationID, toObservationID: observationID)] = RecordChangeDiff(
                        added: diff.appearedCount,
                        disappeared: diff.disappearedCount,
                        isExact: true
                    )
                }
            }

            cachedCountsByObservationID = counts
            cachedDiffsByPair = diffs
        } catch {
            cachedCountsByObservationID = [:]
            cachedDiffsByPair = nil
        }
    }
}

private struct ChartPoint {
    let snapshot: RecordChangeEvolutionSnapshot
    let count: Int
    let diff: RecordChangeDiff
}

private struct RecordChangeDiff {
    let added: Int
    let disappeared: Int
    let isExact: Bool

    init(added: Int = 0, disappeared: Int = 0, isExact: Bool = false) {
        self.added = added
        self.disappeared = disappeared
        self.isExact = isExact
    }

    var isFallback: Bool {
        !isExact
    }
}

private struct ArchiveDiffKey: Hashable {
    let fromObservationID: Int64
    let toObservationID: Int64
}

#Preview {
    let mockSnapshots = (0..<7).map { idx in
        RecordChangeEvolutionSnapshot(
            id: UUID(),
            timestamp: Calendar.current.date(byAdding: .day, value: idx - 3, to: Date.now) ?? .now,
            localSequenceNumber: idx,
            count: Int.random(in: 5000..<15000),
            fallbackAdded: idx.isMultiple(of: 2) ? 240 : 0,
            fallbackDisappeared: idx.isMultiple(of: 3) ? 120 : 0
        )
    }

    RecordChangeEvolutionChart(
        snapshots: mockSnapshots,
        currentSnapshotID: mockSnapshots[3].id,
        typeIdentifier: "HKQuantityTypeIdentifierStepCount",
        displayName: "Step Count"
    )
    .padding(16)
}
