import CryptoKit
import Foundation

private struct DirectGatewayPushRegistrationPayload: Encodable {
    var transport: String = PushTransportMode.direct.rawValue
    var token: String
    var topic: String
    var environment: String
}

private struct RelayGatewayPushRegistrationPayload: Encodable {
    var transport: String = PushTransportMode.relay.rawValue
    var relayHandle: String
    var sendGrant: String
    var gatewayDeviceId: String
    var installationId: String
    var topic: String
    var environment: String
    var distribution: String
    var tokenDebugSuffix: String?
}

struct PushRelayGatewayIdentity: Codable {
    var deviceId: String
    var publicKey: String
}

actor PushRegistrationManager {
    private let buildConfig: PushBuildConfig
    private let relayClient: PushRelayClient?

    var usesRelayTransport: Bool {
        self.buildConfig.transport == .relay
    }

    init(buildConfig: PushBuildConfig = .current) {
        self.buildConfig = buildConfig
        self.relayClient = buildConfig.relayBaseURL.map { PushRelayClient(baseURL: $0) }
    }

    func makeGatewayRegistrationPayload(
        apnsTokenHex: String,
        topic: String,
        gatewayIdentity: PushRelayGatewayIdentity?)
    async throws -> String {
        switch self.buildConfig.transport {
        case .direct:
            return try Self.encodePayload(
                DirectGatewayPushRegistrationPayload(
                    token: apnsTokenHex,
                    topic: topic,
                    environment: self.buildConfig.apnsEnvironment.rawValue))
        case .relay:
            guard let gatewayIdentity else {
                throw PushRelayError.relayMisconfigured("Missing gateway identity for relay registration")
            }
            return try await self.makeRelayPayload(
                apnsTokenHex: apnsTokenHex,
                topic: topic,
                gatewayIdentity: gatewayIdentity)
        }
    }

    private func makeRelayPayload(
        apnsTokenHex: String,
        topic: String,
        gatewayIdentity: PushRelayGatewayIdentity)
    async throws -> String {
        guard self.buildConfig.distribution == .official else {
            throw PushRelayError.relayMisconfigured(
                "Relay transport requires OpenClawPushDistribution=official")
        }
        guard self.buildConfig.apnsEnvironment == .production else {
            throw PushRelayError.relayMisconfigured(
                "Relay transport requires OpenClawPushAPNsEnvironment=production")
        }
        guard let relayClient = self.relayClient else {
            throw PushRelayError.relayBaseURLMissing
        }
        guard let bundleId = Bundle.main.bundleIdentifier?.trimmingCharacters(in: .whitespacesAndNewlines),
              !bundleId.isEmpty
        else {
            throw PushRelayError.relayMisconfigured("Missing bundle identifier for relay registration")
        }
        guard let installationId = GatewaySettingsStore.loadStableInstanceID()?
            .trimmingCharacters(in: .whitespacesAndNewlines),
              !installationId.isEmpty
        else {
            throw PushRelayError.relayMisconfigured("Missing stable installation ID for relay registration")
        }

        let tokenHashHex = Self.sha256Hex(apnsTokenHex)
        let relayOrigin = relayClient.normalizedBaseURLString
        if let stored = PushRelayRegistrationStore.loadRegistrationState(),
           stored.installationId == installationId,
           stored.gatewayDeviceId == gatewayIdentity.deviceId,
           stored.relayOrigin == relayOrigin,
           stored.lastAPNsTokenHashHex == tokenHashHex,
           !Self.isExpired(stored.relayHandleExpiresAtMs)
        {
            return try Self.encodePayload(
                RelayGatewayPushRegistrationPayload(
                    relayHandle: stored.relayHandle,
                    sendGrant: stored.sendGrant,
                    gatewayDeviceId: gatewayIdentity.deviceId,
                    installationId: installationId,
                    topic: topic,
                    environment: self.buildConfig.apnsEnvironment.rawValue,
                    distribution: self.buildConfig.distribution.rawValue,
                    tokenDebugSuffix: stored.tokenDebugSuffix))
        }

        let response = try await relayClient.register(
            installationId: installationId,
            bundleId: bundleId,
            appVersion: DeviceInfoHelper.appVersion(),
            environment: self.buildConfig.apnsEnvironment,
            distribution: self.buildConfig.distribution,
            apnsTokenHex: apnsTokenHex,
            gatewayIdentity: gatewayIdentity)
        let registrationState = PushRelayRegistrationStore.RegistrationState(
            relayHandle: response.relayHandle,
            sendGrant: response.sendGrant,
            relayOrigin: relayOrigin,
            gatewayDeviceId: gatewayIdentity.deviceId,
            relayHandleExpiresAtMs: response.expiresAtMs,
            tokenDebugSuffix: Self.normalizeTokenSuffix(response.tokenSuffix),
            lastAPNsTokenHashHex: tokenHashHex,
            installationId: installationId,
            lastTransport: self.buildConfig.transport.rawValue)
        _ = PushRelayRegistrationStore.saveRegistrationState(registrationState)
        return try Self.encodePayload(
            RelayGatewayPushRegistrationPayload(
                relayHandle: response.relayHandle,
                sendGrant: response.sendGrant,
                gatewayDeviceId: gatewayIdentity.deviceId,
                installationId: installationId,
                topic: topic,
                environment: self.buildConfig.apnsEnvironment.rawValue,
                distribution: self.buildConfig.distribution.rawValue,
                tokenDebugSuffix: registrationState.tokenDebugSuffix))
    }

    private static func isExpired(_ expiresAtMs: Int64?) -> Bool {
        guard let expiresAtMs else { return true }
        let nowMs = Int64(Date().timeIntervalSince1970 * 1000)
        // Refresh shortly before expiry so reconnect-path republishes a live handle.
        return expiresAtMs <= nowMs + 60_000
    }

    private static func sha256Hex(_ value: String) -> String {
        let digest = SHA256.hash(data: Data(value.utf8))
        return digest.map { String(format: "%02x", $0) }.joined()
    }

    private static func normalizeTokenSuffix(_ value: String?) -> String? {
        guard let value else { return nil }
        let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
        return trimmed.isEmpty ? nil : trimmed
    }

    private static func encodePayload(_ payload: some Encodable) throws -> String {
        let data = try JSONEncoder().encode(payload)
        guard let json = String(data: data, encoding: .utf8) else {
            throw PushRelayError.relayMisconfigured("Failed to encode push registration payload as UTF-8")
        }
        return json
    }
}
