import Foundation
import Combine
import BinaryCodable
import SwiftUI

final class PersistentStorage: ObservableObject {

    static var documentDirectory: URL {
        try! FileManager.default.url(
        for: .documentDirectory,
        in: .userDomainMask,
        appropriateFor: nil, create: true)
    }

    @AppStorage("newestDate")
    private var newestMeasurementTime: Int = 0

    /**
     The date of the latest measurement.

     Incoming data older than this date will be rejected to prevent duplicate measurements
     */
    private var newestMeasurementDate: Date {
        get {
            Date(seconds: newestMeasurementTime)
        }
        set {
            newestMeasurementTime = newValue.seconds
        }
    }

    @Published
    var recentMeasurements: [TemperatureMeasurement]
    
    @Published
    var dailyMeasurementCounts: [MeasurementDailyCount] = []

    /// The formatter for the temperature measurement file names
    private let fileNameFormatter: DateFormatter

    /// The storage of daily temperature measurements
    private let temperatureStorageFolderUrl: URL

    /// The storage of the measurement counts per day
    private let dailyCountsFileUrl: URL
    
    private let fm: FileManager

    /// The interval in which the measurements should be kept in `recentMeasurements`
    private let lastValueInterval: TimeInterval

    init(lastMeasurements: [TemperatureMeasurement] = [], lastValueInterval: TimeInterval = 3600) {
        self.recentMeasurements = lastMeasurements
        let documentDirectory = PersistentStorage.documentDirectory
        self.temperatureStorageFolderUrl = documentDirectory.appendingPathComponent("measurements")
        self.dailyCountsFileUrl = documentDirectory.appendingPathComponent("overview.bin")
        self.fm = .default
        self.fileNameFormatter = DateFormatter()
        self.fileNameFormatter.dateFormat = "yyyyMMdd.bin"
        self.lastValueInterval = lastValueInterval
        
        if lastMeasurements.isEmpty {
            loadLastMeasurements()
            loadDailyCounts()
        } else {
            setDailyCounts(from: lastMeasurements)
        }
        
        ensureExistenceOfFolder()
        //recalculateDailyCounts()
        updateTransferCount()
        updateDeviceInfoCount()
    }
    
    private func ensureExistenceOfFolder() {
        guard !fm.fileExists(atPath: temperatureStorageFolderUrl.path) else {
            return
        }
        do {
            try fm.createDirectory(at: temperatureStorageFolderUrl, withIntermediateDirectories: true)
        } catch {
            log.error("Failed to create folder: \(error)")
        }
    }
    
    private func fileName(for date: Date) -> String {
        fileNameFormatter.string(from: date)
    }
    
    private func fileName(for index: Int) -> String {
        String(format: "%08d.bin", index)
    }
    
    private func fileUrl(for dateIndex: Int) -> URL {
        temperatureStorageFolderUrl.appendingPathComponent(fileName(for: dateIndex))
    }
    
    private func fileUrl(for fileName: String) -> URL {
        temperatureStorageFolderUrl.appendingPathComponent(fileName)
    }
    
    private func loadLastMeasurements() {
        let now = Date.now
        let startDate = now.addingTimeInterval(-lastValueInterval)
        let todayIndex = now.dateIndex
        let todayValues = loadMeasurements(for: todayIndex)
            .filter { $0.date >= startDate }
        let dateIndexOfStart = startDate.dateIndex
        guard todayIndex != dateIndexOfStart else {
            recentMeasurements = todayValues
            log.info("Loaded \(recentMeasurements.count) recent measurements")
            return
        }
        let yesterdayValues = loadMeasurements(for: dateIndexOfStart)
            .filter { $0.date >= startDate }
        recentMeasurements = todayValues + yesterdayValues
        log.info("Loaded \(recentMeasurements.count) recent measurements")
    }

    private func updateLastMeasurements(_ measurements: [TemperatureMeasurement]) {
        let startDate = Date().addingTimeInterval(-lastValueInterval).seconds
        recentMeasurements = (measurements + recentMeasurements)
            .filter { $0.id > startDate }.sorted().reversed()
        log.info("\(recentMeasurements.count) recent measurements (with \(measurements.count) new entries)")
    }
    
    private func loadMeasurements(for date: Date) -> [TemperatureMeasurement] {
        loadMeasurements(from: fileName(for: date))
    }
    
    func loadMeasurements(for dateIndex: Int) -> [TemperatureMeasurement] {
        loadMeasurements(from: fileName(for: dateIndex))
    }
    
    private func loadMeasurements(from fileName: String) -> [TemperatureMeasurement] {
        let fileUrl = fileUrl(for: fileName)
        guard fm.fileExists(atPath: fileUrl.path) else {
            log.info("No measurements for \(fileName)")
            return []
        }
        do {
            let content = try Data(contentsOf: fileUrl)
            let points: [TemperatureMeasurement] = try BinaryDecoder.decode(from: content)
            log.info("Loaded \(points.count) points from \(fileName)")
            return points
        } catch {
            log.error("Failed to read file \(fileName): \(error)")
            return []
        }
    }

    func add(_ measurements: [TemperatureMeasurement]) {
        let lastDate = self.newestMeasurementDate.seconds
        let newerValues: [TemperatureMeasurement] = measurements.filter { $0.id > lastDate }.reversed()
        let newValues = newerValues.splitByDate()
        log.info("Adding \(newerValues.count) of \(measurements.count) measurements")

        for (dateIndex, values) in newValues {
            let count = saveNew(values, for: dateIndex)
            setDailyCount(count, for: dateIndex)
        }
        saveDailyCounts()
        updateLastMeasurements(measurements)
        if let newest = newerValues.max()?.id {
            newestMeasurementTime = newest
        }
    }
    
    func removeMeasurements(for dateIndex: Int) {
        defer { saveDailyCounts() }
        let fileUrl = fileUrl(for: dateIndex)
        guard fm.fileExists(atPath: fileUrl.path) else {
            log.warning("No measurements for \(fileUrl.lastPathComponent)")
            dailyMeasurementCounts = dailyMeasurementCounts.filter { $0.dateIndex != dateIndex }
            recentMeasurements = recentMeasurements.filter { $0.date.dateIndex != dateIndex }
            return
        }
        do {
            try fm.removeItem(at: fileUrl)
            dailyMeasurementCounts = dailyMeasurementCounts.filter { $0.dateIndex != dateIndex }
            recentMeasurements = recentMeasurements.filter { $0.date.dateIndex != dateIndex }
        } catch {
            log.error("Failed to delete \(fileUrl.lastPathComponent): \(error)")
        }
    }
    
    /**
     - Returns: The number of new points
     */
    private func saveNew(_ measurements: [TemperatureMeasurement], for dateIndex: Int) -> Int {
        let fileName = fileName(for: dateIndex)
        let values = measurements + loadMeasurements(from: fileName)
        save(values, for: fileName)
        return values.count
    }
    
    private func save(_ measurements: [TemperatureMeasurement], for fileName: String) {
        let fileUrl = fileUrl(for: fileName)
        do {
            let data = try BinaryEncoder.encode(measurements.sorted().reversed())
            try data.write(to: fileUrl)
        } catch {
            log.error("Failed to save \(fileName): \(error)")
        }
    }
    
    // MARK: Daily counts
    
    private func setDailyCount(_ count: Int, for dateIndex: Int) {
        guard let index = dailyMeasurementCounts.firstIndex(where: { $0.dateIndex == dateIndex }) else {
            add(dailyCount: count, for: dateIndex)
            return
        }
        dailyMeasurementCounts[index].count = count
    }
    
    private func add(dailyCount count: Int, for dateIndex: Int) {
        let entry = MeasurementDailyCount(dateIndex: dateIndex, count: count)
        let index = dailyMeasurementCounts.firstIndex(where: { $0.dateIndex > dateIndex }) ?? 0
        dailyMeasurementCounts.insert(entry, at: index)
    }
    
    private func incrementCount(for dateIndex: Int, by increment: Int = 1) {
        guard let index = dailyMeasurementCounts.firstIndex(where: { $0.dateIndex == dateIndex }) else {
            add(dailyCount: increment, for: dateIndex)
            return
        }
        dailyMeasurementCounts[index].count += increment
    }
    
    private func loadDailyCounts() {
        do {
            let data = try Data(contentsOf: dailyCountsFileUrl)
            dailyMeasurementCounts = try BinaryDecoder.decode(from: data)
        } catch {
            log.error("Failed to load overview: \(error)")
        }
    }
    
    private func saveDailyCounts() {
        do {
            let data = try BinaryEncoder.encode(dailyMeasurementCounts)
            try data.write(to: dailyCountsFileUrl)
        } catch {
            log.error("Failed to write overview: \(error)")
        }
    }
    
    private func setDailyCounts(from measurements: [TemperatureMeasurement]) {
        self.dailyMeasurementCounts = measurements.reduce(into: [Int: Int]()) { counts, value in
            let index = value.date.dateIndex
            counts[index] = (counts[index] ?? 0) + 1
        }
        .map { MeasurementDailyCount(dateIndex: $0.key, count: $0.value) }
        .sorted()
        .reversed()
    }
    
    func recalculateDailyCounts() {
        do {
            let files = try fm.contentsOfDirectory(atPath: temperatureStorageFolderUrl.path)
            let newValues: [Int: Int] = files
                .reduce(into: [:]) { counts, fileName in
                    let dateString = fileName.replacingOccurrences(of: ".bin", with: "")
                    guard let dateIndex = Int(dateString) else {
                        log.warning("Found file with invalid name \(fileName)")
                        return
                    }
                    counts[dateIndex] = loadMeasurements(from: fileName).count
                }
            DispatchQueue.main.async {
                self.dailyMeasurementCounts = newValues
                    .map { .init(dateIndex: $0.key, count: $0.value) }
                    .sorted()
                    .reversed()
                log.info("Daily counts recalculated from \(files.count) files")
            }
        } catch {
            log.error("Failed to load daily counts: \(error)")
        }
    }

    // MARK: Device info archive

    @Published
    var numberOfStoredDeviceInfos: Int = 0

    private func updateDeviceInfoCount() {
        let count = countFiles(in: "info")
        DispatchQueue.main.async {
            self.numberOfStoredDeviceInfos = count
        }
    }

    @Published
    var numberOfStoredTransfers: Int = 0

    private func updateTransferCount() {
        let count = countFiles(in: "transfers")
        DispatchQueue.main.async {
            self.numberOfStoredTransfers = count
        }
    }

    private func countFiles(in folder: String) -> Int {
        let folder = PersistentStorage.documentDirectory.appendingPathComponent(folder)
        guard fm.fileExists(atPath: folder.path) else {
            return 0
        }
        do {
            return try fm.contentsOfDirectory(atPath: folder.path).count
        } catch {
            log.error("Failed to count files in '\(folder)': \(error)")
            return -1
        }
    }

    private func save(data: Data, date: Date, in folderName: String) -> Bool {
        let folder = PersistentStorage.documentDirectory.appendingPathComponent(folderName)
        if !fm.fileExists(atPath: folder.path) {
            do {
                try fm.createDirectory(at: folder, withIntermediateDirectories: false)
            } catch {
                log.error("Failed to create folder '\(folderName)': \(error)")
                return false
            }
        }
        let url = folder.appendingPathComponent("\(date.seconds)")
        do {
            try data.write(to: url)
        } catch {
            log.error("Failed to write '\(url.lastPathComponent)' in '\(folder)': \(error)")
            return false
        }
        return true
    }

    @discardableResult
    func save(deviceInfo: DeviceInfo) -> Bool {
        defer { updateDeviceInfoCount() }
        let data: Data
        do {
            data = try BinaryEncoder.encode(deviceInfo)
        } catch {
            log.error("Failed to encode device info for storage: \(error)")
            return false
        }
        return save(data: data, date: deviceInfo.time.date, in: "info")
    }

    @discardableResult
    func saveTransferData(data: Data, date: Date) -> Bool {
        defer { updateTransferCount() }
        return save(data: data, date: date, in: "transfers")
    }
}

private extension Array where Element == TemperatureMeasurement {
    
    @discardableResult
    mutating func insertIntoSorted(_ measurement: TemperatureMeasurement) -> Bool {
        guard !contains(measurement) else {
            return false
        }
        guard let index = self.firstIndex(where: { $0.date > measurement.date }) else {
            append(measurement)
            return true
        }
        insert(measurement, at: index)
        return true
    }
    
    func splitByDate() -> [Int : [TemperatureMeasurement]] {
        reduce(into: [:]) { result, value in
            let dateIndex = value.date.dateIndex
            result[dateIndex] = (result[dateIndex] ?? []) + [value]
        }
    }
}

extension PersistentStorage {
    
    static var mock: PersistentStorage {
        .init(lastMeasurements: TemperatureMeasurement.mockData)
    }
}
