import Foundation /// Configuración centralizada de la API del backend VPS. /// /// `APIConfig` proporciona todos los endpoints y parámetros de configuración /// necesarios para comunicarse con el backend VPS que gestiona el almacenamiento /// y serving de capítulos de manga. /// /// # Example /// ```swift /// let baseURL = APIConfig.baseURL /// let downloadEndpoint = APIConfig.Endpoints.download(mangaSlug: "one-piece", chapterNumber: 1) /// print(downloadEndpoint) // "https://api.example.com/api/v1/download/one-piece/1" /// ``` enum APIConfig { // MARK: - Base Configuration /// URL base del backend VPS /// /// Esta URL se usa para construir todos los endpoints de la API. /// Configurar según el entorno (desarrollo, staging, producción). /// /// # Configuración Actual /// - Producción: `https://gitea.cbcren.online` /// - Puerto: `3001` (se añade automáticamente) /// /// # Notas Importantes /// - Incluir el protocolo (`https://` o `http://`) /// - NO incluir el número de puerto aquí (usar la propiedad `port`) /// - NO incluir slash al final /// - Asegurarse de que el servidor sea accesible desde el dispositivo iOS /// /// # Ejemplos /// - `https://gitea.cbcren.online` (VPS de producción) /// - `http://192.168.1.100` (desarrollo local) /// - `http://localhost` (simulador con servidor local) static let serverURL = "https://gitea.cbcren.online" /// Puerto donde corre el backend API /// /// # Valor por Defecto /// - `3001` - Puerto configurado en el backend VPS /// /// # Notas /// - Asegurarse de que coincida con el puerto configurado en el servidor backend /// - Si se usa HTTPS, asegurar la configuración correcta del certificado SSL /// - Si se usan puertos estándar HTTP (80/443), se puede dejar vacío static let port: Int = 3001 /// URL base completa para requests a la API /// /// Construye automáticamente la URL base combinando la URL del servidor y el puerto. /// Esta es la propiedad recomendada para usar al hacer requests a la API. /// /// # Ejemplo /// ```swift /// let endpoint = "/api/v1/manga" /// let url = URL(string: APIConfig.baseURL + endpoint) /// ``` static var baseURL: String { if port == 80 || port == 443 { return serverURL } return "\(serverURL):\(port)" } /// Versión de la API static var apiVersion: String { return "v1" } /// Path base de la API static var basePath: String { return "\(baseURL)/api/\(apiVersion)" } /// Timeout por defecto para requests (en segundos) static var defaultTimeout: TimeInterval { return 30.0 } /// Timeout para requests de descarga (en segundos) static var downloadTimeout: TimeInterval { return 300.0 // 5 minutos } // MARK: - HTTP Headers /// Headers HTTP comunes para todas las requests static var commonHeaders: [String: String] { return [ "Content-Type": "application/json", "Accept": "application/json" ] } /// Header de autenticación (si se requiere API key o token) /// /// - Parameter token: Token de autenticación (API key, JWT, etc.) /// - Returns: Dictionary con el header de autorización static func authHeader(token: String) -> [String: String] { return [ "Authorization": "Bearer \(token)" ] } // MARK: - Retry Configuration /// Número máximo de intentos de retry para requests fallidas /// /// # Valor por Defecto /// - `3` intentos de retry /// /// # Comportamiento /// - Un valor de `0` significa sin reintentos /// - Los reintentos usan backoff exponencial /// - Solo se reintentan errores recuperables (fallos de red, timeouts, etc.) /// - Errores de cliente (4xx) típicamente no se reintentan static let maxRetries: Int = 3 /// Delay base entre intentos de retry en segundos /// /// # Valor por Defecto /// - `1.0` segundo /// /// # Fórmula /// El delay real usa backoff exponencial: /// ``` /// delay = baseRetryDelay * (2 ^ numeroDeIntento) /// ``` /// - Intento 1: 1 segundo de delay /// - Intento 2: 2 segundos de delay /// - Intento 3: 4 segundos de delay static let baseRetryDelay: TimeInterval = 1.0 // MARK: - Cache Configuration /// Número máximo de respuestas de API a cachear en memoria /// /// # Valor por Defecto /// - `100` respuestas cacheadas /// /// # Notas /// - Cachear ayuda a reducir requests de red y mejorar performance /// - La caché se limpia automáticamente cuando se detecta presión de memoria /// - Valores más grandes pueden aumentar el uso de memoria static let cacheMaxMemoryUsage = 100 /// Tiempo de expiración de caché para respuestas de API en segundos /// /// # Valor por Defecto /// - `300.0` segundos (5 minutos) /// /// # Uso /// - Datos cacheados más viejos que esto se refrescarán del servidor /// - Configurar en `0` para deshabilitar caché /// - Aumentar para datos que cambian infrecuentemente static let cacheExpiryTime: TimeInterval = 300.0 // MARK: - Logging Configuration /// Habilitar logging de requests para debugging /// /// # Valor por Defecto /// - `false` (deshabilitado en producción) /// /// # Comportamiento /// - Cuando es `true`, todas las requests y respuestas se loguean a consola /// - Útil para desarrollo y debugging /// - Debe ser `false` en builds de producción por seguridad /// /// # Recomendación /// Usar configuraciones de build para habilitar solo en debug: /// ```swift /// #if DEBUG /// static let loggingEnabled = true /// #else /// static let loggingEnabled = false /// #endif /// ``` static let loggingEnabled = false // MARK: - Helper Methods /// Construye una URL completa para un endpoint dado /// /// - Parameter endpoint: El path del endpoint de la API (ej: "/manga/popular") /// - Returns: Una URL completa combinando la base URL y el endpoint /// /// # Ejemplo /// ```swift /// let url = APIConfig.url(for: "/manga/popular") /// // Retorna: "https://gitea.cbcren.online:3001/api/v1/manga/popular" /// ``` static func url(for endpoint: String) -> String { // Remover slash inicial si está presente para evitar dobles slashes let cleanEndpoint = endpoint.hasPrefix("/") ? String(endpoint.dropFirst()) : endpoint // Añadir prefix de API si no está ya incluido if cleanEndpoint.hasPrefix("api/") { return baseURL + "/" + cleanEndpoint } else { return baseURL + "/" + cleanEndpoint } } /// Crea un objeto URL para un endpoint dado /// /// - Parameter endpoint: El path del endpoint de la API /// - Returns: Un objeto URL, o nil si el string es inválido /// /// # Ejemplo /// ```swift /// if let url = APIConfig.urlObject(for: "/manga/popular") { /// var request = URLRequest(url: url) /// // Hacer request... /// } /// ``` static func urlObject(for endpoint: String) -> URL? { return URL(string: url(for: endpoint)) } /// Retorna el timeout a usar para un tipo específico de request /// /// - Parameter isResourceRequest: Si esta es una request intensiva de recursos (ej: descargar imágenes) /// - Returns: El valor de timeout apropiado /// /// # Ejemplo /// ```swift /// let timeout = APIConfig.timeoutFor(isResourceRequest: true) /// // Retorna: 300.0 (downloadTimeout) /// ``` static func timeoutFor(isResourceRequest: Bool = false) -> TimeInterval { return isResourceRequest ? downloadTimeout : defaultTimeout } // MARK: - Validation /// Valida que la configuración actual esté correctamente configurada /// /// - Returns: `true` si la configuración parece válida, `false` en caso contrario /// /// # Verificaciones Realizadas /// - URL del servidor no está vacía /// - Puerto está en rango válido (1-65535) /// - Valores de timeout son positivos /// - Cantidad de reintentos es no-negativa /// /// # Uso /// Llamar durante el inicio de la app para asegurar configuración válida: /// ```swift /// assert(APIConfig.isValid, "Configuración de API inválida") /// ``` static var isValid: Bool { // Verificar URL del servidor guard !serverURL.isEmpty else { return false } // Verificar rango de puerto guard (1...65535).contains(port) else { return false } // Verificar timeouts guard defaultTimeout > 0 && downloadTimeout > 0 else { return false } // Verificar cantidad de reintentos guard maxRetries >= 0 else { return false } return true } // MARK: - Endpoints /// Estructura que contiene todos los endpoints de la API enum Endpoints { /// Endpoint para solicitar la descarga de un capítulo al VPS /// /// El backend iniciará el proceso de descarga de las imágenes /// y las almacenará en el VPS. /// /// - Parameters: /// - mangaSlug: Slug del manga a descargar /// - chapterNumber: Número del capítulo /// - Returns: URL completa del endpoint static func download(mangaSlug: String, chapterNumber: Int) -> String { return "\(basePath)/download/\(mangaSlug)/\(chapterNumber)" } /// Endpoint para verificar si un capítulo está descargado en el VPS /// /// - Parameters: /// - mangaSlug: Slug del manga /// - chapterNumber: Número del capítulo /// - Returns: URL completa del endpoint static func checkDownloaded(mangaSlug: String, chapterNumber: Int) -> String { return "\(basePath)/chapters/\(mangaSlug)/\(chapterNumber)" } /// Endpoint para listar todos los capítulos descargados de un manga /// /// - Parameter mangaSlug: Slug del manga /// - Returns: URL completa del endpoint static func listChapters(mangaSlug: String) -> String { return "\(basePath)/chapters/\(mangaSlug)" } /// Endpoint para obtener la URL de una imagen específica de un capítulo /// /// - Parameters: /// - mangaSlug: Slug del manga /// - chapterNumber: Número del capítulo /// - pageIndex: Índice de la página (0-based) /// - Returns: URL completa del endpoint static func getImage(mangaSlug: String, chapterNumber: Int, pageIndex: Int) -> String { return "\(basePath)/images/\(mangaSlug)/\(chapterNumber)/\(pageIndex)" } /// Endpoint para eliminar un capítulo del VPS /// /// - Parameters: /// - mangaSlug: Slug del manga /// - chapterNumber: Número del capítulo /// - Returns: URL completa del endpoint static func deleteChapter(mangaSlug: String, chapterNumber: Int) -> String { return "\(basePath)/chapters/\(mangaSlug)/\(chapterNumber)" } /// Endpoint para obtener estadísticas de almacenamiento del VPS /// /// - Returns: URL completa del endpoint static func storageStats() -> String { return "\(basePath)/storage/stats" } /// Endpoint para hacer ping al servidor (health check) /// /// - Returns: URL completa del endpoint static func health() -> String { return "\(basePath)/health" } } // MARK: - Error Codes /// Códigos de error específicos de la API enum ErrorCodes { static let chapterNotFound = 40401 static let chapterAlreadyDownloaded = 40901 static let storageLimitExceeded = 50701 static let invalidImageFormat = 42201 static let downloadFailed = 50001 } // MARK: - Environment Configuration /// Configuración para entorno de desarrollo /// /// # Uso /// Para usar configuración de desarrollo, modificar en builds de desarrollo: /// ```swift /// #if DEBUG /// static let serverURL = "http://192.168.1.100" /// #else /// static let serverURL = "https://gitea.cbcren.online" /// #endif /// ``` static var development: (serverURL: String, port: Int, timeout: TimeInterval, logging: Bool) { return ( serverURL: "http://192.168.1.100", port: 3001, timeout: 60.0, logging: true ) } /// Configuración para entorno de staging static var staging: (serverURL: String, port: Int, timeout: TimeInterval, logging: Bool) { return ( serverURL: "https://staging.cbcren.online", port: 3001, timeout: 30.0, logging: true ) } /// Configuración para entorno de producción static var production: (serverURL: String, port: Int, timeout: TimeInterval, logging: Bool) { return ( serverURL: "https://gitea.cbcren.online", port: 3001, timeout: 30.0, logging: false ) } } // MARK: - Debug Support #if DEBUG extension APIConfig { /// Configuración de test para unit testing /// /// # Uso /// Usar esta configuración en unit tests para evitar hacer llamadas reales a la API: /// ```swift /// func testAPICall() { /// let testConfig = APIConfig.testing /// // Usar URL de servidor mock /// } /// ``` static var testing: (serverURL: String, port: Int, timeout: TimeInterval, logging: Bool) { return ( serverURL: "http://localhost:3001", port: 3001, timeout: 5.0, logging: true ) } /// Imprime la configuración actual a consola (solo debug) static func printConfiguration() { print("=== API Configuration ===") print("Server URL: \(serverURL)") print("Port: \(port)") print("Base URL: \(baseURL)") print("API Version: \(apiVersion)") print("Default Timeout: \(defaultTimeout)s") print("Download Timeout: \(downloadTimeout)s") print("Max Retries: \(maxRetries)") print("Logging Enabled: \(loggingEnabled)") print("Cache Enabled: \(cacheExpiryTime > 0)") print("=========================") } } #endif