import Foundation

// Pure, platform-agnostic value types shared by the macOS host (NucleicCore) and the
// iPhone client (NucleicRemote). These are the *exact* identifiers the sync wire layer
// projects (SYNC_PROTOCOL §5). They live here, not in NucleicCore, so the protocol layer
// compiles cleanly for iOS without dragging in GRDB / Foundation.Process / Network.
//
// Extracted from Backend.swift (identifiers), Session.swift (SessionStatus), and
// WorktreeManager.swift (DiffStat); NucleicCore re-exports this module (ProtocolExports.swift)
// so existing host code keeps referencing these unqualified.

// MARK: - Identifiers

public struct SessionID: Hashable, Sendable, Codable, CustomStringConvertible {
    public let rawValue: String

    public init(rawValue: String) { self.rawValue = rawValue }
    public static func generate() -> SessionID { SessionID(rawValue: UUID().uuidString) }

    public init(from decoder: Decoder) throws {
        rawValue = try decoder.singleValueContainer().decode(String.self)
    }
    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(rawValue)
    }
    public var description: String { rawValue }
    /// Short prefix for logs — structural metadata only (OBSERVABILITY A.1).
    public var short: String { String(rawValue.prefix(8)) }
}

public struct ApprovalID: Hashable, Sendable, Codable, CustomStringConvertible {
    public let rawValue: String

    public init(rawValue: String) { self.rawValue = rawValue }
    public static func generate() -> ApprovalID { ApprovalID(rawValue: UUID().uuidString) }

    public init(from decoder: Decoder) throws {
        rawValue = try decoder.singleValueContainer().decode(String.self)
    }
    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(rawValue)
    }
    public var description: String { rawValue }
}

public enum BackendID: String, Sendable, Codable {
    case claudeCode
    case codex
    case codexExec
    /// xAI's Grok coding agent over **ACP** (`grok agent stdio`) — standards JSON-RPC 2.0 over
    /// stdio, baked into the binary. A sibling of `codex`: interactive approvals arrive as native
    /// `session/request_permission` requests (GROK_ADAPTER §3–4). Host-only in v1.
    case grok
    /// SST's **OpenCode** over ACP (`opencode acp`). A model-agnostic coding agent that speaks
    /// standards JSON-RPC 2.0 over stdio — a sibling of `grok`, driven by the same generic
    /// `ACPBackend` (ADAPTERS §5). Host-only in v1.
    case opencode
    /// Nous Research's **Hermes Agent** over ACP (`hermes acp`). Another ACP sibling on the shared
    /// `ACPBackend`; model-agnostic (it drives Claude/Codex under the hood). Host-only in v1.
    case hermes
    /// **Cursor CLI agent** over ACP (`cursor-agent acp`). ACP sibling on the shared `ACPBackend`;
    /// authenticates with `CURSOR_API_KEY` / `cursor-agent login`. Host-only in v1.
    case cursorAgent
    /// **OpenClaw** over ACP (`openclaw acp`). ACP sibling on the shared `ACPBackend`, but unlike
    /// the other wrappers `openclaw acp` is a *bridge*, not a self-contained harness: it forwards
    /// prompts over WebSocket to a running OpenClaw Gateway, so a reachable Gateway is a
    /// prerequisite for a turn to succeed. Host-only in v1.
    case openclaw
    /// A user-configured ACP-compatible agent. Its executable and ACP-mode arguments live in
    /// Nucleic's agent settings; the stable backend id survives persistence and mesh sync without
    /// requiring a new protocol enum case for every ACP implementation.
    case acp

    /// Infer the backend from a model SKU so the model picker doubles as the backend
    /// selector: Claude SKUs → `claudeCode`, OpenAI/Codex SKUs (`gpt-*`, `o3*`, `o4*`, or
    /// anything containing `codex`) → `codex`, xAI SKUs (`grok-*`) → `grok`, and each ACP wrapper
    /// agent by its own SKU (`opencode`/`openclaw`/`hermes`/`cursor-agent`). `nil` when unrecognized, so
    /// callers fall back to the project/app default.
    public static func forModel(_ model: String?) -> BackendID? {
        guard let model = model?.lowercased() else { return nil }
        if model.hasPrefix("claude") { return .claudeCode }
        if model.hasPrefix("grok") { return .grok }
        if model.hasPrefix("opencode") { return .opencode }
        if model.hasPrefix("openclaw") { return .openclaw }
        if model.hasPrefix("hermes") { return .hermes }
        if model.hasPrefix("cursor") { return .cursorAgent }
        if model == "acp-agent" { return .acp }
        if model.hasPrefix("gpt") || model.hasPrefix("o3") || model.hasPrefix("o4")
            || model.contains("codex")
        {
            return .codex
        }
        return nil
    }

    /// True for the OpenAI/Codex backends (`codex`, `codexExec`) — the GPT-family agents,
    /// as opposed to `claudeCode`/`grok`/the ACP wrapper agents. Used to bucket sessions when
    /// keeping Claude and GPT agents in separate sandboxes (the two can otherwise compete and kill
    /// each other's processes). The ACP wrappers are not part of this family (they run host-only
    /// in v1).
    public var isCodexFamily: Bool {
        switch self {
        case .codex, .codexExec: return true
        case .claudeCode, .grok, .opencode, .openclaw, .hermes, .cursorAgent, .acp: return false
        }
    }
}

public struct ProjectID: Hashable, Sendable, Codable, CustomStringConvertible {
    public let rawValue: String
    public init(rawValue: String) { self.rawValue = rawValue }
    public static func generate() -> ProjectID { ProjectID(rawValue: UUID().uuidString) }
    public init(from decoder: Decoder) throws {
        rawValue = try decoder.singleValueContainer().decode(String.self)
    }
    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(rawValue)
    }
    public var description: String { rawValue }
}

public struct TodoID: Hashable, Sendable, Codable, Identifiable, CustomStringConvertible {
    public let rawValue: String
    public init(rawValue: String) { self.rawValue = rawValue }
    public static func generate() -> TodoID { TodoID(rawValue: UUID().uuidString) }
    public var id: String { rawValue }
    public init(from decoder: Decoder) throws {
        rawValue = try decoder.singleValueContainer().decode(String.self)
    }
    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(rawValue)
    }
    public var description: String { rawValue }
}

/// Lifecycle of a captured idea (the to-do inbox).
public enum TodoStatus: String, Sendable, Codable {
    case open        // captured, not yet acted on
    case dispatched  // an agent was dispatched for it
    case done        // manually completed / dismissed
}

public typealias WorktreePath = String

// MARK: - WireAttachment (attachment bytes carried device → host by `sendInput` / `startChat`)

/// A file or image the user attached in a composer, carried over the wire from a device (the iOS
/// remote) to the host, which materializes it into the session's working tree via
/// `AttachmentMaterializer`. Just bytes plus a display name — the sender has already read the file
/// (or captured/compressed the picked image) before handing it over. The host equivalent is
/// `PendingAttachment` (NucleicCore); this is its on-the-wire twin so attachment bytes can travel
/// from a device that can't reach the host's working tree.
public struct WireAttachment: Sendable, Codable, Equatable {
    /// The original (or synthesized) file name, e.g. `screenshot.png`. Sanitized host-side before it
    /// hits disk — never trusted as a path.
    public let filename: String
    public let data: Data

    public init(filename: String, data: Data) {
        self.filename = filename
        self.data = data
    }
}

// MARK: - AgentInput (user turn payload; carried over the wire by `sendInput`)

public struct AgentInput: Sendable, Codable, Equatable {
    public enum Part: Sendable, Codable, Equatable {
        case text(String)
        case context(label: String, body: String)
        /// A file/image attached on a device, carried as bytes for the host to materialize into the
        /// working tree. Only ever *originated* by a remote (the phone) — the host strips these out
        /// and folds a path reference into the text before the backend ever sees the turn, so a
        /// backend `Part` consumer never encounters `.attachment`.
        case attachment(WireAttachment)

        private enum CodingKeys: String, CodingKey { case type, text, label, body, filename, data }

        public init(from decoder: Decoder) throws {
            let c = try decoder.container(keyedBy: CodingKeys.self)
            switch try c.decode(String.self, forKey: .type) {
            case "text":
                self = .text(try c.decode(String.self, forKey: .text))
            case "context":
                self = .context(
                    label: try c.decode(String.self, forKey: .label),
                    body: try c.decode(String.self, forKey: .body))
            case "attachment":
                self = .attachment(WireAttachment(
                    filename: try c.decode(String.self, forKey: .filename),
                    data: try c.decode(Data.self, forKey: .data)))
            case let other:
                throw DecodingError.dataCorruptedError(
                    forKey: .type, in: c, debugDescription: "Unknown AgentInput.Part \(other)")
            }
        }

        public func encode(to encoder: Encoder) throws {
            var c = encoder.container(keyedBy: CodingKeys.self)
            switch self {
            case .text(let text):
                try c.encode("text", forKey: .type)
                try c.encode(text, forKey: .text)
            case .context(let label, let body):
                try c.encode("context", forKey: .type)
                try c.encode(label, forKey: .label)
                try c.encode(body, forKey: .body)
            case .attachment(let attachment):
                try c.encode("attachment", forKey: .type)
                try c.encode(attachment.filename, forKey: .filename)
                try c.encode(attachment.data, forKey: .data)
            }
        }
    }

    public let parts: [Part]

    public init(parts: [Part]) { self.parts = parts }
    public init(text: String) { self.parts = [.text(text)] }

    /// Build a user turn from typed text plus attached files, dropping the text part when empty (an
    /// attachment-only turn). Used by the iOS remote's composers.
    public init(text: String, attachments: [WireAttachment]) {
        var parts: [Part] = []
        if !text.isEmpty { parts.append(.text(text)) }
        parts.append(contentsOf: attachments.map { .attachment($0) })
        self.parts = parts
    }

    /// The attachment parts, in order — what the host materializes into the working tree.
    public var attachments: [WireAttachment] {
        parts.compactMap { if case .attachment(let a) = $0 { return a } else { return nil } }
    }

    /// Flattened user-facing text (for the injected `userText` transcript event). Attachment parts
    /// contribute no text — they're referenced by path once the host materializes them.
    public var plainText: String? {
        let pieces = parts.compactMap { part -> String? in
            switch part {
            case .text(let text): return text
            case .context(let label, let body): return "[\(label)]\n\(body)"
            case .attachment: return nil
            }
        }
        let joined = pieces.joined(separator: "\n")
        return joined.isEmpty ? nil : joined
    }
}

// MARK: - SessionStatus (derived state; PLAN "Core domain model", RUNTIME §2)

/// The `SessionController` is the only writer; everyone else (including the phone) observes.
public enum SessionStatus: String, Sendable, Codable {
    /// Created, not yet started.
    case idle
    /// Worktree being created / setup script running.
    case provisioning
    /// Agent actively working a turn.
    case running
    /// Blocked on a human approval decision.
    case awaitingApproval
    /// Turn finished; waiting for the user's next input (interactive run still open).
    case awaitingInput
    /// Run completed normally.
    case finished
    /// Interrupted (cooperative) — resumable.
    case interrupted
    /// Fatal error.
    case error

    public var isTerminal: Bool {
        switch self {
        case .finished, .interrupted, .error: return true
        default: return false
        }
    }

    /// The agent is actively working a turn — running, blocked mid-turn on an approval, or
    /// still provisioning to start one — as opposed to idle/done (`.idle`, `.awaitingInput`,
    /// or terminal). Distinguishes "the agent is busy" from "the agent has returned".
    public var hasTurnInFlight: Bool {
        switch self {
        case .provisioning, .running, .awaitingApproval: return true
        case .idle, .awaitingInput, .finished, .interrupted, .error: return false
        }
    }

    public var displayName: String {
        switch self {
        case .idle: "Idle"
        case .provisioning: "Provisioning"
        case .running: "Running"
        case .awaitingApproval: "Awaiting approval"
        case .awaitingInput: "Awaiting input"
        case .finished: "Finished"
        case .interrupted: "Interrupted"
        case .error: "Error"
        }
    }
}

/// How an agent's most recent turn *ended*, refining `.awaitingInput` so the UI can tell
/// "the agent is done" from "the agent needs you". A soft, presentation-only signal (not part
/// of the state machine); `nil` = not-yet-classified.
public enum TurnDisposition: String, Sendable, Codable {
    /// The turn ended by asking the user something or needing a decision from them.
    case awaitingInput
    /// The turn finished the requested work; nothing is required from the user.
    case completed
    /// The turn ended with a background watch (`nucleic_monitor`) still armed — the agent is
    /// waiting to be re-invoked when it fires, not done and not blocked on the user. Neither
    /// "Done" nor "needs you": the sidebar reads "Monitoring…", it doesn't pull the eye, and the
    /// turn-end notification is suppressed (each monitor tick would otherwise re-alert).
    case waitingBackground
}

// MARK: - DiffStat (worktree diff summary; carried in SessionSummary)

public struct DiffStat: Sendable, Codable, Equatable {
    public let filesChanged: Int
    public let added: Int
    public let removed: Int

    public init(filesChanged: Int, added: Int, removed: Int) {
        self.filesChanged = filesChanged
        self.added = added
        self.removed = removed
    }

    public static let empty = DiffStat(filesChanged: 0, added: 0, removed: 0)
}
