Explore the GmailAI-iOS codebase at /Users/neog/Apps/iOS/GmailAI-iOS to understand:

1. How AI analysis currently works (AIAnalysisService, batch analysis)
2. What actions are available on emails (delete, archive, star, labels)
3. How the Smart Folders feature works (EmailStoreFiltering.swift)
4. Current cleanup/review functionality if any

Focus on:
- AIAnalysisService.swift and AIServiceBatch.swift
- EmailStoreActions.swift
- GmailServiceActions.swift
- Any existing cleanup or review views

Return the key patterns for:
- Batch AI analysis
- Email actions (delete, archive, label)
- Current UI patterns for bulk operations


I'll explore the GmailAI-iOS codebase to understand the AI analysis, email actions, and smart folders functionality.


Bash

find /Users/neog/Apps/iOS/GmailAI-iOS -type f -name "*.swift" | head -20

List Swift files in GmailAI-iOS project


Now let me read the key files I need to understand:


     1→import Foundation
     2→
     3→// MARK: - AI Analysis Service
     4→// Extensions: AIServiceModels.swift, AIServiceBatch.swift
     5→// Helpers: AIPromptBuilder.swift, AIResponseParser.swift, AIChunkingStrategy.swift
     6→
     7→class AIAnalysisService: EmailAnalyzing {
     8→    // MARK: - Cached Codecs (Performance Optimization)
     9→
    10→    static let decoder: JSONDecoder = {
    11→        let decoder = JSONDecoder()
    12→        return decoder
    13→    }()
    14→
    15→    static let encoder: JSONEncoder = {
    16→        let encoder = JSONEncoder()
    17→        return encoder
    18→    }()
    19→    // Keys for secure storage
    20→    private static let apiKeyKeychainKey = "lm_gateway_api_key"
    21→    private static let serverURLKey = "ai_server_url"
    22→
    23→    // Default server URL only - API key must be configured by user
    24→    private static let defaultServerURL = "https://mac-studio-de-leandro.tail0360aa.ts.net"
    25→
    26→    // LM Gateway via Tailscale
    27→    private var baseURL: String {
    28→        UserDefaults.standard.string(forKey: Self.serverURLKey) ?? Self.defaultServerURL
    29→    }
    30→
    31→    // API Key from Keychain (secure storage) - no hardcoded default
    32→    private var apiKey: String {
    33→        KeychainHelper.shared.load(forKey: Self.apiKeyKeychainKey) ?? ""
    34→    }
    35→
    36→    private let defaultModel = "default"
    37→
    38→    // MARK: - Components
    39→
    40→    private let promptBuilder = AIPromptBuilder()
    41→    private let responseParser = AIResponseParser()
    42→    private let chunkingStrategy: AIChunkingStrategy
    43→
    44→    // MARK: - Settings (internal for extensions)
    45→
    46→    var settings: AppSettings { AppSettings.shared }
    47→    var maxConcurrentAnalysis: Int { settings.concurrentAnalysis }
    48→    var retryAttempts: Int { settings.retryAttempts }
    49→
    50→    // MARK: - Initialization
    51→
    52→    init() {
    53→        let settings = AppSettings.shared
    54→        self.chunkingStrategy = AIChunkingStrategy(
    55→            maxChunkSize: settings.maxBodyChunkSize
    56→        )
    57→    }
    58→
    59→    // MARK: - Configuration
    60→
    61→    /// Update API key (stored securely in Keychain)
    62→    func updateAPIKey(_ newKey: String) {
    63→        if newKey.isEmpty {
    64→            KeychainHelper.shared.delete(forKey: Self.apiKeyKeychainKey)
    65→        } else {
    66→            KeychainHelper.shared.save(newKey, forKey: Self.apiKeyKeychainKey)
    67→        }
    68→    }
    69→
    70→    /// Get current API key (for display, masked)
    71→    var maskedAPIKey: String {
    72→        let key = apiKey
    73→        guard key.count > 8 else { return key.isEmpty ? "" : "****" }
    74→        let prefix = String(key.prefix(4))
    75→        let suffix = String(key.suffix(4))
    76→        return "\(prefix)****\(suffix)"
    77→    }
    78→
    79→    /// Check if API key is configured
    80→    var hasAPIKey: Bool {
    81→        !apiKey.isEmpty
    82→    }
    83→
    84→    /// Validate API key format (basic sanity check)
    85→    var isAPIKeyValid: Bool {
    86→        let key = apiKey
    87→        // API keys should be at least 20 characters and contain only valid characters
    88→        guard key.count >= 20 else { return false }
    89→        let validCharacters = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-_"))
    90→        return key.unicodeScalars.allSatisfy { validCharacters.contains($0) }
    91→    }
    92→
    93→    /// Check if service is properly configured
    94→    var isConfigured: Bool {
    95→        hasAPIKey && isAPIKeyValid && !baseURL.isEmpty
    96→    }
    97→
    98→    /// Update server URL
    99→    func updateServerURL(_ url: String) {
   100→        UserDefaults.standard.set(url, forKey: Self.serverURLKey)
   101→    }
   102→
   103→    // MARK: - EmailAnalyzing Protocol
   104→
   105→    func analyzeEmail(_ email: Email) async throws -> EmailAnalysis {
   106→        let subjectPreview = String(email.subject.prefix(30)) + (email.subject.count > 30 ? "..." : "")
   107→        AppLogger.ai("AI: analyzeEmail - '\(subjectPreview)' (\(email.id))", level: .debug)
   108→
   109→        let startTime = CFAbsoluteTimeGetCurrent()
   110→
   111→        let extractedContent = chunkingStrategy.extractKeyContent(email)
   112→        AppLogger.ai("AI: Conteúdo extraído - \(extractedContent.count) chars", level: .debug)
   113→
   114→        let prompt = promptBuilder.buildAnalysisPrompt(for: email, extractedContent: extractedContent)
   115→        AppLogger.ai("AI: Prompt construído - \(prompt.count) chars", level: .debug)
   116→
   117→        let response = try await sendRequest(prompt: prompt)
   118→
   119→        let analysis = try responseParser.parseAnalysisResponse(response)
   120→        let elapsed = CFAbsoluteTimeGetCurrent() - startTime
   121→
   122→        AppLogger.ai("AI: analyzeEmail SUCESSO - priority: \(analysis.priority.rawValue), sentiment: \(analysis.sentiment.rawValue) em \(String(format: "%.2f", elapsed))s", level: .info)
   123→
   124→        return analysis
   125→    }
   126→
   127→    func generateReplyOptions(for email: Email) async throws -> [ReplyOption] {
   128→        AppLogger.ai("AI: generateReplyOptions - gerando 3 opções de resposta", level: .debug)
   129→
   130→        let tones: [ReplyTone] = [.professional, .friendly, .brief]
   131→        var options: [ReplyOption] = []
   132→
   133→        for tone in tones {
   134→            let reply = try await generateReply(for: email, tone: tone)
   135→            options.append(ReplyOption(tone: tone, content: reply))
   136→        }
   137→
   138→        AppLogger.ai("AI: generateReplyOptions SUCESSO - \(options.count) opções geradas", level: .info)
   139→        return options
   140→    }
   141→
   142→    func summarizeThread(_ emails: [Email]) async throws -> String {
   143→        AppLogger.ai("AI: summarizeThread - \(emails.count) emails", level: .debug)
   144→
   145→        let threadContent = chunkingStrategy.formatThreadContent(emails)
   146→        let prompt = promptBuilder.buildThreadSummaryPrompt(threadContent: threadContent)
   147→        let summary = try await sendRequest(prompt: prompt)
   148→
   149→        AppLogger.ai("AI: summarizeThread SUCESSO - \(summary.count) chars", level: .info)
   150→        return summary
   151→    }
   152→
   153→    func generateInboxSummary(emails: [Email]) async throws -> InboxSummary {
   154→        AppLogger.ai("AI: generateInboxSummary - processando \(min(emails.count, 20)) emails", level: .debug)
   155→
   156→        let recentEmails = Array(emails.prefix(20))
   157→        let emailSummaries = chunkingStrategy.formatInboxSummaries(recentEmails)
   158→        let prompt = promptBuilder.buildInboxSummaryPrompt(emailSummaries: emailSummaries)
   159→        let response = try await sendRequest(prompt: prompt, maxTokens: 800)
   160→        let summary = try responseParser.parseInboxSummary(response)
   161→
   162→        AppLogger.ai("AI: generateInboxSummary SUCESSO", level: .info)
   163→        return summary
   164→    }
   165→
   166→    func checkHealth() async -> Bool {
   167→        AppLogger.ai("AI: checkHealth - verificando conexão", level: .debug)
   168→        let result = await testConnection()
   169→        AppLogger.ai("AI: checkHealth - \(result.success ? "OK" : "FALHA"): \(result.message)", level: result.success ? .info : .warning)
   170→        return result.success
   171→    }
   172→
   173→    // MARK: - Model Management
   174→
   175→    func fetchAvailableModels() async throws -> [AIModel] {
   176→        AppLogger.ai("AI: fetchAvailableModels - buscando modelos disponíveis", level: .debug)
   177→
   178→        guard hasAPIKey else {
   179→            AppLogger.ai("AI: fetchAvailableModels - API key não configurada", level: .warning)
   180→            throw AIError.notConfigured("API key required to fetch models")
   181→        }
   182→
   183→        guard let url = URL(string: "\(baseURL)/v1/models") else {
   184→            throw AIError.invalidURL("Invalid server URL")
   185→        }
   186→
   187→        var request = URLRequest(url: url)
   188→        request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
   189→        request.timeoutInterval = 10
   190→
   191→        AppLogger.api("AI API: GET /v1/models", level: .debug)
   192→        let (data, response) = try await URLSession.shared.data(for: request)
   193→
   194→        guard let httpResponse = response as? HTTPURLResponse else {
   195→            throw AIError.invalidResponse
   196→        }
   197→
   198→        AppLogger.api("AI API: Models response HTTP \(httpResponse.statusCode)", level: .debug)
   199→
   200→        guard httpResponse.statusCode == 200 else {
   201→            AppLogger.ai("AI: fetchAvailableModels falhou (\(httpResponse.statusCode))", level: .error)
   202→            throw AIError.serverError(httpResponse.statusCode)
   203→        }
   204→
   205→        let modelsResponse = try Self.decoder.decode(ModelsResponse.self, from: data)
   206→        let models = modelsResponse.data.map { model in
   207→            AIModel(
   208→                id: model.id,
   209→                name: formatModelName(model.id),
   210→                ownedBy: model.owned_by
   211→            )
   212→        }
   213→
   214→        AppLogger.ai("AI: fetchAvailableModels SUCESSO - \(models.count) modelos", level: .info)
   215→        return models
   216→    }
   217→
   218→    func testConnection() async -> (success: Bool, message: String) {
   219→        AppLogger.ai("AI: testConnection - testando \(baseURL)", level: .debug)
   220→
   221→        do {
   222→            guard let url = URL(string: "\(baseURL)/v1/models") else {
   223→                AppLogger.ai("AI: testConnection - URL inválida", level: .error)
   224→                return (false, "Invalid server URL")
   225→            }
   226→            var request = URLRequest(url: url)
   227→            request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
   228→            request.timeoutInterval = 10
   229→
   230→            let (_, response) = try await URLSession.shared.data(for: request)
   231→            let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0
   232→            let success = statusCode == 200
   233→
   234→            AppLogger.ai("AI: testConnection - HTTP \(statusCode) → \(success ? "OK" : "FALHA")", level: success ? .info : .warning)
   235→            return (success, success ? "Connected successfully" : "Connection failed (HTTP \(statusCode))")
   236→        } catch {
   237→            AppLogger.ai("AI: testConnection - erro: \(error.localizedDescription)", level: .error)
   238→            return (false, error.localizedDescription)
   239→        }
   240→    }
   241→
   242→    // MARK: - Reply Generation
   243→
   244→    func generateReply(for email: Email, tone: ReplyTone = .professional) async throws -> String {
   245→        AppLogger.ai("AI: generateReply - tom: \(tone.rawValue)", level: .debug)
   246→
   247→        let prompt = promptBuilder.buildReplyPrompt(for: email, tone: tone)
   248→        let reply = try await sendRequest(prompt: prompt)
   249→
   250→        AppLogger.ai("AI: generateReply SUCESSO - \(reply.count) chars", level: .debug)
   251→        return reply
   252→    }
   253→
   254→    // MARK: - Retry with Exponential Backoff (internal for extensions)
   255→
   256→    func analyzeWithRetry(_ email: Email, maxAttempts: Int? = nil) async throws -> EmailAnalysis {
   257→        let attempts = maxAttempts ?? retryAttempts
   258→        var lastError: Error?
   259→
   260→        for attempt in 1...attempts {
   261→            do {
   262→                return try await analyzeEmail(email)
   263→            } catch {
   264→                lastError = error
   265→                AppLogger.ai("Attempt \(attempt)/\(attempts) failed: \(error.localizedDescription)")
   266→
   267→                if attempt < attempts {
   268→                    let baseDelay = pow(2.0, Double(attempt - 1))
   269→                    let jitter = Double.random(in: 0...0.5)
   270→                    let delay = baseDelay + jitter
   271→
   272→                    try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
   273→                }
   274→            }
   275→        }
   276→
   277→        throw lastError ?? AIError.analysisError("Max retries exceeded")
   278→    }
   279→
   280→    // MARK: - API Communication
   281→
   282→    private func sendRequest(prompt: String, maxTokens: Int? = nil) async throws -> String {
   283→        guard hasAPIKey else {
   284→            AppLogger.ai("AI: sendRequest - API key não configurada", level: .warning)
   285→            throw AIError.notConfigured("API key not configured. Please add your API key in Settings.")
   286→        }
   287→
   288→        guard let url = URL(string: "\(baseURL)/v1/chat/completions") else {
   289→            AppLogger.ai("AI: sendRequest - URL inválida: \(baseURL)", level: .error)
   290→            throw AIError.invalidURL("Invalid server URL: \(baseURL)")
   291→        }
   292→
   293→        let effectiveMaxTokens = maxTokens ?? settings.aiMaxTokens
   294→        let effectiveTemperature = settings.aiTemperature
   295→        let effectiveTimeout = TimeInterval(settings.aiTimeout)
   296→        let effectiveModel = settings.aiModelId.isEmpty ? defaultModel : settings.aiModelId
   297→
   298→        AppLogger.ai("AI: sendRequest - model: \(effectiveModel), maxTokens: \(effectiveMaxTokens), timeout: \(Int(effectiveTimeout))s", level: .debug)
   299→
   300→        var request = URLRequest(url: url)
   301→        request.httpMethod = "POST"
   302→        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
   303→        request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
   304→        request.timeoutInterval = effectiveTimeout
   305→
   306→        let requestBody = ChatCompletionRequest(
   307→            model: effectiveModel,
   308→            messages: [
   309→                ChatMessage(role: "system", content: promptBuilder.systemPrompt),
   310→                ChatMessage(role: "user", content: prompt)
   311→            ],
   312→            maxTokens: effectiveMaxTokens,
   313→            temperature: effectiveTemperature
   314→        )
   315→
   316→        request.httpBody = try Self.encoder.encode(requestBody)
   317→
   318→        AppLogger.api("AI API: POST /v1/chat/completions (prompt: \(prompt.count) chars)", level: .debug)
   319→
   320→        let startTime = CFAbsoluteTimeGetCurrent()
   321→        let (data, response): (Data, URLResponse)
   322→        do {
   323→            (data, response) = try await URLSession.shared.data(for: request)
   324→        } catch let urlError as URLError {
   325→            AppLogger.ai("AI: sendRequest - erro de rede: \(urlError.code.rawValue)", level: .error)
   326→            switch urlError.code {
   327→            case .notConnectedToInternet:
   328→                throw AIError.noInternet
   329→            case .timedOut:
   330→                throw AIError.timeout
   331→            case .cannotConnectToHost, .cannotFindHost:
   332→                throw AIError.serverUnavailable(baseURL)
   333→            default:
   334→                throw AIError.networkError(urlError.localizedDescription)
   335→            }
   336→        }
   337→
   338→        let elapsed = CFAbsoluteTimeGetCurrent() - startTime
   339→
   340→        guard let httpResponse = response as? HTTPURLResponse else {
   341→            throw AIError.invalidResponse
   342→        }
   343→
   344→        AppLogger.api("AI API: Response HTTP \(httpResponse.statusCode) em \(String(format: "%.2f", elapsed))s", level: .debug)
   345→
   346→        switch httpResponse.statusCode {
   347→        case 200:
   348→            break
   349→        case 401:
   350→            AppLogger.ai("AI: sendRequest - não autorizado (401)", level: .error)
   351→            throw AIError.unauthorized("Invalid API key. Please check your API key in Settings.")
   352→        case 403:
   353→            AppLogger.ai("AI: sendRequest - acesso negado (403)", level: .error)
   354→            throw AIError.forbidden("Access denied. Check your API key permissions.")
   355→        case 429:
   356→            AppLogger.ai("AI: sendRequest - rate limited (429)", level: .warning)
   357→            throw AIError.rateLimited("Too many requests. Please wait a moment.")
   358→        case 500...599:
   359→            AppLogger.ai("AI: sendRequest - erro do servidor (\(httpResponse.statusCode))", level: .error)
   360→            throw AIError.serverError(httpResponse.statusCode)
   361→        default:
   362→            AppLogger.ai("AI: sendRequest - erro inesperado (\(httpResponse.statusCode))", level: .error)
   363→            throw AIError.serverError(httpResponse.statusCode)
   364→        }
   365→
   366→        let chatResponse: ChatCompletionResponse
   367→        do {
   368→            chatResponse = try Self.decoder.decode(ChatCompletionResponse.self, from: data)
   369→        } catch {
   370→            AppLogger.ai("AI: sendRequest - erro ao parsear resposta", level: .error)
   371→            if let errorResponse = try? Self.decoder.decode(APIErrorResponse.self, from: data) {
   372→                throw AIError.apiError(errorResponse.error.message)
   373→            }
   374→            throw AIError.parseError
   375→        }
   376→
   377→        guard let content = chatResponse.choices.first?.message.content else {
   378→            AppLogger.ai("AI: sendRequest - resposta vazia", level: .warning)
   379→            throw AIError.emptyResponse
   380→        }
   381→
   382→        AppLogger.ai("AI: sendRequest SUCESSO - \(content.count) chars", level: .debug)
   383→
   384→        return content
   385→    }
   386→
   387→    // MARK: - Private Helpers
   388→
   389→    private func formatModelName(_ id: String) -> String {
   390→        if let lastSlash = id.lastIndex(of: "/") {
   391→            let name = String(id[id.index(after: lastSlash)...])
   392→            return name
   393→                .replacingOccurrences(of: "-GGUF", with: "")
   394→                .replacingOccurrences(of: "-Q4_K_M", with: "")
   395→                .replacingOccurrences(of: "-Q5_K_M", with: "")
   396→                .replacingOccurrences(of: "-Q8_0", with: "")
   397→        }
   398→        return id
   399→    }
   400→}
   401→

<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

// MARK: - AI Analysis Service
// Extensions: AIServiceModels.swift, AIServiceBatch.swift
// Helpers: AIPromptBuilder.swift, AIResponseParser.swift, AIChunkingStrategy.swift

class AIAnalysisService: EmailAnalyzing {
    // MARK: - Cached Codecs (Performance Optimization)

    static let decoder: JSONDecoder = {
        let decoder = JSONDecoder()
        return decoder
    }()

    static let encoder: JSONEncoder = {
        let encoder = JSONEncoder()
        return encoder
    }()
    // Keys for secure storage
    private static let apiKeyKeychainKey = "lm_gateway_api_key"
    private static let serverURLKey = "ai_server_url"

    // Default server URL only - API key must be configured by user
    private static let defaultServerURL = "https://mac-studio-de-leandro.tail0360aa.ts.net"

    // LM Gateway via Tailscale
    private var baseURL: String {
        UserDefaults.standard.string(forKey: Self.serverURLKey) ?? Self.defaultServerURL
    }

    // API Key from Keychain (secure storage) - no hardcoded default
    private var apiKey: String {
        KeychainHelper.shared.load(forKey: Self.apiKeyKeychainKey) ?? ""
    }

    private let defaultModel = "default"

    // MARK: - Components

    private let promptBuilder = AIPromptBuilder()
    private let responseParser = AIResponseParser()
    private let chunkingStrategy: AIChunkingStrategy

    // MARK: - Settings (internal for extensions)

    var settings: AppSettings { AppSettings.shared }
    var maxConcurrentAnalysis: Int { settings.concurrentAnalysis }
    var retryAttempts: Int { settings.retryAttempts }

    // MARK: - Initialization

    init() {
        let settings = AppSettings.shared
        self.chunkingStrategy = AIChunkingStrategy(
            maxChunkSize: settings.maxBodyChunkSize
        )
    }

    // MARK: - Configuration

    /// Update API key (stored securely in Keychain)
    func updateAPIKey(_ newKey: String) {
        if newKey.isEmpty {
            KeychainHelper.shared.delete(forKey: Self.apiKeyKeychainKey)
        } else {
            KeychainHelper.shared.save(newKey, forKey: Self.apiKeyKeychainKey)
        }
    }

    /// Get current API key (for display, masked)
    var maskedAPIKey: String {
        let key = apiKey
        guard key.count > 8 else { return key.isEmpty ? "" : "****" }
        let prefix = String(key.prefix(4))
        let suffix = String(key.suffix(4))
        return "\(prefix)****\(suffix)"
    }

    /// Check if API key is configured
    var hasAPIKey: Bool {
        !apiKey.isEmpty
    }

    /// Validate API key format (basic sanity check)
    var isAPIKeyValid: Bool {
        let key = apiKey
        // API keys should be at least 20 characters and contain only valid characters
        guard key.count >= 20 else { return false }
        let validCharacters = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-_"))
        return key.unicodeScalars.allSatisfy { validCharacters.contains($0) }
    }

    /// Check if service is properly configured
    var isConfigured: Bool {
        hasAPIKey && isAPIKeyValid && !baseURL.isEmpty
    }

    /// Update server URL
    func updateServerURL(_ url: String) {
        UserDefaults.standard.set(url, forKey: Self.serverURLKey)
    }

    // MARK: - EmailAnalyzing Protocol

    func analyzeEmail(_ email: Email) async throws -> EmailAnalysis {
        let subjectPreview = String(email.subject.prefix(30)) + (email.subject.count > 30 ? "..." : "")
        AppLogger.ai("AI: analyzeEmail - '\(subjectPreview)' (\(email.id))", level: .debug)

        let startTime = CFAbsoluteTimeGetCurrent()

        let extractedContent = chunkingStrategy.extractKeyContent(email)
        AppLogger.ai("AI: Conteúdo extraído - \(extractedContent.count) chars", level: .debug)

        let prompt = promptBuilder.buildAnalysisPrompt(for: email, extractedContent: extractedContent)
        AppLogger.ai("AI: Prompt construído - \(prompt.count) chars", level: .debug)

        let response = try await sendRequest(prompt: prompt)

        let analysis = try responseParser.parseAnalysisResponse(response)
        let elapsed = CFAbsoluteTimeGetCurrent() - startTime

        AppLogger.ai("AI: analyzeEmail SUCESSO - priority: \(analysis.priority.rawValue), sentiment: \(analysis.sentiment.rawValue) em \(String(format: "%.2f", elapsed))s", level: .info)

        return analysis
    }

    func generateReplyOptions(for email: Email) async throws -> [ReplyOption] {
        AppLogger.ai("AI: generateReplyOptions - gerando 3 opções de resposta", level: .debug)

        let tones: [ReplyTone] = [.professional, .friendly, .brief]
        var options: [ReplyOption] = []

        for tone in tones {
            let reply = try await generateReply(for: email, tone: tone)
            options.append(ReplyOption(tone: tone, content: reply))
        }

        AppLogger.ai("AI: generateReplyOptions SUCESSO - \(options.count) opções geradas", level: .info)
        return options
    }

    func summarizeThread(_ emails: [Email]) async throws -> String {
        AppLogger.ai("AI: summarizeThread - \(emails.count) emails", level: .debug)

        let threadContent = chunkingStrategy.formatThreadContent(emails)
        let prompt = promptBuilder.buildThreadSummaryPrompt(threadContent: threadContent)
        let summary = try await sendRequest(prompt: prompt)

        AppLogger.ai("AI: summarizeThread SUCESSO - \(summary.count) chars", level: .info)
        return summary
    }

    func generateInboxSummary(emails: [Email]) async throws -> InboxSummary {
        AppLogger.ai("AI: generateInboxSummary - processando \(min(emails.count, 20)) emails", level: .debug)

        let recentEmails = Array(emails.prefix(20))
        let emailSummaries = chunkingStrategy.formatInboxSummaries(recentEmails)
        let prompt = promptBuilder.buildInboxSummaryPrompt(emailSummaries: emailSummaries)
        let response = try await sendRequest(prompt: prompt, maxTokens: 800)
        let summary = try responseParser.parseInboxSummary(response)

        AppLogger.ai("AI: generateInboxSummary SUCESSO", level: .info)
        return summary
    }

    func checkHealth() async -> Bool {
        AppLogger.ai("AI: checkHealth - verificando conexão", level: .debug)
        let result = await testConnection()
        AppLogger.ai("AI: checkHealth - \(result.success ? "OK" : "FALHA"): \(result.message)", level: result.success ? .info : .warning)
        return result.success
    }

    // MARK: - Model Management

    func fetchAvailableModels() async throws -> [AIModel] {
        AppLogger.ai("AI: fetchAvailableModels - buscando modelos disponíveis", level: .debug)

        guard hasAPIKey else {
            AppLogger.ai("AI: fetchAvailableModels - API key não configurada", level: .warning)
            throw AIError.notConfigured("API key required to fetch models")
        }

        guard let url = URL(string: "\(baseURL)/v1/models") else {
            throw AIError.invalidURL("Invalid server URL")
        }

        var request = URLRequest(url: url)
        request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
        request.timeoutInterval = 10

        AppLogger.api("AI API: GET /v1/models", level: .debug)
        let (data, response) = try await URLSession.shared.data(for: request)

        guard let httpResponse = response as? HTTPURLResponse else {
            throw AIError.invalidResponse
        }

        AppLogger.api("AI API: Models response HTTP \(httpResponse.statusCode)", level: .debug)

        guard httpResponse.statusCode == 200 else {
            AppLogger.ai("AI: fetchAvailableModels falhou (\(httpResponse.statusCode))", level: .error)
            throw AIError.serverError(httpResponse.statusCode)
        }

        let modelsResponse = try Self.decoder.decode(ModelsResponse.self, from: data)
        let models = modelsResponse.data.map { model in
            AIModel(
                id: model.id,
                name: formatModelName(model.id),
                ownedBy: model.owned_by
            )
        }

        AppLogger.ai("AI: fetchAvailableModels SUCESSO - \(models.count) modelos", level: .info)
        return models
    }

    func testConnection() async -> (success: Bool, message: String) {
        AppLogger.ai("AI: testConnection - testando \(baseURL)", level: .debug)

        do {
            guard let url = URL(string: "\(baseURL)/v1/models") else {
                AppLogger.ai("AI: testConnection - URL inválida", level: .error)
                return (false, "Invalid server URL")
            }
            var request = URLRequest(url: url)
            request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
            request.timeoutInterval = 10

            let (_, response) = try await URLSession.shared.data(for: request)
            let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0
            let success = statusCode == 200

            AppLogger.ai("AI: testConnection - HTTP \(statusCode) → \(success ? "OK" : "FALHA")", level: success ? .info : .warning)
            return (success, success ? "Connected successfully" : "Connection failed (HTTP \(statusCode))")
        } catch {
            AppLogger.ai("AI: testConnection - erro: \(error.localizedDescription)", level: .error)
            return (false, error.localizedDescription)
        }
    }

    // MARK: - Reply Generation

    func generateReply(for email: Email, tone: ReplyTone = .professional) async throws -> String {
        AppLogger.ai("AI: generateReply - tom: \(tone.rawValue)", level: .debug)

        let prompt = promptBuilder.buildReplyPrompt(for: email, tone: tone)
        let reply = try await sendRequest(prompt: prompt)

        AppLogger.ai("AI: generateReply SUCESSO - \(reply.count) chars", level: .debug)
        return reply
    }

    // MARK: - Retry with Exponential Backoff (internal for extensions)

    func analyzeWithRetry(_ email: Email, maxAttempts: Int? = nil) async throws -> EmailAnalysis {
        let attempts = maxAttempts ?? retryAttempts
        var lastError: Error?

        for attempt in 1...attempts {
            do {
                return try await analyzeEmail(email)
            } catch {
                lastError = error
                AppLogger.ai("Attempt \(attempt)/\(attempts) failed: \(error.localizedDescription)")

                if attempt < attempts {
                    let baseDelay = pow(2.0, Double(attempt - 1))
                    let jitter = Double.random(in: 0...0.5)
                    let delay = baseDelay + jitter

                    try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
                }
            }
        }

        throw lastError ?? AIError.analysisError("Max retries exceeded")
    }

    // MARK: - API Communication

    private func sendRequest(prompt: String, maxTokens: Int? = nil) async throws -> String {
        guard hasAPIKey else {
            AppLogger.ai("AI: sendRequest - API key não configurada", level: .warning)
            throw AIError.notConfigured("API key not configured. Please add your API key in Settings.")
        }

        guard let url = URL(string: "\(baseURL)/v1/chat/completions") else {
            AppLogger.ai("AI: sendRequest - URL inválida: \(baseURL)", level: .error)
            throw AIError.invalidURL("Invalid server URL: \(baseURL)")
        }

        let effectiveMaxTokens = maxTokens ?? settings.aiMaxTokens
        let effectiveTemperature = settings.aiTemperature
        let effectiveTimeout = TimeInterval(settings.aiTimeout)
        let effectiveModel = settings.aiModelId.isEmpty ? defaultModel : settings.aiModelId

        AppLogger.ai("AI: sendRequest - model: \(effectiveModel), maxTokens: \(effectiveMaxTokens), timeout: \(Int(effectiveTimeout))s", level: .debug)

        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
        request.timeoutInterval = effectiveTimeout

        let requestBody = ChatCompletionRequest(
            model: effectiveModel,
            messages: [
                ChatMessage(role: "system", content: promptBuilder.systemPrompt),
                ChatMessage(role: "user", content: prompt)
            ],
            maxTokens: effectiveMaxTokens,
            temperature: effectiveTemperature
        )

        request.httpBody = try Self.encoder.encode(requestBody)

        AppLogger.api("AI API: POST /v1/chat/completions (prompt: \(prompt.count) chars)", level: .debug)

        let startTime = CFAbsoluteTimeGetCurrent()
        let (data, response): (Data, URLResponse)
        do {
            (data, response) = try await URLSession.shared.data(for: request)
        } catch let urlError as URLError {
            AppLogger.ai("AI: sendRequest - erro de rede: \(urlError.code.rawValue)", level: .error)
            switch urlError.code {
            case .notConnectedToInternet:
                throw AIError.noInternet
            case .timedOut:
                throw AIError.timeout
            case .cannotConnectToHost, .cannotFindHost:
                throw AIError.serverUnavailable(baseURL)
            default:
                throw AIError.networkError(urlError.localizedDescription)
            }
        }

        let elapsed = CFAbsoluteTimeGetCurrent() - startTime

        guard let httpResponse = response as? HTTPURLResponse else {
            throw AIError.invalidResponse
        }

        AppLogger.api("AI API: Response HTTP \(httpResponse.statusCode) em \(String(format: "%.2f", elapsed))s", level: .debug)

        switch httpResponse.statusCode {
        case 200:
            break
        case 401:
            AppLogger.ai("AI: sendRequest - não autorizado (401)", level: .error)
            throw AIError.unauthorized("Invalid API key. Please check your API key in Settings.")
        case 403:
            AppLogger.ai("AI: sendRequest - acesso negado (403)", level: .error)
            throw AIError.forbidden("Access denied. Check your API key permissions.")
        case 429:
            AppLogger.ai("AI: sendRequest - rate limited (429)", level: .warning)
            throw AIError.rateLimited("Too many requests. Please wait a moment.")
        case 500...599:
            AppLogger.ai("AI: sendRequest - erro do servidor (\(httpResponse.statusCode))", level: .error)
            throw AIError.serverError(httpResponse.statusCode)
        default:
            AppLogger.ai("AI: sendRequest - erro inesperado (\(httpResponse.statusCode))", level: .error)
            throw AIError.serverError(httpResponse.statusCode)
        }

        let chatResponse: ChatCompletionResponse
        do {
            chatResponse = try Self.decoder.decode(ChatCompletionResponse.self, from: data)
        } catch {
            AppLogger.ai("AI: sendRequest - erro ao parsear resposta", level: .error)
            if let errorResponse = try? Self.decoder.decode(APIErrorResponse.self, from: data) {
                throw AIError.apiError(errorResponse.error.message)
            }
            throw AIError.parseError
        }

        guard let content = chatResponse.choices.first?.message.content else {
            AppLogger.ai("AI: sendRequest - resposta vazia", level: .warning)
            throw AIError.emptyResponse
        }

        AppLogger.ai("AI: sendRequest SUCESSO - \(content.count) chars", level: .debug)

        return content
    }

    // MARK: - Private Helpers

    private func formatModelName(_ id: String) -> String {
        if let lastSlash = id.lastIndex(of: "/") {
            let name = String(id[id.index(after: lastSlash)...])
            return name
                .replacingOccurrences(of: "-GGUF", with: "")
                .replacingOccurrences(of: "-Q4_K_M", with: "")
                .replacingOccurrences(of: "-Q5_K_M", with: "")
                .replacingOccurrences(of: "-Q8_0", with: "")
        }
        return id
    }
}


     1→import Foundation
     2→
     3→// MARK: - Analysis Event Types
     4→
     5→/// Callback for each email analysis event
     6→typealias AnalysisEventCallback = (AnalysisEvent) -> Void
     7→
     8→/// Events emitted during batch analysis
     9→enum AnalysisEvent {
    10→    case started(email: Email)
    11→    case completed(email: Email, analysis: EmailAnalysis, duration: TimeInterval)
    12→    case failed(email: Email, error: String, duration: TimeInterval)
    13→    case progress(completed: Int, total: Int)
    14→}
    15→
    16→// MARK: - Thread-Safe Results Collection
    17→
    18→actor AnalysisResultsActor {
    19→    private var results: [String: EmailAnalysis] = [:]
    20→
    21→    func add(emailId: String, analysis: EmailAnalysis) {
    22→        results[emailId] = analysis
    23→    }
    24→
    25→    var count: Int {
    26→        results.count
    27→    }
    28→
    29→    func getAll() -> [String: EmailAnalysis] {
    30→        results
    31→    }
    32→}
    33→
    34→actor RemainingEmailsActor {
    35→    private var remaining: Set<String>
    36→
    37→    init(emailIds: [String]) {
    38→        self.remaining = Set(emailIds)
    39→    }
    40→
    41→    func markProcessed(_ emailId: String) {
    42→        remaining.remove(emailId)
    43→    }
    44→
    45→    func getRemaining() -> [String] {
    46→        Array(remaining)
    47→    }
    48→
    49→    var count: Int {
    50→        remaining.count
    51→    }
    52→}
    53→
    54→// MARK: - Batch Analysis Extension
    55→
    56→extension AIAnalysisService {
    57→
    58→    // MARK: - Parallel Batch Analysis (5-6x faster)
    59→
    60→    /// Analyze multiple emails in parallel with concurrency limit
    61→    func analyzeEmailsBatch(
    62→        _ emails: [Email],
    63→        maxConcurrent: Int? = nil,
    64→        onProgress: ((Double) -> Void)? = nil
    65→    ) async -> [String: EmailAnalysis] {
    66→        guard !emails.isEmpty else {
    67→            AppLogger.ai("AI Batch: Lista vazia, ignorando", level: .debug)
    68→            return [:]
    69→        }
    70→
    71→        let concurrentLimit = maxConcurrent ?? maxConcurrentAnalysis
    72→        let total = Double(emails.count)
    73→        let resultsActor = AnalysisResultsActor()
    74→
    75→        AppLogger.ai("AI Batch: Iniciando análise de \(emails.count) emails (max concurrent: \(concurrentLimit))", level: .info)
    76→        let batchStartTime = CFAbsoluteTimeGetCurrent()
    77→
    78→        await withTaskGroup(of: (String, EmailAnalysis?).self) { group in
    79→            var pendingEmails = emails[...]
    80→            var inFlight = 0
    81→
    82→            while !pendingEmails.isEmpty || inFlight > 0 {
    83→                while inFlight < concurrentLimit && !pendingEmails.isEmpty {
    84→                    let email = pendingEmails.removeFirst()
    85→                    inFlight += 1
    86→
    87→                    group.addTask { [weak self] in
    88→                        guard let self = self else { return (email.id, nil) }
    89→
    90→                        do {
    91→                            let analysis = try await self.analyzeWithRetry(email)
    92→                            return (email.id, analysis)
    93→                        } catch {
    94→                            AppLogger.ai("Analysis failed for email \(email.id): \(error.localizedDescription)")
    95→                            return (email.id, nil)
    96→                        }
    97→                    }
    98→                }
    99→
   100→                if let result = await group.next() {
   101→                    inFlight -= 1
   102→
   103→                    if let analysis = result.1 {
   104→                        await resultsActor.add(emailId: result.0, analysis: analysis)
   105→                    }
   106→
   107→                    let processedCount = emails.count - (pendingEmails.count + inFlight)
   108→                    onProgress?(Double(processedCount) / total)
   109→                }
   110→            }
   111→        }
   112→
   113→        let results = await resultsActor.getAll()
   114→        let batchElapsed = CFAbsoluteTimeGetCurrent() - batchStartTime
   115→        let successRate = Double(results.count) / Double(emails.count) * 100
   116→
   117→        AppLogger.ai("AI Batch: Concluído - \(results.count)/\(emails.count) (\(String(format: "%.0f", successRate))% sucesso) em \(String(format: "%.1f", batchElapsed))s", level: .info)
   118→        return results
   119→    }
   120→
   121→    // MARK: - Enhanced Batch Analysis with Events & Cancellation
   122→
   123→    func analyzeEmailsBatchWithEvents(
   124→        _ emails: [Email],
   125→        maxConcurrent: Int? = nil,
   126→        onEvent: AnalysisEventCallback? = nil
   127→    ) async -> (results: [String: EmailAnalysis], remainingIds: [String]) {
   128→        guard !emails.isEmpty else { return ([:], []) }
   129→
   130→        let concurrentLimit = maxConcurrent ?? maxConcurrentAnalysis
   131→        let resultsActor = AnalysisResultsActor()
   132→        let remainingActor = RemainingEmailsActor(emailIds: emails.map { $0.id })
   133→        var processedCount = 0
   134→
   135→        await withTaskGroup(of: (String, EmailAnalysis?, TimeInterval).self) { group in
   136→            var pendingEmails = emails[...]
   137→            var inFlight = 0
   138→
   139→            while !pendingEmails.isEmpty || inFlight > 0 {
   140→                if Task.isCancelled { break }
   141→
   142→                while inFlight < concurrentLimit && !pendingEmails.isEmpty {
   143→                    if Task.isCancelled { break }
   144→
   145→                    let email = pendingEmails.removeFirst()
   146→                    inFlight += 1
   147→
   148→                    group.addTask { [weak self] in
   149→                        guard let self = self else { return (email.id, nil, 0) }
   150→
   151→                        let startTime = Date()
   152→
   153→                        await MainActor.run {
   154→                            onEvent?(.started(email: email))
   155→                        }
   156→
   157→                        do {
   158→                            let analysis = try await self.analyzeWithRetry(email)
   159→                            let duration = Date().timeIntervalSince(startTime)
   160→
   161→                            await MainActor.run {
   162→                                onEvent?(.completed(email: email, analysis: analysis, duration: duration))
   163→                            }
   164→
   165→                            return (email.id, analysis, duration)
   166→                        } catch {
   167→                            let duration = Date().timeIntervalSince(startTime)
   168→
   169→                            await MainActor.run {
   170→                                onEvent?(.failed(email: email, error: error.localizedDescription, duration: duration))
   171→                            }
   172→
   173→                            return (email.id, nil, duration)
   174→                        }
   175→                    }
   176→                }
   177→
   178→                if let result = await group.next() {
   179→                    inFlight -= 1
   180→                    processedCount += 1
   181→
   182→                    await remainingActor.markProcessed(result.0)
   183→
   184→                    if let analysis = result.1 {
   185→                        await resultsActor.add(emailId: result.0, analysis: analysis)
   186→                    }
   187→
   188→                    let total = emails.count
   189→                    await MainActor.run {
   190→                        onEvent?(.progress(completed: processedCount, total: total))
   191→                    }
   192→                }
   193→            }
   194→
   195→            if Task.isCancelled {
   196→                group.cancelAll()
   197→            }
   198→        }
   199→
   200→        let results = await resultsActor.getAll()
   201→        let remaining = await remainingActor.getRemaining()
   202→
   203→        if Task.isCancelled {
   204→            AppLogger.ai("Analysis paused: \(results.count) completed, \(remaining.count) remaining")
   205→        } else {
   206→            AppLogger.ai("Batch analysis with events complete: \(results.count)/\(emails.count) successful")
   207→        }
   208→
   209→        return (results, remaining)
   210→    }
   211→}
   212→

<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

// MARK: - Analysis Event Types

/// Callback for each email analysis event
typealias AnalysisEventCallback = (AnalysisEvent) -> Void

/// Events emitted during batch analysis
enum AnalysisEvent {
    case started(email: Email)
    case completed(email: Email, analysis: EmailAnalysis, duration: TimeInterval)
    case failed(email: Email, error: String, duration: TimeInterval)
    case progress(completed: Int, total: Int)
}

// MARK: - Thread-Safe Results Collection

actor AnalysisResultsActor {
    private var results: [String: EmailAnalysis] = [:]

    func add(emailId: String, analysis: EmailAnalysis) {
        results[emailId] = analysis
    }

    var count: Int {
        results.count
    }

    func getAll() -> [String: EmailAnalysis] {
        results
    }
}

actor RemainingEmailsActor {
    private var remaining: Set<String>

    init(emailIds: [String]) {
        self.remaining = Set(emailIds)
    }

    func markProcessed(_ emailId: String) {
        remaining.remove(emailId)
    }

    func getRemaining() -> [String] {
        Array(remaining)
    }

    var count: Int {
        remaining.count
    }
}

// MARK: - Batch Analysis Extension

extension AIAnalysisService {

    // MARK: - Parallel Batch Analysis (5-6x faster)

    /// Analyze multiple emails in parallel with concurrency limit
    func analyzeEmailsBatch(
        _ emails: [Email],
        maxConcurrent: Int? = nil,
        onProgress: ((Double) -> Void)? = nil
    ) async -> [String: EmailAnalysis] {
        guard !emails.isEmpty else {
            AppLogger.ai("AI Batch: Lista vazia, ignorando", level: .debug)
            return [:]
        }

        let concurrentLimit = maxConcurrent ?? maxConcurrentAnalysis
        let total = Double(emails.count)
        let resultsActor = AnalysisResultsActor()

        AppLogger.ai("AI Batch: Iniciando análise de \(emails.count) emails (max concurrent: \(concurrentLimit))", level: .info)
        let batchStartTime = CFAbsoluteTimeGetCurrent()

        await withTaskGroup(of: (String, EmailAnalysis?).self) { group in
            var pendingEmails = emails[...]
            var inFlight = 0

            while !pendingEmails.isEmpty || inFlight > 0 {
                while inFlight < concurrentLimit && !pendingEmails.isEmpty {
                    let email = pendingEmails.removeFirst()
                    inFlight += 1

                    group.addTask { [weak self] in
                        guard let self = self else { return (email.id, nil) }

                        do {
                            let analysis = try await self.analyzeWithRetry(email)
                            return (email.id, analysis)
                        } catch {
                            AppLogger.ai("Analysis failed for email \(email.id): \(error.localizedDescription)")
                            return (email.id, nil)
                        }
                    }
                }

                if let result = await group.next() {
                    inFlight -= 1

                    if let analysis = result.1 {
                        await resultsActor.add(emailId: result.0, analysis: analysis)
                    }

                    let processedCount = emails.count - (pendingEmails.count + inFlight)
                    onProgress?(Double(processedCount) / total)
                }
            }
        }

        let results = await resultsActor.getAll()
        let batchElapsed = CFAbsoluteTimeGetCurrent() - batchStartTime
        let successRate = Double(results.count) / Double(emails.count) * 100

        AppLogger.ai("AI Batch: Concluído - \(results.count)/\(emails.count) (\(String(format: "%.0f", successRate))% sucesso) em \(String(format: "%.1f", batchElapsed))s", level: .info)
        return results
    }

    // MARK: - Enhanced Batch Analysis with Events & Cancellation

    func analyzeEmailsBatchWithEvents(
        _ emails: [Email],
        maxConcurrent: Int? = nil,
        onEvent: AnalysisEventCallback? = nil
    ) async -> (results: [String: EmailAnalysis], remainingIds: [String]) {
        guard !emails.isEmpty else { return ([:], []) }

        let concurrentLimit = maxConcurrent ?? maxConcurrentAnalysis
        let resultsActor = AnalysisResultsActor()
        let remainingActor = RemainingEmailsActor(emailIds: emails.map { $0.id })
        var processedCount = 0

        await withTaskGroup(of: (String, EmailAnalysis?, TimeInterval).self) { group in
            var pendingEmails = emails[...]
            var inFlight = 0

            while !pendingEmails.isEmpty || inFlight > 0 {
                if Task.isCancelled { break }

                while inFlight < concurrentLimit && !pendingEmails.isEmpty {
                    if Task.isCancelled { break }

                    let email = pendingEmails.removeFirst()
                    inFlight += 1

                    group.addTask { [weak self] in
                        guard let self = self else { return (email.id, nil, 0) }

                        let startTime = Date()

                        await MainActor.run {
                            onEvent?(.started(email: email))
                        }

                        do {
                            let analysis = try await self.analyzeWithRetry(email)
                            let duration = Date().timeIntervalSince(startTime)

                            await MainActor.run {
                                onEvent?(.completed(email: email, analysis: analysis, duration: duration))
                            }

                            return (email.id, analysis, duration)
                        } catch {
                            let duration = Date().timeIntervalSince(startTime)

                            await MainActor.run {
                                onEvent?(.failed(email: email, error: error.localizedDescription, duration: duration))
                            }

                            return (email.id, nil, duration)
                        }
                    }
                }

                if let result = await group.next() {
                    inFlight -= 1
                    processedCount += 1

                    await remainingActor.markProcessed(result.0)

                    if let analysis = result.1 {
                        await resultsActor.add(emailId: result.0, analysis: analysis)
                    }

                    let total = emails.count
                    await MainActor.run {
                        onEvent?(.progress(completed: processedCount, total: total))
                    }
                }
            }

            if Task.isCancelled {
                group.cancelAll()
            }
        }

        let results = await resultsActor.getAll()
        let remaining = await remainingActor.getRemaining()

        if Task.isCancelled {
            AppLogger.ai("Analysis paused: \(results.count) completed, \(remaining.count) remaining")
        } else {
            AppLogger.ai("Batch analysis with events complete: \(results.count)/\(emails.count) successful")
        }

        return (results, remaining)
    }
}


     1→import Foundation
     2→
     3→// MARK: - EmailStore Actions Extension
     4→
     5→extension EmailStore {
     6→
     7→    // MARK: - Mark Read/Unread
     8→
     9→    func markAsRead(_ email: Email) async {
    10→        guard let index = emails.firstIndex(where: { $0.id == email.id }) else { return }
    11→
    12→        do {
    13→            try await gmailService.markAsRead(emailId: email.id)
    14→            emails[index].isRead = true
    15→            cacheService.saveEmails(emails)
    16→            updateStats()
    17→        } catch {
    18→            self.error = error.localizedDescription
    19→        }
    20→    }
    21→
    22→    func markAsUnread(_ email: Email) async {
    23→        guard let index = emails.firstIndex(where: { $0.id == email.id }) else { return }
    24→
    25→        do {
    26→            try await gmailService.markAsUnread(emailId: email.id)
    27→            emails[index].isRead = false
    28→            cacheService.saveEmails(emails)
    29→            updateStats()
    30→        } catch {
    31→            self.error = error.localizedDescription
    32→        }
    33→    }
    34→
    35→    // MARK: - Archive Email
    36→
    37→    /// Archive email (remove from inbox but keep in All Mail)
    38→    @discardableResult
    39→    func archiveEmail(_ email: Email) async -> Bool {
    40→        do {
    41→            try await gmailService.archiveEmail(emailId: email.id)
    42→
    43→            // Remove from local array (archived emails don't show in inbox)
    44→            emails.removeAll { $0.id == email.id }
    45→
    46→            // Update cache
    47→            cacheService.saveEmails(emails)
    48→
    49→            updateStats()
    50→            return true
    51→        } catch {
    52→            self.error = "Failed to archive: \(error.localizedDescription)"
    53→            return false
    54→        }
    55→    }
    56→
    57→    /// Unarchive email (add back to inbox)
    58→    @discardableResult
    59→    func unarchiveEmail(_ email: Email) async -> Bool {
    60→        do {
    61→            try await gmailService.unarchiveEmail(emailId: email.id)
    62→
    63→            // Add back to local array
    64→            var updatedEmail = email
    65→            if !updatedEmail.labels.contains("INBOX") {
    66→                updatedEmail.labels.append("INBOX")
    67→            }
    68→            emails.insert(updatedEmail, at: 0)
    69→            emails.sort { $0.date > $1.date }
    70→
    71→            // Update cache
    72→            cacheService.saveEmails(emails)
    73→
    74→            updateStats()
    75→            return true
    76→        } catch {
    77→            self.error = "Failed to unarchive: \(error.localizedDescription)"
    78→            return false
    79→        }
    80→    }
    81→
    82→    /// Archive multiple emails (batch)
    83→    func archiveEmails(_ emailsToArchive: [Email]) async -> Int {
    84→        guard !emailsToArchive.isEmpty else { return 0 }
    85→
    86→        do {
    87→            // Use batch API for efficiency
    88→            let emailIds = emailsToArchive.map { $0.id }
    89→            try await gmailService.batchModifyLabels(emailIds: emailIds, removeLabels: ["INBOX"])
    90→
    91→            // Remove from local array
    92→            let archivedIds = Set(emailIds)
    93→            emails.removeAll { archivedIds.contains($0.id) }
    94→
    95→            // Update cache
    96→            cacheService.saveEmails(emails)
    97→
    98→            updateStats()
    99→            return emailsToArchive.count
   100→        } catch {
   101→            self.error = "Failed to archive: \(error.localizedDescription)"
   102→            return 0
   103→        }
   104→    }
   105→
   106→    // MARK: - Batch Operations
   107→
   108→    /// Mark multiple emails as read (batch)
   109→    func markEmailsAsRead(_ emailsToMark: [Email]) async -> Int {
   110→        guard !emailsToMark.isEmpty else { return 0 }
   111→
   112→        do {
   113→            let emailIds = emailsToMark.map { $0.id }
   114→            try await gmailService.batchModifyLabels(emailIds: emailIds, removeLabels: ["UNREAD"])
   115→
   116→            // Update local state
   117→            let markedIds = Set(emailIds)
   118→            for i in emails.indices {
   119→                if markedIds.contains(emails[i].id) {
   120→                    emails[i].isRead = true
   121→                }
   122→            }
   123→
   124→            cacheService.saveEmails(emails)
   125→            updateStats()
   126→            return emailsToMark.count
   127→        } catch {
   128→            self.error = "Failed to mark as read: \(error.localizedDescription)"
   129→            return 0
   130→        }
   131→    }
   132→
   133→    /// Mark multiple emails as unread (batch)
   134→    func markEmailsAsUnread(_ emailsToMark: [Email]) async -> Int {
   135→        guard !emailsToMark.isEmpty else { return 0 }
   136→
   137→        do {
   138→            let emailIds = emailsToMark.map { $0.id }
   139→            try await gmailService.batchModifyLabels(emailIds: emailIds, addLabels: ["UNREAD"])
   140→
   141→            // Update local state
   142→            let markedIds = Set(emailIds)
   143→            for i in emails.indices {
   144→                if markedIds.contains(emails[i].id) {
   145→                    emails[i].isRead = false
   146→                }
   147→            }
   148→
   149→            cacheService.saveEmails(emails)
   150→            updateStats()
   151→            return emailsToMark.count
   152→        } catch {
   153→            self.error = "Failed to mark as unread: \(error.localizedDescription)"
   154→            return 0
   155→        }
   156→    }
   157→
   158→    /// Delete multiple emails using batch API
   159→    func deleteEmailsBatch(_ emailsToDelete: [Email]) async -> Int {
   160→        guard !emailsToDelete.isEmpty else {
   161→            AppLogger.warning("deleteEmailsBatch: Empty array, nothing to delete", category: .gmail)
   162→            return 0
   163→        }
   164→
   165→        AppLogger.debug("deleteEmailsBatch: Starting deletion of \(emailsToDelete.count) emails", category: .gmail)
   166→
   167→        do {
   168→            // Use batch API for efficiency - move to trash
   169→            let emailIds = emailsToDelete.map { $0.id }
   170→            try await gmailService.batchModifyLabels(emailIds: emailIds, addLabels: ["TRASH"], removeLabels: ["INBOX"])
   171→            AppLogger.debug("deleteEmailsBatch: Gmail API succeeded", category: .gmail)
   172→
   173→            // Remove from local array
   174→            let deletedIds = Set(emailIds)
   175→            emails.removeAll { deletedIds.contains($0.id) }
   176→
   177→            // Update cache
   178→            cacheService.saveEmails(emails)
   179→
   180→            updateStats()
   181→            return emailsToDelete.count
   182→        } catch {
   183→            AppLogger.error("deleteEmailsBatch FAILED: \(error.localizedDescription)", category: .gmail)
   184→            self.error = "Failed to delete: \(error.localizedDescription)"
   185→            return 0
   186→        }
   187→    }
   188→
   189→    // MARK: - Delete Email
   190→
   191→    /// Move email to trash (can be recovered)
   192→    @discardableResult
   193→    func deleteEmail(_ email: Email) async -> Bool {
   194→        AppLogger.debug("deleteEmail: Deleting email \(email.id) - \(email.subject)", category: .gmail)
   195→
   196→        do {
   197→            try await gmailService.trashEmail(emailId: email.id)
   198→
   199→            // Remove from local array
   200→            emails.removeAll { $0.id == email.id }
   201→
   202→            // Update cache
   203→            cacheService.saveEmails(emails)
   204→
   205→            updateStats()
   206→            return true
   207→        } catch {
   208→            AppLogger.error("deleteEmail FAILED: \(error)", category: .gmail)
   209→            self.error = "Failed to delete: \(error.localizedDescription)"
   210→            return false
   211→        }
   212→    }
   213→
   214→    /// Permanently delete email (cannot be recovered)
   215→    @discardableResult
   216→    func deleteEmailPermanently(_ email: Email) async -> Bool {
   217→        do {
   218→            try await gmailService.deleteEmailPermanently(emailId: email.id)
   219→
   220→            // Remove from local array
   221→            emails.removeAll { $0.id == email.id }
   222→
   223→            // Update cache
   224→            cacheService.saveEmails(emails)
   225→
   226→            updateStats()
   227→            return true
   228→        } catch {
   229→            self.error = "Failed to delete: \(error.localizedDescription)"
   230→            return false
   231→        }
   232→    }
   233→
   234→    /// Delete multiple emails (batch)
   235→    func deleteEmails(_ emailsToDelete: [Email]) async -> Int {
   236→        var deletedCount = 0
   237→
   238→        for email in emailsToDelete {
   239→            if await deleteEmail(email) {
   240→                deletedCount += 1
   241→            }
   242→        }
   243→
   244→        return deletedCount
   245→    }
   246→
   247→    // MARK: - Star
   248→
   249→    func toggleStar(_ email: Email) async {
   250→        guard let index = emails.firstIndex(where: { $0.id == email.id }) else { return }
   251→
   252→        do {
   253→            try await gmailService.toggleStar(emailId: email.id, starred: !email.isStarred)
   254→            emails[index].isStarred.toggle()
   255→        } catch {
   256→            self.error = error.localizedDescription
   257→        }
   258→    }
   259→
   260→    // MARK: - Send Email
   261→
   262→    @discardableResult
   263→    func sendEmail(
   264→        to: String,
   265→        subject: String,
   266→        body: String,
   267→        cc: String? = nil,
   268→        threadId: String? = nil,
   269→        inReplyTo: String? = nil,
   270→        references: String? = nil
   271→    ) async throws -> String {
   272→        let messageId = try await gmailService.sendEmail(
   273→            to: to,
   274→            subject: subject,
   275→            body: body,
   276→            cc: cc,
   277→            threadId: threadId,
   278→            inReplyTo: inReplyTo,
   279→            references: references
   280→        )
   281→
   282→        // Refresh emails to show the sent message in thread
   283→        if threadId != nil {
   284→            await refreshEmails()
   285→        }
   286→
   287→        return messageId
   288→    }
   289→}
   290→

<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

// MARK: - EmailStore Actions Extension

extension EmailStore {

    // MARK: - Mark Read/Unread

    func markAsRead(_ email: Email) async {
        guard let index = emails.firstIndex(where: { $0.id == email.id }) else { return }

        do {
            try await gmailService.markAsRead(emailId: email.id)
            emails[index].isRead = true
            cacheService.saveEmails(emails)
            updateStats()
        } catch {
            self.error = error.localizedDescription
        }
    }

    func markAsUnread(_ email: Email) async {
        guard let index = emails.firstIndex(where: { $0.id == email.id }) else { return }

        do {
            try await gmailService.markAsUnread(emailId: email.id)
            emails[index].isRead = false
            cacheService.saveEmails(emails)
            updateStats()
        } catch {
            self.error = error.localizedDescription
        }
    }

    // MARK: - Archive Email

    /// Archive email (remove from inbox but keep in All Mail)
    @discardableResult
    func archiveEmail(_ email: Email) async -> Bool {
        do {
            try await gmailService.archiveEmail(emailId: email.id)

            // Remove from local array (archived emails don't show in inbox)
            emails.removeAll { $0.id == email.id }

            // Update cache
            cacheService.saveEmails(emails)

            updateStats()
            return true
        } catch {
            self.error = "Failed to archive: \(error.localizedDescription)"
            return false
        }
    }

    /// Unarchive email (add back to inbox)
    @discardableResult
    func unarchiveEmail(_ email: Email) async -> Bool {
        do {
            try await gmailService.unarchiveEmail(emailId: email.id)

            // Add back to local array
            var updatedEmail = email
            if !updatedEmail.labels.contains("INBOX") {
                updatedEmail.labels.append("INBOX")
            }
            emails.insert(updatedEmail, at: 0)
            emails.sort { $0.date > $1.date }

            // Update cache
            cacheService.saveEmails(emails)

            updateStats()
            return true
        } catch {
            self.error = "Failed to unarchive: \(error.localizedDescription)"
            return false
        }
    }

    /// Archive multiple emails (batch)
    func archiveEmails(_ emailsToArchive: [Email]) async -> Int {
        guard !emailsToArchive.isEmpty else { return 0 }

        do {
            // Use batch API for efficiency
            let emailIds = emailsToArchive.map { $0.id }
            try await gmailService.batchModifyLabels(emailIds: emailIds, removeLabels: ["INBOX"])

            // Remove from local array
            let archivedIds = Set(emailIds)
            emails.removeAll { archivedIds.contains($0.id) }

            // Update cache
            cacheService.saveEmails(emails)

            updateStats()
            return emailsToArchive.count
        } catch {
            self.error = "Failed to archive: \(error.localizedDescription)"
            return 0
        }
    }

    // MARK: - Batch Operations

    /// Mark multiple emails as read (batch)
    func markEmailsAsRead(_ emailsToMark: [Email]) async -> Int {
        guard !emailsToMark.isEmpty else { return 0 }

        do {
            let emailIds = emailsToMark.map { $0.id }
            try await gmailService.batchModifyLabels(emailIds: emailIds, removeLabels: ["UNREAD"])

            // Update local state
            let markedIds = Set(emailIds)
            for i in emails.indices {
                if markedIds.contains(emails[i].id) {
                    emails[i].isRead = true
                }
            }

            cacheService.saveEmails(emails)
            updateStats()
            return emailsToMark.count
        } catch {
            self.error = "Failed to mark as read: \(error.localizedDescription)"
            return 0
        }
    }

    /// Mark multiple emails as unread (batch)
    func markEmailsAsUnread(_ emailsToMark: [Email]) async -> Int {
        guard !emailsToMark.isEmpty else { return 0 }

        do {
            let emailIds = emailsToMark.map { $0.id }
            try await gmailService.batchModifyLabels(emailIds: emailIds, addLabels: ["UNREAD"])

            // Update local state
            let markedIds = Set(emailIds)
            for i in emails.indices {
                if markedIds.contains(emails[i].id) {
                    emails[i].isRead = false
                }
            }

            cacheService.saveEmails(emails)
            updateStats()
            return emailsToMark.count
        } catch {
            self.error = "Failed to mark as unread: \(error.localizedDescription)"
            return 0
        }
    }

    /// Delete multiple emails using batch API
    func deleteEmailsBatch(_ emailsToDelete: [Email]) async -> Int {
        guard !emailsToDelete.isEmpty else {
            AppLogger.warning("deleteEmailsBatch: Empty array, nothing to delete", category: .gmail)
            return 0
        }

        AppLogger.debug("deleteEmailsBatch: Starting deletion of \(emailsToDelete.count) emails", category: .gmail)

        do {
            // Use batch API for efficiency - move to trash
            let emailIds = emailsToDelete.map { $0.id }
            try await gmailService.batchModifyLabels(emailIds: emailIds, addLabels: ["TRASH"], removeLabels: ["INBOX"])
            AppLogger.debug("deleteEmailsBatch: Gmail API succeeded", category: .gmail)

            // Remove from local array
            let deletedIds = Set(emailIds)
            emails.removeAll { deletedIds.contains($0.id) }

            // Update cache
            cacheService.saveEmails(emails)

            updateStats()
            return emailsToDelete.count
        } catch {
            AppLogger.error("deleteEmailsBatch FAILED: \(error.localizedDescription)", category: .gmail)
            self.error = "Failed to delete: \(error.localizedDescription)"
            return 0
        }
    }

    // MARK: - Delete Email

    /// Move email to trash (can be recovered)
    @discardableResult
    func deleteEmail(_ email: Email) async -> Bool {
        AppLogger.debug("deleteEmail: Deleting email \(email.id) - \(email.subject)", category: .gmail)

        do {
            try await gmailService.trashEmail(emailId: email.id)

            // Remove from local array
            emails.removeAll { $0.id == email.id }

            // Update cache
            cacheService.saveEmails(emails)

            updateStats()
            return true
        } catch {
            AppLogger.error("deleteEmail FAILED: \(error)", category: .gmail)
            self.error = "Failed to delete: \(error.localizedDescription)"
            return false
        }
    }

    /// Permanently delete email (cannot be recovered)
    @discardableResult
    func deleteEmailPermanently(_ email: Email) async -> Bool {
        do {
            try await gmailService.deleteEmailPermanently(emailId: email.id)

            // Remove from local array
            emails.removeAll { $0.id == email.id }

            // Update cache
            cacheService.saveEmails(emails)

            updateStats()
            return true
        } catch {
            self.error = "Failed to delete: \(error.localizedDescription)"
            return false
        }
    }

    /// Delete multiple emails (batch)
    func deleteEmails(_ emailsToDelete: [Email]) async -> Int {
        var deletedCount = 0

        for email in emailsToDelete {
            if await deleteEmail(email) {
                deletedCount += 1
            }
        }

        return deletedCount
    }

    // MARK: - Star

    func toggleStar(_ email: Email) async {
        guard let index = emails.firstIndex(where: { $0.id == email.id }) else { return }

        do {
            try await gmailService.toggleStar(emailId: email.id, starred: !email.isStarred)
            emails[index].isStarred.toggle()
        } catch {
            self.error = error.localizedDescription
        }
    }

    // MARK: - Send Email

    @discardableResult
    func sendEmail(
        to: String,
        subject: String,
        body: String,
        cc: String? = nil,
        threadId: String? = nil,
        inReplyTo: String? = nil,
        references: String? = nil
    ) async throws -> String {
        let messageId = try await gmailService.sendEmail(
            to: to,
            subject: subject,
            body: body,
            cc: cc,
            threadId: threadId,
            inReplyTo: inReplyTo,
            references: references
        )

        // Refresh emails to show the sent message in thread
        if threadId != nil {
            await refreshEmails()
        }

        return messageId
    }
}