import Foundation

// SessionStatus and TurnDisposition moved to NucleicProtocol/CoreIdentifiers.swift (shared
// with the iOS client). The event-driven `transition(_:on:)` state machine stays here.

extension SessionStatus {
    /// Pure state-machine transition from an ingested event (RUNTIME §2). Terminal
    /// states are sticky within a run — a fresh `start`/`resume` resets status
    /// explicitly rather than relying on an event to un-terminal it.
    public static func transition(_ current: SessionStatus, on kind: AgentEvent.Kind) -> SessionStatus {
        if current.isTerminal { return current }
        switch kind {
        case .sessionStarted:
            return .running
        case .approvalRequested:
            return .awaitingApproval
        case .approvalResolved:
            return .running
        case .turnCompleted:
            return .awaitingInput
        case .runFinished(let finished):
            switch finished.outcome {
            case .completed, .maxTurns: return .finished
            case .interrupted: return .interrupted
            case .errored: return .error
            }
        case .error(let err):
            return err.recoverable ? current : .error
        case .usage, .rateLimit, .note:
            // Pure telemetry / injected log lines — never change the lifecycle state.
            return current
        case .userText:
            // The user submitted a turn; the agent is about to work.
            return .running
        case .assistantText, .thinking, .toolCallStarted, .toolCallInputDelta,
             .toolCallCompleted, .toolResult, .fileChange, .raw:
            // Any work event implies the agent is running (also covers resuming from
            // awaitingInput/awaitingApproval if a stray event arrives).
            return .running
        }
    }
}

/// One agent run. Metadata mirror of the `session` table (RUNTIME §4); event
/// bodies live in the JSONL transcript, not here.
public struct Session: Identifiable, Sendable, Codable, Equatable {
    public let id: SessionID
    public let projectID: ProjectID
    public let backend: BackendID
    public var backendSessionID: String?
    public var title: String
    public var status: SessionStatus
    public var worktreePath: String?
    public var branch: String?
    public var baseSHA: String?
    /// The branch this session merges into and **releases its locks against** — its release
    /// target + update-on-grant source (LOCKING §3, §4.4). A project branch ("dev") for a
    /// top-level session, or a parent session's branch ("nucleic/<slug>") for a nested child.
    /// Cascade rewrites it to the grandparent. For a worktree-less session it is the branch it
    /// edits — "landing in the parent" means committing to that branch.
    public var parentRef: String?
    /// The top-level project branch this session's lineage flows to — its **lock domain**
    /// (LOCKING §2). All sessions sharing a `rootRef` contend on files; different roots don't.
    /// Invariant under cascade (a child re-targets `parentRef`, never `rootRef`).
    public var rootRef: String?
    /// Set when `parentRef` is another session's branch — the parent session's id. Drives the
    /// dependency tree and cascade (LOCKING §5). nil when the parent is a project branch.
    public var parentSessionID: SessionID?
    /// Model alias or full id passed via `--model` (nil = CLI default).
    public var model: String?
    /// Reasoning effort passed via `--effort` (low/medium/high/xhigh/max; nil = default).
    public var effort: String?
    public var lastSeq: UInt64
    public var transcriptPath: String
    public var nativeTranscriptPath: String?
    public var diffStat: DiffStat?
    public var ahead: Int?
    public var behind: Int?
    /// Auto-approval mode: run with Claude's `--permission-mode auto` (its classifier
    /// auto-approves safe actions; destructive ones still route to our approval UI).
    public var auto: Bool
    /// Autoship mode: when the agent finishes its work (turn classified `.completed`),
    /// the session's branch is squash-merged into the project's default branch via the
    /// merge queue. Implies `auto` (an agent can't ship if every tool call blocks on a
    /// human). A merge *conflict* no longer clears this flag — autoship stays armed and the
    /// conflict is surfaced via `autoShipConflict` (so it retries on the next merge). A hard
    /// merge *error* still disables autoship via `autoShipFailed`.
    public var autoShip: Bool
    /// Per-session override for the branch autoship merges this session's work into.
    /// `nil` → inherit the project's `resolvedAutoShipBranch` (which itself defaults to the
    /// project root repo's default branch). See `shipDestination(in:)`.
    public var shipBranch: String?
    public var favorite: Bool
    public var archived: Bool
    /// When this session was archived, or nil while it's active. Set the moment a chat is
    /// archived and cleared on unarchive. Drives the archived-worktree cleanup sweep, which
    /// reclaims the worktree checkout once a chat has stayed archived past the configured
    /// interval — using a dedicated stamp rather than `updatedAt` (which bumps on unrelated
    /// activity) so the "archived for longer than X" test is exact.
    public var archivedAt: Date?
    /// The chat reached a completed state (see `SessionSummary.isCompleted`) that the
    /// local user hasn't opened yet — drives the sidebar's "unread" dot. Set when a
    /// turn finishes while the chat isn't the open one; cleared when the user opens it.
    public var unseenCompletion: Bool
    /// Whether the most recent thing to happen in this session was a *successful*
    /// autoship merge (the merge-queue's "merged" note). Set/cleared by the event
    /// pipeline as transcript events arrive; drives the sidebar's shipped-package icon.
    /// A later user message, agent turn, or even a subsequent failed/conflict autoship
    /// note clears it — the marker tracks the latest outcome, not merely that autoship
    /// ran. See `NoteEvent.autoshipShipped`.
    public var lastEventWasAutoship: Bool
    /// Whether autoship disabled itself on a failure (merge conflict or merge error)
    /// and the user hasn't yet seen the failure — drives a distinct "autoship needs
    /// attention" sidebar icon. Sticky (unlike `lastEventWasAutoship`): set when the
    /// merge queue aborts and turns autoship off, cleared when the user re-enables
    /// autoship or opens the chat (seeing the failure note in the transcript). A clean
    /// user-initiated toggle-off does *not* set it.
    public var autoShipFailed: Bool
    /// Whether an autoship merge hit a *conflict* while autoship stayed ARMED — a sticky
    /// "needs attention" marker, distinct from `autoShipFailed` (which disarms on a hard
    /// error). Set when the merge queue reports a conflict; the branch is left intact and
    /// autoship keeps trying. Cleared when the work finally lands (the queue's next merge or
    /// the agent's own merge, observed in-container) or when the user re-arms autoship. Drives
    /// the same sidebar "needs attention" icon as `autoShipFailed`, but never implies "off".
    public var autoShipConflict: Bool
    /// Cached model-generated summary so it isn't regenerated on every open.
    public var summary: String?
    /// How the last turn ended (asking vs. done), refining `.awaitingInput` for the
    /// sidebar. `nil` until the Intelligence provider classifies the turn; cleared
    /// when a new turn starts. See `TurnDisposition`.
    public var lastTurnDisposition: TurnDisposition?
    /// A follow-up message the user submitted while a turn was still in flight, held by the
    /// `SessionController` to send the instant the current turn finishes. `nil` = nothing
    /// queued. Purely transient live state — never persisted (no `SessionRow` column), so it
    /// resets to `nil` on reload. Drives the composer's "queued" indicator.
    public var queuedMessage: String?
    /// When the local user last submitted a message in this chat. Drives the sidebar's
    /// session ordering (most-recently-messaged first), distinct from `updatedAt`, which
    /// bumps on *any* activity (agent output, diff refresh, autoship notes). `nil` for a
    /// chat that's never received a user message (and for sessions created before this
    /// was tracked — they fall back to `updatedAt` when sorting).
    public var lastUserMessageAt: Date?
    public let createdAt: Date
    public var updatedAt: Date

    public init(
        id: SessionID,
        projectID: ProjectID,
        backend: BackendID,
        backendSessionID: String? = nil,
        title: String,
        status: SessionStatus = .idle,
        worktreePath: String? = nil,
        branch: String? = nil,
        baseSHA: String? = nil,
        parentRef: String? = nil,
        rootRef: String? = nil,
        parentSessionID: SessionID? = nil,
        model: String? = nil,
        effort: String? = nil,
        lastSeq: UInt64 = 0,
        transcriptPath: String,
        nativeTranscriptPath: String? = nil,
        diffStat: DiffStat? = nil,
        ahead: Int? = nil,
        behind: Int? = nil,
        auto: Bool = false,
        autoShip: Bool = false,
        shipBranch: String? = nil,
        favorite: Bool = false,
        archived: Bool = false,
        archivedAt: Date? = nil,
        unseenCompletion: Bool = false,
        lastEventWasAutoship: Bool = false,
        autoShipFailed: Bool = false,
        autoShipConflict: Bool = false,
        summary: String? = nil,
        lastTurnDisposition: TurnDisposition? = nil,
        queuedMessage: String? = nil,
        lastUserMessageAt: Date? = nil,
        createdAt: Date,
        updatedAt: Date
    ) {
        self.id = id
        self.projectID = projectID
        self.backend = backend
        self.backendSessionID = backendSessionID
        self.title = title
        self.status = status
        self.worktreePath = worktreePath
        self.branch = branch
        self.baseSHA = baseSHA
        self.parentRef = parentRef
        self.rootRef = rootRef
        self.parentSessionID = parentSessionID
        self.model = model
        self.effort = effort
        self.lastSeq = lastSeq
        self.transcriptPath = transcriptPath
        self.nativeTranscriptPath = nativeTranscriptPath
        self.diffStat = diffStat
        self.ahead = ahead
        self.behind = behind
        self.auto = auto
        self.autoShip = autoShip
        self.shipBranch = shipBranch
        self.favorite = favorite
        self.archived = archived
        self.archivedAt = archivedAt
        self.unseenCompletion = unseenCompletion
        self.lastEventWasAutoship = lastEventWasAutoship
        self.autoShipFailed = autoShipFailed
        self.autoShipConflict = autoShipConflict
        self.summary = summary
        self.lastTurnDisposition = lastTurnDisposition
        self.queuedMessage = queuedMessage
        self.lastUserMessageAt = lastUserMessageAt
        self.createdAt = createdAt
        self.updatedAt = updatedAt
    }
}

extension Session {
    /// The branch this session's work autoship-merges (and manually integrates) into. Resolves
    /// the per-session override (`shipBranch`) first, then the project-wide `autoShipBranch`,
    /// then the legacy default — the branch this session forked from (`parentRef`), falling back
    /// to the project's `defaultBranch`. Both overrides default to nil, so out of the box this is
    /// the project root repo's default branch (and, for a nested child, its parent branch).
    public func shipDestination(in project: Project) -> GitRef {
        if let override = shipBranch?.trimmingCharacters(in: .whitespacesAndNewlines),
           !override.isEmpty {
            return GitRef(override)
        }
        if let projectBranch = project.autoShipBranch { return projectBranch }
        return parentRef.map { GitRef($0) } ?? project.defaultBranch
    }
}
