import Foundation
import WireProtocol

/// APNs device-token registration (T-iOS-38 · frozen wire shape, orchestrator
/// ruling — mirrors the `POST /push/subscribe` conventions, src/server.ts:461-480):
///
/// - `POST /push/apns-token`   body `{"token":"<64-160 hex chars, lowercase>"}`
///   → **204** (idempotent upsert)
/// - `DELETE /push/apns-token` body 同形 → **204** (idempotent; unknown token
///   still 204)
/// - Both are **G endpoints**: `Origin` guard (`requireAllowedOrigin`) → 403.
/// - Server limits: 400 invalid token/shape · **429 rate limit 5/min/IP** ·
///   `express.json` body limit **8 KB** (SUBSCRIBE_RATE_MAX, src/server.ts:73).
///
/// The hex rule is enforced CLIENT-SIDE too (validate at the boundary, plan §4):
/// uppercase input is lowercase-normalized, anything not 64–160 hex chars is
/// rejected with `.invalidApnsToken` BEFORE any network I/O.
enum ApnsTokenRule {
    /// APNs device tokens are ≥ 32 bytes (64 hex chars) today; the frozen
    /// shape allows up to 160 hex chars of forward headroom.
    static let minHexLength = 64
    static let maxHexLength = 160
    /// ASCII-only hex alphabet — deliberately NOT `Character.isHexDigit`
    /// (which also accepts fullwidth digit forms the server regex rejects).
    private static let hexDigits = Set("0123456789abcdef")

    /// Lowercase-normalize and validate; nil = not a valid wire token.
    static func normalize(_ raw: String) -> String? {
        let lowered = raw.lowercased()
        guard (minHexLength...maxHexLength).contains(lowered.count),
              lowered.allSatisfy({ hexDigits.contains($0) })
        else { return nil }
        return lowered
    }
}

extension Endpoints {
    static let apnsTokenPath = "/push/apns-token"

    /// `POST /push/apns-token` — G. `normalized` MUST come from
    /// `ApnsTokenRule.normalize` (single validation point).
    static func registerApnsToken(normalized: String) throws -> APIRoute {
        APIRoute(
            method: .post, path: apnsTokenPath, originPolicy: .guarded,
            body: try JSONEncoder().encode(ApnsTokenBody(token: normalized))
        )
    }

    /// `DELETE /push/apns-token` — G, idempotent (unknown token still 204).
    static func unregisterApnsToken(normalized: String) throws -> APIRoute {
        APIRoute(
            method: .delete, path: apnsTokenPath, originPolicy: .guarded,
            body: try JSONEncoder().encode(ApnsTokenBody(token: normalized))
        )
    }

    private struct ApnsTokenBody: Encodable {
        let token: String
    }
}

extension APIClient {
    /// Register this device's APNs token (idempotent upsert → 204).
    /// G endpoint — Origin byte-equal to `endpoint.originHeader`.
    /// Server limits (documented, enforced server-side): body ≤ 8 KB,
    /// ≤ 5 requests/min/IP → `.rateLimited`. Invalid hex is rejected
    /// client-side (`.invalidApnsToken`) before any network I/O.
    public func registerApnsToken(_ token: String) async throws {
        try await sendApnsToken(token, build: Endpoints.registerApnsToken(normalized:))
    }

    /// Unregister a previously registered token (idempotent → 204 even for an
    /// unknown token). Same G guard and limits as `registerApnsToken`.
    public func unregisterApnsToken(_ token: String) async throws {
        try await sendApnsToken(token, build: Endpoints.unregisterApnsToken(normalized:))
    }

    private func sendApnsToken(
        _ token: String,
        build: (String) throws -> APIRoute
    ) async throws {
        guard let normalized = ApnsTokenRule.normalize(token) else {
            throw APIClientError.invalidApnsToken
        }
        let (_, response) = try await perform(try build(normalized))
        switch response.statusCode {
        case HTTPStatus.noContent:
            return
        case HTTPStatus.badRequest:
            throw APIClientError.invalidApnsToken
        case HTTPStatus.forbidden:
            throw APIClientError.forbidden
        case HTTPStatus.tooManyRequests:
            throw APIClientError.rateLimited
        default:
            throw APIClientError.unexpectedStatus(response.statusCode)
        }
    }
}
