import Foundation

#if canImport(Network)
import Network

/// A single-shot localhost HTTP listener that catches an OAuth redirect during a browser sign-in.
///
/// Nucleic advertises `http://localhost:<port><path>` as the `redirect_uri`, opens the consent
/// page, and the browser is redirected here with `?code=…&state=…` the moment the user approves.
/// This is what makes sign-in feel Nucleic-mediated: no code to copy, no terminal. It answers
/// exactly one request with a tiny "you can close this tab" page, then tears down.
///
/// Claude binds an *ephemeral* loopback port (`start()`); Codex requires a *fixed* one — its public
/// OAuth client only allows `http://localhost:1455/auth/callback` — so `start(port:path:)` takes the
/// concrete port. A failed bind returns nil, and the caller reports it rather than hanging.
final class OAuthLoopback: @unchecked Sendable {
    /// The captured redirect parameters.
    struct Callback: Sendable {
        let code: String
        let state: String
    }

    private let listener: NWListener
    private let queue = DispatchQueue(label: "xyz.blakeslee.nucleic.oauth.loopback")
    private let lock = NSLock()
    private var continuation: CheckedContinuation<Callback, Error>?
    private var pending: Callback?
    private var finished = false

    /// The loopback URL to hand the provider as `redirect_uri`.
    let redirectURI: String

    enum LoopbackError: Error { case cancelled, timedOut }

    private init(listener: NWListener, port: UInt16, path: String) {
        self.listener = listener
        self.redirectURI = "http://localhost:\(port)\(path)"
    }

    /// Bind a loopback port and begin listening. Pass `port` to require a specific one (Codex's
    /// fixed 1455); omit it for an ephemeral port (Claude). `path` is the redirect path advertised
    /// in `redirectURI` — the request path itself isn't matched, only its `code`/`state` query. The
    /// connection handler is wired before `start`, so a redirect that races ahead of
    /// ``waitForCallback`` is buffered rather than lost. Returns nil when the OS won't grant the port
    /// (e.g. 1455 already held by a running `codex login`).
    static func start(port desiredPort: UInt16? = nil, path: String = "/callback") -> OAuthLoopback? {
        let params = NWParameters.tcp
        // Bind loopback only — this port must never be reachable off-box.
        let endpointPort: NWEndpoint.Port = desiredPort.flatMap { NWEndpoint.Port(rawValue: $0) } ?? .any
        params.requiredLocalEndpoint = NWEndpoint.hostPort(host: "127.0.0.1", port: endpointPort)
        params.allowLocalEndpointReuse = true
        guard let listener = try? NWListener(using: params) else { return nil }

        let ready = DispatchSemaphore(value: 0)
        let box = PortBox()
        listener.stateUpdateHandler = { state in
            switch state {
            case .ready:
                box.set(listener.port?.rawValue)
                ready.signal()
            case .failed, .cancelled:
                ready.signal()
            default:
                break
            }
        }

        // Hold the connection handler in a box so it can reference the instance created after the
        // port is known; connections can't arrive until the browser is redirected, well after start.
        let handlerBox = ConnectionHandlerBox()
        listener.newConnectionHandler = { connection in handlerBox.handle(connection) }

        let queue = DispatchQueue(label: "xyz.blakeslee.nucleic.oauth.loopback")
        listener.start(queue: queue)
        // A loopback bind is immediate; cap the wait so a wedged listener can't hang sign-in.
        guard ready.wait(timeout: .now() + 2) == .success, let port = box.value else {
            listener.cancel()
            return nil
        }
        let loopback = OAuthLoopback(listener: listener, port: port, path: path)
        handlerBox.owner = loopback
        return loopback
    }

    /// Await the redirect (or `timeout` seconds, whichever comes first). Resolves exactly once.
    func waitForCallback(timeout: TimeInterval) async throws -> Callback {
        let timeoutTask = Task { [weak self] in
            try? await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
            self?.fail(.timedOut)
        }
        defer { timeoutTask.cancel() }

        return try await withCheckedThrowingContinuation { continuation in
            lock.lock()
            if finished {
                lock.unlock()
                continuation.resume(throwing: LoopbackError.cancelled)
                return
            }
            if let pending {
                self.pending = nil
                finished = true
                lock.unlock()
                continuation.resume(returning: pending)
                return
            }
            self.continuation = continuation
            lock.unlock()
        }
    }

    /// Tear the listener down; safe to call more than once.
    func stop() {
        fail(.cancelled)
        listener.cancel()
    }

    // MARK: - Connection handling

    fileprivate func handle(_ connection: NWConnection) {
        connection.start(queue: queue)
        connection.receive(minimumIncompleteLength: 1, maximumLength: 64 * 1024) {
            [weak self] data, _, _, _ in
            guard let self else { connection.cancel(); return }
            let request = data.flatMap { String(data: $0, encoding: .utf8) } ?? ""
            let callback = Self.parseCallback(fromRequestLine: request)
            let response = Self.httpResponse(success: callback != nil)
            connection.send(content: response, completion: .contentProcessed { _ in
                connection.cancel()
            })
            if let callback { self.deliver(callback) }
        }
    }

    /// Extract `code`/`state` from the first line of an HTTP request
    /// (`GET /callback?code=…&state=… HTTP/1.1`). Returns nil for the favicon probe or any request
    /// without both parameters, so a browser's incidental hits don't resolve the wait early.
    static func parseCallback(fromRequestLine request: String) -> Callback? {
        guard let firstLine = request.split(whereSeparator: \.isNewline).first else { return nil }
        let fields = firstLine.split(separator: " ")
        guard fields.count >= 2 else { return nil }
        let target = String(fields[1])
        guard let components = URLComponents(string: "http://localhost\(target)"),
              let items = components.queryItems
        else { return nil }
        let code = items.first { $0.name == "code" }?.value
        let state = items.first { $0.name == "state" }?.value
        guard let code, !code.isEmpty, let state, !state.isEmpty else { return nil }
        return Callback(code: code, state: state)
    }

    private static func httpResponse(success: Bool) -> Data {
        let title = success ? "Signed in" : "Sign-in error"
        let message = success
            ? "Nucleic captured your sign-in. You can close this tab and return to Nucleic."
            : "Nucleic could not read the sign-in response. Return to Nucleic and try again."
        let html = """
        <!doctype html><html><head><meta charset="utf-8"><title>\(title)</title>
        <style>body{font:15px -apple-system,system-ui,sans-serif;background:#0b0b0d;color:#e6e6e6;\
        display:flex;min-height:100vh;align-items:center;justify-content:center;margin:0}\
        .card{max-width:26rem;padding:2rem;text-align:center;line-height:1.5}</style></head>
        <body><div class="card"><h2>\(title)</h2><p>\(message)</p></div></body></html>
        """
        let body = Data(html.utf8)
        let header = "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\n"
            + "Content-Length: \(body.count)\r\nConnection: close\r\n\r\n"
        return Data(header.utf8) + body
    }

    // MARK: - One-shot resolution

    private func deliver(_ callback: Callback) {
        lock.lock()
        guard !finished else { lock.unlock(); return }
        if let continuation = self.continuation {
            finished = true
            self.continuation = nil
            lock.unlock()
            continuation.resume(returning: callback)
        } else {
            // Arrived before waitForCallback — buffer it.
            pending = callback
            lock.unlock()
        }
    }

    private func fail(_ error: LoopbackError) {
        lock.lock()
        guard !finished else { lock.unlock(); return }
        finished = true
        let continuation = self.continuation
        self.continuation = nil
        lock.unlock()
        continuation?.resume(throwing: error)
    }
}

/// Bridges the listener's `newConnectionHandler` (set before the instance exists) to the instance
/// once its port is known.
private final class ConnectionHandlerBox: @unchecked Sendable {
    private let lock = NSLock()
    private var _owner: OAuthLoopback?
    var owner: OAuthLoopback? {
        get { lock.lock(); defer { lock.unlock() }; return _owner }
        set { lock.lock(); _owner = newValue; lock.unlock() }
    }
    func handle(_ connection: NWConnection) {
        if let owner { owner.handle(connection) } else { connection.cancel() }
    }
}

/// Tiny lock-guarded box so the `stateUpdateHandler` (dispatch queue) can hand the resolved port
/// back to `start()` (which is waiting on a semaphore).
private final class PortBox: @unchecked Sendable {
    private let lock = NSLock()
    private var _value: UInt16?
    var value: UInt16? { lock.lock(); defer { lock.unlock() }; return _value }
    func set(_ v: UInt16?) { lock.lock(); _value = v; lock.unlock() }
}
#endif
