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

    /// 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`. `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("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`. Used to bucket sessions when keeping Claude and GPT
    /// agents in separate sandboxes (the two can otherwise compete and kill each other's
    /// processes). Grok is not part of this family (it runs host-only in v1).
    public var isCodexFamily: Bool {
        switch self {
        case .codex, .codexExec: return true
        case .claudeCode, .grok: 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: - 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)

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

        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 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)
            }
        }
    }

    public let parts: [Part]

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

    /// Flattened user-facing text (for the injected `userText` transcript event).
    public var plainText: String? {
        let pieces = parts.map { part -> String in
            switch part {
            case .text(let text): return text
            case .context(let label, let body): return "[\(label)]\n\(body)"
            }
        }
        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
}

// 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)
}
