import Foundation enum PostDraftValidationError: LocalizedError { case emptyTopic case emptySubreddit case invalidSubreddit case emptyTitle case titleTooLong case bodyTooLong case missingImage case invalidLinkURL case emptyLinkURL case invalidVideoURL case emptyVideoURL case insufficientPollOptions case emptyPollOption var errorDescription: String? { switch self { case .emptyTopic: "Enter a topic or keywords for AI generation." case .emptySubreddit: "Enter a subreddit to tailor the post." case .invalidSubreddit: "Subreddit names must be 3–21 characters and use only letters, numbers, or underscores." case .emptyTitle: "Add a post title before exporting or submitting." case .titleTooLong: "Title must be 300 characters or fewer." case .bodyTooLong: "Post body must be 40,000 characters or fewer." case .missingImage: "Upload an image for image posts." case .invalidLinkURL: "Enter a valid http or https URL for the link." case .emptyLinkURL: "Enter a URL for link posts." case .invalidVideoURL: "Enter a valid http or https URL for the video." case .emptyVideoURL: "Enter a video URL for video posts." case .insufficientPollOptions: "Polls need at least two non-empty options." case .emptyPollOption: "Fill in all poll options or remove empty ones." } } } enum PostDraftValidator { static let maxTitleLength = 300 static let maxBodyLength = 40_000 static func normalizedSubreddit(_ raw: String) -> String { raw.trimmingCharacters(in: .whitespaces) .replacingOccurrences(of: "^r/", with: "", options: .regularExpression) .lowercased() } static func isValidSubreddit(_ raw: String) -> Bool { let name = normalizedSubreddit(raw) guard name.count >= 3, name.count <= 21 else { return false } let pattern = "^[a-z0-9_]+$" return name.range(of: pattern, options: .regularExpression) != nil } static func isValidHTTPURL(_ string: String) -> Bool { let trimmed = string.trimmingCharacters(in: .whitespaces) guard let url = URL(string: trimmed), let scheme = url.scheme?.lowercased(), scheme == "http" || scheme == "https", url.host != nil else { return false } return true } static func validateForGeneration(topic: String, subreddit: String) throws { guard !topic.trimmingCharacters(in: .whitespaces).isEmpty else { throw PostDraftValidationError.emptyTopic } guard !subreddit.trimmingCharacters(in: .whitespaces).isEmpty else { throw PostDraftValidationError.emptySubreddit } guard isValidSubreddit(subreddit) else { throw PostDraftValidationError.invalidSubreddit } } static func validateForExport(_ draft: PostDraft) throws { try validateForGeneration(topic: draft.topic, subreddit: draft.subreddit) let title = draft.title.trimmingCharacters(in: .whitespaces) guard !title.isEmpty else { throw PostDraftValidationError.emptyTitle } guard title.count <= maxTitleLength else { throw PostDraftValidationError.titleTooLong } guard draft.body.count <= maxBodyLength else { throw PostDraftValidationError.bodyTooLong } switch draft.postType { case .text: break case .image: guard draft.imageFileURL != nil else { throw PostDraftValidationError.missingImage } case .link: let url = draft.linkURL.trimmingCharacters(in: .whitespaces) guard !url.isEmpty else { throw PostDraftValidationError.emptyLinkURL } guard isValidHTTPURL(url) else { throw PostDraftValidationError.invalidLinkURL } case .video: let url = draft.videoURL.trimmingCharacters(in: .whitespaces) guard !url.isEmpty else { throw PostDraftValidationError.emptyVideoURL } guard isValidHTTPURL(url) else { throw PostDraftValidationError.invalidVideoURL } case .poll: let filled = draft.pollOptions.map(\.text).map { $0.trimmingCharacters(in: .whitespaces) }.filter { !$0.isEmpty } guard filled.count >= 2 else { throw PostDraftValidationError.insufficientPollOptions } } } static func canGenerate(from draft: PostDraft, isGenerating: Bool) -> Bool { guard !isGenerating else { return false } return (try? validateForGeneration(topic: draft.topic, subreddit: draft.subreddit)) != nil } static func canExport(_ draft: PostDraft) -> Bool { (try? validateForExport(draft)) != nil } }