import Foundation import OSLog private let webDataLogger = Logger(subsystem: "com.treyt.mlbTVOS", category: "WebData") private func logWebData(_ message: String) { webDataLogger.debug("\(message, privacy: .public)") print("[WebData] \(message)") } // MARK: - Models struct NewsHeadline: Identifiable, Sendable { let id: String let title: String let summary: String let timestamp: Date let imageURL: URL? } struct InjuryReport: Identifiable, Sendable { let id: String let teamCode: String let playerName: String let position: String let status: String // "10-Day IL", "60-Day IL", "Day-to-Day" let date: Date let notes: String } struct Transaction: Identifiable, Sendable { let id: String let teamCode: String let description: String let date: Date let type: String // "Trade", "DFA", "Call-Up", "Placed on IL" } struct LeagueLeader: Identifiable, Sendable { let id: String let rank: Int let playerName: String let teamCode: String let value: String let personId: Int? } struct LeaderCategory: Identifiable, Sendable { let id: String let name: String let leaders: [LeagueLeader] } // MARK: - Service actor MLBWebDataService { private let statsAPI = MLBStatsAPI() private let session: URLSession init() { let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = 15 session = URLSession(configuration: config) } // MARK: - League Leaders (via Stats API) func fetchLeagueLeaders() async throws -> [LeaderCategory] { let categories = [ ("homeRuns", "Home Runs"), ("battingAverage", "Batting Average"), ("runsBattedIn", "RBI"), ("earnedRunAverage", "ERA"), ("strikeouts", "Strikeouts"), ("wins", "Wins"), ("saves", "Saves"), ("stolenBases", "Stolen Bases"), ("onBasePlusSlugging", "OPS"), ("walksHitsPerInningPitched", "WHIP"), ] var results: [LeaderCategory] = [] for (code, name) in categories { let url = URL(string: "https://statsapi.mlb.com/api/v1/stats/leaders?leaderCategories=\(code)&season=\(currentSeason)&limit=5&sportId=1")! logWebData("fetchLeagueLeaders category=\(code)") do { let (data, _) = try await session.data(from: url) let response = try JSONDecoder().decode(LeaderResponse.self, from: data) if let category = response.leagueLeaders?.first { let leaders = (category.leaders ?? []).prefix(5).enumerated().map { index, leader in LeagueLeader( id: "\(code)-\(index)", rank: leader.rank ?? (index + 1), playerName: leader.person?.fullName ?? "Unknown", teamCode: leader.team?.abbreviation ?? "", value: leader.value ?? "", personId: leader.person?.id ) } results.append(LeaderCategory(id: code, name: name, leaders: Array(leaders))) } } catch { logWebData("fetchLeagueLeaders FAILED category=\(code) error=\(error)") } } logWebData("fetchLeagueLeaders complete categories=\(results.count)") return results } // MARK: - News Headlines (from MLB.com) func fetchNewsHeadlines() async -> [NewsHeadline] { let url = URL(string: "https://statsapi.mlb.com/api/v1/news?sportId=1")! logWebData("fetchNewsHeadlines") do { let (data, _) = try await session.data(from: url) let response = try JSONDecoder().decode(NewsResponse.self, from: data) let headlines = (response.articles ?? []).prefix(15).map { article in NewsHeadline( id: article.slug ?? UUID().uuidString, title: article.headline ?? "Untitled", summary: article.subhead ?? "", timestamp: ISO8601DateFormatter().date(from: article.date ?? "") ?? Date(), imageURL: article.image?.cuts?.first?.src.flatMap(URL.init) ) } logWebData("fetchNewsHeadlines success count=\(headlines.count)") return Array(headlines) } catch { logWebData("fetchNewsHeadlines FAILED error=\(error)") return [] } } // MARK: - Transactions (from Stats API) func fetchTransactions() async -> [Transaction] { let formatter = DateFormatter() formatter.dateFormat = "MM/dd/yyyy" let today = formatter.string(from: Date()) let weekAgo = formatter.string(from: Calendar.current.date(byAdding: .day, value: -7, to: Date())!) let url = URL(string: "https://statsapi.mlb.com/api/v1/transactions?startDate=\(weekAgo)&endDate=\(today)&sportId=1")! logWebData("fetchTransactions from=\(weekAgo) to=\(today)") do { let (data, _) = try await session.data(from: url) let response = try JSONDecoder().decode(TransactionResponse.self, from: data) let transactions = (response.transactions ?? []).prefix(30).enumerated().map { index, tx in Transaction( id: "\(tx.id ?? index)", teamCode: tx.team?.abbreviation ?? "", description: tx.description ?? "", date: ISO8601DateFormatter().date(from: tx.date ?? "") ?? Date(), type: tx.typeDesc ?? "Transaction" ) } logWebData("fetchTransactions success count=\(transactions.count)") return Array(transactions) } catch { logWebData("fetchTransactions FAILED error=\(error)") return [] } } private var currentSeason: Int { Calendar.current.component(.year, from: Date()) } } // MARK: - API Response Models private struct LeaderResponse: Codable { let leagueLeaders: [LeaderCategoryResponse]? } private struct LeaderCategoryResponse: Codable { let leaderCategory: String? let leaders: [LeaderEntry]? } private struct LeaderEntry: Codable { let rank: Int? let value: String? let person: PersonRef? let team: TeamRef? } private struct PersonRef: Codable { let id: Int? let fullName: String? } private struct TeamRef: Codable { let id: Int? let abbreviation: String? } private struct NewsResponse: Codable { let articles: [NewsArticle]? } private struct NewsArticle: Codable { let slug: String? let headline: String? let subhead: String? let date: String? let image: NewsImage? } private struct NewsImage: Codable { let cuts: [NewsImageCut]? } private struct NewsImageCut: Codable { let src: String? } private struct TransactionResponse: Codable { let transactions: [TransactionEntry]? } private struct TransactionEntry: Codable { let id: Int? let description: String? let date: String? let typeDesc: String? let team: TeamRef? }