import Foundation

// Value types for the sidebar "Control" panel — the cross-session view of the whole Nucleic
// Control subsystem (the shared sandbox container, autoship/merge-queue, file locks, and the
// git-interceptor activity feed). All four are Control-only by design, so the panel is the one
// place that gathers them. `AppStore.controlSnapshot()` builds a `ControlSnapshot`; the view
// polls it.

/// Which in-container interceptor shim emitted an activity event.
public enum InterceptorTool: String, Sendable, Equatable {
    case git
    case gh
}

/// One operation an in-container interceptor shim observed for a Nucleic Control session, recorded
/// for the Control panel's activity feed. Every mutating op a shim reports — `git` (POST
/// `/git-event`, merges/commits/rebases) or `gh` (POST `/gh-event`, PR/release/API actions) —
/// becomes one of these, newest-first and capped, so the feed shows what the agents' own tools did
/// (detected with certainty), not a guess.
public struct GitInterceptorEvent: Identifiable, Sendable, Equatable {
    public let id: UUID
    public let sessionID: SessionID
    public let sessionTitle: String
    /// Which shim observed this op.
    public let tool: InterceptorTool
    /// Normalized op kind: from `GitCommandSummary.classify` (`merge`, `commit`, …) for `git`, or
    /// `GhCommandSummary.classify` (`pr.create`, `pr.merge`, `release.create`, …) for `gh`.
    public let kind: String
    /// Human one-liner (e.g. "Merge feature/x", "Open PR: Fix the bug").
    public let label: String
    /// SF Symbol for the op's glyph (shared with the step-list rendering).
    public let symbol: String
    /// A merge landing — `git merge` or `gh pr merge`; the op autoship + lock-release care about.
    public let isMerge: Bool
    /// The real tool's exit code (0 = succeeded). A non-zero op is shown as failed.
    public let exitCode: Int
    public let at: Date

    public var succeeded: Bool { exitCode == 0 }

    public init(
        id: UUID = UUID(), sessionID: SessionID, sessionTitle: String, tool: InterceptorTool = .git,
        kind: String, label: String, symbol: String, isMerge: Bool, exitCode: Int, at: Date
    ) {
        self.id = id
        self.sessionID = sessionID
        self.sessionTitle = sessionTitle
        self.tool = tool
        self.kind = kind
        self.label = label
        self.symbol = symbol
        self.isMerge = isMerge
        self.exitCode = exitCode
        self.at = at
    }
}

/// One ordinary (non-git) command the in-container interceptor observed for a Nucleic Control
/// session — `grep`, `cat`, `find`, `npm`, … — recorded for the Control panel's command feed and
/// the structured log. Reported by the command-interceptor shim (rich: argv + capped output) or
/// the bash command tracer (metadata only), classified by ``CommandSummary``. Newest-first and
/// capped, so the feed shows the ground truth of what the agent's shell actually ran.
public struct CommandInterceptorEvent: Identifiable, Sendable, Equatable {
    /// How the op was observed: the output-capturing shim, or the lightweight bash tracer.
    public enum Source: String, Sendable, Equatable {
        case shim     // the per-command Node shim — carries capped stdout/stderr
        case tracer   // the bash BASH_ENV tracer — metadata only (line/cwd/exit/duration)
    }

    public let id: UUID
    public let sessionID: SessionID
    public let sessionTitle: String
    /// The resolved command name (`grep`, `cat`, `npm`, …).
    public let command: String
    /// Coarse bucket from ``CommandSummary`` (`read`, `search`, `list`, `text`, `fs`, `delete`,
    /// `build`, `run`, `network`, `archive`, `info`, `other`) — for grouping/coloring.
    public let category: String
    /// Human one-liner (e.g. `Search for "TODO"`, `Read main.swift`).
    public let label: String
    /// SF Symbol for the op's glyph (shared with the feed row rendering).
    public let symbol: String
    /// True for filesystem writes/deletes the UI may want to emphasize.
    public let isMutating: Bool
    /// The real command's exit code (0 = succeeded). A non-zero op is shown as failed.
    public let exitCode: Int
    /// Wall-clock duration of the command, when the reporter measured it (nil = unknown).
    public let durationMs: Int?
    /// A capped, single-line preview of the command's output (first non-empty line of stdout, or
    /// stderr when stdout was empty), for the feed's secondary line. Empty when none was captured
    /// (the tracer captures no output).
    public let outputPreview: String
    public let source: Source
    public let at: Date

    public var succeeded: Bool { exitCode == 0 }

    public init(
        id: UUID = UUID(), sessionID: SessionID, sessionTitle: String, command: String,
        category: String, label: String, symbol: String, isMutating: Bool, exitCode: Int,
        durationMs: Int?, outputPreview: String, source: Source, at: Date
    ) {
        self.id = id
        self.sessionID = sessionID
        self.sessionTitle = sessionTitle
        self.command = command
        self.category = category
        self.label = label
        self.symbol = symbol
        self.isMutating = isMutating
        self.exitCode = exitCode
        self.durationMs = durationMs
        self.outputPreview = outputPreview
        self.source = source
        self.at = at
    }
}

/// The autoship state of one Nucleic Control session, for the Control panel. A session appears
/// when it has autoship armed (`Session.autoShip`) or the merge queue has acted on it (a non-nil
/// `status`), so the section reads as "armed → in-flight → landed/failed".
public struct AutoshipEntry: Identifiable, Sendable, Equatable {
    public let id: SessionID
    public let title: String
    /// The branch this session ships into (e.g. `dev`).
    public let target: String
    /// Latest merge-queue status, or nil for "armed, nothing in flight yet".
    public let status: ShipStatus?

    public init(id: SessionID, title: String, target: String, status: ShipStatus?) {
        self.id = id
        self.title = title
        self.target = target
        self.status = status
    }
}

/// Health of the shared Nucleic Control container, for the Control panel's container section and
/// the header rollup. Resources come from settings; `running`/`exists` are a best-effort probe of
/// the `container` runtime; the session/project counts come from `AppStore`'s own state.
/// One shared control container as shown in the Control panel: its friendly **type** (the heading —
/// "Shared" / "Claude" / "Codex" / "xAI") and its **actual** container name (opaque, randomized per
/// creation so an agent can't recognize or target a sibling sandbox), shown as subtext.
public struct ControlContainerEntry: Sendable, Equatable, Identifiable {
    public let typeLabel: String
    public let name: String
    public let running: Bool
    public var id: String { name }

    public init(typeLabel: String, name: String, running: Bool) {
        self.typeLabel = typeLabel
        self.name = name
        self.running = running
    }
}

/// Progress of acquiring the sandbox container's on-disk artifacts — the one-time downloads that
/// gate a Control session's first turn and are the usual cause of a slow first response: the Linux
/// kernel, the vminitd init filesystem, and (the dominant cost) the `nucleic-sandbox` base image,
/// then the image's unpack to the cached ext4 rootfs. `ContainerEngine` publishes this while a
/// pull/download is in flight so the Control panel can show a progress bar and the chat can explain
/// the delay; it's `nil` once every artifact is cached (the steady state on every later launch).
public struct ContainerDownloadProgress: Sendable, Equatable {
    /// Which artifact the engine is acquiring right now.
    public enum Phase: String, Sendable, Equatable {
        case kernel   // the Linux kernel blob
        case initfs   // the vminitd init filesystem image
        case image    // the sandbox base image — the large, dominant download
        case unpack   // unpacking the pulled image into the cached ext4 rootfs
    }

    public let phase: Phase
    /// Bytes pulled so far for the current phase (best-effort; 0 when the source reports no size,
    /// e.g. an indeterminate kernel blob or the unpack step).
    public let receivedBytes: Int64
    /// Total bytes expected for the current phase, or 0 when unknown (→ an indeterminate bar).
    public let totalBytes: Int64

    public init(phase: Phase, receivedBytes: Int64 = 0, totalBytes: Int64 = 0) {
        self.phase = phase
        self.receivedBytes = max(0, receivedBytes)
        self.totalBytes = max(0, totalBytes)
    }

    /// A 0…1 completion fraction when the total is known, else `nil` — the caller then shows an
    /// indeterminate (sweeping) bar rather than a misleading 0%.
    public var fraction: Double? {
        totalBytes > 0 ? min(1, max(0, Double(receivedBytes) / Double(totalBytes))) : nil
    }

    /// Short human label for the active phase, for the panel bar and the chat delay line.
    public var label: String {
        switch phase {
        case .kernel: return "Downloading runtime kernel"
        case .initfs: return "Downloading container runtime"
        case .image:  return "Downloading sandbox image"
        case .unpack: return "Unpacking sandbox image"
        }
    }

    /// A compact "12 / 480 MB" byte readout when a total is known, else `nil`.
    public var byteDetail: String? {
        guard totalBytes > 0 else { return nil }
        func mb(_ b: Int64) -> String { String(format: "%.0f", Double(b) / 1_048_576) }
        return "\(mb(receivedBytes)) / \(mb(totalBytes)) MB"
    }
}

public struct ControlContainerInfo: Sendable, Equatable {
    /// The app-wide container-service master switch. When off, Control features are inert.
    public let serviceEnabled: Bool
    /// A shared control container exists (running or stopped).
    public let exists: Bool
    /// It is currently running.
    public let running: Bool
    /// The materialized control container(s) this run, each with its type label + actual name. Empty
    /// when none has been created yet (the panel then shows the aggregate "Not created" state).
    public let entries: [ControlContainerEntry]
    public let cpus: Int
    public let memoryGiB: Int
    /// Live (non-archived) sessions across Nucleic Control projects.
    public let activeSessions: Int
    /// Number of Nucleic Control projects.
    public let controlProjects: Int
    /// Live CPU/memory usage of the running container, for the resource monitor. `nil` when the
    /// container isn't running or the probe failed (the monitor hides rather than showing zeros).
    public let usage: ContainerResourceSample?

    public init(
        serviceEnabled: Bool, exists: Bool, running: Bool, entries: [ControlContainerEntry], cpus: Int,
        memoryGiB: Int, activeSessions: Int, controlProjects: Int,
        usage: ContainerResourceSample? = nil
    ) {
        self.serviceEnabled = serviceEnabled
        self.exists = exists
        self.running = running
        self.entries = entries
        self.cpus = cpus
        self.memoryGiB = memoryGiB
        self.activeSessions = activeSessions
        self.controlProjects = controlProjects
        self.usage = usage
    }
}

/// Everything the Control sidebar panel renders, gathered in one call so the view polls a single
/// `@MainActor` entry point (`AppStore.controlSnapshot()`).
public struct ControlSnapshot: Sendable, Equatable {
    public let container: ControlContainerInfo
    public let autoship: [AutoshipEntry]
    public let locks: LockQueueSnapshot
    public let activity: [GitInterceptorEvent]
    /// The non-git command feed: `grep`/`cat`/`find`/`npm`/… the interceptor observed, newest-first.
    public let commands: [CommandInterceptorEvent]

    public init(
        container: ControlContainerInfo, autoship: [AutoshipEntry],
        locks: LockQueueSnapshot, activity: [GitInterceptorEvent],
        commands: [CommandInterceptorEvent] = []
    ) {
        self.container = container
        self.autoship = autoship
        self.locks = locks
        self.activity = activity
        self.commands = commands
    }
}
