import Foundation

/// The normalized event model (BACKEND_PROTOCOL §3). Every backend translates its
/// native output into this; UI, sync, and persistence consume only this.
public struct AgentEvent: Sendable, Codable, Equatable {
    public let sessionID: SessionID
    /// Monotonic per session. Adapters stamp a provisional value; the canonical
    /// seq is (re)assigned by TranscriptWriter at append (RUNTIME §2.1).
    public let seq: UInt64
    public let at: Date
    public let backend: BackendID
    /// Original CLI type, for debugging/forensics.
    public let nativeType: String?
    public let kind: Kind

    public init(
        sessionID: SessionID,
        seq: UInt64,
        at: Date,
        backend: BackendID,
        nativeType: String?,
        kind: Kind
    ) {
        self.sessionID = sessionID
        self.seq = seq
        self.at = at
        self.backend = backend
        self.nativeType = nativeType
        self.kind = kind
    }

    /// Copy with a different seq — used by TranscriptWriter to assign the canonical value.
    public func withSeq(_ seq: UInt64) -> AgentEvent {
        AgentEvent(
            sessionID: sessionID, seq: seq, at: at, backend: backend,
            nativeType: nativeType, kind: kind)
    }
}

extension AgentEvent {
    public enum Kind: Sendable, Equatable {
        case sessionStarted(SessionStarted)
        /// A user turn we injected (the agent CLI doesn't echo input in stream mode);
        /// part of the transcript so chat history is complete and replayable.
        case userText(TextChunk)
        case assistantText(TextChunk)
        case thinking(TextChunk)
        case toolCallStarted(ToolCall)
        case toolCallInputDelta(ToolInputDelta)
        case toolCallCompleted(ToolCall)
        case toolResult(ToolResult)
        case fileChange(FileChange)
        case approvalRequested(ApprovalRequest)
        case approvalResolved(ApprovalResolved)
        case usage(Usage)
        case rateLimit(RateLimit)
        case turnCompleted(TurnCompleted)
        case runFinished(RunFinished)
        case error(AgentError)
        /// A system-generated log line injected into the transcript — e.g. a
        /// user-triggered git action from the header. Not produced by any backend;
        /// rendered as a muted note so it reads as app chatter, not the agent's words.
        case note(NoteEvent)
        /// Unrecognized native event, passthrough (drift signal — OBSERVABILITY A.5).
        case raw(Raw)

        /// Stable tag, used as the Codable discriminator and in logs/metrics.
        public var tag: String {
            switch self {
            case .sessionStarted: "sessionStarted"
            case .userText: "userText"
            case .assistantText: "assistantText"
            case .thinking: "thinking"
            case .toolCallStarted: "toolCallStarted"
            case .toolCallInputDelta: "toolCallInputDelta"
            case .toolCallCompleted: "toolCallCompleted"
            case .toolResult: "toolResult"
            case .fileChange: "fileChange"
            case .approvalRequested: "approvalRequested"
            case .approvalResolved: "approvalResolved"
            case .usage: "usage"
            case .rateLimit: "rateLimit"
            case .turnCompleted: "turnCompleted"
            case .runFinished: "runFinished"
            case .error: "error"
            case .note: "note"
            case .raw: "raw"
            }
        }
    }
}

// MARK: - Payloads (BACKEND_PROTOCOL §3.1)

public struct SessionStarted: Sendable, Codable, Equatable {
    /// Native session id — captured for resume.
    public let backendSessionID: String
    public let model: String
    public let cwd: String
    public let toolNames: [String]
    public let nativeTranscriptPath: String?

    public init(
        backendSessionID: String, model: String, cwd: String,
        toolNames: [String], nativeTranscriptPath: String? = nil
    ) {
        self.backendSessionID = backendSessionID
        self.model = model
        self.cwd = cwd
        self.toolNames = toolNames
        self.nativeTranscriptPath = nativeTranscriptPath
    }
}

public struct TextChunk: Sendable, Codable, Equatable {
    /// Groups deltas of one assistant message.
    public let messageID: String
    /// Delta if isPartial, else full text.
    public let text: String
    public let isPartial: Bool
    /// When this message was produced *inside* a spawned subagent, the id of the parent `Task`
    /// tool call (Claude's `parent_tool_use_id`); nil for the main agent's own turns. Lets the
    /// transcript nest a subagent's prose/thinking under its card instead of leaking it into
    /// the top-level conversation. Optional, so older transcripts (which lack it) decode as nil.
    public let parentToolCallID: String?

    public init(messageID: String, text: String, isPartial: Bool, parentToolCallID: String? = nil) {
        self.messageID = messageID
        self.text = text
        self.isPartial = isPartial
        self.parentToolCallID = parentToolCallID
    }
}

public struct ToolCall: Sendable, Codable, Equatable {
    public let toolCallID: String
    public let name: String
    public let input: JSONValue
    public let parentToolCallID: String?

    public init(toolCallID: String, name: String, input: JSONValue, parentToolCallID: String? = nil) {
        self.toolCallID = toolCallID
        self.name = name
        self.input = input
        self.parentToolCallID = parentToolCallID
    }
}

public struct ToolInputDelta: Sendable, Codable, Equatable {
    public let toolCallID: String
    public let partialJSON: String

    public init(toolCallID: String, partialJSON: String) {
        self.toolCallID = toolCallID
        self.partialJSON = partialJSON
    }
}

public struct ToolResult: Sendable, Codable, Equatable {
    public let toolCallID: String
    public let content: JSONValue
    public let isError: Bool

    public init(toolCallID: String, content: JSONValue, isError: Bool) {
        self.toolCallID = toolCallID
        self.content = content
        self.isError = isError
    }
}

public struct FileChange: Sendable, Codable, Equatable {
    public enum ChangeKind: String, Sendable, Codable {
        case add, update, delete, rename
    }
    public let path: String
    public let kind: ChangeKind
    public let toolCallID: String?
    public let diffHint: String?

    public init(path: String, kind: ChangeKind, toolCallID: String? = nil, diffHint: String? = nil) {
        self.path = path
        self.kind = kind
        self.toolCallID = toolCallID
        self.diffHint = diffHint
    }
}

public struct Usage: Sendable, Codable, Equatable {
    public let inputTokens: Int?
    public let cachedInputTokens: Int?
    /// Tokens written to the prompt cache this turn (`cache_creation_input_tokens`).
    /// Billed at a premium; a healthy multi-turn session writes the stable prefix
    /// once and then reads it. A large value on *every* turn is the signature of a
    /// prefix that keeps changing (cache thrash) — see token-cost diagnostics.
    public let cacheCreationInputTokens: Int?
    public let outputTokens: Int?
    public let reasoningTokens: Int?
    public let costUSD: Double?
    /// The input size (regular + cache-read + cache-creation) of the turn's *final*
    /// model call — i.e. the live context-window occupancy. Distinct from the token
    /// fields above, which are summed across every call in an agentic turn (and so far
    /// exceed the window): use this, not their sum, for context-usage %. From the result
    /// `usage.iterations` last entry; nil if the backend doesn't report per-call usage.
    public let contextInputTokens: Int?

    public init(
        inputTokens: Int? = nil, cachedInputTokens: Int? = nil,
        cacheCreationInputTokens: Int? = nil, outputTokens: Int? = nil,
        reasoningTokens: Int? = nil, costUSD: Double? = nil,
        contextInputTokens: Int? = nil
    ) {
        self.inputTokens = inputTokens
        self.cachedInputTokens = cachedInputTokens
        self.cacheCreationInputTokens = cacheCreationInputTokens
        self.outputTokens = outputTokens
        self.reasoningTokens = reasoningTokens
        self.costUSD = costUSD
        self.contextInputTokens = contextInputTokens
    }

    /// Every token this turn moved — new input, cache reads, cache writes, output, and any
    /// separately-reported reasoning — folded into one "tokens used" figure. The categories are
    /// disjoint (a provider's `inputTokens` excludes the cached portion, and `reasoningTokens` is
    /// reported apart from `outputTokens` only when it isn't already folded into it), so summing
    /// them never double-counts. This is what the home dashboard totals and what shades the
    /// activity grid by day. Distinct from ``contextInputTokens``, which is a single call's
    /// window occupancy, not the turn's throughput.
    public var totalTokens: Int {
        (inputTokens ?? 0) + (cachedInputTokens ?? 0) + (cacheCreationInputTokens ?? 0)
            + (outputTokens ?? 0) + (reasoningTokens ?? 0)
    }
}

/// Rate-limit telemetry the CLI emits out of band (`rate_limit_event`, claude
/// 2.1.167 — M0 finding 6). Surfaced as a first-class event so the UI can warn the
/// user before a window resets rather than letting it pass through as `.raw`.
public struct RateLimit: Sendable, Codable, Equatable {
    /// e.g. "allowed" | "allowed_warning" | "rejected".
    public let status: String?
    /// e.g. "five_hour".
    public let rateLimitType: String?
    /// When the current window resets.
    public let resetsAt: Date?
    public let isUsingOverage: Bool?
    public let overageStatus: String?

    public init(
        status: String? = nil, rateLimitType: String? = nil, resetsAt: Date? = nil,
        isUsingOverage: Bool? = nil, overageStatus: String? = nil
    ) {
        self.status = status
        self.rateLimitType = rateLimitType
        self.resetsAt = resetsAt
        self.isUsingOverage = isUsingOverage
        self.overageStatus = overageStatus
    }
}

public struct TurnCompleted: Sendable, Codable, Equatable {
    public let stopReason: String?
    public let usage: Usage?

    public init(stopReason: String?, usage: Usage?) {
        self.stopReason = stopReason
        self.usage = usage
    }
}

public struct RunFinished: Sendable, Codable, Equatable {
    public enum Outcome: String, Sendable, Codable {
        case completed, interrupted, errored, maxTurns
    }
    public let outcome: Outcome
    public let finalText: String?
    public let totalUsage: Usage?
    public let durationMs: Int?

    public init(outcome: Outcome, finalText: String? = nil, totalUsage: Usage? = nil, durationMs: Int? = nil) {
        self.outcome = outcome
        self.finalText = finalText
        self.totalUsage = totalUsage
        self.durationMs = durationMs
    }
}

/// Structured file-lock lifecycle detail carried on a lock `NoteEvent` (LOCKING §4): which
/// moment it records and the repo-relative paths it concerns. Lets the transcript projection
/// **fold the event onto the edit's tool card** it brackets (by path overlap) so the lock reads
/// as part of that edit — rather than as a free-floating row that, because the card is anchored
/// at the tool-use *start* (before the lock is acquired at the permission gate), always sorts
/// *after* the call. `nil` on non-lock notes and on the re-ground / update-conflict notes, which
/// carry no structured paths.
public struct NoteLock: Sendable, Codable, Equatable {
    public enum State: String, Sendable, Codable {
        /// Queued — blocked waiting for these paths, which another session holds.
        case waiting
        /// Newly granted — the session now holds these paths and may edit.
        case acquired
        /// Released these paths.
        case released
    }
    public let state: State
    /// Repo-relative, normalized, sorted paths this lock moment concerns.
    public let paths: [String]
    public init(state: State, paths: [String]) {
        self.state = state
        self.paths = paths
    }
}

public struct NoteEvent: Sendable, Codable, Equatable {
    /// The log line to show in the transcript.
    public let text: String
    /// Optional SF Symbol for the row's leading glyph (defaults to a git-branch icon).
    public let icon: String?
    /// Whether this note records a *successful* autoship merge — as opposed to an
    /// in-progress, conflict, or failure note (which all share `autoshipPrefix`).
    /// Only a success lights the sidebar's shipped-package marker, which must track
    /// the most recent autoship's *outcome*, not merely that autoship ran. `false`
    /// for every non-autoship note.
    public let autoshipShipped: Bool
    /// Whether this note records a file-lock lifecycle moment (waiting/acquired/released).
    /// Lets the transcript hide the whole class when the "Show lock events" setting is off,
    /// without coupling the view to the notes' exact wording or glyphs. `false` for every
    /// non-lock note.
    public let lockEvent: Bool
    /// Structured lock detail when this is a lock note, so the transcript can fold it onto the
    /// edit's tool card instead of leaving it as a standalone row. `nil` for non-lock notes and
    /// for the re-ground / update-conflict notes (which set `lockEvent` but carry no paths).
    public let lock: NoteLock?

    public init(
        text: String, icon: String? = nil, autoshipShipped: Bool = false,
        lockEvent: Bool = false, lock: NoteLock? = nil
    ) {
        self.text = text
        self.icon = icon
        self.autoshipShipped = autoshipShipped
        self.lockEvent = lockEvent
        self.lock = lock
    }

    private enum CodingKeys: String, CodingKey { case text, icon, autoshipShipped, lockEvent, lock }

    public init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        self.text = try c.decode(String.self, forKey: .text)
        self.icon = try c.decodeIfPresent(String.self, forKey: .icon)
        // Tolerate notes persisted before this field existed: they predate the
        // success marker and default to "not a successful ship".
        self.autoshipShipped = try c.decodeIfPresent(Bool.self, forKey: .autoshipShipped) ?? false
        // Likewise tolerate notes predating the lock-event flag (default "not a lock note").
        self.lockEvent = try c.decodeIfPresent(Bool.self, forKey: .lockEvent) ?? false
        // Older lock notes have no structured detail; they render as plain standalone rows.
        self.lock = try c.decodeIfPresent(NoteLock.self, forKey: .lock)
    }

    public func encode(to encoder: Encoder) throws {
        var c = encoder.container(keyedBy: CodingKeys.self)
        try c.encode(text, forKey: .text)
        try c.encodeIfPresent(icon, forKey: .icon)
        // Keep transcript lines lean — only emit each flag when it's actually set.
        if autoshipShipped { try c.encode(autoshipShipped, forKey: .autoshipShipped) }
        if lockEvent { try c.encode(lockEvent, forKey: .lockEvent) }
        try c.encodeIfPresent(lock, forKey: .lock)
    }

    /// Prefix shared by every autoship transcript note (merging/merged/conflict/failed).
    /// Lets the event pipeline recognize when autoship was the most recent thing to
    /// happen in a session without coupling to each outcome's exact wording.
    public static let autoshipPrefix = "Autoship:"

    /// Whether this note records autoship activity of any outcome. Use
    /// `autoshipShipped` to single out a *successful* merge.
    public var isAutoship: Bool { text.hasPrefix(Self.autoshipPrefix) }

    /// Prefix shared by every nvrsion transcript note (landed / pre-land-rejected /
    /// could-not-land). The nvrsion-mode analog of `autoshipPrefix`: where autoship logs
    /// commit/merge lines into a worktree session's chat, nvrsion logs its per-edit
    /// trunk lands here, so the pipeline/UI can recognize them as a class without coupling
    /// to each line's exact wording.
    public static let nvrsionPrefix = "nvrsion:"

    /// Whether this note records nvrsion trunk activity of any outcome.
    public var isNvrsion: Bool { text.hasPrefix(Self.nvrsionPrefix) }
}

public struct AgentError: Sendable, Codable, Equatable {
    /// e.g. api_retry (true) vs fatal (false).
    public let recoverable: Bool
    public let message: String
    public let native: JSONValue?

    public init(recoverable: Bool, message: String, native: JSONValue? = nil) {
        self.recoverable = recoverable
        self.message = message
        self.native = native
    }
}

public struct Raw: Sendable, Codable, Equatable {
    public let native: JSONValue

    public init(native: JSONValue) { self.native = native }
}

// MARK: - Kind Codable
// Wire shape: {"type":"assistantText", ...payload fields merged at the same level}.
// Payload key sets are disjoint from "type", so encoding the payload into the same
// encoder flattens cleanly and keeps transcript lines readable.

extension AgentEvent.Kind: Codable {
    private enum TypeKey: String, CodingKey { case type }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: TypeKey.self)
        let tag = try container.decode(String.self, forKey: .type)
        switch tag {
        case "sessionStarted": self = .sessionStarted(try SessionStarted(from: decoder))
        case "userText": self = .userText(try TextChunk(from: decoder))
        case "assistantText": self = .assistantText(try TextChunk(from: decoder))
        case "thinking": self = .thinking(try TextChunk(from: decoder))
        case "toolCallStarted": self = .toolCallStarted(try ToolCall(from: decoder))
        case "toolCallInputDelta": self = .toolCallInputDelta(try ToolInputDelta(from: decoder))
        case "toolCallCompleted": self = .toolCallCompleted(try ToolCall(from: decoder))
        case "toolResult": self = .toolResult(try ToolResult(from: decoder))
        case "fileChange": self = .fileChange(try FileChange(from: decoder))
        case "approvalRequested": self = .approvalRequested(try ApprovalRequest(from: decoder))
        case "approvalResolved": self = .approvalResolved(try ApprovalResolved(from: decoder))
        case "usage": self = .usage(try Usage(from: decoder))
        case "rateLimit": self = .rateLimit(try RateLimit(from: decoder))
        case "turnCompleted": self = .turnCompleted(try TurnCompleted(from: decoder))
        case "runFinished": self = .runFinished(try RunFinished(from: decoder))
        case "error": self = .error(try AgentError(from: decoder))
        case "note": self = .note(try NoteEvent(from: decoder))
        case "raw": self = .raw(try Raw(from: decoder))
        default:
            throw DecodingError.dataCorruptedError(
                forKey: .type, in: container, debugDescription: "Unknown event kind \(tag)")
        }
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: TypeKey.self)
        try container.encode(tag, forKey: .type)
        switch self {
        case .sessionStarted(let p): try p.encode(to: encoder)
        case .userText(let p): try p.encode(to: encoder)
        case .assistantText(let p): try p.encode(to: encoder)
        case .thinking(let p): try p.encode(to: encoder)
        case .toolCallStarted(let p): try p.encode(to: encoder)
        case .toolCallInputDelta(let p): try p.encode(to: encoder)
        case .toolCallCompleted(let p): try p.encode(to: encoder)
        case .toolResult(let p): try p.encode(to: encoder)
        case .fileChange(let p): try p.encode(to: encoder)
        case .approvalRequested(let p): try p.encode(to: encoder)
        case .approvalResolved(let p): try p.encode(to: encoder)
        case .usage(let p): try p.encode(to: encoder)
        case .rateLimit(let p): try p.encode(to: encoder)
        case .turnCompleted(let p): try p.encode(to: encoder)
        case .runFinished(let p): try p.encode(to: encoder)
        case .error(let p): try p.encode(to: encoder)
        case .note(let p): try p.encode(to: encoder)
        case .raw(let p): try p.encode(to: encoder)
        }
    }
}
