import Foundation // MARK: - Content Models for RTSDA v2.0 struct ChurchEvent: Identifiable, Codable, Hashable { let id: String let title: String let description: String // Raw ISO timestamps for calendar/system APIs let startTime: String let endTime: String // Formatted display strings from Rust (RTSDA Architecture Rules compliance) let formattedTime: String // "6:00 PM - 8:00 PM" let formattedDate: String // "Friday, August 15, 2025" let formattedDateTime: String // "Friday, August 15, 2025 at 6:00 PM" // Additional display fields from Rust (RTSDA Architecture Rules compliance) let dayOfMonth: String // "15" let monthAbbreviation: String // "AUG" let timeString: String // "6:00 PM - 8:00 PM" (alias for formattedTime) let isMultiDay: Bool // true if event spans multiple days let detailedTimeDisplay: String // Full time range for detail views let location: String let locationUrl: String? let image: String? let thumbnail: String? let category: String let isFeatured: Bool let recurringType: String? let createdAt: String let updatedAt: String enum CodingKeys: String, CodingKey { case id, title, description, location, image, thumbnail, category case startTime = "start_time" case endTime = "end_time" case formattedTime = "formatted_time" case formattedDate = "formatted_date" case formattedDateTime = "formatted_date_time" case dayOfMonth = "day_of_month" case monthAbbreviation = "month_abbreviation" case timeString = "time_string" case isMultiDay = "is_multi_day" case detailedTimeDisplay = "detailed_time_display" case locationUrl = "location_url" case isFeatured = "is_featured" case recurringType = "recurring_type" case createdAt = "created_at" case updatedAt = "updated_at" } // All formatting now handled by Rust church-core crate (RTSDA Architecture Rules compliance) /// Returns formatted date range for display - now using Rust-provided formattedDate var formattedDateRange: String { return formattedDate } // MARK: - Sample Data static func sampleEvent() -> ChurchEvent { return ChurchEvent( id: "sample-event-1", title: "Community Potluck", description: "Join us for fellowship and food", startTime: "2025-01-15T18:00:00-05:00", endTime: "2025-01-15T20:00:00-05:00", formattedTime: "6:00 PM - 8:00 PM", formattedDate: "January 15, 2025", formattedDateTime: "January 15, 2025 at 6:00 PM", dayOfMonth: "15", monthAbbreviation: "JAN", timeString: "6:00 PM - 8:00 PM", isMultiDay: false, detailedTimeDisplay: "6:00 PM - 8:00 PM", location: "Fellowship Hall", locationUrl: nil, image: nil, thumbnail: nil, category: "Social", isFeatured: false, recurringType: nil, createdAt: "2025-01-10T09:00:00-05:00", updatedAt: "2025-01-10T09:00:00-05:00" ) } } struct Sermon: Identifiable, Codable { let id: String let title: String let speaker: String let description: String? let date: String? let audioUrl: String? let videoUrl: String? let duration: String? let mediaType: String? let thumbnail: String? let image: String? let scriptureReading: String? // CodingKeys no longer needed - Rust now sends camelCase field names var formattedDate: String { return date ?? "Date unknown" // Already formatted by API } var durationFormatted: String? { return duration // Already formatted by API } // MARK: - Sample Data static func sampleSermon() -> Sermon { return Sermon( id: "sample-1", title: "Walking in Faith During Difficult Times", speaker: "Pastor John Smith", description: "A message about trusting God during challenging times.", date: "January 10th, 2025", audioUrl: nil, videoUrl: "https://example.com/video.mp4", duration: "35:42", mediaType: "Video", thumbnail: nil, image: nil, scriptureReading: "Philippians 4:13" ) } } struct ChurchBulletin: Identifiable, Codable { let id: String let title: String let date: String let sabbathSchool: String let divineWorship: String let scriptureReading: String let sunset: String let pdfPath: String? let coverImage: String? let isActive: Bool enum CodingKeys: String, CodingKey { case id, title, date, sunset case sabbathSchool = "sabbath_school" case divineWorship = "divine_worship" case scriptureReading = "scripture_reading" case pdfPath = "pdf_path" case coverImage = "cover_image" case isActive = "is_active" } var formattedDate: String { // Parse ISO8601 or YYYY-MM-DD format and return US-friendly date let formatter = ISO8601DateFormatter() if let isoDate = formatter.date(from: date) { let usFormatter = DateFormatter() usFormatter.dateStyle = .long // "August 2, 2025" return usFormatter.string(from: isoDate) } else if date.contains("-") { // Try parsing YYYY-MM-DD format let components = date.split(separator: "-") if components.count >= 3, let year = Int(components[0]), let month = Int(components[1]), let day = Int(components[2]) { let dateComponents = DateComponents(year: year, month: month, day: day) if let parsedDate = Calendar.current.date(from: dateComponents) { let usFormatter = DateFormatter() usFormatter.dateStyle = .long // "August 2, 2025" return usFormatter.string(from: parsedDate) } } } return date // Fallback to original if parsing fails } } struct BibleVerse: Identifiable, Codable { let text: String let reference: String let version: String? let book: String? let chapter: UInt32? let verse: UInt32? let category: String? // Computed property for Identifiable var id: String { return reference + text.prefix(50) // Use reference + start of text as unique ID } } // MARK: - API Response Models struct EventsResponse: Codable { let items: [ChurchEvent] let total: Int let page: Int let perPage: Int let hasMore: Bool enum CodingKeys: String, CodingKey { case items, total, page case perPage = "per_page" case hasMore = "has_more" } } struct ContactSubmissionResult: Codable { let success: Bool let message: String? } // MARK: - Content Feed Item enum FeedItemType { case sermon(Sermon) case event(ChurchEvent) case bulletin(ChurchBulletin) case verse(BibleVerse) } // MARK: - Rust Feed Item (from church-core) struct RustFeedItem: Identifiable, Codable { let id: String let feedType: RustFeedItemType enum CodingKeys: String, CodingKey { case id case feedType = "feed_type" case timestamp case priority } let timestamp: String // ISO8601 format let priority: Int32 } enum RustFeedItemType: Codable { case event(ChurchEvent) case sermon(Sermon) case bulletin(ChurchBulletin) case verse(BibleVerse) enum CodingKeys: String, CodingKey { case type case event case sermon case bulletin case verse } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let type = try container.decode(String.self, forKey: .type) switch type { case "event": let event = try container.decode(ChurchEvent.self, forKey: .event) self = .event(event) case "sermon": let sermon = try container.decode(Sermon.self, forKey: .sermon) self = .sermon(sermon) case "bulletin": let bulletin = try container.decode(ChurchBulletin.self, forKey: .bulletin) self = .bulletin(bulletin) case "verse": let verse = try container.decode(BibleVerse.self, forKey: .verse) self = .verse(verse) default: throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unknown feed item type: \(type)")) } } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) switch self { case .event(let event): try container.encode("event", forKey: .type) try container.encode(event, forKey: .event) case .sermon(let sermon): try container.encode("sermon", forKey: .type) try container.encode(sermon, forKey: .sermon) case .bulletin(let bulletin): try container.encode("bulletin", forKey: .type) try container.encode(bulletin, forKey: .bulletin) case .verse(let verse): try container.encode("verse", forKey: .type) try container.encode(verse, forKey: .verse) } } } // MARK: - Swift Feed Item (legacy) struct FeedItem: Identifiable { let id = UUID() let type: FeedItemType let timestamp: Date var title: String { switch type { case .sermon(let sermon): return sermon.title case .event(let event): return event.title case .bulletin(let bulletin): return bulletin.title case .verse(let verse): return verse.reference } } var subtitle: String? { switch type { case .sermon(let sermon): return sermon.speaker case .event(let event): return event.formattedDate case .bulletin(let bulletin): return bulletin.formattedDate case .verse(let verse): return verse.text } } } // MARK: - Search Utilities struct SearchUtils { /// Searches events by title, description, location, and date static func searchEvents(_ events: [ChurchEvent], searchText: String) -> [ChurchEvent] { guard !searchText.isEmpty else { return events } return events.filter { event in let titleMatch = event.title.localizedCaseInsensitiveContains(searchText) let descMatch = event.description.localizedCaseInsensitiveContains(searchText) let locationMatch = event.location.localizedCaseInsensitiveContains(searchText) let dateMatch = event.formattedDate.localizedCaseInsensitiveContains(searchText) let smartDateMatch = checkSmartDateMatch(searchText: searchText, sermonDate: event.formattedDate) return titleMatch || descMatch || locationMatch || dateMatch || smartDateMatch } } /// Searches sermons by title, speaker, description, and date static func searchSermons(_ sermons: [Sermon], searchText: String, contentType: String = "sermons") -> [Sermon] { guard !searchText.isEmpty else { return sermons } return sermons.filter { sermon in let titleMatch = sermon.title.localizedCaseInsensitiveContains(searchText) let speakerMatch = sermon.speaker.localizedCaseInsensitiveContains(searchText) let descMatch = sermon.description?.localizedCaseInsensitiveContains(searchText) ?? false let dateMatch = sermon.date?.localizedCaseInsensitiveContains(searchText) ?? false let smartDateMatch = checkSmartDateMatch(searchText: searchText, sermonDate: sermon.date) return titleMatch || speakerMatch || descMatch || dateMatch || smartDateMatch } } /// Smart date matching for patterns like "January 2025", "Jan 2024", etc. private static func checkSmartDateMatch(searchText: String, sermonDate: String?) -> Bool { guard let sermonDate = sermonDate else { return false } let searchWords = searchText.lowercased().components(separatedBy: .whitespaces).filter { !$0.isEmpty } guard searchWords.count >= 2 else { return false } // Check for month + year patterns like "January 2025" or "Jan 2024" let monthNames = ["january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december"] let monthAbbrevs = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"] for i in 0..= 2000 && year <= 2100 { // Check if sermon date contains both the month and year if sermonDateLower.contains(fullMonthName) && sermonDateLower.contains(String(year)) { return true } } // Check if second word could be a day OR part of a year else if let number = Int(word2) { // Check if sermon date contains both the month and this number anywhere // This catches both day matches (e.g., "January 20th") and year substring matches (e.g., "January" + "20" in "2020") if sermonDateLower.contains(fullMonthName) && sermonDateLower.contains(String(number)) { return true } } } } return false } }