import Foundation
import LibreTransmitter
import Swinject

struct Calibration: JSON, Hashable, Identifiable {
    let x: Double
    let y: Double
    var date = Date()

    static let zero = Calibration(x: 0, y: 0)

    var id = UUID()
}

protocol CalibrationService {
    var slope: Double { get }
    var intercept: Double { get }
    var calibrations: [Calibration] { get }

    func addCalibration(_ calibration: Calibration)
    func removeCalibration(_ calibration: Calibration)
    func removeAllCalibrations()
    func removeLast()

    func calibrate(value: Double) -> Double
}

final class BaseCalibrationService: CalibrationService, Injectable {
    private enum Config {
        static let minSlope = 0.8
        static let maxSlope = 1.25
        static let minIntercept = -100.0
        static let maxIntercept = 100.0
        static let maxValue = 500.0
        static let minValue = 0.0
    }

    @Injected() var storage: FileStorage!
    @Injected() var notificationCenter: NotificationCenter!
    private var lifetime = Lifetime()

    private(set) var calibrations: [Calibration] = [] {
        didSet {
            storage.save(calibrations, as: OpenAPS.FreeAPS.calibrations)
        }
    }

    init(resolver: Resolver) {
        injectServices(resolver)
        calibrations = storage.retrieve(OpenAPS.FreeAPS.calibrations, as: [Calibration].self) ?? []
        subscribe()
    }

    private func subscribe() {
        notificationCenter.publisher(for: .newSensorDetected)
            .sink { [weak self] _ in
                self?.removeAllCalibrations()
            }
            .store(in: &lifetime)
    }

    var slope: Double {
        guard calibrations.count >= 2 else {
            return 1
        }

        let xs = calibrations.map(\.x)
        let ys = calibrations.map(\.y)
        let sum1 = average(multiply(xs, ys)) - average(xs) * average(ys)
        let sum2 = average(multiply(xs, xs)) - pow(average(xs), 2)
        let slope = sum1 / sum2

        return min(max(slope, Config.minSlope), Config.maxSlope)
    }

    var intercept: Double {
        guard calibrations.count >= 1 else {
            return 0
        }
        let xs = calibrations.map(\.x)
        let ys = calibrations.map(\.y)

        let intercept = average(ys) - slope * average(xs)

        return min(max(intercept, Config.minIntercept), Config.maxIntercept)
    }

    func calibrate(value: Double) -> Double {
        linearRegression(value)
    }

    func addCalibration(_ calibration: Calibration) {
        calibrations.append(calibration)
    }

    func removeCalibration(_ calibration: Calibration) {
        calibrations.removeAll { $0 == calibration }
    }

    func removeAllCalibrations() {
        calibrations.removeAll()
    }

    func removeLast() {
        calibrations.removeLast()
    }

    private func average(_ input: [Double]) -> Double {
        input.reduce(0, +) / Double(input.count)
    }

    private func multiply(_ a: [Double], _ b: [Double]) -> [Double] {
        zip(a, b).map(*)
    }

    private func linearRegression(_ x: Double) -> Double {
        (intercept + slope * x).clamped(Config.minValue ... Config.maxValue)
    }
}
