//  OpenHaystack – Tracking personal Bluetooth devices via Apple's Find My network
//
//  Copyright © 2021 Secure Mobile Networking Lab (SEEMOO)
//  Copyright © 2021 The Open Wireless Link Project
//
//  SPDX-License-Identifier: AGPL-3.0-only

// swiftlint:disable identifier_name

import Foundation
import CoreLocation

struct FindMyDevice: Codable, Hashable {

    let deviceId: String
    var keys = [FindMyKey]()

    var catalinaBigSurKeyFiles: [Data]?

    /// KeyHash: Report results
    var reports: [FindMyReport]?

    var decryptedReports: [FindMyLocationReport]?

    func hash(into hasher: inout Hasher) {
        hasher.combine(deviceId)
    }

    static func == (lhs: FindMyDevice, rhs: FindMyDevice) -> Bool {
        lhs.deviceId == rhs.deviceId
    }
}

struct FindMyKey: Codable {
    internal init(advertisedKey: Data, hashedKey: Data, privateKey: Data, startTime: Date?, duration: Double?, pu: Data?, yCoordinate: Data?, fullKey: Data?) {
        self.advertisedKey = advertisedKey
        self.hashedKey = hashedKey
        // The private key should only be 28 bytes long. If a 85 bytes full private public key is entered we truncate it here
        if privateKey.count == 85 {
            self.privateKey = privateKey.subdata(in: 57..<privateKey.endIndex)
        } else {
            self.privateKey = privateKey
        }

        self.startTime = startTime
        self.duration = duration
        self.pu = pu
        self.yCoordinate = yCoordinate
        self.fullKey = fullKey
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.advertisedKey = try container.decode(Data.self, forKey: .advertisedKey)
        self.hashedKey = try container.decode(Data.self, forKey: .hashedKey)
        let privateKey = try container.decode(Data.self, forKey: .privateKey)
        if privateKey.count == 85 {
            self.privateKey = privateKey.subdata(in: 57..<privateKey.endIndex)
        } else {
            self.privateKey = privateKey
        }

        self.startTime = try? container.decode(Date.self, forKey: .startTime)
        self.duration = try? container.decode(Double.self, forKey: .duration)
        self.pu = try? container.decode(Data.self, forKey: .pu)
        self.yCoordinate = try? container.decode(Data.self, forKey: .yCoordinate)
        self.fullKey = try? container.decode(Data.self, forKey: .fullKey)
    }

    /// The advertising key
    let advertisedKey: Data
    /// Hashed advertisement key using SHA256
    let hashedKey: Data
    /// The private key from which the advertisement keys can be derived
    let privateKey: Data
    /// When this key was used to send out BLE advertisements
    let startTime: Date?
    /// Duration from start time how long the key has been used to send out BLE advertisements
    let duration: Double?
    /// ?
    let pu: Data?

    /// As exported from Big Sur
    let yCoordinate: Data?
    /// As exported from BigSur
    let fullKey: Data?
}

struct FindMyReportResults: Codable {
    let results: [FindMyReport]
}

struct FindMyReport: Codable {
    let datePublished: Date
    let payload: Data
    let id: String
    let statusCode: Int

    let confidence: UInt8
    let timestamp: Date

    enum CodingKeys: CodingKey {
        case datePublished
        case payload
        case id
        case statusCode
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        let dateTimestamp = try values.decode(Double.self, forKey: .datePublished)
        // Convert from milis to time interval
        let dP = Date(timeIntervalSince1970: dateTimestamp/1000)
        let df = DateFormatter()
        df.dateFormat = "YYYY-MM-dd"

        if dP < df.date(from: "2020-01-01")! {
            self.datePublished = Date(timeIntervalSince1970: dateTimestamp)
        } else {
            self.datePublished = dP
        }

        self.statusCode = try values.decode(Int.self, forKey: .statusCode)
        let payloadBase64 = try values.decode(String.self, forKey: .payload)

        guard let payload = Data(base64Encoded: payloadBase64) else {
            throw DecodingError.dataCorruptedError(forKey: CodingKeys.payload, in: values, debugDescription: "")
        }
        self.payload = payload

        var timestampData = payload.subdata(in: 0..<4)
        let timestamp: Int32 = withUnsafeBytes(of: &timestampData) { (pointer) -> Int32 in
            // Convert the endianness
            pointer.load(as: Int32.self).bigEndian
        }

        // It's a cocoa time stamp (counting from 2001)
        self.timestamp = Date(timeIntervalSinceReferenceDate: TimeInterval(timestamp))
        self.confidence = payload[4]

        self.id = try values.decode(String.self, forKey: .id)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(self.datePublished.timeIntervalSince1970 * 1000, forKey: .datePublished)
        try container.encode(self.payload.base64EncodedString(), forKey: .payload)
        try container.encode(self.id, forKey: .id)
        try container.encode(self.statusCode, forKey: .statusCode)
    }
}

struct FindMyLocationReport: Codable {
    let latitude: Double
    let longitude: Double
    let accuracy: UInt8
    let datePublished: Date
    let timestamp: Date?
    let confidence: UInt8?

    var location: CLLocation {
        return CLLocation(latitude: latitude, longitude: longitude)
    }

    init(lat: Double, lng: Double, acc: UInt8, dP: Date, t: Date, c: UInt8) {
        self.latitude = lat
        self.longitude = lng
        self.accuracy = acc
        self.datePublished = dP
        self.timestamp = t
        self.confidence = c
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)

        self.latitude = try values.decode(Double.self, forKey: .latitude)
        self.longitude = try values.decode(Double.self, forKey: .longitude)

        do {
            let uAcc = try values.decode(UInt8.self, forKey: .accuracy)
            self.accuracy = uAcc
        } catch {
            let iAcc = try values.decode(Int8.self, forKey: .accuracy)
            self.accuracy = UInt8(bitPattern: iAcc)
        }

        self.datePublished = try values.decode(Date.self, forKey: .datePublished)
        self.timestamp = try? values.decode(Date.self, forKey: .timestamp)
        self.confidence = try? values.decode(UInt8.self, forKey: .confidence)
    }

}

enum FindMyError: Error {
    case decryptionError(description: String)
}
