     1→import Foundation
     2→import os.log
     3→
     4→// MARK: - API Errors
     5→
     6→enum BCBAPIError: LocalizedError {
     7→    case invalidURL
     8→    case networkError(Error)
     9→    case decodingError(Error)
    10→    case serverError(Int)
    11→    case noData
    12→    case rateLimited
    13→
    14→    var errorDescription: String? {
    15→        switch self {
    16→        case .invalidURL:
    17→            return "URL inválida"
    18→        case .networkError(let error):
    19→            return "Erro de rede: \(error.localizedDescription)"
    20→        case .decodingError(let error):
    21→            return "Erro ao processar dados: \(error.localizedDescription)"
    22→        case .serverError(let code):
    23→            return "Erro do servidor: \(code)"
    24→        case .noData:
    25→            return "Nenhum dado disponível"
    26→        case .rateLimited:
    27→            return "Limite de requisições excedido"
    28→        }
    29→    }
    30→}
    31→
    32→// MARK: - API Client
    33→
    34→@MainActor
    35→class BCBAPIClient: ObservableObject {
    36→    static let shared = BCBAPIClient()
    37→
    38→    @Published var isLoading = false
    39→    @Published var lastError: BCBAPIError?
    40→
    41→    private let session: URLSession
    42→    private let decoder: JSONDecoder
    43→
    44→    // Base URLs
    45→    enum BaseURL {
    46→        static let olinda = "https://olinda.bcb.gov.br/olinda/servico"
    47→        static let sgs = "https://api.bcb.gov.br/dados/serie/bcdata.sgs"
    48→    }
    49→
    50→    private init() {
    51→        let config = URLSessionConfiguration.default
    52→        config.timeoutIntervalForRequest = 30
    53→        config.timeoutIntervalForResource = 60
    54→        self.session = URLSession(configuration: config)
    55→
    56→        self.decoder = JSONDecoder()
    57→    }
    58→
    59→    // MARK: - Generic Fetch
    60→
    61→    func fetch<T: Codable>(url: URL, endpoint: String = "Unknown") async throws -> T {
    62→        isLoading = true
    63→        defer { isLoading = false }
    64→
    65→        let startTime = Date()
    66→        let log = LogManager.shared
    67→
    68→        log.logAPIRequest(endpoint: endpoint, url: url.absoluteString)
    69→
    70→        do {
    71→            let (data, response) = try await session.data(from: url)
    72→            let duration = Date().timeIntervalSince(startTime)
    73→
    74→            guard let httpResponse = response as? HTTPURLResponse else {
    75→                log.logAPIError(endpoint: endpoint, error: URLError(.badServerResponse), duration: duration)
    76→                throw BCBAPIError.networkError(URLError(.badServerResponse))
    77→            }
    78→
    79→            log.logAPIResponse(endpoint: endpoint, statusCode: httpResponse.statusCode, duration: duration, dataSize: data.count)
    80→
    81→            guard (200...299).contains(httpResponse.statusCode) else {
    82→                if httpResponse.statusCode == 429 {
    83→                    log.error("Rate limited by BCB API", category: "API", details: "Endpoint: \(endpoint)")
    84→                    throw BCBAPIError.rateLimited
    85→                }
    86→                log.error("Server error", category: "API", details: "Endpoint: \(endpoint) | Status: \(httpResponse.statusCode)")
    87→                throw BCBAPIError.serverError(httpResponse.statusCode)
    88→            }
    89→
    90→            let decoded = try decoder.decode(T.self, from: data)
    91→            lastError = nil
    92→
    93→            // Log decoded count if it's an array
    94→            if let array = decoded as? [Any] {
    95→                log.logAPIDecoding(endpoint: endpoint, type: T.self, count: array.count)
    96→            } else {
    97→                log.logAPIDecoding(endpoint: endpoint, type: T.self)
    98→            }
    99→
   100→            return decoded
   101→
   102→        } catch let error as BCBAPIError {
   103→            let duration = Date().timeIntervalSince(startTime)
   104→            log.logAPIError(endpoint: endpoint, error: error, duration: duration)
   105→            lastError = error
   106→            throw error
   107→        } catch let error as DecodingError {
   108→            let duration = Date().timeIntervalSince(startTime)
   109→            let apiError = BCBAPIError.decodingError(error)
   110→            log.logAPIError(endpoint: endpoint, error: error, duration: duration)
   111→            log.error("Decoding failed", category: "API", details: describeDecodingError(error))
   112→            lastError = apiError
   113→            throw apiError
   114→        } catch {
   115→            let duration = Date().timeIntervalSince(startTime)
   116→            let apiError = BCBAPIError.networkError(error)
   117→            log.logAPIError(endpoint: endpoint, error: error, duration: duration)
   118→            lastError = apiError
   119→            throw apiError
   120→        }
   121→    }
   122→
   123→    private func describeDecodingError(_ error: DecodingError) -> String {
   124→        switch error {
   125→        case .keyNotFound(let key, let context):
   126→            return "Key '\(key.stringValue)' not found: \(context.debugDescription)"
   127→        case .typeMismatch(let type, let context):
   128→            return "Type mismatch for \(type): \(context.debugDescription)"
   129→        case .valueNotFound(let type, let context):
   130→            return "Value not found for \(type): \(context.debugDescription)"
   131→        case .dataCorrupted(let context):
   132→            return "Data corrupted: \(context.debugDescription)"
   133→        @unknown default:
   134→            return error.localizedDescription
   135→        }
   136→    }
   137→
   138→    // MARK: - PTAX API
   139→
   140→    func fetchCurrencies() async throws -> [CurrencyInfo] {
   141→        let urlString = "\(BaseURL.olinda)/PTAX/versao/v1/odata/Moedas?$format=json"
   142→        guard let url = URL(string: urlString) else {
   143→            throw BCBAPIError.invalidURL
   144→        }
   145→
   146→        let response: PTAXResponse<CurrencyInfo> = try await fetch(url: url, endpoint: "PTAX/Moedas")
   147→        return response.value
   148→    }
   149→
   150→    func fetchDollarQuote(date: Date) async throws -> [DollarQuote] {
   151→        let dateString = date.toBCBFormat()
   152→        let urlString = "\(BaseURL.olinda)/PTAX/versao/v1/odata/CotacaoDolarDia(dataCotacao=@dataCotacao)?@dataCotacao='\(dateString)'&$format=json"
   153→        guard let url = URL(string: urlString) else {
   154→            throw BCBAPIError.invalidURL
   155→        }
   156→
   157→        let response: PTAXResponse<DollarQuote> = try await fetch(url: url, endpoint: "PTAX/CotacaoDolarDia")
   158→        return response.value
   159→    }
   160→
   161→    func fetchDollarQuotePeriod(startDate: Date, endDate: Date) async throws -> [DollarQuote] {
   162→        let startString = startDate.toBCBFormat()
   163→        let endString = endDate.toBCBFormat()
   164→        let urlString = "\(BaseURL.olinda)/PTAX/versao/v1/odata/CotacaoDolarPeriodo(dataInicial=@dataInicial,dataFinalCotacao=@dataFinalCotacao)?@dataInicial='\(startString)'&@dataFinalCotacao='\(endString)'&$format=json"
   165→        guard let url = URL(string: urlString) else {
   166→            throw BCBAPIError.invalidURL
   167→        }
   168→
   169→        let response: PTAXResponse<DollarQuote> = try await fetch(url: url, endpoint: "PTAX/CotacaoDolarPeriodo")
   170→        return response.value
   171→    }
   172→
   173→    func fetchCurrencyQuote(symbol: String, date: Date) async throws -> [CurrencyQuote] {
   174→        let dateString = date.toBCBFormat()
   175→        let urlString = "\(BaseURL.olinda)/PTAX/versao/v1/odata/CotacaoMoedaDia(moeda=@moeda,dataCotacao=@dataCotacao)?@moeda='\(symbol)'&@dataCotacao='\(dateString)'&$format=json"
   176→        guard let url = URL(string: urlString) else {
   177→            throw BCBAPIError.invalidURL
   178→        }
   179→
   180→        let response: PTAXResponse<CurrencyQuote> = try await fetch(url: url, endpoint: "PTAX/CotacaoMoeda/\(symbol)")
   181→        return response.value
   182→    }
   183→
   184→    // MARK: - SGS API
   185→
   186→    func fetchTimeSeries(code: Int, lastN: Int = 10) async throws -> [TimeSeriesValue] {
   187→        let urlString = "\(BaseURL.sgs).\(code)/dados/ultimos/\(lastN)?formato=json"
   188→        guard let url = URL(string: urlString) else {
   189→            throw BCBAPIError.invalidURL
   190→        }
   191→
   192→        return try await fetch(url: url, endpoint: "SGS/Serie/\(code)")
   193→    }
   194→
   195→    func fetchTimeSeriesPeriod(code: Int, startDate: Date, endDate: Date) async throws -> [TimeSeriesValue] {
   196→        let formatter = DateFormatter()
   197→        formatter.dateFormat = "dd/MM/yyyy"
   198→        let startString = formatter.string(from: startDate)
   199→        let endString = formatter.string(from: endDate)
   200→
   201→        let urlString = "\(BaseURL.sgs).\(code)/dados?formato=json&dataInicial=\(startString)&dataFinal=\(endString)"
   202→        guard let url = URL(string: urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? urlString) else {
   203→            throw BCBAPIError.invalidURL
   204→        }
   205→
   206→        return try await fetch(url: url, endpoint: "SGS/Serie/\(code)/Period")
   207→    }
   208→
   209→    // MARK: - Institutions API (New Working Endpoints)
   210→
   211→    /// Fetch all banks (Bancos Múltiplos, Comerciais, etc.)
   212→    func fetchBanks() async throws -> [Bank] {
   213→        let urlString = "\(BaseURL.olinda)/Instituicoes_em_funcionamento/versao/v1/odata/SedesBancoComMultCE?$format=json"
   214→        guard let url = URL(string: urlString) else {
   215→            throw BCBAPIError.invalidURL
   216→        }
   217→
   218→        let response: BanksResponse = try await fetch(url: url, endpoint: "Instituicoes/Bancos")
   219→        return response.value
   220→    }
   221→
   222→    /// Fetch all credit cooperatives
   223→    func fetchCooperatives() async throws -> [Cooperative] {
   224→        let urlString = "\(BaseURL.olinda)/Instituicoes_em_funcionamento/versao/v1/odata/SedesCooperativas?$format=json"
   225→        guard let url = URL(string: urlString) else {
   226→            throw BCBAPIError.invalidURL
   227→        }
   228→
   229→        let response: CooperativesResponse = try await fetch(url: url, endpoint: "Instituicoes/Cooperativas")
   230→        return response.value
   231→    }
   232→
   233→    /// Fetch all consortium administrators
   234→    func fetchConsortia() async throws -> [Consortium] {
   235→        let urlString = "\(BaseURL.olinda)/Instituicoes_em_funcionamento/versao/v1/odata/SedesConsorcios?$format=json"
   236→        guard let url = URL(string: urlString) else {
   237→            throw BCBAPIError.invalidURL
   238→        }
   239→
   240→        let response: ConsortiaResponse = try await fetch(url: url, endpoint: "Instituicoes/Consorcios")
   241→        return response.value
   242→    }
   243→
   244→    /// Fetch all sociedades (brokers, SCDs, payment institutions, etc.)
   245→    func fetchSociedades() async throws -> [Sociedade] {
   246→        let urlString = "\(BaseURL.olinda)/Instituicoes_em_funcionamento/versao/v1/odata/SedesSociedades?$format=json"
   247→        guard let url = URL(string: urlString) else {
   248→            throw BCBAPIError.invalidURL
   249→        }
   250→
   251→        let response: SociedadesResponse = try await fetch(url: url, endpoint: "Instituicoes/Sociedades")
   252→        return response.value
   253→    }
   254→
   255→    /// Fetch all institution types in parallel
   256→    func fetchAllInstitutions() async throws -> (banks: [Bank], cooperatives: [Cooperative], consortia: [Consortium], sociedades: [Sociedade]) {
   257→        let log = LogManager.shared
   258→        log.info("Fetching all institutions in parallel", category: "API")
   259→
   260→        async let banksTask = fetchBanks()
   261→        async let cooperativesTask = fetchCooperatives()
   262→        async let consortiaTask = fetchConsortia()
   263→        async let sociedadesTask = fetchSociedades()
   264→
   265→        let (banks, cooperatives, consortia, sociedades) = try await (banksTask, cooperativesTask, consortiaTask, sociedadesTask)
   266→
   267→        log.info("All institutions loaded", category: "API", details: "Banks: \(banks.count) | Coops: \(cooperatives.count) | Consortia: \(consortia.count) | Sociedades: \(sociedades.count)")
   268→        return (banks, cooperatives, consortia, sociedades)
   269→    }
   270→
   271→    // Legacy - kept for compatibility
   272→    func fetchInstitutions(top: Int = 100) async throws -> [Institution] {
   273→        LogManager.shared.warning("Legacy fetchInstitutions called - use fetchAllInstitutions instead", category: "API")
   274→        return []
   275→    }
   276→
   277→    func searchInstitutions(query: String) async throws -> [Institution] {
   278→        LogManager.shared.warning("Legacy searchInstitutions called", category: "API")
   279→        return []
   280→    }
   281→
   282→    // MARK: - PIX SPI API
   283→
   284→    /// Fetch daily PIX settlement data
   285→    func fetchPIXDaily() async throws -> [PIXDaily] {
   286→        let urlString = "\(BaseURL.olinda)/SPI/versao/v1/odata/PixLiquidadosAtual?$format=json"
   287→        guard let url = URL(string: urlString) else {
   288→            throw BCBAPIError.invalidURL
   289→        }
   290→
   291→        let response: PIXDailyResponse = try await fetch(url: url, endpoint: "SPI/PixLiquidadosAtual")
   292→        return response.value
   293→    }
   294→
   295→    /// Fetch hourly PIX averages
   296→    func fetchPIXIntraday() async throws -> [PIXIntraday] {
   297→        let urlString = "\(BaseURL.olinda)/SPI/versao/v1/odata/PixLiquidadosIntradia?$format=json"
   298→        guard let url = URL(string: urlString) else {
   299→            throw BCBAPIError.invalidURL
   300→        }
   301→
   302→        let response: PIXIntradayResponse = try await fetch(url: url, endpoint: "SPI/PixLiquidadosIntradia")
   303→        return response.value
   304→    }
   305→
   306→    /// Fetch PIX availability index
   307→    func fetchPIXAvailability() async throws -> [PIXAvailability] {
   308→        let urlString = "\(BaseURL.olinda)/SPI/versao/v1/odata/PixDisponibilidadeSPI?$format=json&$orderby=DataBase%20desc&$top=12"
   309→        guard let url = URL(string: urlString) else {
   310→            throw BCBAPIError.invalidURL
   311→        }
   312→
   313→        let response: PIXAvailabilityResponse = try await fetch(url: url, endpoint: "SPI/PixDisponibilidadeSPI")
   314→        return response.value
   315→    }
   316→
   317→    /// Fetch PIX account remuneration
   318→    func fetchPIXRemuneration() async throws -> [PIXRemuneration] {
   319→        let urlString = "\(BaseURL.olinda)/SPI/versao/v1/odata/PixRemuneracaoContaPI?$format=json&$orderby=DataBase%20desc&$top=30"
   320→        guard let url = URL(string: urlString) else {
   321→            throw BCBAPIError.invalidURL
   322→        }
   323→
   324→        let response: PIXRemunerationResponse = try await fetch(url: url, endpoint: "SPI/PixRemuneracaoContaPI")
   325→        return response.value
   326→    }
   327→
   328→    /// Fetch all PIX statistics (with graceful error handling for unstable endpoints)
   329→    func fetchAllPIXStats() async throws -> PIXStats {
   330→        let log = LogManager.shared
   331→        log.info("Fetching PIX stats", category: "API")
   332→
   333→        // These endpoints are more stable
   334→        async let dailyTask = fetchPIXDaily()
   335→        async let intradayTask = fetchPIXIntraday()
   336→
   337→        // Get the stable data first
   338→        let (daily, intraday) = try await (dailyTask, intradayTask)
   339→
   340→        // Try to get availability and remuneration data, but don't fail if they error
   341→        var availability: [PIXAvailability] = []
   342→        var remuneration: [PIXRemuneration] = []
   343→
   344→        // Availability endpoint (may return 500 - BCB server issue)
   345→        do {
   346→            availability = try await fetchPIXAvailability()
   347→        } catch {
   348→            log.warning("PIX Availability endpoint failed (BCB known issue)", category: "API", details: error.localizedDescription)
   349→        }
   350→
   351→        // Remuneration endpoint (may return 500 - BCB server issue)
   352→        do {
   353→            remuneration = try await fetchPIXRemuneration()
   354→        } catch {
   355→            log.warning("PIX Remuneration endpoint failed (BCB known issue)", category: "API", details: error.localizedDescription)
   356→        }
   357→
   358→        log.info("PIX stats loaded", category: "API", details: "Daily: \(daily.count) | Intraday: \(intraday.count) | Availability: \(availability.count) | Remuneration: \(remuneration.count)")
   359→
   360→        return PIXStats(
   361→            dailyData: daily,
   362→            intradayData: intraday,
   363→            availabilityData: availability,
   364→            remunerationData: remuneration
   365→        )
   366→    }
   367→
   368→    // MARK: - Expectations API (Focus)
   369→
   370→    /// Fetch SELIC expectations by COPOM meeting
   371→    func fetchSelicExpectationsByMeeting() async throws -> [SelicExpectation] {
   372→        let urlString = "\(BaseURL.olinda)/Expectativas/versao/v1/odata/ExpectativasMercadoSelic?$format=json&$top=100&$orderby=Data%20desc,Reuniao%20desc"
   373→        guard let url = URL(string: urlString) else {
   374→            throw BCBAPIError.invalidURL
   375→        }
   376→
   377→        let response: SelicExpectationResponse = try await fetch(url: url, endpoint: "Focus/SELIC")
   378→        return response.value
   379→    }
   380→
   381→    /// Fetch IPCA 12-month inflation expectations
   382→    func fetchIPCA12MonthExpectations() async throws -> [InflationExpectation] {
   383→        let urlString = "\(BaseURL.olinda)/Expectativas/versao/v1/odata/ExpectativasMercadoInflacao12Meses?$filter=Indicador%20eq%20'IPCA'&$format=json&$top=50&$orderby=Data%20desc"
   384→        guard let url = URL(string: urlString) else {
   385→            throw BCBAPIError.invalidURL
   386→        }
   387→
   388→        let response: InflationExpectationResponse = try await fetch(url: url, endpoint: "Focus/IPCA12m")
   389→        return response.value
   390→    }
   391→
   392→    /// Fetch IGP-M 12-month inflation expectations
   393→    func fetchIGPM12MonthExpectations() async throws -> [InflationExpectation] {
   394→        let urlString = "\(BaseURL.olinda)/Expectativas/versao/v1/odata/ExpectativasMercadoInflacao12Meses?$filter=Indicador%20eq%20'IGP-M'&$format=json&$top=50&$orderby=Data%20desc"
   395→        guard let url = URL(string: urlString) else {
   396→            throw BCBAPIError.invalidURL
   397→        }
   398→
   399→        let response: InflationExpectationResponse = try await fetch(url: url, endpoint: "Focus/IGPM12m")
   400→        return response.value
   401→    }
   402→
   403→    /// Fetch annual IGP-M expectations
   404→    func fetchAnnualIGPMExpectations() async throws -> [AnnualExpectation] {
   405→        let urlString = "\(BaseURL.olinda)/Expectativas/versao/v1/odata/ExpectativasMercadoAnuais?$filter=Indicador%20eq%20'IGP-M'&$format=json&$top=50&$orderby=Data%20desc,DataReferencia%20desc"
   406→        guard let url = URL(string: urlString) else {
   407→            throw BCBAPIError.invalidURL
   408→        }
   409→
   410→        let response: AnnualExpectationResponse = try await fetch(url: url, endpoint: "Focus/IGPMAnual")
   411→        return response.value
   412→    }
   413→
   414→    /// Fetch annual IPCA expectations
   415→    func fetchAnnualIPCAExpectations() async throws -> [AnnualExpectation] {
   416→        let urlString = "\(BaseURL.olinda)/Expectativas/versao/v1/odata/ExpectativasMercadoAnuais?$filter=Indicador%20eq%20'IPCA'&$format=json&$top=50&$orderby=Data%20desc,DataReferencia%20desc"
   417→        guard let url = URL(string: urlString) else {
   418→            throw BCBAPIError.invalidURL
   419→        }
   420→
   421→        let response: AnnualExpectationResponse = try await fetch(url: url, endpoint: "Focus/IPCAAnual")
   422→        return response.value
   423→    }
   424→
   425→    /// Fetch annual PIB expectations
   426→    func fetchAnnualPIBExpectations() async throws -> [AnnualExpectation] {
   427→        let urlString = "\(BaseURL.olinda)/Expectativas/versao/v1/odata/ExpectativasMercadoAnuais?$filter=Indicador%20eq%20'PIB%20Total'&$format=json&$top=50&$orderby=Data%20desc,DataReferencia%20desc"
   428→        guard let url = URL(string: urlString) else {
   429→            throw BCBAPIError.invalidURL
   430→        }
   431→
   432→        let response: AnnualExpectationResponse = try await fetch(url: url, endpoint: "Focus/PIBAnual")
   433→        return response.value
   434→    }
   435→
   436→    /// Fetch annual Câmbio expectations
   437→    func fetchAnnualCambioExpectations() async throws -> [AnnualExpectation] {
   438→        // Note: Câmbio must be URL encoded as C%C3%A2mbio
   439→        let urlString = "\(BaseURL.olinda)/Expectativas/versao/v1/odata/ExpectativasMercadoAnuais?$filter=Indicador%20eq%20'C%C3%A2mbio'&$format=json&$top=50&$orderby=Data%20desc,DataReferencia%20desc"
   440→        guard let url = URL(string: urlString) else {
   441→            throw BCBAPIError.invalidURL
   442→        }
   443→
   444→        let response: AnnualExpectationResponse = try await fetch(url: url, endpoint: "Focus/CambioAnual")
   445→        return response.value
   446→    }
   447→
   448→    /// Fetch all Focus stats in parallel
   449→    func fetchAllFocusStats() async throws -> FocusStats {
   450→        let log = LogManager.shared
   451→        log.info("Fetching Focus stats (7 endpoints in parallel)", category: "API")
   452→
   453→        async let selicTask = fetchSelicExpectationsByMeeting()
   454→        async let ipcaTask = fetchIPCA12MonthExpectations()
   455→        async let igpmTask = fetchIGPM12MonthExpectations()
   456→        async let annualIPCATask = fetchAnnualIPCAExpectations()
   457→        async let annualIGPMTask = fetchAnnualIGPMExpectations()
   458→        async let annualPIBTask = fetchAnnualPIBExpectations()
   459→        async let annualCambioTask = fetchAnnualCambioExpectations()
   460→
   461→        let (selic, ipca, igpm, annualIPCA, annualIGPM, annualPIB, annualCambio) = try await (selicTask, ipcaTask, igpmTask, annualIPCATask, annualIGPMTask, annualPIBTask, annualCambioTask)
   462→
   463→        log.info("Focus stats loaded", category: "API", details: "SELIC: \(selic.count) | IPCA: \(ipca.count) | IGPM: \(igpm.count) | PIB: \(annualPIB.count) | Cambio: \(annualCambio.count)")
   464→
   465→        return FocusStats(
   466→            selicExpectations: selic,
   467→            ipcaExpectations: ipca,
   468→            igpmExpectations: igpm,
   469→            annualIPCA: annualIPCA,
   470→            annualIGPM: annualIGPM,
   471→            annualPIB: annualPIB,
   472→            annualCambio: annualCambio
   473→        )
   474→    }
   475→
   476→    // Legacy compatibility methods
   477→    func fetchInflationExpectations() async throws -> [MarketExpectation] {
   478→        LogManager.shared.warning("Legacy fetchInflationExpectations called", category: "API")
   479→        let urlString = "\(BaseURL.olinda)/Expectativas/versao/v1/odata/ExpectativasMercadoAnuais?$filter=Indicador%20eq%20'IPCA'&$format=json&$top=50&$orderby=Data%20desc,DataReferencia%20desc"
   480→        guard let url = URL(string: urlString) else {
   481→            throw BCBAPIError.invalidURL
   482→        }
   483→
   484→        let response: ExpectationsResponse = try await fetch(url: url, endpoint: "Legacy/Inflation")
   485→        return response.value
   486→    }
   487→
   488→    func fetchSelicExpectations() async throws -> [MarketExpectation] {
   489→        LogManager.shared.warning("Legacy fetchSelicExpectations called", category: "API")
   490→        let urlString = "\(BaseURL.olinda)/Expectativas/versao/v1/odata/ExpectativasMercadoAnuais?$filter=Indicador%20eq%20'Selic'&$format=json&$top=50&$orderby=Data%20desc,DataReferencia%20desc"
   491→        guard let url = URL(string: urlString) else {
   492→            throw BCBAPIError.invalidURL
   493→        }
   494→
   495→        let response: ExpectationsResponse = try await fetch(url: url, endpoint: "Legacy/SELIC")
   496→        return response.value
   497→    }
   498→}
   499→
   500→// MARK: - Cache Manager
   501→
   502→@MainActor
   503→class CacheManager: ObservableObject {
   504→    static let shared = CacheManager()
   505→
   506→    private let defaults = UserDefaults.standard
   507→    private let encoder = JSONEncoder()
   508→    private let decoder = JSONDecoder()
   509→
   510→    private let cacheExpirationMinutes: Double = 5
   511→
   512→    private init() {}
   513→
   514→    func save<T: Codable>(_ data: T, forKey key: String) {
   515→        if let encoded = try? encoder.encode(data) {
   516→            defaults.set(encoded, forKey: key)
   517→            defaults.set(Date(), forKey: "\(key)_timestamp")
   518→        }
   519→    }
   520→
   521→    func load<T: Codable>(forKey key: String) -> T? {
   522→        guard let data = defaults.data(forKey: key),
   523→              let timestamp = defaults.object(forKey: "\(key)_timestamp") as? Date else {
   524→            return nil
   525→        }
   526→
   527→        // Check if cache is expired
   528→        let elapsed = Date().timeIntervalSince(timestamp) / 60
   529→        if elapsed > cacheExpirationMinutes {
   530→            return nil
   531→        }
   532→
   533→        return try? decoder.decode(T.self, from: data)
   534→    }
   535→
   536→    func clearAll() {
   537→        let domain = Bundle.main.bundleIdentifier ?? ""
   538→        defaults.removePersistentDomain(forName: domain)
   539→    }
   540→
   541→    func isCacheValid(forKey key: String) -> Bool {
   542→        guard let timestamp = defaults.object(forKey: "\(key)_timestamp") as? Date else {
   543→            return false
   544→        }
   545→        let elapsed = Date().timeIntervalSince(timestamp) / 60
   546→        return elapsed <= cacheExpirationMinutes
   547→    }
   548→}
   549→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

import Foundation
import os.log

// MARK: - API Errors

enum BCBAPIError: LocalizedError {
    case invalidURL
    case networkError(Error)
    case decodingError(Error)
    case serverError(Int)
    case noData
    case rateLimited

    var errorDescription: String? {
        switch self {
        case .invalidURL:
            return "URL inválida"
        case .networkError(let error):
            return "Erro de rede: \(error.localizedDescription)"
        case .decodingError(let error):
            return "Erro ao processar dados: \(error.localizedDescription)"
        case .serverError(let code):
            return "Erro do servidor: \(code)"
        case .noData:
            return "Nenhum dado disponível"
        case .rateLimited:
            return "Limite de requisições excedido"
        }
    }
}

// MARK: - API Client

@MainActor
class BCBAPIClient: ObservableObject {
    static let shared = BCBAPIClient()

    @Published var isLoading = false
    @Published var lastError: BCBAPIError?

    private let session: URLSession
    private let decoder: JSONDecoder

    // Base URLs
    enum BaseURL {
        static let olinda = "https://olinda.bcb.gov.br/olinda/servico"
        static let sgs = "https://api.bcb.gov.br/dados/serie/bcdata.sgs"
    }

    private init() {
        let config = URLSessionConfiguration.default
        config.timeoutIntervalForRequest = 30
        config.timeoutIntervalForResource = 60
        self.session = URLSession(configuration: config)

        self.decoder = JSONDecoder()
    }

    // MARK: - Generic Fetch

    func fetch<T: Codable>(url: URL, endpoint: String = "Unknown") async throws -> T {
        isLoading = true
        defer { isLoading = false }

        let startTime = Date()
        let log = LogManager.shared

        log.logAPIRequest(endpoint: endpoint, url: url.absoluteString)

        do {
            let (data, response) = try await session.data(from: url)
            let duration = Date().timeIntervalSince(startTime)

            guard let httpResponse = response as? HTTPURLResponse else {
                log.logAPIError(endpoint: endpoint, error: URLError(.badServerResponse), duration: duration)
                throw BCBAPIError.networkError(URLError(.badServerResponse))
            }

            log.logAPIResponse(endpoint: endpoint, statusCode: httpResponse.statusCode, duration: duration, dataSize: data.count)

            guard (200...299).contains(httpResponse.statusCode) else {
                if httpResponse.statusCode == 429 {
                    log.error("Rate limited by BCB API", category: "API", details: "Endpoint: \(endpoint)")
                    throw BCBAPIError.rateLimited
                }
                log.error("Server error", category: "API", details: "Endpoint: \(endpoint) | Status: \(httpResponse.statusCode)")
                throw BCBAPIError.serverError(httpResponse.statusCode)
            }

            let decoded = try decoder.decode(T.self, from: data)
            lastError = nil

            // Log decoded count if it's an array
            if let array = decoded as? [Any] {
                log.logAPIDecoding(endpoint: endpoint, type: T.self, count: array.count)
            } else {
                log.logAPIDecoding(endpoint: endpoint, type: T.self)
            }

            return decoded

        } catch let error as BCBAPIError {
            let duration = Date().timeIntervalSince(startTime)
            log.logAPIError(endpoint: endpoint, error: error, duration: duration)
            lastError = error
            throw error
        } catch let error as DecodingError {
            let duration = Date().timeIntervalSince(startTime)
            let apiError = BCBAPIError.decodingError(error)
            log.logAPIError(endpoint: endpoint, error: error, duration: duration)
            log.error("Decoding failed", category: "API", details: describeDecodingError(error))
            lastError = apiError
            throw apiError
        } catch {
            let duration = Date().timeIntervalSince(startTime)
            let apiError = BCBAPIError.networkError(error)
            log.logAPIError(endpoint: endpoint, error: error, duration: duration)
            lastError = apiError
            throw apiError
        }
    }

    private func describeDecodingError(_ error: DecodingError) -> String {
        switch error {
        case .keyNotFound(let key, let context):
            return "Key '\(key.stringValue)' not found: \(context.debugDescription)"
        case .typeMismatch(let type, let context):
            return "Type mismatch for \(type): \(context.debugDescription)"
        case .valueNotFound(let type, let context):
            return "Value not found for \(type): \(context.debugDescription)"
        case .dataCorrupted(let context):
            return "Data corrupted: \(context.debugDescription)"
        @unknown default:
            return error.localizedDescription
        }
    }

    // MARK: - PTAX API

    func fetchCurrencies() async throws -> [CurrencyInfo] {
        let urlString = "\(BaseURL.olinda)/PTAX/versao/v1/odata/Moedas?$format=json"
        guard let url = URL(string: urlString) else {
            throw BCBAPIError.invalidURL
        }

        let response: PTAXResponse<CurrencyInfo> = try await fetch(url: url, endpoint: "PTAX/Moedas")
        return response.value
    }

    func fetchDollarQuote(date: Date) async throws -> [DollarQuote] {
        let dateString = date.toBCBFormat()
        let urlString = "\(BaseURL.olinda)/PTAX/versao/v1/odata/CotacaoDolarDia(dataCotacao=@dataCotacao)?@dataCotacao='\(dateString)'&$format=json"
        guard let url = URL(string: urlString) else {
            throw BCBAPIError.invalidURL
        }

        let response: PTAXResponse<DollarQuote> = try await fetch(url: url, endpoint: "PTAX/CotacaoDolarDia")
        return response.value
    }

    func fetchDollarQuotePeriod(startDate: Date, endDate: Date) async throws -> [DollarQuote] {
        let startString = startDate.toBCBFormat()
        let endString = endDate.toBCBFormat()
        let urlString = "\(BaseURL.olinda)/PTAX/versao/v1/odata/CotacaoDolarPeriodo(dataInicial=@dataInicial,dataFinalCotacao=@dataFinalCotacao)?@dataInicial='\(startString)'&@dataFinalCotacao='\(endString)'&$format=json"
        guard let url = URL(string: urlString) else {
            throw BCBAPIError.invalidURL
        }

        let response: PTAXResponse<DollarQuote> = try await fetch(url: url, endpoint: "PTAX/CotacaoDolarPeriodo")
        return response.value
    }

    func fetchCurrencyQuote(symbol: String, date: Date) async throws -> [CurrencyQuote] {
        let dateString = date.toBCBFormat()
        let urlString = "\(BaseURL.olinda)/PTAX/versao/v1/odata/CotacaoMoedaDia(moeda=@moeda,dataCotacao=@dataCotacao)?@moeda='\(symbol)'&@dataCotacao='\(dateString)'&$format=json"
        guard let url = URL(string: urlString) else {
            throw BCBAPIError.invalidURL
        }

        let response: PTAXResponse<CurrencyQuote> = try await fetch(url: url, endpoint: "PTAX/CotacaoMoeda/\(symbol)")
        return response.value
    }

    // MARK: - SGS API

    func fetchTimeSeries(code: Int, lastN: Int = 10) async throws -> [TimeSeriesValue] {
        let urlString = "\(BaseURL.sgs).\(code)/dados/ultimos/\(lastN)?formato=json"
        guard let url = URL(string: urlString) else {
            throw BCBAPIError.invalidURL
        }

        return try await fetch(url: url, endpoint: "SGS/Serie/\(code)")
    }

    func fetchTimeSeriesPeriod(code: Int, startDate: Date, endDate: Date) async throws -> [TimeSeriesValue] {
        let formatter = DateFormatter()
        formatter.dateFormat = "dd/MM/yyyy"
        let startString = formatter.string(from: startDate)
        let endString = formatter.string(from: endDate)

        let urlString = "\(BaseURL.sgs).\(code)/dados?formato=json&dataInicial=\(startString)&dataFinal=\(endString)"
        guard let url = URL(string: urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? urlString) else {
            throw BCBAPIError.invalidURL
        }

        return try await fetch(url: url, endpoint: "SGS/Serie/\(code)/Period")
    }

    // MARK: - Institutions API (New Working Endpoints)

    /// Fetch all banks (Bancos Múltiplos, Comerciais, etc.)
    func fetchBanks() async throws -> [Bank] {
        let urlString = "\(BaseURL.olinda)/Instituicoes_em_funcionamento/versao/v1/odata/SedesBancoComMultCE?$format=json"
        guard let url = URL(string: urlString) else {
            throw BCBAPIError.invalidURL
        }

        let response: BanksResponse = try await fetch(url: url, endpoint: "Instituicoes/Bancos")
        return response.value
    }

    /// Fetch all credit cooperatives
    func fetchCooperatives() async throws -> [Cooperative] {
        let urlString = "\(BaseURL.olinda)/Instituicoes_em_funcionamento/versao/v1/odata/SedesCooperativas?$format=json"
        guard let url = URL(string: urlString) else {
            throw BCBAPIError.invalidURL
        }

        let response: CooperativesResponse = try await fetch(url: url, endpoint: "Instituicoes/Cooperativas")
        return response.value
    }

    /// Fetch all consortium administrators
    func fetchConsortia() async throws -> [Consortium] {
        let urlString = "\(BaseURL.olinda)/Instituicoes_em_funcionamento/versao/v1/odata/SedesConsorcios?$format=json"
        guard let url = URL(string: urlString) else {
            throw BCBAPIError.invalidURL
        }

        let response: ConsortiaResponse = try await fetch(url: url, endpoint: "Instituicoes/Consorcios")
        return response.value
    }

    /// Fetch all sociedades (brokers, SCDs, payment institutions, etc.)
    func fetchSociedades() async throws -> [Sociedade] {
        let urlString = "\(BaseURL.olinda)/Instituicoes_em_funcionamento/versao/v1/odata/SedesSociedades?$format=json"
        guard let url = URL(string: urlString) else {
            throw BCBAPIError.invalidURL
        }

        let response: SociedadesResponse = try await fetch(url: url, endpoint: "Instituicoes/Sociedades")
        return response.value
    }

    /// Fetch all institution types in parallel
    func fetchAllInstitutions() async throws -> (banks: [Bank], cooperatives: [Cooperative], consortia: [Consortium], sociedades: [Sociedade]) {
        let log = LogManager.shared
        log.info("Fetching all institutions in parallel", category: "API")

        async let banksTask = fetchBanks()
        async let cooperativesTask = fetchCooperatives()
        async let consortiaTask = fetchConsortia()
        async let sociedadesTask = fetchSociedades()

        let (banks, cooperatives, consortia, sociedades) = try await (banksTask, cooperativesTask, consortiaTask, sociedadesTask)

        log.info("All institutions loaded", category: "API", details: "Banks: \(banks.count) | Coops: \(cooperatives.count) | Consortia: \(consortia.count) | Sociedades: \(sociedades.count)")
        return (banks, cooperatives, consortia, sociedades)
    }

    // Legacy - kept for compatibility
    func fetchInstitutions(top: Int = 100) async throws -> [Institution] {
        LogManager.shared.warning("Legacy fetchInstitutions called - use fetchAllInstitutions instead", category: "API")
        return []
    }

    func searchInstitutions(query: String) async throws -> [Institution] {
        LogManager.shared.warning("Legacy searchInstitutions called", category: "API")
        return []
    }

    // MARK: - PIX SPI API

    /// Fetch daily PIX settlement data
    func fetchPIXDaily() async throws -> [PIXDaily] {
        let urlString = "\(BaseURL.olinda)/SPI/versao/v1/odata/PixLiquidadosAtual?$format=json"
        guard let url = URL(string: urlString) else {
            throw BCBAPIError.invalidURL
        }

        let response: PIXDailyResponse = try await fetch(url: url, endpoint: "SPI/PixLiquidadosAtual")
        return response.value
    }

    /// Fetch hourly PIX averages
    func fetchPIXIntraday() async throws -> [PIXIntraday] {
        let urlString = "\(BaseURL.olinda)/SPI/versao/v1/odata/PixLiquidadosIntradia?$format=json"
        guard let url = URL(string: urlString) else {
            throw BCBAPIError.invalidURL
        }

        let response: PIXIntradayResponse = try await fetch(url: url, endpoint: "SPI/PixLiquidadosIntradia")
        return response.value
    }

    /// Fetch PIX availability index
    func fetchPIXAvailability() async throws -> [PIXAvailability] {
        let urlString = "\(BaseURL.olinda)/SPI/versao/v1/odata/PixDisponibilidadeSPI?$format=json&$orderby=DataBase%20desc&$top=12"
        guard let url = URL(string: urlString) else {
            throw BCBAPIError.invalidURL
        }

        let response: PIXAvailabilityResponse = try await fetch(url: url, endpoint: "SPI/PixDisponibilidadeSPI")
        return response.value
    }

    /// Fetch PIX account remuneration
    func fetchPIXRemuneration() async throws -> [PIXRemuneration] {
        let urlString = "\(BaseURL.olinda)/SPI/versao/v1/odata/PixRemuneracaoContaPI?$format=json&$orderby=DataBase%20desc&$top=30"
        guard let url = URL(string: urlString) else {
            throw BCBAPIError.invalidURL
        }

        let response: PIXRemunerationResponse = try await fetch(url: url, endpoint: "SPI/PixRemuneracaoContaPI")
        return response.value
    }

    /// Fetch all PIX statistics (with graceful error handling for unstable endpoints)
    func fetchAllPIXStats() async throws -> PIXStats {
        let log = LogManager.shared
        log.info("Fetching PIX stats", category: "API")

        // These endpoints are more stable
        async let dailyTask = fetchPIXDaily()
        async let intradayTask = fetchPIXIntraday()

        // Get the stable data first
        let (daily, intraday) = try await (dailyTask, intradayTask)

        // Try to get availability and remuneration data, but don't fail if they error
        var availability: [PIXAvailability] = []
        var remuneration: [PIXRemuneration] = []

        // Availability endpoint (may return 500 - BCB server issue)
        do {
            availability = try await fetchPIXAvailability()
        } catch {
            log.warning("PIX Availability endpoint failed (BCB known issue)", category: "API", details: error.localizedDescription)
        }

        // Remuneration endpoint (may return 500 - BCB server issue)
        do {
            remuneration = try await fetchPIXRemuneration()
        } catch {
            log.warning("PIX Remuneration endpoint failed (BCB known issue)", category: "API", details: error.localizedDescription)
        }

        log.info("PIX stats loaded", category: "API", details: "Daily: \(daily.count) | Intraday: \(intraday.count) | Availability: \(availability.count) | Remuneration: \(remuneration.count)")

        return PIXStats(
            dailyData: daily,
            intradayData: intraday,
            availabilityData: availability,
            remunerationData: remuneration
        )
    }

    // MARK: - Expectations API (Focus)

    /// Fetch SELIC expectations by COPOM meeting
    func fetchSelicExpectationsByMeeting() async throws -> [SelicExpectation] {
        let urlString = "\(BaseURL.olinda)/Expectativas/versao/v1/odata/ExpectativasMercadoSelic?$format=json&$top=100&$orderby=Data%20desc,Reuniao%20desc"
        guard let url = URL(string: urlString) else {
            throw BCBAPIError.invalidURL
        }

        let response: SelicExpectationResponse = try await fetch(url: url, endpoint: "Focus/SELIC")
        return response.value
    }

    /// Fetch IPCA 12-month inflation expectations
    func fetchIPCA12MonthExpectations() async throws -> [InflationExpectation] {
        let urlString = "\(BaseURL.olinda)/Expectativas/versao/v1/odata/ExpectativasMercadoInflacao12Meses?$filter=Indicador%20eq%20'IPCA'&$format=json&$top=50&$orderby=Data%20desc"
        guard let url = URL(string: urlString) else {
            throw BCBAPIError.invalidURL
        }

        let response: InflationExpectationResponse = try await fetch(url: url, endpoint: "Focus/IPCA12m")
        return response.value
    }

    /// Fetch IGP-M 12-month inflation expectations
    func fetchIGPM12MonthExpectations() async throws -> [InflationExpectation] {
        let urlString = "\(BaseURL.olinda)/Expectativas/versao/v1/odata/ExpectativasMercadoInflacao12Meses?$filter=Indicador%20eq%20'IGP-M'&$format=json&$top=50&$orderby=Data%20desc"
        guard let url = URL(string: urlString) else {
            throw BCBAPIError.invalidURL
        }

        let response: InflationExpectationResponse = try await fetch(url: url, endpoint: "Focus/IGPM12m")
        return response.value
    }

    /// Fetch annual IGP-M expectations
    func fetchAnnualIGPMExpectations() async throws -> [AnnualExpectation] {
        let urlString = "\(BaseURL.olinda)/Expectativas/versao/v1/odata/ExpectativasMercadoAnuais?$filter=Indicador%20eq%20'IGP-M'&$format=json&$top=50&$orderby=Data%20desc,DataReferencia%20desc"
        guard let url = URL(string: urlString) else {
            throw BCBAPIError.invalidURL
        }

        let response: AnnualExpectationResponse = try await fetch(url: url, endpoint: "Focus/IGPMAnual")
        return response.value
    }

    /// Fetch annual IPCA expectations
    func fetchAnnualIPCAExpectations() async throws -> [AnnualExpectation] {
        let urlString = "\(BaseURL.olinda)/Expectativas/versao/v1/odata/ExpectativasMercadoAnuais?$filter=Indicador%20eq%20'IPCA'&$format=json&$top=50&$orderby=Data%20desc,DataReferencia%20desc"
        guard let url = URL(string: urlString) else {
            throw BCBAPIError.invalidURL
        }

        let response: AnnualExpectationResponse = try await fetch(url: url, endpoint: "Focus/IPCAAnual")
        return response.value
    }

    /// Fetch annual PIB expectations
    func fetchAnnualPIBExpectations() async throws -> [AnnualExpectation] {
        let urlString = "\(BaseURL.olinda)/Expectativas/versao/v1/odata/ExpectativasMercadoAnuais?$filter=Indicador%20eq%20'PIB%20Total'&$format=json&$top=50&$orderby=Data%20desc,DataReferencia%20desc"
        guard let url = URL(string: urlString) else {
            throw BCBAPIError.invalidURL
        }

        let response: AnnualExpectationResponse = try await fetch(url: url, endpoint: "Focus/PIBAnual")
        return response.value
    }

    /// Fetch annual Câmbio expectations
    func fetchAnnualCambioExpectations() async throws -> [AnnualExpectation] {
        // Note: Câmbio must be URL encoded as C%C3%A2mbio
        let urlString = "\(BaseURL.olinda)/Expectativas/versao/v1/odata/ExpectativasMercadoAnuais?$filter=Indicador%20eq%20'C%C3%A2mbio'&$format=json&$top=50&$orderby=Data%20desc,DataReferencia%20desc"
        guard let url = URL(string: urlString) else {
            throw BCBAPIError.invalidURL
        }

        let response: AnnualExpectationResponse = try await fetch(url: url, endpoint: "Focus/CambioAnual")
        return response.value
    }

    /// Fetch all Focus stats in parallel
    func fetchAllFocusStats() async throws -> FocusStats {
        let log = LogManager.shared
        log.info("Fetching Focus stats (7 endpoints in parallel)", category: "API")

        async let selicTask = fetchSelicExpectationsByMeeting()
        async let ipcaTask = fetchIPCA12MonthExpectations()
        async let igpmTask = fetchIGPM12MonthExpectations()
        async let annualIPCATask = fetchAnnualIPCAExpectations()
        async let annualIGPMTask = fetchAnnualIGPMExpectations()
        async let annualPIBTask = fetchAnnualPIBExpectations()
        async let annualCambioTask = fetchAnnualCambioExpectations()

        let (selic, ipca, igpm, annualIPCA, annualIGPM, annualPIB, annualCambio) = try await (selicTask, ipcaTask, igpmTask, annualIPCATask, annualIGPMTask, annualPIBTask, annualCambioTask)

        log.info("Focus stats loaded", category: "API", details: "SELIC: \(selic.count) | IPCA: \(ipca.count) | IGPM: \(igpm.count) | PIB: \(annualPIB.count) | Cambio: \(annualCambio.count)")

        return FocusStats(
            selicExpectations: selic,
            ipcaExpectations: ipca,
            igpmExpectations: igpm,
            annualIPCA: annualIPCA,
            annualIGPM: annualIGPM,
            annualPIB: annualPIB,
            annualCambio: annualCambio
        )
    }

    // Legacy compatibility methods
    func fetchInflationExpectations() async throws -> [MarketExpectation] {
        LogManager.shared.warning("Legacy fetchInflationExpectations called", category: "API")
        let urlString = "\(BaseURL.olinda)/Expectativas/versao/v1/odata/ExpectativasMercadoAnuais?$filter=Indicador%20eq%20'IPCA'&$format=json&$top=50&$orderby=Data%20desc,DataReferencia%20desc"
        guard let url = URL(string: urlString) else {
            throw BCBAPIError.invalidURL
        }

        let response: ExpectationsResponse = try await fetch(url: url, endpoint: "Legacy/Inflation")
        return response.value
    }

    func fetchSelicExpectations() async throws -> [MarketExpectation] {
        LogManager.shared.warning("Legacy fetchSelicExpectations called", category: "API")
        let urlString = "\(BaseURL.olinda)/Expectativas/versao/v1/odata/ExpectativasMercadoAnuais?$filter=Indicador%20eq%20'Selic'&$format=json&$top=50&$orderby=Data%20desc,DataReferencia%20desc"
        guard let url = URL(string: urlString) else {
            throw BCBAPIError.invalidURL
        }

        let response: ExpectationsResponse = try await fetch(url: url, endpoint: "Legacy/SELIC")
        return response.value
    }
}

// MARK: - Cache Manager

@MainActor
class CacheManager: ObservableObject {
    static let shared = CacheManager()

    private let defaults = UserDefaults.standard
    private let encoder = JSONEncoder()
    private let decoder = JSONDecoder()

    private let cacheExpirationMinutes: Double = 5

    private init() {}

    func save<T: Codable>(_ data: T, forKey key: String) {
        if let encoded = try? encoder.encode(data) {
            defaults.set(encoded, forKey: key)
            defaults.set(Date(), forKey: "\(key)_timestamp")
        }
    }

    func load<T: Codable>(forKey key: String) -> T? {
        guard let data = defaults.data(forKey: key),
              let timestamp = defaults.object(forKey: "\(key)_timestamp") as? Date else {
            return nil
        }

        // Check if cache is expired
        let elapsed = Date().timeIntervalSince(timestamp) / 60
        if elapsed > cacheExpirationMinutes {
            return nil
        }

        return try? decoder.decode(T.self, from: data)
    }

    func clearAll() {
        let domain = Bundle.main.bundleIdentifier ?? ""
        defaults.removePersistentDomain(forName: domain)
    }

    func isCacheValid(forKey key: String) -> Bool {
        guard let timestamp = defaults.object(forKey: "\(key)_timestamp") as? Date else {
            return false
        }
        let elapsed = Date().timeIntervalSince(timestamp) / 60
        return elapsed <= cacheExpirationMinutes
    }
}


     1→import SwiftUI
     2→import Combine
     3→
     4→@MainActor
     5→class DashboardViewModel: ObservableObject {
     6→    // MARK: - Published Properties
     7→
     8→    @Published var isLoading = false {
     9→        didSet { log.logStateChange("isLoading", oldValue: oldValue, newValue: isLoading) }
    10→    }
    11→    @Published var errorMessage: String? {
    12→        didSet {
    13→            if let error = errorMessage {
    14→                log.error("ViewModel error", category: "ViewModel", details: error)
    15→            }
    16→        }
    17→    }
    18→    @Published var lastUpdate: Date?
    19→
    20→    private let log = LogManager.shared
    21→
    22→    // Currency Data
    23→    @Published var dollarQuote: DollarQuote?
    24→    @Published var dollarHistory: [DollarQuote] = []
    25→    @Published var euroQuote: CurrencyQuote?
    26→    @Published var currencies: [CurrencyInfo] = []
    27→
    28→    // Rates Data
    29→    @Published var selicTarget: TimeSeriesValue?
    30→    @Published var selicDaily: TimeSeriesValue?
    31→    @Published var cdi: TimeSeriesValue?
    32→    @Published var ipcaMonthly: TimeSeriesValue?
    33→    @Published var ipca12m: TimeSeriesValue?
    34→    @Published var selicHistory: [TimeSeriesValue] = []
    35→    @Published var ipcaHistory: [TimeSeriesValue] = []
    36→
    37→    // Institutions (new data structure)
    38→    @Published var banks: [Bank] = []
    39→    @Published var cooperatives: [Cooperative] = []
    40→    @Published var consortia: [Consortium] = []
    41→    @Published var sociedades: [Sociedade] = []
    42→    @Published var institutionsLoading = false
    43→    @Published var institutionStats = InstitutionStats()
    44→
    45→    // Legacy
    46→    @Published var institutions: [Institution] = []
    47→    @Published var searchQuery = ""
    48→
    49→    // PIX Statistics
    50→    @Published var pixStats = PIXStats()
    51→    @Published var pixLoading = false
    52→
    53→    // Expectations (Focus)
    54→    @Published var focusStats = FocusStats()
    55→    @Published var focusLoading = false
    56→
    57→    // Legacy expectations
    58→    @Published var inflationExpectations: [MarketExpectation] = []
    59→    @Published var selicExpectations: [MarketExpectation] = []
    60→
    61→    // Services
    62→    private let apiClient = BCBAPIClient.shared
    63→    private let cache = CacheManager.shared
    64→    private let notificationService = NotificationService.shared
    65→    private var refreshTimer: Timer?
    66→
    67→    // MARK: - Computed Properties
    68→
    69→    var dollarChange: Double {
    70→        guard dollarHistory.count >= 2 else { return 0 }
    71→        let current = dollarHistory.last?.sellRate ?? 0
    72→        let previous = dollarHistory[dollarHistory.count - 2].sellRate
    73→        return ((current - previous) / previous) * 100
    74→    }
    75→
    76→    var realInterestRate: Double {
    77→        guard let selic = selicTarget?.numericValue,
    78→              let ipca = ipca12m?.numericValue else { return 0 }
    79→        return ((1 + selic / 100) / (1 + ipca / 100) - 1) * 100
    80→    }
    81→
    82→    // MARK: - Initialization
    83→
    84→    init() {
    85→        log.info("DashboardViewModel initialized", category: "ViewModel")
    86→        setupAutoRefresh()
    87→        requestNotificationPermission()
    88→    }
    89→
    90→    private func requestNotificationPermission() {
    91→        Task {
    92→            log.debug("Requesting notification permission", category: "ViewModel")
    93→            let granted = await notificationService.requestAuthorization()
    94→            log.info("Notification permission", category: "ViewModel", details: granted ? "Granted" : "Denied")
    95→        }
    96→    }
    97→
    98→    deinit {
    99→        refreshTimer?.invalidate()
   100→    }
   101→
   102→    // MARK: - Auto Refresh
   103→
   104→    private var refreshObserver: NSObjectProtocol?
   105→
   106→    private func setupAutoRefresh() {
   107→        // Cancel existing timer
   108→        refreshTimer?.invalidate()
   109→        refreshTimer = nil
   110→
   111→        let interval = UserDefaults.standard.integer(forKey: "refreshInterval")
   112→
   113→        // -1 means disabled, skip setup
   114→        guard interval >= 0 else {
   115→            log.info("Auto-refresh disabled", category: "ViewModel")
   116→            return
   117→        }
   118→
   119→        // Calculate interval in seconds (0 = 30 seconds, otherwise minutes * 60)
   120→        let intervalSeconds: TimeInterval = interval == 0 ? 30 : TimeInterval(interval * 60)
   121→        log.info("Auto-refresh configured", category: "ViewModel", details: "Interval: \(Int(intervalSeconds))s")
   122→
   123→        refreshTimer = Timer.scheduledTimer(withTimeInterval: intervalSeconds, repeats: true) { [weak self] _ in
   124→            Task { @MainActor in
   125→                self?.log.logRefresh("Auto-refresh timer")
   126→                await self?.refreshAll()
   127→            }
   128→        }
   129→
   130→        // Only add observer once
   131→        if refreshObserver == nil {
   132→            refreshObserver = NotificationCenter.default.addObserver(forName: UserDefaults.didChangeNotification, object: nil, queue: .main) { [weak self] _ in
   133→                Task { @MainActor in
   134→                    self?.setupAutoRefresh()
   135→                }
   136→            }
   137→        }
   138→    }
   139→
   140→    func updateRefreshInterval() {
   141→        log.debug("Refresh interval updated by user", category: "ViewModel")
   142→        setupAutoRefresh()
   143→    }
   144→
   145→    // MARK: - Data Loading
   146→
   147→    func refreshAll() async {
   148→        log.logRefresh("Manual/Initial")
   149→        let startTime = Date()
   150→        isLoading = true
   151→        errorMessage = nil
   152→
   153→        async let currencyTask: () = loadCurrencyData()
   154→        async let ratesTask: () = loadRatesData()
   155→
   156→        await currencyTask
   157→        await ratesTask
   158→
   159→        lastUpdate = Date()
   160→        isLoading = false
   161→
   162→        let duration = Date().timeIntervalSince(startTime)
   163→        log.info("Refresh completed", category: "ViewModel", details: String(format: "Duration: %.2fs", duration))
   164→    }
   165→
   166→    func loadCurrencyData() async {
   167→        log.info("Loading currency data", category: "ViewModel")
   168→        let startTime = Date()
   169→
   170→        do {
   171→            // Try to get today's quote, if not available try previous days
   172→            var quotes: [DollarQuote] = []
   173→            var attempts = 0
   174→
   175→            while quotes.isEmpty && attempts < 5 {
   176→                let date = Calendar.current.date(byAdding: .day, value: -attempts, to: Date()) ?? Date()
   177→                quotes = try await apiClient.fetchDollarQuote(date: date)
   178→                attempts += 1
   179→            }
   180→
   181→            if attempts > 1 {
   182→                log.debug("Dollar quote found after \(attempts) attempts", category: "ViewModel")
   183→            }
   184→
   185→            if let quote = quotes.last {
   186→                // Check for dollar change and notify
   187→                notificationService.checkDollarChange(newRate: quote.sellRate)
   188→                dollarQuote = quote
   189→                log.info("Dollar quote updated", category: "ViewModel", details: String(format: "R$ %.4f", quote.sellRate))
   190→            }
   191→
   192→            // Get dollar history (last 30 days)
   193→            let endDate = Date()
   194→            let startDate = Calendar.current.date(byAdding: .day, value: -30, to: endDate) ?? endDate
   195→            dollarHistory = try await apiClient.fetchDollarQuotePeriod(startDate: startDate, endDate: endDate)
   196→            log.logDataLoaded("Dollar History", count: dollarHistory.count)
   197→
   198→            // Get Euro quote
   199→            attempts = 0
   200→            var euroQuotes: [CurrencyQuote] = []
   201→            while euroQuotes.isEmpty && attempts < 5 {
   202→                let date = Calendar.current.date(byAdding: .day, value: -attempts, to: Date()) ?? Date()
   203→                euroQuotes = try await apiClient.fetchCurrencyQuote(symbol: "EUR", date: date)
   204→                attempts += 1
   205→            }
   206→
   207→            if let quote = euroQuotes.last(where: { $0.isPTAXClosing }) ?? euroQuotes.last {
   208→                // Check for euro change and notify
   209→                notificationService.checkEuroChange(newRate: quote.sellRate)
   210→                euroQuote = quote
   211→                log.info("Euro quote updated", category: "ViewModel", details: String(format: "R$ %.4f", quote.sellRate))
   212→            }
   213→
   214→            // Get available currencies
   215→            currencies = try await apiClient.fetchCurrencies()
   216→            log.logDataLoaded("Currencies", count: currencies.count)
   217→
   218→            let duration = Date().timeIntervalSince(startTime)
   219→            log.info("Currency data loaded", category: "ViewModel", details: String(format: "Duration: %.2fs", duration))
   220→
   221→        } catch {
   222→            log.error("Failed to load currency data", category: "ViewModel", details: error.localizedDescription)
   223→            errorMessage = error.localizedDescription
   224→        }
   225→    }
   226→
   227→    func loadRatesData() async {
   228→        log.info("Loading rates data", category: "ViewModel")
   229→        let startTime = Date()
   230→
   231→        do {
   232→            // SELIC
   233→            async let selicTargetData = apiClient.fetchTimeSeries(code: SeriesCode.selicTarget.rawValue, lastN: 2)
   234→            async let selicDailyData = apiClient.fetchTimeSeries(code: SeriesCode.selicDaily.rawValue, lastN: 1)
   235→            async let cdiData = apiClient.fetchTimeSeries(code: SeriesCode.cdi.rawValue, lastN: 1)
   236→
   237→            // IPCA
   238→            async let ipcaMonthlyData = apiClient.fetchTimeSeries(code: SeriesCode.ipcaMonthly.rawValue, lastN: 12)
   239→            async let ipca12mData = apiClient.fetchTimeSeries(code: SeriesCode.ipca12m.rawValue, lastN: 2)
   240→
   241→            // History
   242→            async let selicHistoryData = apiClient.fetchTimeSeries(code: SeriesCode.selicTarget.rawValue, lastN: 20)
   243→
   244→            let (selicT, selicD, cdiVal, ipcaM, ipca12, selicH) = try await (
   245→                selicTargetData, selicDailyData, cdiData, ipcaMonthlyData, ipca12mData, selicHistoryData
   246→            )
   247→
   248→            selicTarget = selicT.last
   249→            selicDaily = selicD.last
   250→            cdi = cdiVal.last
   251→            ipcaMonthly = ipcaM.last
   252→            ipca12m = ipca12.last
   253→            ipcaHistory = ipcaM
   254→            selicHistory = selicH
   255→
   256→            let duration = Date().timeIntervalSince(startTime)
   257→            log.info("Rates data loaded", category: "ViewModel", details: String(format: "SELIC: %.2f%% | IPCA: %.2f%% | CDI: %.4f%% | Duration: %.2fs",
   258→                                                                                selicTarget?.numericValue ?? 0,
   259→                                                                                ipca12m?.numericValue ?? 0,
   260→                                                                                cdi?.numericValue ?? 0,
   261→                                                                                duration))
   262→
   263→        } catch {
   264→            log.error("Failed to load rates data", category: "ViewModel", details: error.localizedDescription)
   265→            errorMessage = error.localizedDescription
   266→        }
   267→    }
   268→
   269→    func loadInstitutions() async {
   270→        log.info("Loading institutions", category: "ViewModel")
   271→        let startTime = Date()
   272→        institutionsLoading = true
   273→        defer { institutionsLoading = false }
   274→
   275→        do {
   276→            let result = try await apiClient.fetchAllInstitutions()
   277→
   278→            banks = result.banks.sorted { $0.name < $1.name }
   279→            cooperatives = result.cooperatives.sorted { $0.name < $1.name }
   280→            consortia = result.consortia.sorted { $0.name < $1.name }
   281→            sociedades = result.sociedades.sorted { $0.name < $1.name }
   282→
   283→            // Update stats
   284→            institutionStats.totalBanks = banks.count
   285→            institutionStats.totalCooperatives = cooperatives.count
   286→            institutionStats.totalConsortia = consortia.count
   287→            institutionStats.totalSociedades = sociedades.count
   288→
   289→            // Calculate state distribution
   290→            var stateCount: [String: Int] = [:]
   291→            for bank in banks {
   292→                if let state = bank.state {
   293→                    stateCount[state, default: 0] += 1
   294→                }
   295→            }
   296→            for coop in cooperatives {
   297→                if let state = coop.state {
   298→                    stateCount[state, default: 0] += 1
   299→                }
   300→            }
   301→            for cons in consortia {
   302→                if let state = cons.state {
   303→                    stateCount[state, default: 0] += 1
   304→                }
   305→            }
   306→            for soc in sociedades {
   307→                if let state = soc.state {
   308→                    stateCount[state, default: 0] += 1
   309→                }
   310→            }
   311→            institutionStats.stateDistribution = stateCount
   312→
   313→            let duration = Date().timeIntervalSince(startTime)
   314→            log.info("Institutions loaded", category: "ViewModel", details: "Total: \(institutionStats.total) | Duration: \(String(format: "%.2fs", duration))")
   315→
   316→        } catch {
   317→            log.error("Failed to load institutions", category: "ViewModel", details: error.localizedDescription)
   318→            errorMessage = error.localizedDescription
   319→        }
   320→    }
   321→
   322→    func searchInstitutions() async {
   323→        log.warning("Legacy searchInstitutions called", category: "ViewModel")
   324→    }
   325→
   326→    func loadExpectations() async {
   327→        log.warning("Legacy loadExpectations called", category: "ViewModel")
   328→        do {
   329→            async let inflationData = apiClient.fetchInflationExpectations()
   330→            async let selicData = apiClient.fetchSelicExpectations()
   331→
   332→            let (inflation, selic) = try await (inflationData, selicData)
   333→            inflationExpectations = inflation
   334→            selicExpectations = selic
   335→        } catch {
   336→            errorMessage = error.localizedDescription
   337→        }
   338→    }
   339→
   340→    func loadFocusStats() async {
   341→        log.info("Loading Focus stats", category: "ViewModel")
   342→        let startTime = Date()
   343→        focusLoading = true
   344→        defer { focusLoading = false }
   345→
   346→        do {
   347→            focusStats = try await apiClient.fetchAllFocusStats()
   348→            let duration = Date().timeIntervalSince(startTime)
   349→            log.info("Focus stats loaded", category: "ViewModel", details: "SELIC expectations: \(focusStats.selicExpectations.count) | Duration: \(String(format: "%.2fs", duration))")
   350→        } catch {
   351→            log.error("Failed to load Focus stats", category: "ViewModel", details: error.localizedDescription)
   352→            errorMessage = error.localizedDescription
   353→        }
   354→    }
   355→
   356→    func loadPIXStats() async {
   357→        log.info("Loading PIX stats", category: "ViewModel")
   358→        let startTime = Date()
   359→        pixLoading = true
   360→        defer { pixLoading = false }
   361→
   362→        do {
   363→            pixStats = try await apiClient.fetchAllPIXStats()
   364→            let duration = Date().timeIntervalSince(startTime)
   365→            log.info("PIX stats loaded", category: "ViewModel", details: "Daily: \(pixStats.dailyData.count) | Intraday: \(pixStats.intradayData.count) | Duration: \(String(format: "%.2fs", duration))")
   366→        } catch {
   367→            log.error("Failed to load PIX stats", category: "ViewModel", details: error.localizedDescription)
   368→            errorMessage = error.localizedDescription
   369→        }
   370→    }
   371→
   372→    // MARK: - Cache
   373→
   374→    func clearCache() {
   375→        log.info("Clearing cache", category: "ViewModel")
   376→        cache.clearAll()
   377→        Task {
   378→            await refreshAll()
   379→        }
   380→    }
   381→
   382→    // MARK: - Utility
   383→
   384→    func dismissError() {
   385→        errorMessage = nil
   386→    }
   387→}
   388→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

import SwiftUI
import Combine

@MainActor
class DashboardViewModel: ObservableObject {
    // MARK: - Published Properties

    @Published var isLoading = false {
        didSet { log.logStateChange("isLoading", oldValue: oldValue, newValue: isLoading) }
    }
    @Published var errorMessage: String? {
        didSet {
            if let error = errorMessage {
                log.error("ViewModel error", category: "ViewModel", details: error)
            }
        }
    }
    @Published var lastUpdate: Date?

    private let log = LogManager.shared

    // Currency Data
    @Published var dollarQuote: DollarQuote?
    @Published var dollarHistory: [DollarQuote] = []
    @Published var euroQuote: CurrencyQuote?
    @Published var currencies: [CurrencyInfo] = []

    // Rates Data
    @Published var selicTarget: TimeSeriesValue?
    @Published var selicDaily: TimeSeriesValue?
    @Published var cdi: TimeSeriesValue?
    @Published var ipcaMonthly: TimeSeriesValue?
    @Published var ipca12m: TimeSeriesValue?
    @Published var selicHistory: [TimeSeriesValue] = []
    @Published var ipcaHistory: [TimeSeriesValue] = []

    // Institutions (new data structure)
    @Published var banks: [Bank] = []
    @Published var cooperatives: [Cooperative] = []
    @Published var consortia: [Consortium] = []
    @Published var sociedades: [Sociedade] = []
    @Published var institutionsLoading = false
    @Published var institutionStats = InstitutionStats()

    // Legacy
    @Published var institutions: [Institution] = []
    @Published var searchQuery = ""

    // PIX Statistics
    @Published var pixStats = PIXStats()
    @Published var pixLoading = false

    // Expectations (Focus)
    @Published var focusStats = FocusStats()
    @Published var focusLoading = false

    // Legacy expectations
    @Published var inflationExpectations: [MarketExpectation] = []
    @Published var selicExpectations: [MarketExpectation] = []

    // Services
    private let apiClient = BCBAPIClient.shared
    private let cache = CacheManager.shared
    private let notificationService = NotificationService.shared
    private var refreshTimer: Timer?

    // MARK: - Computed Properties

    var dollarChange: Double {
        guard dollarHistory.count >= 2 else { return 0 }
        let current = dollarHistory.last?.sellRate ?? 0
        let previous = dollarHistory[dollarHistory.count - 2].sellRate
        return ((current - previous) / previous) * 100
    }

    var realInterestRate: Double {
        guard let selic = selicTarget?.numericValue,
              let ipca = ipca12m?.numericValue else { return 0 }
        return ((1 + selic / 100) / (1 + ipca / 100) - 1) * 100
    }

    // MARK: - Initialization

    init() {
        log.info("DashboardViewModel initialized", category: "ViewModel")
        setupAutoRefresh()
        requestNotificationPermission()
    }

    private func requestNotificationPermission() {
        Task {
            log.debug("Requesting notification permission", category: "ViewModel")
            let granted = await notificationService.requestAuthorization()
            log.info("Notification permission", category: "ViewModel", details: granted ? "Granted" : "Denied")
        }
    }

    deinit {
        refreshTimer?.invalidate()
    }

    // MARK: - Auto Refresh

    private var refreshObserver: NSObjectProtocol?

    private func setupAutoRefresh() {
        // Cancel existing timer
        refreshTimer?.invalidate()
        refreshTimer = nil

        let interval = UserDefaults.standard.integer(forKey: "refreshInterval")

        // -1 means disabled, skip setup
        guard interval >= 0 else {
            log.info("Auto-refresh disabled", category: "ViewModel")
            return
        }

        // Calculate interval in seconds (0 = 30 seconds, otherwise minutes * 60)
        let intervalSeconds: TimeInterval = interval == 0 ? 30 : TimeInterval(interval * 60)
        log.info("Auto-refresh configured", category: "ViewModel", details: "Interval: \(Int(intervalSeconds))s")

        refreshTimer = Timer.scheduledTimer(withTimeInterval: intervalSeconds, repeats: true) { [weak self] _ in
            Task { @MainActor in
                self?.log.logRefresh("Auto-refresh timer")
                await self?.refreshAll()
            }
        }

        // Only add observer once
        if refreshObserver == nil {
            refreshObserver = NotificationCenter.default.addObserver(forName: UserDefaults.didChangeNotification, object: nil, queue: .main) { [weak self] _ in
                Task { @MainActor in
                    self?.setupAutoRefresh()
                }
            }
        }
    }

    func updateRefreshInterval() {
        log.debug("Refresh interval updated by user", category: "ViewModel")
        setupAutoRefresh()
    }

    // MARK: - Data Loading

    func refreshAll() async {
        log.logRefresh("Manual/Initial")
        let startTime = Date()
        isLoading = true
        errorMessage = nil

        async let currencyTask: () = loadCurrencyData()
        async let ratesTask: () = loadRatesData()

        await currencyTask
        await ratesTask

        lastUpdate = Date()
        isLoading = false

        let duration = Date().timeIntervalSince(startTime)
        log.info("Refresh completed", category: "ViewModel", details: String(format: "Duration: %.2fs", duration))
    }

    func loadCurrencyData() async {
        log.info("Loading currency data", category: "ViewModel")
        let startTime = Date()

        do {
            // Try to get today's quote, if not available try previous days
            var quotes: [DollarQuote] = []
            var attempts = 0

            while quotes.isEmpty && attempts < 5 {
                let date = Calendar.current.date(byAdding: .day, value: -attempts, to: Date()) ?? Date()
                quotes = try await apiClient.fetchDollarQuote(date: date)
                attempts += 1
            }

            if attempts > 1 {
                log.debug("Dollar quote found after \(attempts) attempts", category: "ViewModel")
            }

            if let quote = quotes.last {
                // Check for dollar change and notify
                notificationService.checkDollarChange(newRate: quote.sellRate)
                dollarQuote = quote
                log.info("Dollar quote updated", category: "ViewModel", details: String(format: "R$ %.4f", quote.sellRate))
            }

            // Get dollar history (last 30 days)
            let endDate = Date()
            let startDate = Calendar.current.date(byAdding: .day, value: -30, to: endDate) ?? endDate
            dollarHistory = try await apiClient.fetchDollarQuotePeriod(startDate: startDate, endDate: endDate)
            log.logDataLoaded("Dollar History", count: dollarHistory.count)

            // Get Euro quote
            attempts = 0
            var euroQuotes: [CurrencyQuote] = []
            while euroQuotes.isEmpty && attempts < 5 {
                let date = Calendar.current.date(byAdding: .day, value: -attempts, to: Date()) ?? Date()
                euroQuotes = try await apiClient.fetchCurrencyQuote(symbol: "EUR", date: date)
                attempts += 1
            }

            if let quote = euroQuotes.last(where: { $0.isPTAXClosing }) ?? euroQuotes.last {
                // Check for euro change and notify
                notificationService.checkEuroChange(newRate: quote.sellRate)
                euroQuote = quote
                log.info("Euro quote updated", category: "ViewModel", details: String(format: "R$ %.4f", quote.sellRate))
            }

            // Get available currencies
            currencies = try await apiClient.fetchCurrencies()
            log.logDataLoaded("Currencies", count: currencies.count)

            let duration = Date().timeIntervalSince(startTime)
            log.info("Currency data loaded", category: "ViewModel", details: String(format: "Duration: %.2fs", duration))

        } catch {
            log.error("Failed to load currency data", category: "ViewModel", details: error.localizedDescription)
            errorMessage = error.localizedDescription
        }
    }

    func loadRatesData() async {
        log.info("Loading rates data", category: "ViewModel")
        let startTime = Date()

        do {
            // SELIC
            async let selicTargetData = apiClient.fetchTimeSeries(code: SeriesCode.selicTarget.rawValue, lastN: 2)
            async let selicDailyData = apiClient.fetchTimeSeries(code: SeriesCode.selicDaily.rawValue, lastN: 1)
            async let cdiData = apiClient.fetchTimeSeries(code: SeriesCode.cdi.rawValue, lastN: 1)

            // IPCA
            async let ipcaMonthlyData = apiClient.fetchTimeSeries(code: SeriesCode.ipcaMonthly.rawValue, lastN: 12)
            async let ipca12mData = apiClient.fetchTimeSeries(code: SeriesCode.ipca12m.rawValue, lastN: 2)

            // History
            async let selicHistoryData = apiClient.fetchTimeSeries(code: SeriesCode.selicTarget.rawValue, lastN: 20)

            let (selicT, selicD, cdiVal, ipcaM, ipca12, selicH) = try await (
                selicTargetData, selicDailyData, cdiData, ipcaMonthlyData, ipca12mData, selicHistoryData
            )

            selicTarget = selicT.last
            selicDaily = selicD.last
            cdi = cdiVal.last
            ipcaMonthly = ipcaM.last
            ipca12m = ipca12.last
            ipcaHistory = ipcaM
            selicHistory = selicH

            let duration = Date().timeIntervalSince(startTime)
            log.info("Rates data loaded", category: "ViewModel", details: String(format: "SELIC: %.2f%% | IPCA: %.2f%% | CDI: %.4f%% | Duration: %.2fs",
                                                                                selicTarget?.numericValue ?? 0,
                                                                                ipca12m?.numericValue ?? 0,
                                                                                cdi?.numericValue ?? 0,
                                                                                duration))

        } catch {
            log.error("Failed to load rates data", category: "ViewModel", details: error.localizedDescription)
            errorMessage = error.localizedDescription
        }
    }

    func loadInstitutions() async {
        log.info("Loading institutions", category: "ViewModel")
        let startTime = Date()
        institutionsLoading = true
        defer { institutionsLoading = false }

        do {
            let result = try await apiClient.fetchAllInstitutions()

            banks = result.banks.sorted { $0.name < $1.name }
            cooperatives = result.cooperatives.sorted { $0.name < $1.name }
            consortia = result.consortia.sorted { $0.name < $1.name }
            sociedades = result.sociedades.sorted { $0.name < $1.name }

            // Update stats
            institutionStats.totalBanks = banks.count
            institutionStats.totalCooperatives = cooperatives.count
            institutionStats.totalConsortia = consortia.count
            institutionStats.totalSociedades = sociedades.count

            // Calculate state distribution
            var stateCount: [String: Int] = [:]
            for bank in banks {
                if let state = bank.state {
                    stateCount[state, default: 0] += 1
                }
            }
            for coop in cooperatives {
                if let state = coop.state {
                    stateCount[state, default: 0] += 1
                }
            }
            for cons in consortia {
                if let state = cons.state {
                    stateCount[state, default: 0] += 1
                }
            }
            for soc in sociedades {
                if let state = soc.state {
                    stateCount[state, default: 0] += 1
                }
            }
            institutionStats.stateDistribution = stateCount

            let duration = Date().timeIntervalSince(startTime)
            log.info("Institutions loaded", category: "ViewModel", details: "Total: \(institutionStats.total) | Duration: \(String(format: "%.2fs", duration))")

        } catch {
            log.error("Failed to load institutions", category: "ViewModel", details: error.localizedDescription)
            errorMessage = error.localizedDescription
        }
    }

    func searchInstitutions() async {
        log.warning("Legacy searchInstitutions called", category: "ViewModel")
    }

    func loadExpectations() async {
        log.warning("Legacy loadExpectations called", category: "ViewModel")
        do {
            async let inflationData = apiClient.fetchInflationExpectations()
            async let selicData = apiClient.fetchSelicExpectations()

            let (inflation, selic) = try await (inflationData, selicData)
            inflationExpectations = inflation
            selicExpectations = selic
        } catch {
            errorMessage = error.localizedDescription
        }
    }

    func loadFocusStats() async {
        log.info("Loading Focus stats", category: "ViewModel")
        let startTime = Date()
        focusLoading = true
        defer { focusLoading = false }

        do {
            focusStats = try await apiClient.fetchAllFocusStats()
            let duration = Date().timeIntervalSince(startTime)
            log.info("Focus stats loaded", category: "ViewModel", details: "SELIC expectations: \(focusStats.selicExpectations.count) | Duration: \(String(format: "%.2fs", duration))")
        } catch {
            log.error("Failed to load Focus stats", category: "ViewModel", details: error.localizedDescription)
            errorMessage = error.localizedDescription
        }
    }

    func loadPIXStats() async {
        log.info("Loading PIX stats", category: "ViewModel")
        let startTime = Date()
        pixLoading = true
        defer { pixLoading = false }

        do {
            pixStats = try await apiClient.fetchAllPIXStats()
            let duration = Date().timeIntervalSince(startTime)
            log.info("PIX stats loaded", category: "ViewModel", details: "Daily: \(pixStats.dailyData.count) | Intraday: \(pixStats.intradayData.count) | Duration: \(String(format: "%.2fs", duration))")
        } catch {
            log.error("Failed to load PIX stats", category: "ViewModel", details: error.localizedDescription)
            errorMessage = error.localizedDescription
        }
    }

    // MARK: - Cache

    func clearCache() {
        log.info("Clearing cache", category: "ViewModel")
        cache.clearAll()
        Task {
            await refreshAll()
        }
    }

    // MARK: - Utility

    func dismissError() {
        errorMessage = nil
    }
}


Summary: 96Blocks iOS App: CloudKit Crash Fix


Summary: 96Blocks iOS: CloudKit Crash Fix & App Analysis


Summary: 96Blocks iOS: CloudKit crash fix & UI analysis


Summary: 96Blocks iOS: CloudKit crash fix and comprehensive bug analysis


Deixe-me ver mais alguns arquivos para completar a análise.


Summary: 96Blocks iOS: CloudKit Fix & UI Analysis


Summary: 96Blocks iOS: CloudKit crash fix, UI cleanup, deduplication


Summary: 96Blocks iOS: Fix crashes, deduplicate code, localize strings


Summary: 96Blocks iOS: Bug fixes, CloudKit crash, deduplication


Summary: 96Blocks iOS: Bug fixes, CloudKit setup, code deduplication


Summary: Fix duplicate data & implement Apple Sign In