import Foundation

struct LocalMetricTimeoutProfile: Codable, Equatable, Identifiable, Sendable {
    static let defaultInitialTimeout: TimeInterval = 15
    static let maximumTimeout: TimeInterval = 120
    private static let sampleLimit = 30
    private static let p95MinimumSampleCount = 5

    var id: String { metricIdentifier }
    var metricIdentifier: String
    var displayName: String
    var lastSuccessfulElapsed: TimeInterval = 0
    var averageSuccessfulElapsed: TimeInterval = 0
    var p95SuccessfulElapsed: TimeInterval = 0
    var lastTimeoutElapsed: TimeInterval = 0
    var timeoutCount: Int = 0
    var successCount: Int = 0
    var lastUpdatedAt: Date = Date.distantPast
    var currentTimeout: TimeInterval = LocalMetricTimeoutProfile.defaultInitialTimeout
    var isManuallyOverridden: Bool = false
    var manualTimeoutValue: TimeInterval = LocalMetricTimeoutProfile.defaultInitialTimeout
    var successfulElapsedSamples: [TimeInterval] = []

    init(metricIdentifier: String, displayName: String) {
        self.metricIdentifier = metricIdentifier
        self.displayName = displayName
    }

    var effectiveTimeout: TimeInterval {
        clamp(isManuallyOverridden ? manualTimeoutValue : currentTimeout)
    }

    var timeoutMode: String {
        if isManuallyOverridden { return "manual" }
        return successCount == 0 && timeoutCount == 0 ? "default" : "adaptive"
    }

    var p95SuccessfulElapsedIfAvailable: TimeInterval? {
        successfulElapsedSamples.count >= Self.p95MinimumSampleCount ? p95SuccessfulElapsed : nil
    }

    var suggestedRetryTimeout: TimeInterval {
        clamp(max(effectiveTimeout * 2, lastSuccessfulElapsed * 2, Self.defaultInitialTimeout))
    }

    mutating func recordSuccess(elapsed: TimeInterval, displayName: String) {
        self.displayName = displayName
        lastSuccessfulElapsed = elapsed
        successCount += 1
        lastUpdatedAt = Date()

        successfulElapsedSamples.append(elapsed)
        if successfulElapsedSamples.count > Self.sampleLimit {
            successfulElapsedSamples.removeFirst(successfulElapsedSamples.count - Self.sampleLimit)
        }

        averageSuccessfulElapsed = successfulElapsedSamples.reduce(0, +) / Double(successfulElapsedSamples.count)
        p95SuccessfulElapsed = Self.percentile95(successfulElapsedSamples)
        currentTimeout = automaticTimeout(from: successfulElapsedSamples)
    }

    mutating func recordTimeout(elapsed: TimeInterval, displayName: String) {
        self.displayName = displayName
        lastTimeoutElapsed = elapsed
        timeoutCount += 1
        lastUpdatedAt = Date()

        guard !isManuallyOverridden else { return }
        let progressive = max(currentTimeout * 1.5, elapsed * 2, Self.defaultInitialTimeout)
        currentTimeout = clamp(progressive)
    }

    mutating func resetLearning(displayName: String? = nil) {
        if let displayName { self.displayName = displayName }
        lastSuccessfulElapsed = 0
        averageSuccessfulElapsed = 0
        p95SuccessfulElapsed = 0
        lastTimeoutElapsed = 0
        timeoutCount = 0
        successCount = 0
        lastUpdatedAt = Date()
        currentTimeout = Self.defaultInitialTimeout
        isManuallyOverridden = false
        manualTimeoutValue = Self.defaultInitialTimeout
        successfulElapsedSamples = []
    }

    mutating func setManualTimeout(_ value: TimeInterval) {
        manualTimeoutValue = clamp(value)
        isManuallyOverridden = true
        lastUpdatedAt = Date()
    }

    mutating func returnToAutomatic() {
        isManuallyOverridden = false
        lastUpdatedAt = Date()
    }

    private func automaticTimeout(from samples: [TimeInterval]) -> TimeInterval {
        let basis = samples.count >= Self.p95MinimumSampleCount ? Self.percentile95(samples) : samples.max() ?? 0
        return clamp(max(Self.defaultInitialTimeout, basis * 2, currentTimeout * 0.9))
    }

    private func clamp(_ value: TimeInterval) -> TimeInterval {
        min(max(value, Self.defaultInitialTimeout), Self.maximumTimeout)
    }

    private static func percentile95(_ samples: [TimeInterval]) -> TimeInterval {
        guard !samples.isEmpty else { return 0 }
        let sorted = samples.sorted()
        let rawIndex = Int(ceil(Double(sorted.count) * 0.95)) - 1
        let index = min(max(rawIndex, 0), sorted.count - 1)
        return sorted[index]
    }
}

enum LocalMetricTimeoutProfileStore {
    private static let key = "hp_localMetricTimeoutProfiles"

    static func allProfiles(for monitoredTypes: [MonitoredType] = HealthKitService.allTypes) -> [LocalMetricTimeoutProfile] {
        let existing = Dictionary(uniqueKeysWithValues: load().map { ($0.metricIdentifier, $0) })
        return monitoredTypes.map { type in
            var profile = existing[type.id] ?? LocalMetricTimeoutProfile(metricIdentifier: type.id, displayName: type.displayName)
            profile.displayName = type.displayName
            return profile
        }
        .sorted { $0.displayName.localizedCaseInsensitiveCompare($1.displayName) == .orderedAscending }
    }

    static func profile(for monitoredType: MonitoredType) -> LocalMetricTimeoutProfile {
        allProfiles(for: [monitoredType]).first ?? LocalMetricTimeoutProfile(
            metricIdentifier: monitoredType.id,
            displayName: monitoredType.displayName
        )
    }

    static func save(_ profile: LocalMetricTimeoutProfile) {
        var profiles = load()
        if let index = profiles.firstIndex(where: { $0.metricIdentifier == profile.metricIdentifier }) {
            profiles[index] = profile
        } else {
            profiles.append(profile)
        }
        save(profiles)
    }

    static func resetAll(monitoredTypes: [MonitoredType] = HealthKitService.allTypes) {
        let profiles = monitoredTypes.map { type in
            LocalMetricTimeoutProfile(metricIdentifier: type.id, displayName: type.displayName)
        }
        save(profiles)
    }

    static func removeAll() {
        UserDefaults.standard.removeObject(forKey: key)
    }

    private static func load() -> [LocalMetricTimeoutProfile] {
        guard let data = UserDefaults.standard.data(forKey: key) else { return [] }
        return (try? JSONDecoder().decode([LocalMetricTimeoutProfile].self, from: data)) ?? []
    }

    private static func save(_ profiles: [LocalMetricTimeoutProfile]) {
        let sorted = profiles.sorted {
            $0.displayName.localizedCaseInsensitiveCompare($1.displayName) == .orderedAscending
        }
        UserDefaults.standard.set(try? JSONEncoder().encode(sorted), forKey: key)
    }
}
