import Foundation
import CommonCrypto

enum CryptoAlgorithm {
    case MD5, SHA1, SHA224, SHA256, SHA384, SHA512

    var HMACAlgorithm: CCHmacAlgorithm {
        var result: Int = 0
        switch self {
        case .MD5:      result = kCCHmacAlgMD5
        case .SHA1:     result = kCCHmacAlgSHA1
        case .SHA224:   result = kCCHmacAlgSHA224
        case .SHA256:   result = kCCHmacAlgSHA256
        case .SHA384:   result = kCCHmacAlgSHA384
        case .SHA512:   result = kCCHmacAlgSHA512
        }
        return CCHmacAlgorithm(result)
    }

    var digestLength: Int {
        var result: Int32 = 0
        switch self {
        case .MD5:      result = CC_MD5_DIGEST_LENGTH
        case .SHA1:     result = CC_SHA1_DIGEST_LENGTH
        case .SHA224:   result = CC_SHA224_DIGEST_LENGTH
        case .SHA256:   result = CC_SHA256_DIGEST_LENGTH
        case .SHA384:   result = CC_SHA384_DIGEST_LENGTH
        case .SHA512:   result = CC_SHA512_DIGEST_LENGTH
        }
        return Int(result)
    }
}

extension String {

    func hmac(algorithm: CryptoAlgorithm, key: String) -> String {
        let str = self.cString(using: String.Encoding.utf8)
        let strLen = Int(self.lengthOfBytes(using: String.Encoding.utf8))
        let digestLen = algorithm.digestLength
        let result = UnsafeMutablePointer<UInt8>.allocate(capacity: digestLen)
        let keyStr = key.cString(using: String.Encoding.utf8)
        let keyLen = Int(key.lengthOfBytes(using: String.Encoding.utf8))

        CCHmac(algorithm.HMACAlgorithm, keyStr!, keyLen, str!, strLen, result)

        let array  = Array(UnsafeBufferPointer(start: result, count: digestLen))
        let encodedArray = Base64FS.encode(data: array)
        let string = String(data: Data(encodedArray), encoding: .utf8)!.trimmingCharacters(in: CharacterSet(charactersIn: "="))
        
        result.deallocate()

        return string
    }

    private func stringFromResult(result: UnsafeMutablePointer<CUnsignedChar>, length: Int) -> String {
        let hash = NSMutableString()
        for i in 0..<length {
            hash.appendFormat("%02x", result[i])
        }
        return String(hash).lowercased()
    }
}

protocol JwtPayload: Codable {
    var exp: Int { get }
}

extension JwtPayload {
    var exp: Int { 0 }
}

struct EmptyPayload: JwtPayload { }

struct FirebasePayload: JwtPayload {
    var email: String
    var name: String
}

struct NumberPayload: JwtPayload {
    var plateNumber: String
}

class JWT<T> where T: JwtPayload {
    private var jwt: String
    public let payload: T
    public var expired: Bool {
        return TimeInterval(self.payload.exp) < Date().timeIntervalSince1970
    }
    
    init?(string: String) {
        self.jwt = string
        
        let parts = string.split(separator: ".")
        if parts.count == 3 {
            var payloadStr = String(parts[1])
            if (payloadStr.count % 4 != 0) {
                payloadStr += String(repeating: "=", count: (4 - payloadStr.count % 4))
            }
            if let json = Data(base64Encoded: payloadStr, options: [.ignoreUnknownCharacters]) {
                do {
                    self.payload = try JSONDecoder().decode(T.self, from: json)
                } catch {
                    print(error)
                    return nil
                }
            } else {
                return nil
            }
        } else {
            return nil
        }
    }
    
    static func generate(for plateNumber: String) throws -> String {
        let header = #"{ "typ": "JWT", "alg": "HS256" }"#
        let bodyDict: [String: Any] = [
            "iat": Date().timeIntervalSince1970,
            "exp": Date(timeIntervalSinceNow: 24*60*60).timeIntervalSince1970,
            "plateNumber": plateNumber
        ]
        let bodyData = try JSONSerialization.data(withJSONObject: bodyDict, options: [])
        
        guard let body = String(data: bodyData, encoding: .utf8) else {
            throw CocoaError.error("Error", reason: "Failed to generate JWT for sharing report via link")
        }
        let twoParts = Base64FS.encodeString(str: header).trimmingCharacters(in: CharacterSet(charactersIn: "=")) + "." + Base64FS.encodeString(str: body).trimmingCharacters(in: CharacterSet(charactersIn: "="))
        let signature = twoParts.hmac(algorithm: .SHA256, key: Constants.reportLinkTokenSecret)
        return twoParts + "." + signature
    }
}
