import Foundation

// Wire types for the Covalence runner (docs/COVALENCE_RUNNER.md §5–6): intelligence
// delegation (a cloud host asking an Apple-Intelligence-capable device to run one
// `IntelligenceProviding` call) and the credential mesh (devices sealing agent/platform
// credentials directly to a runner, mirroring the set across the group, and electing one
// refresher per credential kind). All additive and capability-gated per MESH_TRANSFER's
// locked invariants.

// MARK: - Intelligence delegation

/// What kind of intelligence work a host is delegating (mirrors `IntelligenceProviding`'s
/// methods). Raw-string-backed for forward compatibility — a future kind decodes to its raw
/// value rather than throwing, and an executor that doesn't recognize one answers with an
/// error result rather than crashing the stream.
public struct IntelligenceRequestKind: RawRepresentable, Sendable, Codable, Equatable, Hashable {
    public let rawValue: String
    public init(rawValue: String) { self.rawValue = rawValue }

    public static let classifyTurn = IntelligenceRequestKind(rawValue: "classifyTurn")
    public static let sessionName = IntelligenceRequestKind(rawValue: "sessionName")
    public static let summarizeSession = IntelligenceRequestKind(rawValue: "summarizeSession")
    public static let summarizeTodos = IntelligenceRequestKind(rawValue: "summarizeTodos")
    public static let summarizeTodo = IntelligenceRequestKind(rawValue: "summarizeTodo")
    public static let triageTodos = IntelligenceRequestKind(rawValue: "triageTodos")
    public static let summarizeBashCommand = IntelligenceRequestKind(rawValue: "summarizeBashCommand")
    public static let mergeBashSummaries = IntelligenceRequestKind(rawValue: "mergeBashSummaries")
    public static let summarizeToolFamily = IntelligenceRequestKind(rawValue: "summarizeToolFamily")
    public static let updateTrunkSummary = IntelligenceRequestKind(rawValue: "updateTrunkSummary")
    public static let auditHostExecReason = IntelligenceRequestKind(rawValue: "auditHostExecReason")
    public static let classifyPromptPurpose = IntelligenceRequestKind(rawValue: "classifyPromptPurpose")
}

/// One delegated intelligence call (`HostMsg.intelligenceRequest`), pushed by a host that
/// advertised `WireCapabilities.canDelegateIntelligence` to a client that advertised
/// `WireClientCapabilities.canProvideIntelligence`. Inputs are plain strings (the provider
/// methods are string-in/string-out at heart); the executor maps `kind` onto its local
/// `IntelligenceProviding` (the AFM queue on a Mac/iPhone) and answers with
/// `ClientMsg.intelligenceResult` carrying the same `id`. Content-minimal by design: the host
/// sends exactly the text the provider method needs, never whole transcripts.
public struct WireIntelligenceRequest: Sendable, Codable, Equatable {
    public let id: String
    public let kind: IntelligenceRequestKind
    /// Positional inputs for the kind (e.g. `[lastReply]` for `classifyTurn`, the command list
    /// for `mergeBashSummaries`).
    public let inputs: [String]
    /// Optional secondary context (e.g. the digest for `sessionName(fromFirstMessage:context:)`).
    public let context: String?
    /// How long the host will wait before falling back to heuristics. Advisory.
    public let deadlineSeconds: Double?
    /// How urgent this job is (`IntelligencePriority`) — drives the mesh queue's ordering and
    /// its device-class placement gate (the two highest tiers never run on an iPhone/iPad), and
    /// the executor's slot on its local AFM queue. Omitted ⇒ background.
    public let priority: IntelligencePriority?

    public init(
        id: String, kind: IntelligenceRequestKind, inputs: [String],
        context: String? = nil, deadlineSeconds: Double? = nil,
        priority: IntelligencePriority? = nil
    ) {
        self.id = id
        self.kind = kind
        self.inputs = inputs
        self.context = context
        self.deadlineSeconds = deadlineSeconds
        self.priority = priority
    }

    private enum CodingKeys: String, CodingKey {
        case id, kind, inputs, context, deadlineSeconds, priority
    }
    public init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        self.id = try c.decode(String.self, forKey: .id)
        self.kind = try c.decode(IntelligenceRequestKind.self, forKey: .kind)
        self.inputs = try c.decodeIfPresent([String].self, forKey: .inputs) ?? []
        self.context = try c.decodeIfPresent(String.self, forKey: .context)
        self.deadlineSeconds = try c.decodeIfPresent(Double.self, forKey: .deadlineSeconds)
        self.priority = try c.decodeIfPresent(IntelligencePriority.self, forKey: .priority)
    }
}

/// The executor's answer (`ClientMsg.intelligenceResult`), keyed by the request `id`. `outputs`
/// is positional like `inputs` (one line for most kinds; per-item lines for the batch kinds).
/// A device that can't serve the kind (model off, unknown kind, timeout) answers with `error`
/// set so the host falls back to heuristics immediately instead of waiting out the deadline.
public struct WireIntelligenceResult: Sendable, Codable, Equatable {
    public let id: String
    public let outputs: [String]
    public let error: String?

    public init(id: String, outputs: [String] = [], error: String? = nil) {
        self.id = id
        self.outputs = outputs
        self.error = error
    }

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

// MARK: - Remote project creation

/// `ClientMsg.createProject` (CLOUD_RUNTIME §4.3; COVALENCE_RUNNER §0.2 item 1): ask a host to
/// clone a git repository and register it as a project — the verb that lets a device seed work
/// onto a fresh runner, whose store starts empty (`startChat` needs an existing project and
/// transfer import rejects unknown ones). Works against any host advertising
/// `WireCapabilities.canCreateProjects`; the clone lands as a Nucleic-controlled repo. Errors
/// come back as `HostMsg.error`; success surfaces through the dashboard broadcast carrying the
/// new project.
public struct WireCreateProjectRequest: Sendable, Codable, Equatable {
    /// The git URL to clone (https or scp-style ssh).
    public let gitURL: String
    /// The branch to check out; nil ⇒ the remote's default branch.
    public let branch: String?
    /// Display name for the project; nil/empty ⇒ derived from the URL's repo folder name.
    public let name: String?
    /// Client-chosen correlation id, echoed back in `HostMsg.projectCreated` so the requesting
    /// UI can match the outcome to its in-flight sheet. Optional — a fire-and-forget caller
    /// (boot tooling, scripts) can omit it and rely on the dashboard broadcast alone.
    public let requestID: String?

    public init(gitURL: String, branch: String? = nil, name: String? = nil, requestID: String? = nil) {
        self.gitURL = gitURL
        self.branch = branch
        self.name = name
        self.requestID = requestID
    }

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

/// The outcome of a `ClientMsg.createProject` (`HostMsg.projectCreated`), correlated by the
/// request's `requestID`. Success carries the registered project's id + display name (the full
/// row arrives via the dashboard broadcast); failure carries `error` and no project. Sent only
/// to the requesting connection — other devices learn the project from the dashboard push.
public struct WireProjectCreated: Sendable, Codable, Equatable {
    public let requestID: String?
    public let projectID: ProjectID?
    public let name: String?
    /// Human-readable failure (clone failed, bad URL, …); nil on success.
    public let error: String?

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

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

// MARK: - Pool credential on the mesh (docs/COVALENCE_RUNNER.md §8, §0.2 item 4)

/// The owner's runner-pool credential riding the mesh: pushed post-hello to control-scope
/// peers (`HostMsg.runnerPoolCredential`) the way relay memberships are, so every trusted
/// device manages the SAME pool — rather than PoP-enrolling its own, which rotates the secret
/// out from under whoever shared it. Receivers converge on `updatedAt` (newest wins), the
/// mesh's usual monotonic-merge discipline.
public struct WireRunnerPoolCredential: Sendable, Codable, Equatable {
    /// `sha256(enrolling device's static key)` — which pool this credential manages.
    public let poolID: String
    public let secret: String
    /// Control-plane base URL override (dev/self-host); nil ⇒ production.
    public let url: String?
    /// When this revision was minted (enrollment/rotation time) — the merge key.
    public let updatedAt: Date

    public init(poolID: String, secret: String, url: String? = nil, updatedAt: Date) {
        self.poolID = poolID
        self.secret = secret
        self.url = url
        self.updatedAt = updatedAt
    }

    private enum CodingKeys: String, CodingKey { case poolID = "poolId", secret, url, updatedAt }
    public init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        self.poolID = try c.decode(String.self, forKey: .poolID)
        self.secret = try c.decode(String.self, forKey: .secret)
        self.url = try c.decodeIfPresent(String.self, forKey: .url)
        self.updatedAt = try c.decode(Date.self, forKey: .updatedAt)
    }
}

// MARK: - Credential mesh

/// What a mirrored credential record is (docs/COVALENCE_RUNNER.md §6). Raw-string-backed so
/// new kinds ride through old devices' manifests untouched. The payload each kind carries is
/// exactly what the local sandbox seeding uses today (SessionController's per-turn seeding):
/// the Claude OAuth keychain JSON, `~/.codex/auth.json`, agent home files, API keys, the
/// GitHub token.
public struct CredentialKind: RawRepresentable, Sendable, Codable, Equatable, Hashable {
    public let rawValue: String
    public init(rawValue: String) { self.rawValue = rawValue }

    public static let claudeOAuth = CredentialKind(rawValue: "claude-oauth")
    public static let anthropicAPIKey = CredentialKind(rawValue: "anthropic-api-key")
    public static let codexAuth = CredentialKind(rawValue: "codex-auth")
    public static let openAIAPIKey = CredentialKind(rawValue: "openai-api-key")
    public static let grokConfig = CredentialKind(rawValue: "grok-config")
    public static let xaiAPIKey = CredentialKind(rawValue: "xai-api-key")
    public static let githubToken = CredentialKind(rawValue: "github-token")
    /// The user's whole Managed Git identity (Settings ▸ Git): the auth mode, its secret(s) —
    /// an HTTPS token, a user `.ssh` key + passphrase, or the Nucleic-managed key — and the
    /// signing/force-SSH/commit-identity flags, JSON-encoded. Unlike the file-native kinds this
    /// lands into the runner's credential store + defaults so `GitHubCredentialConfig.current()`
    /// resolves exactly as on the Mac, and every git op (clone, push, PR, commit signing) behaves
    /// identically (docs/COVALENCE_RUNNER.md §6).
    public static let gitCredentialBundle = CredentialKind(rawValue: "git-credential-bundle")
}

/// One manifest entry: which credential a device holds, how fresh it is, and where it came
/// from — never the secret bytes. Mirroring converges on `updatedAt` (latest wins per kind),
/// the same monotonic-merge discipline as mesh roster gossip.
public struct CredentialRecordDescriptor: Sendable, Codable, Equatable {
    public let kind: CredentialKind
    public let updatedAt: Date
    /// The device that produced this revision (initial capture or a refresh).
    public let provenanceDeviceID: String

    public init(kind: CredentialKind, updatedAt: Date, provenanceDeviceID: String) {
        self.kind = kind
        self.updatedAt = updatedAt
        self.provenanceDeviceID = provenanceDeviceID
    }
}

/// The refresh lease: which mesh member runs token refreshes for one credential kind, so a
/// rotating refresh token is only ever advanced by ONE writer (concurrent refreshers revoke
/// each other — the exact race the Codex api-key mode exists to dodge). Gossiped inside the
/// manifest; `merged` below is the group-wide arbitration rule.
public struct CredentialRefreshLease: Sendable, Codable, Equatable {
    public let kind: CredentialKind
    public let holderDeviceID: String
    public let acquiredAt: Date
    public let expiresAt: Date

    public init(kind: CredentialKind, holderDeviceID: String, acquiredAt: Date, expiresAt: Date) {
        self.kind = kind
        self.holderDeviceID = holderDeviceID
        self.acquiredAt = acquiredAt
        self.expiresAt = expiresAt
    }

    /// Whether this lease still binds at `now`.
    public func isLive(at now: Date) -> Bool { now < expiresAt }

    /// Deterministic arbitration between two claims on the same kind: a live lease beats a dead
    /// one; between two live leases the *earlier* claim wins (first writer keeps the lease —
    /// stability over freshness, so a flapping device can't steal it), with the lower device ID
    /// breaking exact ties. Everyone applying this rule converges on one holder.
    public static func merged(
        _ a: CredentialRefreshLease?, _ b: CredentialRefreshLease?, at now: Date
    ) -> CredentialRefreshLease? {
        switch (a, b) {
        case (nil, nil): return nil
        case (let one?, nil): return one.isLive(at: now) ? one : nil
        case (nil, let one?): return one.isLive(at: now) ? one : nil
        case (let x?, let y?):
            switch (x.isLive(at: now), y.isLive(at: now)) {
            case (true, false): return x
            case (false, true): return y
            case (false, false): return nil
            case (true, true):
                if x.acquiredAt != y.acquiredAt { return x.acquiredAt < y.acquiredAt ? x : y }
                return x.holderDeviceID <= y.holderDeviceID ? x : y
            }
        }
    }
}

/// A credential kind was deleted mesh-wide (key deletion from any device — the follow-up to
/// remote agent sign-in). Propagates like a mesh roster tombstone: `deletedAt`-monotonic per
/// kind, so a stale holder's manifest or provision can never resurrect what the user removed —
/// only a *genuinely newer* login (its record `updatedAt` after `deletedAt`) wins again.
///
/// A holder that applies a tombstone and finds its own copy stamped *fresher* than `deletedAt`
/// (Claude's `updatedAt` is the token's future `expiresAt`) bumps `deletedAt` past that copy's
/// freshness before re-gossiping — the tombstone absorbs the freshness of everything it
/// deletes, so convergence never depends on the deleting device knowing every holder's stamp.
public struct CredentialTombstone: Sendable, Codable, Equatable {
    public let kind: CredentialKind
    /// When the deletion takes effect — the monotonic merge key. Any credential record whose
    /// `updatedAt` is at or before this instant is suppressed (not landed, not manifested,
    /// not provisioned).
    public let deletedAt: Date
    /// The device that performed (or last bumped) the deletion. Informational.
    public let originDeviceID: String

    public init(kind: CredentialKind, deletedAt: Date, originDeviceID: String) {
        self.kind = kind
        self.deletedAt = deletedAt
        self.originDeviceID = originDeviceID
    }

    private enum CodingKeys: String, CodingKey { case kind, deletedAt, originDeviceID }
    public init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        self.kind = try c.decode(CredentialKind.self, forKey: .kind)
        self.deletedAt = try c.decode(Date.self, forKey: .deletedAt)
        self.originDeviceID = try c.decodeIfPresent(String.self, forKey: .originDeviceID) ?? ""
    }

    /// Whether this tombstone suppresses a credential revision stamped `updatedAt`: deleted-at
    /// wins ties, so re-provisioning the exact revision that was deleted stays dead; only a
    /// strictly newer login resurrects the kind.
    public func suppresses(recordUpdatedAt: Date) -> Bool {
        recordUpdatedAt <= deletedAt
    }

    /// This tombstone, extended to also cover a local copy stamped `freshness` (a holder
    /// applying the deletion absorbs its copy's stamp so no other stale holder can resurrect
    /// it). No-op when the tombstone already covers it.
    public func absorbing(freshness: Date) -> CredentialTombstone {
        guard freshness >= deletedAt else { return self }
        return CredentialTombstone(
            kind: kind, deletedAt: freshness.addingTimeInterval(1), originDeviceID: originDeviceID)
    }

    /// Deterministic per-kind arbitration: the later `deletedAt` wins (a bump supersedes the
    /// original), the lower origin id breaking exact ties — everyone converges on one stone.
    public static func merged(
        _ a: CredentialTombstone?, _ b: CredentialTombstone?
    ) -> CredentialTombstone? {
        switch (a, b) {
        case (nil, nil): return nil
        case (let one?, nil), (nil, let one?): return one
        case (let x?, let y?):
            if x.deletedAt != y.deletedAt { return x.deletedAt > y.deletedAt ? x : y }
            return x.originDeviceID <= y.originDeviceID ? x : y
        }
    }

    /// Merge two tombstone sets per kind (idempotent, order-independent).
    public static func merged(
        _ a: [CredentialTombstone], _ b: [CredentialTombstone]
    ) -> [CredentialTombstone] {
        var byKind: [CredentialKind: CredentialTombstone] = [:]
        for stone in a + b {
            byKind[stone.kind] = merged(byKind[stone.kind], stone)
        }
        return byKind.values.sorted { $0.kind.rawValue < $1.kind.rawValue }
    }
}

/// A device's view of the mirrored credential set: descriptors (no secrets) plus the refresh
/// leases it knows. Pushed via `ClientMsg.credentialManifest` (gated on
/// `WireCapabilities.canReceiveSealedCredentials`) so a runner knows what it can ask for and
/// who refreshes what. Merging is per-kind `updatedAt`-monotonic and idempotent.
public struct CredentialManifest: Sendable, Codable, Equatable {
    public let records: [CredentialRecordDescriptor]
    public let leases: [CredentialRefreshLease]
    /// This device's own credential-sealing X25519 public key (32 bytes), so a runner that
    /// rotates a token can seal the new revision *back* to this device (`HostMsg.credentialUpdate`
    /// — the cloud analogue of `syncClaudeLoginBack`). Distinct from the Noise identity, like the
    /// runner's own sealing key. Optional (decode-defaulted) — a device that only ever *provides*
    /// credentials, never receiving updates, may omit it.
    public let sealingPublicKey: Data?
    /// Mesh-wide credential deletions this device knows (decode-defaulted — a manifest from a
    /// device that predates deletion omits it). Riding the manifest is what makes a tombstone
    /// resurrection-proof against stale holders: the receiving host merges these before it ever
    /// trusts the manifest's `records`.
    public let tombstones: [CredentialTombstone]

    public init(
        records: [CredentialRecordDescriptor] = [], leases: [CredentialRefreshLease] = [],
        sealingPublicKey: Data? = nil, tombstones: [CredentialTombstone] = []
    ) {
        self.records = records
        self.leases = leases
        self.sealingPublicKey = sealingPublicKey
        self.tombstones = tombstones
    }

    private enum CodingKeys: String, CodingKey { case records, leases, sealingPublicKey, tombstones }
    public init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        self.records = try c.decodeIfPresent([CredentialRecordDescriptor].self, forKey: .records) ?? []
        self.leases = try c.decodeIfPresent([CredentialRefreshLease].self, forKey: .leases) ?? []
        self.sealingPublicKey = try c.decodeIfPresent(Data.self, forKey: .sealingPublicKey)
        self.tombstones = try c.decodeIfPresent([CredentialTombstone].self, forKey: .tombstones) ?? []
    }
}

/// A host asking connected devices for credentials it lacks (`HostMsg.credentialNeeded`):
/// which kinds, and the X25519 public key to seal them to. The sealing key is generated inside
/// the runner at provisioning (distinct from its Noise identity, so credentials can be
/// re-sealed across identity rotation) and every ciphertext binds to it — the relay and the
/// runner control plane only ever carry sealed boxes (docs/COVALENCE_RUNNER.md §6).
public struct WireCredentialNeed: Sendable, Codable, Equatable {
    public let kinds: [CredentialKind]
    /// The host's credential-sealing X25519 public key (32 bytes).
    public let sealingPublicKey: Data

    public init(kinds: [CredentialKind], sealingPublicKey: Data) {
        self.kinds = kinds
        self.sealingPublicKey = sealingPublicKey
    }
}

/// One sealed credential revision in flight: the kind + freshness ride in the clear (they're
/// manifest metadata already) and are cryptographically bound to the ciphertext as AAD, so a
/// box can't be replayed as a different kind or re-dated.
public struct SealedCredentialRecord: Sendable, Codable, Equatable {
    public let kind: CredentialKind
    public let updatedAt: Date
    public let box: SealedCredentialBox

    public init(kind: CredentialKind, updatedAt: Date, box: SealedCredentialBox) {
        self.kind = kind
        self.updatedAt = updatedAt
        self.box = box
    }

    /// The AAD both sides bind: the kind string. (The recipient key is already bound inside
    /// `SealedCredentialBox`'s KDF; the kind is what distinguishes records in one envelope.)
    public var additionalData: Data { Data(kind.rawValue.utf8) }
}

/// A batch of sealed records — `ClientMsg.credentialProvision` (device → host, answering a
/// `credentialNeeded` or proactively after a refresh) and `HostMsg.credentialUpdate` (host →
/// device, mirroring a rotation the runner itself performed, sealed to that device's own
/// sealing key from its last manifest).
public struct SealedCredentialEnvelope: Sendable, Codable, Equatable {
    public let records: [SealedCredentialRecord]

    public init(records: [SealedCredentialRecord]) {
        self.records = records
    }
}
