import Combine
import Foundation

enum ChatHistoryStore {
    private static let folderName = "chat_history"
    private static let debounceMs: UInt64 = 400_000_000

    static var rootURL: URL {
        let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
            .appendingPathComponent("studydeck", isDirectory: true)
        let dir = base.appendingPathComponent(folderName, isDirectory: true)
        try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
        return dir
    }

    static func fileURL(sessionId: String) -> URL {
        let safe = sessionId.map { char in
            char.isASCII && (char.isLetter || char.isNumber || char == "-" || char == "_") ? char : "_"
        }.reduce(into: "", { $0.append($1) })
        return rootURL.appendingPathComponent("\(safe).json")
    }

    static func load(sessionId: String) throws -> ChatHistoryFile {
        let url = fileURL(sessionId: sessionId)
        guard FileManager.default.fileExists(atPath: url.path) else {
            return emptyFile(sessionId: sessionId)
        }
        let data = try Data(contentsOf: url)
        var file = try JSONDecoder().decode(ChatHistoryFile.self, from: data)
        return migrate(&file, sessionId: sessionId)
    }

    static func save(_ file: ChatHistoryFile) throws {
        var copy = file
        copy.updatedAt = isoNow()
        let url = fileURL(sessionId: file.sessionId)
        let data = try JSONEncoder().encode(copy)
        try data.write(to: url, options: .atomic)
    }

    static func remove(sessionId: String) {
        let url = fileURL(sessionId: sessionId)
        try? FileManager.default.removeItem(at: url)
    }

    static func threadSummaries(from file: ChatHistoryFile) -> [ChatThreadSummary] {
        file.threads.map { thread in
            ChatThreadSummary(
                id: thread.id,
                title: thread.title,
                updatedAt: thread.updatedAt,
                messageCount: thread.messages.count,
                preview: ChatHistoryHelper.preview(from: thread.messages)
            )
        }.sorted { $0.updatedAt > $1.updatedAt }
    }

    static func newThreadId() -> String {
        "thr_\(Int(Date().timeIntervalSince1970 * 1_000))"
    }

    static func isoNow() -> String {
        ISO8601DateFormatter().string(from: Date())
    }

    private static func emptyFile(sessionId: String) -> ChatHistoryFile {
        let now = isoNow()
        let id = newThreadId()
        return ChatHistoryFile(
            sessionId: sessionId,
            updatedAt: now,
            activeThreadId: id,
            threads: [ChatThread(id: id, title: nil, updatedAt: now, messages: [])],
            messages: nil
        )
    }

    private static func migrate(_ file: inout ChatHistoryFile, sessionId: String) -> ChatHistoryFile {
        if file.sessionId.isEmpty {
            file.sessionId = sessionId
        }
        if file.threads.isEmpty {
            let now = file.updatedAt.isEmpty ? isoNow() : file.updatedAt
            let id = newThreadId()
            let legacy = ChatHistoryHelper.normalizeMessages(file.messages ?? [])
            file.threads = [ChatThread(
                id: id,
                title: ChatHistoryHelper.title(from: legacy),
                updatedAt: now,
                messages: legacy
            )]
            file.activeThreadId = id
            file.updatedAt = now
        } else if file.activeThreadId.isEmpty, let first = file.threads.first {
            file.activeThreadId = first.id
        }
        return file
    }
}

/// Debounced saver with hydrate guard (F109).
@MainActor
final class ChatHistoryPersister: ObservableObject {
    private var file: ChatHistoryFile?
    private var persistEnabled = false
    private var skipNext = false
    private var task: Task<Void, Never>?

    var isPersistEnabled: Bool { persistEnabled }

    func hydrate(sessionId: String) {
        persistEnabled = false
        skipNext = true
        do {
            file = try ChatHistoryStore.load(sessionId: sessionId)
            persistEnabled = true
        } catch {
            file = nil
            persistEnabled = false
        }
    }

    func currentFile() -> ChatHistoryFile? { file }

    func replaceFile(_ newFile: ChatHistoryFile, skipPersist: Bool = false) {
        file = newFile
        if skipPersist {
            skipNext = true
        }
    }

    func scheduleSave(messages: [ChatMessage], activeThreadId: String) {
        guard persistEnabled, var current = file else { return }
        if skipNext {
            skipNext = false
            return
        }
        let normalized = ChatHistoryHelper.normalizeMessages(messages)
        guard let idx = current.threads.firstIndex(where: { $0.id == activeThreadId }) else { return }
        current.threads[idx].messages = normalized
        current.threads[idx].updatedAt = ChatHistoryStore.isoNow()
        if current.threads[idx].title == nil {
            current.threads[idx].title = ChatHistoryHelper.title(from: normalized)
        }
        current.activeThreadId = activeThreadId
        file = current

        task?.cancel()
        task = Task {
            try? await Task.sleep(nanoseconds: 400_000_000)
            guard !Task.isCancelled, let file = self.file else { return }
            try? ChatHistoryStore.save(file)
        }
    }

    func flush() {
        task?.cancel()
        guard persistEnabled, let file else { return }
        try? ChatHistoryStore.save(file)
    }
}
