import Foundation

/// B3 · The phone-track bootstrap: exchanges the operator's one-time credential
/// for a short-lived `device:enroll` bearer at the control-plane.
///
/// PINNED login contract (all pieces MUST agree — iOS / Android / server B1):
///
///   `POST /auth/login { "password": "<operator secret>" }`
///     → 201 { "enrollToken": "<jwt/capability>", "accountId": "<uuid>", "expiresIn": <seconds> }
///
/// The returned `enrollToken` is then presented as `Authorization: Bearer …` to
/// `DeviceEnrollmentClient.enroll`. This client is deliberately logic-free about
/// TLS and reuses the SAME `EnrollmentTransport` seam as `DeviceEnrollmentClient`,
/// so production injects `URLSessionHTTPTransport` and tests inject a stub.
///
/// Security posture:
/// - The password is validated at the boundary (non-empty) and sent ONCE; it is
///   never persisted, never logged, and no other field rides along in the body.
/// - The `enrollToken` is a secret bearer: it is never logged here either. Only
///   the transport (which sees it in a header) and the caller hold it, briefly.
public struct ControlPlaneLoginClient: Sendable {
    private let baseURL: URL
    private let transport: any EnrollmentTransport

    public init(baseURL: URL, transport: any EnrollmentTransport) {
        self.baseURL = baseURL
        self.transport = transport
    }

    /// Exchange the operator credential for a `device:enroll` bearer.
    ///
    /// - Throws: `ControlPlaneLoginError.emptyPassword` (before any network),
    ///   `.http(status:code:)` on a non-201 (401 = bad/missing credential),
    ///   `.malformedResponse` on a 201 body that does not decode.
    public func login(password: String) async throws -> LoginResult {
        // Boundary validation — fail fast, never send an empty credential.
        guard !password.isEmpty else { throw ControlPlaneLoginError.emptyPassword }

        guard let url = URL(string: "/auth/login", relativeTo: baseURL) else {
            throw ControlPlaneLoginError.malformedResponse
        }
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        // Password ONLY — no accountId / device fields leak from the client.
        request.httpBody = try JSONSerialization.data(withJSONObject: ["password": password])

        let (data, response) = try await transport.send(request)
        guard response.statusCode == 201 else {
            throw ControlPlaneLoginError.http(status: response.statusCode, code: errorCode(in: data))
        }
        guard let dto = try? JSONDecoder().decode(LoginResponseDTO.self, from: data) else {
            throw ControlPlaneLoginError.malformedResponse
        }
        return dto.toResult()
    }

    private func errorCode(in data: Data) -> String? {
        (try? JSONDecoder().decode(LoginErrorDTO.self, from: data))?.error
    }
}

// MARK: - Result & errors

/// The login exchange result. The `enrollToken` is a short-lived secret bearer;
/// hold it only as long as needed to drive `DeviceEnrollmentClient.enroll`.
public struct LoginResult: Equatable, Sendable {
    /// The `device:enroll` capability bearer to present to `/device/enroll`.
    public let enrollToken: String
    /// The authenticated account (the token `sub`); the device enrolls under a
    /// subdomain this account owns (server enforces ownership, deny-by-default).
    public let accountId: String
    /// Token lifetime in seconds (minutes-scale). Advisory: the enrollment must
    /// complete within this window or the bearer is re-minted via another login.
    public let expiresIn: Int

    public init(enrollToken: String, accountId: String, expiresIn: Int) {
        self.enrollToken = enrollToken
        self.accountId = accountId
        self.expiresIn = expiresIn
    }
}

public enum ControlPlaneLoginError: Error, Equatable, Sendable {
    /// The password was empty — rejected before any network call.
    case emptyPassword
    /// Non-success HTTP status with the server's uniform `{ error }` code, if any
    /// (401 = bad/missing credential, 429 = rate_limited).
    case http(status: Int, code: String?)
    /// A 2xx body that did not decode to the expected shape.
    case malformedResponse
}

// MARK: - Wire DTOs

private struct LoginResponseDTO: Decodable {
    let enrollToken: String
    let accountId: String
    let expiresIn: Int

    func toResult() -> LoginResult {
        LoginResult(enrollToken: enrollToken, accountId: accountId, expiresIn: expiresIn)
    }
}

private struct LoginErrorDTO: Decodable {
    let error: String
}
