import ClientTLS
import Foundation
import WireProtocol

/// T-iOS-15 · Production `HTTPTransport` (the WireProtocol seam's doc reserves
/// the URLSession wrapper for the production side; no package owns it, so the
/// assembly layer provides it). Deliberately logic-free: `APIClient` builds
/// every request — including the Origin-iff-G rule (plan §3.4 铁律) — and this
/// type only performs the exchange. Adding ANY header/URL logic here would
/// bypass that single audited point (review CRITICAL).
struct URLSessionHTTPTransport: HTTPTransport {
    private let session: URLSession
    /// Strong reference to the mTLS delegate. URLSession retains its delegate
    /// until invalidated, but this ephemeral session is never explicitly
    /// invalidated, so holding it here documents the ownership and keeps the
    /// transport a self-contained value.
    private let tlsDelegate: LazyClientTLSSessionDelegate

    /// Fixed-identity convenience (snapshot callers / tests): wraps a constant
    /// provider, so behaviour is identical to capturing the identity directly.
    init(identity: ClientIdentity? = nil) {
        self.init(identityProvider: { identity })
    }

    /// C-iOS-2 (MEDIUM no-relaunch fix) · The session's mTLS delegate resolves
    /// the device identity LAZILY, per client-certificate challenge, from
    /// `identityProvider`. A `ClientCertificate` challenge from a tunneled host
    /// is thus answered with whatever cert is installed AT CHALLENGE TIME — so a
    /// certificate imported mid-run is presented on the NEXT TLS handshake
    /// without an app relaunch (a snapshot captured here would stay stale). The
    /// session itself stays a single long-lived value (no per-request churn).
    /// Local http/https hosts never issue that challenge, so the provider is
    /// never even consulted for them (and a `nil` result is inert regardless).
    ///
    /// EPHEMERAL, not `.shared` (T-iOS-19 finding): RO GET bodies include
    /// `/live-sessions/:id/preview` — raw terminal ring-buffer bytes that may
    /// contain printed secrets — and `.shared`'s default URLCache writes
    /// responses to disk. Ephemeral keeps them memory-only, matching the WS
    /// transport and the privacy-shade posture.
    init(identityProvider: @escaping @Sendable () -> ClientIdentity?) {
        let delegate = LazyClientTLSSessionDelegate(identityProvider: identityProvider)
        self.tlsDelegate = delegate
        self.session = URLSession(
            configuration: .ephemeral, delegate: delegate, delegateQueue: nil
        )
    }

    func send(_ request: URLRequest) async throws -> (Data, HTTPURLResponse) {
        let (data, response) = try await session.data(for: request)
        guard let httpResponse = response as? HTTPURLResponse else {
            // http(s)-only endpoints (HostEndpoint validates) always produce
            // an HTTPURLResponse; anything else is a transport-level anomaly.
            throw URLError(.badServerResponse)
        }
        return (data, httpResponse)
    }
}

/// C-iOS-2 (MEDIUM no-relaunch fix) · Session-level mTLS delegate that resolves
/// the device identity FRESH per client-certificate challenge — the App-layer
/// twin of ClientTLS's fixed-identity `ClientTLSSessionDelegate` (which captures
/// the identity once). The provider is only consulted for a `ClientCertificate`
/// challenge, so a `ServerTrust` challenge never triggers a keychain read; the
/// pure decision itself is delegated to the shared `MutualTLSChallengeResponder`
/// (single truth table, unit-tested in ClientTLS).
///
/// `@unchecked Sendable`: URLSession retains its delegate and invokes it from
/// arbitrary queues; every stored field is an immutable `let` over a `@Sendable`
/// value (the provider is `@Sendable`, the responder is stateless).
private final class LazyClientTLSSessionDelegate:
    NSObject, URLSessionDelegate, @unchecked Sendable {
    private let identityProvider: @Sendable () -> ClientIdentity?
    private let responder = MutualTLSChallengeResponder()

    init(identityProvider: @escaping @Sendable () -> ClientIdentity?) {
        self.identityProvider = identityProvider
        super.init()
    }

    func urlSession(
        _ session: URLSession,
        didReceive challenge: URLAuthenticationChallenge,
        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
    ) {
        // Only a client-certificate challenge needs the identity; resolving it
        // for a server-trust challenge would do a needless keychain read/import
        // on every HTTPS handshake.
        let isClientCert = challenge.protectionSpace.authenticationMethod
            == NSURLAuthenticationMethodClientCertificate
        let identity = isClientCert ? identityProvider() : nil
        let resolution = responder.resolve(challenge, identity: identity)
        completionHandler(resolution.disposition, resolution.credential)
    }
}
