import Foundation

// Remote agent sign-in (docs/REMOTE_AGENT_LOGIN.md): a device asks a host — a Mac or a cloud
// runner — to run one of the OAuth flows Nucleic already brokers (Claude, Codex). The host owns
// everything secret (PKCE verifier, token exchange, persistence); the device contributes the two
// things only it has: a browser next to the user, and — because the browser runs *on the device*,
// where `localhost` resolves — the loopback listener the vendor's redirect lands on. The captured
// authorization code rides back over the E2EE channel; it is single-use and worthless without the
// PKCE verifier, which never leaves the host.

/// Which agent provider a remote sign-in targets. Raw-string-backed so new providers ride
/// through old peers untouched (the same forward-compat shape as `CredentialKind`).
public struct AgentLoginProvider: RawRepresentable, Sendable, Codable, Equatable, Hashable {
    public let rawValue: String
    public init(rawValue: String) { self.rawValue = rawValue }

    public static let claude = AgentLoginProvider(rawValue: "claude")
    public static let codex = AgentLoginProvider(rawValue: "codex")

    /// The provider that brokers sign-in for `backend` — nil when that backend's CLI owns its
    /// own auth (Grok/ACP agents). Drives the in-chat "Sign in" affordance on auth failures.
    public static func forBackend(_ backend: BackendID) -> AgentLoginProvider? {
        switch backend {
        case .claudeCode: .claude
        case .codex, .codexExec: .codex
        default: nil
        }
    }
}

/// How the initiating device must capture the vendor's redirect for one sign-in attempt.
/// Mode is raw-string-backed (with the payload fields alongside) so a future capture shape
/// decodes on old devices without throwing — the device treats an unknown mode as a failed
/// attempt rather than an undecodable frame.
public struct AgentLoginCapture: Sendable, Codable, Equatable {
    public let mode: String
    /// `.loopback` only: the loopback port the device must be listening on (it told the host
    /// what it bound in `WireAgentLoginBegin.boundLoopbackPort`; the host echoes the one the
    /// authorize URL was built against).
    public let port: UInt16?
    /// `.loopback` only: the redirect path (e.g. "/callback", Codex's "/auth/callback").
    public let path: String?

    public static let loopbackMode = "loopback"
    public static let pasteCodeMode = "pasteCode"

    /// Bind 127.0.0.1:`port`, wait for `GET path?code=…&state=…`, forward via
    /// `ClientMsg.agentLoginCallback`.
    public static func loopback(port: UInt16, path: String) -> AgentLoginCapture {
        AgentLoginCapture(mode: loopbackMode, port: port, path: path)
    }
    /// The consent page renders `code#state` for the user to copy-paste back into the app
    /// (Claude's console-redirect fallback).
    public static var pasteCode: AgentLoginCapture {
        AgentLoginCapture(mode: pasteCodeMode, port: nil, path: nil)
    }

    public init(mode: String, port: UInt16? = nil, path: String? = nil) {
        self.mode = mode
        self.port = port
        self.path = path
    }

    private enum CodingKeys: String, CodingKey { case mode, port, path }
    public init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        self.mode = try c.decode(String.self, forKey: .mode)
        self.port = try c.decodeIfPresent(UInt16.self, forKey: .port)
        self.path = try c.decodeIfPresent(String.self, forKey: .path)
    }
}

/// A device asking a host to start one agent sign-in (`ClientMsg.agentLoginBegin`, `.control`
/// scope, gated on `WireCapabilities.canBrokerAgentLogin`). `requestID` is minted by the device
/// and correlates the whole attempt (challenge → callback → result); one-shot — a host answers a
/// repeated id with the attempt already in flight failing fast, never a second flow.
public struct WireAgentLoginBegin: Sendable, Codable, Equatable {
    public let requestID: String
    public let provider: AgentLoginProvider
    /// The loopback port the device already bound (127.0.0.1) for redirect capture, if any.
    /// Codex requires exactly 1455 (the vendor client's fixed redirect); Claude accepts any
    /// port. Nil ⇒ the device couldn't (or didn't) bind — the host falls back to a paste-code
    /// capture where the provider supports one, else fails the attempt.
    public let boundLoopbackPort: UInt16?

    public init(requestID: String, provider: AgentLoginProvider, boundLoopbackPort: UInt16? = nil) {
        self.requestID = requestID
        self.provider = provider
        self.boundLoopbackPort = boundLoopbackPort
    }

    private enum CodingKeys: String, CodingKey { case requestID, provider, boundLoopbackPort }
    public init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        self.requestID = try c.decode(String.self, forKey: .requestID)
        self.provider = try c.decode(AgentLoginProvider.self, forKey: .provider)
        self.boundLoopbackPort = try c.decodeIfPresent(UInt16.self, forKey: .boundLoopbackPort)
    }
}

/// The host's consent challenge (`HostMsg.agentLoginChallenge`): open `authorizeURL` in a
/// browser and capture the redirect as `capture` says. Sent only to the initiating connection.
public struct WireAgentLoginChallenge: Sendable, Codable, Equatable {
    public let requestID: String
    public let authorizeURL: String
    public let capture: AgentLoginCapture
    /// When the host expires the attempt's PKCE state (and pushes a failed result) if no
    /// callback arrived — the device can show a countdown / stop listening after this.
    public let expiresAt: Date

    public init(requestID: String, authorizeURL: String, capture: AgentLoginCapture, expiresAt: Date) {
        self.requestID = requestID
        self.authorizeURL = authorizeURL
        self.capture = capture
        self.expiresAt = expiresAt
    }
}

/// The captured redirect (`ClientMsg.agentLoginCallback`): the authorization code — from the
/// device's loopback listener, or pasted by the user — plus the redirect's `state` where the
/// capture carried one (loopback; a pasted `code#state` blob rides whole in `code`, state nil).
/// The host verifies `state` against the attempt's PKCE state before exchanging.
public struct WireAgentLoginCallback: Sendable, Codable, Equatable {
    public let requestID: String
    public let code: String
    public let state: String?

    public init(requestID: String, code: String, state: String? = nil) {
        self.requestID = requestID
        self.code = code
        self.state = state
    }

    private enum CodingKeys: String, CodingKey { case requestID, code, state }
    public init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        self.requestID = try c.decode(String.self, forKey: .requestID)
        self.code = try c.decode(String.self, forKey: .code)
        self.state = try c.decodeIfPresent(String.self, forKey: .state)
    }
}

/// The attempt's definitive outcome (`HostMsg.agentLoginResult`), keyed by `requestID` — the
/// same success/failure shape as `WireProjectCreated`. Sent only to the initiator; every other
/// device learns the new auth state from the `agentAuthStatus` push that follows a success.
public struct WireAgentLoginResult: Sendable, Codable, Equatable {
    public let requestID: String
    /// Human-readable failure (cancelled, exchange failed, port busy, …); nil on success.
    public let error: String?
    /// Who is now signed in, where cheaply known (e.g. the ChatGPT account email) — lets the
    /// device confirm *which* account before the mesh converges on it.
    public let accountLabel: String?

    public init(requestID: String, error: String? = nil, accountLabel: String? = nil) {
        self.requestID = requestID
        self.error = error
        self.accountLabel = accountLabel
    }

    public var succeeded: Bool { error == nil }

    private enum CodingKeys: String, CodingKey { case requestID, error, accountLabel }
    public init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        self.requestID = try c.decode(String.self, forKey: .requestID)
        self.error = try c.decodeIfPresent(String.self, forKey: .error)
        self.accountLabel = try c.decodeIfPresent(String.self, forKey: .accountLabel)
    }
}

/// One provider's install + auth state on a host (`HostMsg.agentAuthStatus`, pushed post-hello
/// and on every change) — the wire projection of the Mac's Settings ▸ Agents list, and the
/// device's map of *which providers this host can broker a sign-in for* (`canBrokerLogin`
/// per-provider, so adding one later needs no new capability bit).
public struct WireProviderAuthStatus: Sendable, Codable, Equatable {
    public let provider: AgentLoginProvider
    /// User-facing provider name, e.g. "Claude Code".
    public let name: String
    public let installed: Bool
    public let authenticated: Bool
    /// How it's authenticated when it is: "oauth" | "apiKey"; nil when not (or unknown).
    public let method: String?
    /// Who's signed in, where cheaply known — surfaced before a re-login overwrites the
    /// mesh-wide credential (the multi-account honesty affordance).
    public let accountLabel: String?
    /// Whether this host can run the remote sign-in flow for this provider.
    public let canBrokerLogin: Bool

    public init(
        provider: AgentLoginProvider, name: String, installed: Bool, authenticated: Bool,
        method: String? = nil, accountLabel: String? = nil, canBrokerLogin: Bool = false
    ) {
        self.provider = provider
        self.name = name
        self.installed = installed
        self.authenticated = authenticated
        self.method = method
        self.accountLabel = accountLabel
        self.canBrokerLogin = canBrokerLogin
    }

    private enum CodingKeys: String, CodingKey {
        case provider, name, installed, authenticated, method, accountLabel, canBrokerLogin
    }
    public init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        self.provider = try c.decode(AgentLoginProvider.self, forKey: .provider)
        self.name = try c.decodeIfPresent(String.self, forKey: .name) ?? ""
        self.installed = try c.decodeIfPresent(Bool.self, forKey: .installed) ?? false
        self.authenticated = try c.decodeIfPresent(Bool.self, forKey: .authenticated) ?? false
        self.method = try c.decodeIfPresent(String.self, forKey: .method)
        self.accountLabel = try c.decodeIfPresent(String.self, forKey: .accountLabel)
        self.canBrokerLogin = try c.decodeIfPresent(Bool.self, forKey: .canBrokerLogin) ?? false
    }
}
