import Foundation

/// Line 0 of every transcript file (BACKEND_PROTOCOL §6).
public struct SessionHeader: Sendable, Codable, Equatable {
    public let version: Int
    public let sessionID: SessionID
    public let backend: BackendID
    public var backendSessionID: String?
    public let worktree: WorktreePath
    public let model: String?
    public let nativeTranscriptPath: String?
    public let createdAt: Date

    public init(
        version: Int = 1,
        sessionID: SessionID,
        backend: BackendID,
        backendSessionID: String? = nil,
        worktree: WorktreePath,
        model: String? = nil,
        nativeTranscriptPath: String? = nil,
        createdAt: Date
    ) {
        self.version = version
        self.sessionID = sessionID
        self.backend = backend
        self.backendSessionID = backendSessionID
        self.worktree = worktree
        self.model = model
        self.nativeTranscriptPath = nativeTranscriptPath
        self.createdAt = createdAt
    }
}

/// Append-only JSONL transcript — the UI's source of truth (locked decision).
/// The canonical, session-wide `seq` is (re)assigned here at append time, the
/// single serialization point (RUNTIME §2.1): adapters' provisional seq is only
/// an ordering hint, and synthetic events (setup logs, approval echoes) need
/// numbering too.
public actor TranscriptWriter {
    private let url: URL
    private let fileHandle: FileHandle
    private let encoder: JSONEncoder
    public private(set) var lastSeq: UInt64

    public init(url: URL, header: SessionHeader) throws {
        self.url = url
        try FileManager.default.createDirectory(
            at: url.deletingLastPathComponent(), withIntermediateDirectories: true)

        encoder = JSONEncoder()
        encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
        encoder.dateEncodingStrategy = .iso8601

        FileManager.default.createFile(atPath: url.path, contents: nil)
        fileHandle = try FileHandle(forWritingTo: url)
        lastSeq = 0

        var headerLine = try encoder.encode(header)
        headerLine.append(0x0A)
        try fileHandle.write(contentsOf: headerLine)
    }

    /// Reopen an existing transcript for appending (resume after relaunch). Does not
    /// rewrite the header; continues numbering from `lastSeq`.
    public init(appendingTo url: URL, lastSeq: UInt64) throws {
        self.url = url
        encoder = JSONEncoder()
        encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
        encoder.dateEncodingStrategy = .iso8601
        fileHandle = try FileHandle(forWritingTo: url)
        try fileHandle.seekToEnd()
        self.lastSeq = lastSeq
    }

    /// Appends and returns the event re-stamped with its canonical seq.
    @discardableResult
    public func append(_ event: AgentEvent) throws -> AgentEvent {
        lastSeq += 1
        let canonical = event.withSeq(lastSeq)
        var line = try encoder.encode(canonical)
        line.append(0x0A)
        try fileHandle.write(contentsOf: line)
        return canonical
    }

    /// Rewrite the transcript so it ends at `events` (the session header on line 0 is
    /// preserved verbatim), discarding everything after — used when the user reverts the
    /// conversation to an earlier point. Events keep their existing canonical seqs; the
    /// append cursor is repositioned to the new end and `lastSeq` reset, so the next turn
    /// continues numbering cleanly. Append-only is the steady state (RUNTIME §2.1); this
    /// is the one deliberate, user-initiated rewrite.
    public func truncate(to events: [AgentEvent]) throws {
        let existing = try Data(contentsOf: url)
        let headerLine: Data =
            existing.firstIndex(of: 0x0A).map { existing.subdata(in: existing.startIndex..<$0) }
            ?? existing
        var out = headerLine
        out.append(0x0A)
        for event in events {
            out.append(try encoder.encode(event))
            out.append(0x0A)
        }
        try fileHandle.seek(toOffset: 0)
        try fileHandle.write(contentsOf: out)
        try fileHandle.truncate(atOffset: UInt64(out.count))
        try fileHandle.synchronize()
        lastSeq = events.last?.seq ?? 0
    }

    /// Durability point — called at turn boundaries and approval persistence
    /// (RUNTIME open-Q #2: batched fsync).
    public func sync() throws {
        try fileHandle.synchronize()
    }

    public func close() throws {
        try fileHandle.close()
    }
}

/// Reads a transcript back for resume/history paging. No CLI involved — the
/// transcript is canonical (RUNTIME §6).
public struct TranscriptReader: Sendable {
    public let url: URL

    public init(url: URL) {
        self.url = url
    }

    public func read() throws -> (header: SessionHeader, events: [AgentEvent]) {
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601

        let data = try Data(contentsOf: url)
        var lines = data.split(separator: 0x0A, omittingEmptySubsequences: true)
        guard !lines.isEmpty else {
            throw CocoaError(.fileReadCorruptFile, userInfo: [NSFilePathErrorKey: url.path])
        }
        let header = try decoder.decode(SessionHeader.self, from: lines.removeFirst())
        let events = try lines.map { try decoder.decode(AgentEvent.self, from: $0) }
        return (header, events)
    }
}
