import Foundation final class SentenceExplanationStore { static let shared = SentenceExplanationStore() private let root: URL init() { let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] root = documents.appendingPathComponent("SentenceExplanations", isDirectory: true) try? FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) } func load(mediaId: String, sentenceIndex: Int) -> String? { let url = fileURL(mediaId: mediaId, sentenceIndex: sentenceIndex) guard FileManager.default.fileExists(atPath: url.path) else { return nil } let text = try? String(contentsOf: url, encoding: .utf8) let trimmed = text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" return trimmed.isEmpty ? nil : trimmed } @discardableResult func save(mediaId: String, sentenceIndex: Int, explanation: String) -> Bool { let trimmed = explanation.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return false } let url = fileURL(mediaId: mediaId, sentenceIndex: sentenceIndex) try? FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true) do { try trimmed.write(to: url, atomically: true, encoding: .utf8) return true } catch { return false } } func sync(mediaId: String, sentences: [SentenceBoundary]) { guard !sentences.isEmpty else { return } let directory = root.appendingPathComponent(Self.sanitize(mediaId), isDirectory: true) let revision = Self.revision(for: sentences) let marker = directory.appendingPathComponent("revision.txt") let previous = (try? String(contentsOf: marker, encoding: .utf8))? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" guard previous != revision else { return } try? FileManager.default.removeItem(at: directory) try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) try? revision.write(to: marker, atomically: true, encoding: .utf8) } private func fileURL(mediaId: String, sentenceIndex: Int) -> URL { let directory = root.appendingPathComponent(Self.sanitize(mediaId), isDirectory: true) return directory.appendingPathComponent("\(sentenceIndex).txt") } static func revision(for sentences: [SentenceBoundary]) -> String { sentences.map { sentence in let text = (sentence.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines) return "\(sentence.index)\t\(text)" }.joined(separator: "\n") } static func sanitize(_ mediaId: String) -> String { let allowed = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "._-")) let filtered = mediaId.lowercased().unicodeScalars.map { allowed.contains($0) ? Character($0) : "_" } let value = String(filtered).trimmingCharacters(in: CharacterSet(charactersIn: "_")) return value.isEmpty || value == "." || value == ".." ? "unknown" : value } }