import Foundation
import NucleicProtocol

/// On-disk cache of transcripts this Mac has synced from other devices (full-transcript mesh
/// sync). Each mirror is a verbatim copy of a peer's append-only JSONL transcript, so a device
/// can render a peer's *full* history instantly on open — instead of the 200-event tail a cold
/// `subscribe` returns — and can itself serve that history to a third device when the owner is
/// offline ("get it from another device synced to the host").
///
/// Layout: `<root>/<sourceDeviceID>/<sessionID>/transcript.jsonl`, a sibling of the owned
/// `sessions/` tree. Keying by source device means mirrors never collide with owned sessions and
/// unpairing a device is a single directory drop (`removeSource`). This store is deliberately
/// self-contained — it holds no rows in the session table; a mirror is a cache located by
/// `sessionID`, and its provenance rides its path — so it needs no schema migration.
///
/// Writes are verbatim (`TranscriptWriter.appendVerbatim`): the source already assigned canonical
/// seqs, and re-stamping would break the seq-cursor dedup the sync coordinator relies on.
public actor TranscriptMirrorStore {
    private let root: URL

    private struct Entry {
        let sourceDeviceID: String
        let url: URL
        var heldSeq: UInt64
    }

    /// Lazily built on first access by scanning `root`. `nil` until then.
    private var index: [SessionID: Entry]?

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

    /// The highest **contiguous** seq held for `id`, or 0 if nothing is mirrored — the exclusive
    /// `afterSeq` cursor a top-up fetch resumes from.
    public func heldSeq(_ id: SessionID) -> UInt64 {
        ensureIndex()[id]?.heldSeq ?? 0
    }

    /// The mirror transcript file for `id`, if one exists — the on-disk source a non-owning peer
    /// serves `fetchTranscript` from, and the seed for an instant full-history open.
    public func url(_ id: SessionID) -> URL? {
        ensureIndex()[id]?.url
    }

    /// The device this session's mirror was synced from (the `HostID` in its path), if mirrored.
    public func sourceDeviceID(_ id: SessionID) -> String? {
        ensureIndex()[id]?.sourceDeviceID
    }

    /// Whether a mirror exists for `id`.
    public func has(_ id: SessionID) -> Bool {
        ensureIndex()[id] != nil
    }

    /// Write a freshly fetched full transcript, replacing any existing mirror for `id`. `header`
    /// is line 0; `events` are written verbatim in ascending seq order. Returns the new held seq.
    @discardableResult
    public func writeFull(
        _ id: SessionID, sourceDeviceID: String, header: SessionHeader, events: [AgentEvent]
    ) async throws -> UInt64 {
        var map = ensureIndex()
        // Drop any prior mirror (possibly from a different source device) so a full refresh is
        // authoritative and we don't leave a stale directory behind.
        if let old = map[id] {
            try? FileManager.default.removeItem(at: old.url.deletingLastPathComponent())
        }
        let fileURL = fileURL(for: id, sourceDeviceID: sourceDeviceID)
        let writer = try TranscriptWriter(url: fileURL, header: header)
        var held: UInt64 = 0
        for event in events.sorted(by: { $0.seq < $1.seq }) {
            try await writer.appendVerbatim(event)
            held = event.seq
        }
        try await writer.close()
        map[id] = Entry(sourceDeviceID: sourceDeviceID, url: fileURL, heldSeq: held)
        index = map
        return held
    }

    /// Append the contiguous run of `events` that immediately follows the current held seq,
    /// stopping at the first gap (a non-contiguous stream is treated as incomplete — the caller
    /// tries another holder). Returns the new held seq. Throws if no mirror exists yet for `id`
    /// (the caller must `writeFull` first).
    @discardableResult
    public func append(_ id: SessionID, events: [AgentEvent]) async throws -> UInt64 {
        var map = ensureIndex()
        guard var entry = map[id] else { throw MirrorError.notMirrored(id) }
        let writer = try TranscriptWriter(appendingTo: entry.url, lastSeq: entry.heldSeq)
        var held = entry.heldSeq
        for event in events.sorted(by: { $0.seq < $1.seq }) where event.seq == held + 1 {
            try await writer.appendVerbatim(event)
            held = event.seq
        }
        try await writer.close()
        entry.heldSeq = held
        map[id] = entry
        index = map
        return held
    }

    /// Drop the mirror for one session (it was archived, moved away, or is now owned here).
    public func remove(_ id: SessionID) {
        var map = ensureIndex()
        if let entry = map.removeValue(forKey: id) {
            try? FileManager.default.removeItem(at: entry.url.deletingLastPathComponent())
            index = map
        }
    }

    /// Drop every mirror synced from one device (it was unpaired / revoked) — a directory drop.
    public func removeSource(_ deviceID: String) {
        try? FileManager.default.removeItem(at: root.appendingPathComponent(deviceID, isDirectory: true))
        // Rebuild lazily rather than filter, so a concurrent scan can't resurrect stale entries.
        index = nil
    }

    // MARK: - Index

    private func fileURL(for id: SessionID, sourceDeviceID: String) -> URL {
        root.appendingPathComponent(sourceDeviceID, isDirectory: true)
            .appendingPathComponent(id.rawValue, isDirectory: true)
            .appendingPathComponent("transcript.jsonl")
    }

    /// Build the `sessionID → Entry` map by scanning `<root>/<device>/<session>/transcript.jsonl`.
    /// Cached after the first call; invalidated (set to nil) on `removeSource`.
    private func ensureIndex() -> [SessionID: Entry] {
        if let index { return index }
        var map: [SessionID: Entry] = [:]
        let fm = FileManager.default
        let devices = (try? fm.contentsOfDirectory(
            at: root, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles])) ?? []
        for deviceDir in devices {
            let deviceID = deviceDir.lastPathComponent
            let sessions = (try? fm.contentsOfDirectory(
                at: deviceDir, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles])) ?? []
            for sessionDir in sessions {
                let fileURL = sessionDir.appendingPathComponent("transcript.jsonl")
                guard fm.fileExists(atPath: fileURL.path) else { continue }
                let id = SessionID(rawValue: sessionDir.lastPathComponent)
                map[id] = Entry(
                    sourceDeviceID: deviceID, url: fileURL,
                    heldSeq: Self.tailSeq(ofFileAt: fileURL))
            }
        }
        index = map
        return map
    }

    /// The seq of the last event line in a transcript file (0 if only the header is present).
    /// Reads just the tail — no need to decode every event to learn the held cursor.
    private static func tailSeq(ofFileAt url: URL) -> UInt64 {
        guard let data = try? Data(contentsOf: url) else { return 0 }
        let lines = data.split(separator: 0x0A, omittingEmptySubsequences: true)
        // Line 0 is the header; a lone header ⇒ nothing held.
        guard lines.count > 1, let last = lines.last else { return 0 }
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601
        return (try? decoder.decode(AgentEvent.self, from: last))?.seq ?? 0
    }

    public enum MirrorError: Error {
        case notMirrored(SessionID)
    }
}
