package com.docmost.app.api import android.content.Context import android.net.Uri import android.util.Log import com.docmost.app.BuildConfig import com.docmost.app.utils.NetworkConnectivityObserver import com.docmost.app.data.PageCacheManager import com.docmost.app.data.SpaceCacheManager import com.docmost.app.data.SyncManager import java.io.File import java.io.IOException class CachedApi( private val api: DocmostApi, private val pageCache: PageCacheManager, private val spaceCache: SpaceCacheManager, private val syncManager: SyncManager, private val networkObserver: NetworkConnectivityObserver ) { var forcedOffline: Boolean = false var isSyncing: Boolean = false private set val isOffline: Boolean get() = forcedOffline || networkObserver.isOffline() data class SyncProgress( val spaceName: String, val spaceIndex: Int, val totalSpaces: Int, val pageTitle: String, val pageCount: Int, val totalPages: Int, val skipped: Int, val errors: Int ) private fun cacheOrThrow(block: () -> T): T { return try { block() } catch (e: Exception) { throw OfflineNotAvailableException("No disponible sin conexión: ${e.message}") } } // ── Spaces ── suspend fun getSpaces(): List { if (isOffline) { val cached = spaceCache.getCachedSpaces() if (cached.isNotEmpty()) return cached throw OfflineNotAvailableException("No hay espacios en caché") } val spaces = api.getSpaces() spaceCache.cacheSpaces(spaces) return spaces } suspend fun createSpace(name: String, slug: String, description: String?): Space { if (isOffline) { val tempId = "temp_${System.currentTimeMillis()}" syncManager.enqueueCreateSpace(name, slug, description, tempId) val localSpace = Space( id = tempId, name = name, description = description ) spaceCache.cacheSpace(localSpace) return localSpace } val space = api.createSpace(name, slug, description) spaceCache.cacheSpace(space) return space } suspend fun updateSpace(spaceId: String, name: String?, description: String?): Space { if (isOffline) { syncManager.enqueueUpdateSpace(spaceId, name, description) val cached = spaceCache.getCachedSpace(spaceId) if (cached != null) { val updated = cached.copy( name = name ?: cached.name, description = description ?: cached.description ) spaceCache.cacheSpace(updated) return updated } throw OfflineNotAvailableException("Espacio no encontrado en caché") } val space = api.updateSpace(spaceId, name, description) spaceCache.cacheSpace(space) return space } suspend fun deleteSpace(spaceId: String) { if (isOffline) { syncManager.enqueueDeleteSpace(spaceId) spaceCache.removeCachedSpace(spaceId) return } api.deleteSpace(spaceId) spaceCache.removeCachedSpace(spaceId) } suspend fun uploadSpaceIcon(spaceId: String, imageUri: Uri, context: Context): String { if (isOffline) { throw OfflineNotAvailableException("Subir iconos no disponible sin conexión") } return api.uploadSpaceIcon(spaceId, imageUri, context) } suspend fun removeSpaceIcon(spaceId: String) { if (isOffline) throw OfflineNotAvailableException("No disponible sin conexión") api.removeSpaceIcon(spaceId) } // ── Pages ── suspend fun getSidebarPages(spaceId: String, pageId: String? = null): List { if (isOffline) { val cached = pageCache.getCachedPagesForSpace(spaceId) if (cached != null) return cached return emptyList() } val pages = api.getSidebarPages(spaceId, pageId) pageCache.cachePagesForSpace(spaceId, pages) return pages } suspend fun getPageInfo(pageId: String): Page { if (isOffline) { val cached = pageCache.getCachedPage(pageId) if (cached != null) return cached throw OfflineNotAvailableException("Página no encontrada en caché") } val page = api.getPageInfo(pageId) pageCache.cachePage(page) return page } suspend fun createPage(title: String, spaceId: String): Page { if (isOffline) { val tempId = "temp_${System.currentTimeMillis()}" syncManager.enqueueCreatePage(title, spaceId, tempId) val localPage = Page( id = tempId, title = title, spaceId = spaceId ) pageCache.cachePage(localPage) val existing = pageCache.getCachedPagesForSpace(spaceId)?.toMutableList() ?: mutableListOf() existing.add(localPage) pageCache.cachePagesForSpace(spaceId, existing) return localPage } val page = api.createPage(title, spaceId) pageCache.cachePage(page) val existing = pageCache.getCachedPagesForSpace(spaceId)?.toMutableList() ?: mutableListOf() existing.add(page) pageCache.cachePagesForSpace(spaceId, existing) return page } suspend fun updatePage(pageId: String, content: String?, title: String?, icon: String? = null, isJson: Boolean = false): Page { if (isOffline) { syncManager.enqueueUpdatePage(pageId, content ?: "", title ?: "", icon) val cached = pageCache.getCachedPage(pageId) if (cached != null) { val updated = cached.copy( title = title ?: cached.title, icon = icon ?: cached.icon, content = content ?: cached.content ) pageCache.cachePage(updated) return updated } throw OfflineNotAvailableException("Página no encontrada en caché") } val page = api.updatePage(pageId, content, title, icon, isJson) pageCache.cachePage(page) return page } suspend fun deletePage(pageId: String, spaceId: String? = null) { if (isOffline) { syncManager.enqueueDeletePage(pageId) pageCache.removeFromCache(pageId) if (spaceId != null) { val cached = pageCache.getCachedPagesForSpace(spaceId)?.filter { it.id != pageId } if (cached != null) pageCache.cachePagesForSpace(spaceId, cached) } return } api.deletePage(pageId) pageCache.removeFromCache(pageId) if (spaceId != null) { val cached = pageCache.getCachedPagesForSpace(spaceId)?.filter { it.id != pageId } if (cached != null) pageCache.cachePagesForSpace(spaceId, cached) } } suspend fun duplicatePage(pageId: String, targetSpaceId: String? = null): Page { if (isOffline) throw OfflineNotAvailableException("Duplicar no disponible sin conexión") val page = api.duplicatePage(pageId, targetSpaceId) pageCache.cachePage(page) return page } suspend fun movePageToSpace(pageId: String, spaceId: String) { if (isOffline) { syncManager.enqueueUpdatePage(pageId, "", "", null) return } api.movePageToSpace(pageId, spaceId) } suspend fun movePageToPage(pageId: String, parentPageId: String?, position: String) { if (isOffline) { syncManager.enqueueUpdatePage(pageId, "", "", null) return } api.movePageToPage(pageId, parentPageId, position) } // ── Recent ── suspend fun getRecentPages(): List { if (isOffline) throw OfflineNotAvailableException("Recientes no disponible sin conexión") return api.getRecentPages() } // ── Shares ── suspend fun getShares(): List { if (isOffline) throw OfflineNotAvailableException("Compartir no disponible sin conexión") return api.getShares() } suspend fun createShare(pageId: String): Share { if (isOffline) throw OfflineNotAvailableException("Crear enlaces no disponible sin conexión") return api.createShare(pageId) } suspend fun deleteShare(shareId: String) { if (isOffline) throw OfflineNotAvailableException("No disponible sin conexión") api.deleteShare(shareId) } // ── Comments ── suspend fun getComments(pageId: String): List { if (isOffline) throw OfflineNotAvailableException("Comentarios no disponible sin conexión") return api.getComments(pageId) } suspend fun createComment(pageId: String, content: String): Comment { if (isOffline) throw OfflineNotAvailableException("Comentarios no disponible sin conexión") return api.createComment(pageId, content) } suspend fun updateComment(commentId: String, content: String): Comment { if (isOffline) throw OfflineNotAvailableException("Comentarios no disponible sin conexión") return api.updateComment(commentId, content) } suspend fun deleteComment(commentId: String) { if (isOffline) throw OfflineNotAvailableException("Comentarios no disponible sin conexión") api.deleteComment(commentId) } // ── History ── suspend fun getPageHistory(pageId: String): List { if (isOffline) throw OfflineNotAvailableException("Historial no disponible sin conexión") return api.getPageHistory(pageId) } suspend fun getPageHistoryInfo(pageId: String, historyId: String): PageHistory { if (isOffline) throw OfflineNotAvailableException("Historial no disponible sin conexión") return api.getPageHistoryInfo(pageId, historyId) } // ── User ── suspend fun getCurrentUser(): User { if (isOffline) throw OfflineNotAvailableException("Usuario no disponible sin conexión") return api.getCurrentUser() } suspend fun updateUser(name: String?): User { if (isOffline) throw OfflineNotAvailableException("No disponible sin conexión") return api.updateUser(name) } // ── Search ── suspend fun searchPages(query: String): List { if (isOffline) throw OfflineNotAvailableException("Búsqueda no disponible sin conexión") return api.searchPages(query) } // ── Attachments ── suspend fun uploadAttachment(pageId: String, fileUri: Uri, context: Context): Attachment { if (isOffline) { val fileName = getFileNameFromUri(context, fileUri) ?: "file" val mimeType = context.contentResolver.getType(fileUri) ?: "application/octet-stream" val pendingDir = File(context.cacheDir, "attachments_pending/$pageId") pendingDir.mkdirs() val localFile = File(pendingDir, fileName) try { context.contentResolver.openInputStream(fileUri)?.use { input -> localFile.outputStream().use { output -> input.copyTo(output) } } } catch (e: Exception) { throw IOException("No se pudo guardar el archivo localmente: ${e.message}") } syncManager.enqueueUploadAttachment(pageId, localFile.absolutePath, fileName, mimeType) return Attachment( id = "pending_${System.currentTimeMillis()}", fileName = fileName, fileSize = localFile.length(), mimeType = mimeType, url = localFile.toURI().toString(), pageId = pageId ) } return api.uploadAttachment(pageId, fileUri, context) } suspend fun getPageAttachments(pageId: String): List { if (isOffline) throw OfflineNotAvailableException("Archivos adjuntos no disponible sin conexión") return api.getPageAttachments(pageId) } suspend fun getPageContent(pageId: String): PageContent { if (isOffline) throw OfflineNotAvailableException("No disponible sin conexión") return api.getPageContent(pageId) } // ── Trash ── suspend fun getDeletedPages(spaceId: String): List { if (isOffline) throw OfflineNotAvailableException("Papelera no disponible sin conexión") return api.getDeletedPages(spaceId) } suspend fun restorePage(pageId: String, spaceId: String? = null) { if (isOffline) throw OfflineNotAvailableException("No disponible sin conexión") api.restorePage(pageId, spaceId) } // ── Helpers ── suspend fun getPageHtml(pageId: String): String { if (isOffline) throw OfflineNotAvailableException("No disponible sin conexión") return api.getPageHtml(pageId) } fun getAttachmentUrl(attachmentType: String, fileName: String): String { return api.getAttachmentUrl(attachmentType, fileName) } private fun getFileNameFromUri(context: Context, uri: Uri): String? { var fileName: String? = null val cursor = context.contentResolver.query(uri, null, null, null, null) cursor?.use { if (it.moveToFirst()) { val displayNameIndex = it.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME) if (displayNameIndex >= 0) { fileName = it.getString(displayNameIndex) } } } return fileName ?: uri.lastPathSegment } // ── Sync All ── suspend fun syncAllPages(onProgress: (SyncProgress) -> Unit): Pair { if (isOffline) throw OfflineNotAvailableException("No disponible sin conexión") if (isSyncing) throw IllegalStateException("Sync already in progress") isSyncing = true var skipped = 0 var errors = 0 var totalSynced = 0 try { val spaces = api.getSpaces() spaceCache.cacheSpaces(spaces) val allRootPages = mutableMapOf>() for ((spaceIdx, space) in spaces.withIndex()) { try { val rootPages = api.getSidebarPages(space.id) allRootPages[space.id] = rootPages pageCache.cachePagesForSpace(space.id, rootPages) val allPageIds = mutableListOf() allPageIds.addAll(rootPages) rootPages.forEach { page -> if (page.hasChildren) { val children = fetchAllChildren(space.id, page) allPageIds.addAll(children) } } var pageIdx = 0 for (page in allPageIds) { if (!isSyncing) break pageIdx++ onProgress(SyncProgress( spaceName = space.name, spaceIndex = spaceIdx + 1, totalSpaces = spaces.size, pageTitle = page.title, pageCount = pageIdx, totalPages = allPageIds.size, skipped = skipped, errors = errors )) try { val cached = pageCache.getCachedPage(page.id) if (cached != null && cached.updatedAt == page.updatedAt) { skipped++ continue } val fetchedPage = api.getPageInfo(page.id) pageCache.cachePage(fetchedPage) totalSynced++ } catch (e: Exception) { errors++ if (BuildConfig.DEBUG) Log.e("CachedApi", "Sync page error: ${e.message}") } } } catch (e: Exception) { errors++ if (BuildConfig.DEBUG) Log.e("CachedApi", "Sync space error: ${e.message}") } } } finally { isSyncing = false } return Pair(totalSynced, errors) } private suspend fun fetchAllChildren(spaceId: String, parentPage: Page): List { val children = mutableListOf() try { val childPages = api.getSidebarPages(spaceId, parentPage.id) children.addAll(childPages) childPages.forEach { child -> if (child.hasChildren) { children.addAll(fetchAllChildren(spaceId, child)) } } } catch (_: Exception) { } return children } fun stopSync() { isSyncing = false } fun clearAllCachedPages(): Int { return pageCache.clearAllCachedPages() } fun getCachedPageCount(): Int = pageCache.getCachedPageCount() }