import ExpoModulesCore
import DeviceCheck
import CryptoKit

public class IntegrityModule: Module {
  private let service = DCAppAttestService.shared

  public func definition() -> ModuleDefinition {
    Name("ExpoAppIntegrity")

    Constant("isSupported") {
      return service.isSupported
    }

    AsyncFunction("generateKeyAsync") {
      do {
        return try await service.generateKey()
      } catch let error {
        throw handleIntegrityCheckError(error)
      }
    }

    AsyncFunction("attestKeyAsync") { (key: String, challenge: String) in
      let data = Data(challenge.utf8)
      let clientDataHash = Data(SHA256.hash(data: data))

      do {
        let result = try await service.attestKey(key, clientDataHash: clientDataHash)
        return result.base64EncodedString()
      } catch let error {
        throw handleIntegrityCheckError(error)
      }
    }

    AsyncFunction("generateAssertionAsync") { (key: String, challenge: String) -> String in
      let data = Data(challenge.utf8)
      let clientDataHash = Data(SHA256.hash(data: data))
      do {
        let result = try await service.generateAssertion(key, clientDataHash: clientDataHash)
        return result.base64EncodedString()
      } catch let error {
        throw handleIntegrityCheckError(error)
      }
    }
  }

  // https://developer.apple.com/documentation/devicecheck/dcerror-swift.struct
  private func handleIntegrityCheckError(_ error: Error) -> Exception {
    if let error = error as? DCError {
      switch error.code {
      case .featureUnsupported:
        return IntegrityException("This feature is not supported on this device", code: IntegrityErrorCodes.featureUnsupported)
      case .invalidInput:
        return IntegrityException("Invalid input provided", code: IntegrityErrorCodes.invalidInput)
      case .invalidKey:
        return IntegrityException("Invalid key provided", code: IntegrityErrorCodes.invalidKey)
      case .serverUnavailable:
        return IntegrityException("Server unavailable", code: IntegrityErrorCodes.serverUnavailable)
      case .unknownSystemFailure:
        return IntegrityException("Unknown system failure", code: IntegrityErrorCodes.systemFailure)
      @unknown default:
        return IntegrityException("Unknown error", code: IntegrityErrorCodes.unknown)
      }
    }

    return IntegrityException("Unknown error", code: IntegrityErrorCodes.unknown)
  }
}
