# API Documentation - MangaReader

Este documento proporciona la documentación completa de la API de MangaReader, incluyendo modelos de datos, servicios, ViewModels y sus responsabilidades.

## Tabla de Contenidos

- [Modelos de Datos](#modelos-de-datos)
- [Servicios](#servicios)
- [ViewModels](#viewmodels)
- [Views](#views)
- [Errores y Manejo de Excepciones](#errores-y-manejo-de-excepciones)

---

## Modelos de Datos

Los modelos de datos se encuentran en `ios-app/Sources/Models/Manga.swift`. Son estructuras inmutables que conforman a protocolos Swift estándar para serialización e identificación.

### Manga

Representa la información completa de un manga.

```swift
struct Manga: Codable, Identifiable, Hashable {
    let id: String { slug }
    let slug: String
    let title: String
    let description: String
    let genres: [String]
    let status: String
    let url: String
    let coverImage: String?

    enum CodingKeys: String, CodingKey {
        case slug, title, description, genres, status, url, coverImage
    }

    var displayStatus: String { ... }
}
```

#### Propiedades

| Propiedad | Tipo | Descripción | Ejemplo |
|-----------|------|-------------|---------|
| `id` | `String` | Identificador único (computed, igual a `slug`) | `"one-piece_1695365223767"` |
| `slug` | `String` | Slug único del manga usado en URLs | `"one-piece_1695365223767"` |
| `title` | `String` | Título del manga | `"One Piece"` |
| `description` | `String` | Descripción o sinopsis | `"La historia de piratas..."` |
| `genres` | `[String]` | Array de géneros literarios | `["Acción", "Aventura"]` |
| `status` | `String` | Estado de publicación (crudo) | `"PUBLICANDOSE"` |
| `url` | `String` | URL completa del manga | `"https://manhwaweb.com/manga/..."` |
| `coverImage` | `String?` | URL de imagen de portada (opcional) | `"https://..."` |

#### Métodos Computados

**`displayStatus: String`**
- Retorna el estado traducido y formateado para mostrar en UI
- Mapeos:
  - `"PUBLICANDOSE"` → `"En publicación"`
  - `"FINALIZADO"` → `"Finalizado"`
  - `"EN_PAUSA"`, `"EN_ESPERA"` → `"En pausa"`
  - Otro → retorna valor original

#### Ejemplo de Uso

```swift
let manga = Manga(
    slug: "one-piece_1695365223767",
    title: "One Piece",
    description: "La historia de Monkey D. Luffy...",
    genres: ["Acción", "Aventura", "Comedia"],
    status: "PUBLICANDOSE",
    url: "https://manhwaweb.com/manga/one-piece_1695365223767",
    coverImage: "https://example.com/cover.jpg"
)

print(manga.displayStatus) // "En publicación"
print(manga.id) // "one-piece_1695365223767"
```

---

### Chapter

Representa un capítulo individual de un manga.

```swift
struct Chapter: Codable, Identifiable, Hashable {
    let id: Int { number }
    let number: Int
    let title: String
    let url: String
    let slug: String
    var isRead: Bool = false
    var isDownloaded: Bool = false
    var lastReadPage: Int = 0

    var displayNumber: String { ... }
    var progress: Double { ... }
}
```

#### Propiedades

| Propiedad | Tipo | Descripción | Ejemplo |
|-----------|------|-------------|---------|
| `id` | `Int` | Identificador único (computed, igual a `number`) | `1` |
| `number` | `Int` | Número del capítulo | `1` |
| `title` | `String` | Título del capítulo | `"El inicio de la aventura"` |
| `url` | `String` | URL completa del capítulo | `"https://manhwaweb.com/leer/..."` |
| `slug` | `String` | Slug para identificar el capítulo | `"one-piece/capitulo-1"` |
| `isRead` | `Bool` | Estado de lectura (mutable) | `false` |
| `isDownloaded` | `Bool` | Estado de descarga (mutable) | `false` |
| `lastReadPage` | `Int` | Última página leída (mutable) | `5` |

#### Métodos Computados

**`displayNumber: String`**
- Retorna string formateado para mostrar
- Formato: `"Capítulo {number}"`

**`progress: Double`**
- Retorna progreso como `Double` para ProgressViews
- Valor: `Double(lastReadPage)`

#### Ejemplo de Uso

```swift
var chapter = Chapter(
    number: 1,
    title: "El inicio",
    url: "https://manhwaweb.com/leer/one-piece/1",
    slug: "one-piece/1"
)

chapter.isRead = true
chapter.lastReadPage = 15

print(chapter.displayNumber) // "Capítulo 1"
print(chapter.progress) // 15.0
```

---

### MangaPage

Representa una página individual (imagen) de un capítulo.

```swift
struct MangaPage: Codable, Identifiable, Hashable {
    let id: String { url }
    let url: String
    let index: Int
    var isCached: Bool = false

    var thumbnailURL: String { ... }
}
```

#### Propiedades

| Propiedad | Tipo | Descripción | Ejemplo |
|-----------|------|-------------|---------|
| `id` | `String` | Identificador único (computed, igual a `url`) | `"https://..."` |
| `url` | `String` | URL completa de la imagen | `"https://example.com/page1.jpg"` |
| `index` | `Int` | Índice de la página en el capítulo | `0` |
| `isCached` | `Bool` | Estado de cache local (mutable) | `false` |

#### Métodos Computados

**`thumbnailURL: String`**
- Actualmente retorna la misma URL
- Futuro: implementar versión thumbnail optimizada

---

### ReadingProgress

Almacena el progreso de lectura de un usuario.

```swift
struct ReadingProgress: Codable {
    let mangaSlug: String
    let chapterNumber: Int
    let pageNumber: Int
    let timestamp: Date

    var isCompleted: Bool { ... }
}
```

#### Propiedades

| Propiedad | Tipo | Descripción |
|-----------|------|-------------|
| `mangaSlug` | `String` | Slug del manga |
| `chapterNumber` | `Int` | Número del capítulo |
| `pageNumber` | `Int` | Página actual |
| `timestamp` | `Date` | Fecha/hora de lectura |

#### Métodos Computados

**`isCompleted: Bool`**
- Retorna `true` si el usuario leyó más de 5 páginas
- Lógica: `return pageNumber > 5`

---

### DownloadedChapter

Representa un capítulo descargado localmente.

```swift
struct DownloadedChapter: Codable, Identifiable {
    let id: String { "\(mangaSlug)-chapter\(chapterNumber)" }
    let mangaSlug: String
    let mangaTitle: String
    let chapterNumber: Int
    let pages: [MangaPage]
    let downloadedAt: Date
    var totalSize: Int64 = 0

    var displayTitle: String { ... }
}
```

#### Propiedades

| Propiedad | Tipo | Descripción |
|-----------|------|-------------|
| `id` | `String` | ID compuesto único |
| `mangaSlug` | `String` | Slug del manga |
| `mangaTitle` | `String` | Título del manga |
| `chapterNumber` | `Int` | Número del capítulo |
| `pages` | `[MangaPage]` | Array de páginas |
| `downloadedAt` | `Date` | Fecha de descarga |
| `totalSize` | `Int64` | Tamaño total en bytes |

---

## Servicios

Los servicios encapsulan la lógica de negocio y se encuentran en `ios-app/Sources/Services/`.

### ManhwaWebScraper

Servicio responsable del scraping de contenido web desde manhwaweb.com.

```swift
@MainActor
class ManhwaWebScraper: NSObject, ObservableObject
```

#### Propiedades

| Propiedad | Tipo | Acceso | Descripción |
|-----------|------|--------|-------------|
| `shared` | `ManhwaWebScraper` | `static` | Instancia singleton compartida |
| `webView` | `WKWebView?` | `private` | WebView para ejecutar JavaScript |

#### Métodos Públicos

##### `scrapeMangaInfo(mangaSlug:)`

Obtiene la información completa de un manga.

**Firma:**
```swift
func scrapeMangaInfo(mangaSlug: String) async throws -> Manga
```

**Parámetros:**
- `mangaSlug`: `String` - Slug único del manga

**Retorna:**
- `Manga` - Objeto con información completa del manga

**Errors:**
- `ScrapingError.webViewNotInitialized`
- `ScrapingError.pageLoadFailed`
- `ScrapingError.noContentFound`

**Ejemplo:**
```swift
do {
    let manga = try await ManhwaWebScraper.shared.scrapeMangaInfo(
        mangaSlug: "one-piece_1695365223767"
    )
    print("Manga: \(manga.title)")
} catch {
    print("Error: \(error.localizedDescription)")
}
```

**Proceso Interno:**
1. Construye URL: `https://manhwaweb.com/manga/{slug}`
2. Carga URL en WKWebView
3. Espera 3 segundos a que JavaScript renderice
4. Ejecuta JavaScript para extraer:
   - Título (de `<h1>` o `<title>`)
   - Descripción (de `<p>` con >100 caracteres)
   - Géneros (de links `/genero/*`)
   - Estado (via regex)
   - Cover image (de `.cover img`)
5. Parsea resultado a `Manga`

---

##### `scrapeChapters(mangaSlug:)`

Obtiene la lista de capítulos de un manga.

**Firma:**
```swift
func scrapeChapters(mangaSlug: String) async throws -> [Chapter]
```

**Parámetros:**
- `mangaSlug`: `String` - Slug del manga

**Retorna:**
- `[Chapter]` - Array de capítulos ordenados descendente

**Errors:**
- `ScrapingError.webViewNotInitialized`
- `ScrapingError.pageLoadFailed`
- `ScrapingError.parsingError`

**Ejemplo:**
```swift
do {
    let chapters = try await ManhwaWebScraper.shared.scrapeChapters(
        mangaSlug: "one-piece_1695365223767"
    )
    print("Found \(chapters.count) chapters")
} catch {
    print("Error: \(error.localizedDescription)")
}
```

**Proceso Interno:**
1. Carga página del manga
2. Ejecuta JavaScript que:
   - Busca todos los links `/leer/*`
   - Extrae número de capítulo via regex
   - Filtra duplicados
   - Ordena descendente
3. Parsea a array de `Chapter`

---

##### `scrapeChapterImages(chapterSlug:)`

Obtiene las URLs de las imágenes de un capítulo.

**Firma:**
```swift
func scrapeChapterImages(chapterSlug: String) async throws -> [String]
```

**Parámetros:**
- `chapterSlug`: `String` - Slug del capítulo

**Retorna:**
- `[String]` - Array de URLs de imágenes en orden

**Errors:**
- `ScrapingError.webViewNotInitialized`
- `ScrapingError.pageLoadFailed`

**Ejemplo:**
```swift
do {
    let images = try await ManhwaWebScraper.shared.scrapeChapterImages(
        chapterSlug: "one-piece/1"
    )
    print("Found \(images.count) pages")
    for (index, imageUrl) in images.enumerated() {
        print("Page \(index + 1): \(imageUrl)")
    }
} catch {
    print("Error: \(error.localizedDescription)")
}
```

**Proceso Interno:**
1. Carga URL del capítulo
2. Espera 5 segundos (para imágenes)
3. Ejecuta JavaScript que:
   - Selecciona todas las etiquetas `<img>`
   - Filtra elementos de UI (avatars, icons, logos)
   - Elimina duplicados
4. Retorna array de URLs

---

#### Métodos Privados

##### `setupWebView()`

Configura el WKWebView con preferencias optimizadas.

##### `loadURLAndWait(_:waitForImages:)`

Carga una URL y espera a que JavaScript termine de renderizar.

**Parámetros:**
- `url`: `URL` - URL a cargar
- `waitForImages`: `Bool` - Si `true`, espera 5 segundos; si `false`, 3 segundos

---

### StorageService

Servicio responsable del almacenamiento local de datos.

```swift
class StorageService
```

#### Propiedades

| Propiedad | Tipo | Acceso | Descripción |
|-----------|------|--------|-------------|
| `shared` | `StorageService` | `static` | Instancia singleton compartida |
| `documentsDirectory` | `URL` | `private` | Directorio de documentos |
| `chaptersDirectory` | `URL` | `private` | Directorio de capítulos |

#### Gestión de Favoritos

##### `getFavorites()`

```swift
func getFavorites() -> [String]
```
Retorna array de slugs de mangas favoritos.

##### `saveFavorite(mangaSlug:)`

```swift
func saveFavorite(mangaSlug: String)
```
Guarda un manga como favorito (no duplica si ya existe).

##### `removeFavorite(mangaSlug:)`

```swift
func removeFavorite(mangaSlug: String)
```
Elimina un manga de favoritos.

##### `isFavorite(mangaSlug:)`

```swift
func isFavorite(mangaSlug: String) -> Bool
```
Verifica si un manga es favorito.

**Ejemplo:**
```swift
let storage = StorageService.shared

// Guardar favorito
storage.saveFavorite(mangaSlug: "one-piece_1695365223767")

// Verificar
if storage.isFavorite(mangaSlug: "one-piece_1695365223767") {
    print("Es favorito")
}

// Listar todos
let favorites = storage.getFavorites()
print("Tienes \(favorites.count) favoritos")

// Eliminar
storage.removeFavorite(mangaSlug: "one-piece_1695365223767")
```

---

#### Gestión de Progreso de Lectura

##### `saveReadingProgress(_:)`

```swift
func saveReadingProgress(_ progress: ReadingProgress)
```
Guarda o actualiza el progreso de lectura.

##### `getReadingProgress(mangaSlug:chapterNumber:)`

```swift
func getReadingProgress(mangaSlug: String, chapterNumber: Int) -> ReadingProgress?
```
Retorna el progreso de un capítulo específico.

##### `getAllReadingProgress()`

```swift
func getAllReadingProgress() -> [ReadingProgress]
```
Retorna todo el progreso guardado.

##### `getLastReadChapter(mangaSlug:)`

```swift
func getLastReadChapter(mangaSlug: String) -> ReadingProgress?
```
Retorna el capítulo más reciente leído de un manga.

**Ejemplo:**
```swift
let storage = StorageService.shared

// Guardar progreso
let progress = ReadingProgress(
    mangaSlug: "one-piece_1695365223767",
    chapterNumber: 1,
    pageNumber: 15,
    timestamp: Date()
)
storage.saveReadingProgress(progress)

// Recuperar progreso
if let savedProgress = storage.getReadingProgress(
    mangaSlug: "one-piece_1695365223767",
    chapterNumber: 1
) {
    print("Última página: \(savedProgress.pageNumber)")
}

// Último capítulo leído
if let lastChapter = storage.getLastReadChapter(mangaSlug: "one-piece_1695365223767") {
    print("Último capítulo: \(lastChapter.chapterNumber)")
}
```

---

#### Gestión de Capítulos Descargados

##### `saveDownloadedChapter(_:)`

```swift
func saveDownloadedChapter(_ chapter: DownloadedChapter)
```
Guarda metadata de un capítulo descargado.

##### `getDownloadedChapters()`

```swift
func getDownloadedChapters() -> [DownloadedChapter]
```
Retorna todos los capítulos descargados.

##### `isChapterDownloaded(mangaSlug:chapterNumber:)`

```swift
func isChapterDownloaded(mangaSlug: String, chapterNumber: Int) -> Bool
```
Verifica si un capítulo está descargado.

##### `deleteDownloadedChapter(mangaSlug:chapterNumber:)`

```swift
func deleteDownloadedChapter(mangaSlug: String, chapterNumber: Int)
```
Elimina un capítulo descargado (archivos + metadata).

**Ejemplo:**
```swift
let storage = StorageService.shared

// Verificar si está descargado
if storage.isChapterDownloaded(
    mangaSlug: "one-piece_1695365223767",
    chapterNumber: 1
) {
    print("Capítulo ya descargado")
} else {
    print("Capítulo no descargado")
}

// Eliminar capítulo
storage.deleteDownloadedChapter(
    mangaSlug: "one-piece_1695365223767",
    chapterNumber: 1
)
```

---

#### Gestión de Imágenes

##### `saveImage(_:mangaSlug:chapterNumber:pageIndex:)`

```swift
func saveImage(_ image: UIImage, mangaSlug: String, chapterNumber: Int, pageIndex: Int) async throws -> URL
```
Guarda una imagen en disco local.

**Parámetros:**
- `image`: `UIImage` - Imagen a guardar
- `mangaSlug`: `String` - Slug del manga
- `chapterNumber`: `Int` - Número del capítulo
- `pageIndex`: `Int` - Índice de la página

**Retorna:**
- `URL` - Ruta del archivo guardado

**Errors:**
- Error si no se puede convertir o guardar

**Ejemplo:**
```swift
let storage = StorageService.shared

do {
    let imageURL = try await storage.saveImage(
        image: myUIImage,
        mangaSlug: "one-piece_1695365223767",
        chapterNumber: 1,
        pageIndex: 0
    )
    print("Imagen guardada en: \(imageURL.path)")
} catch {
    print("Error guardando imagen: \(error)")
}
```

##### `loadImage(mangaSlug:chapterNumber:pageIndex:)`

```swift
func loadImage(mangaSlug: String, chapterNumber: Int, pageIndex: Int) -> UIImage?
```
Carga una imagen desde disco.

##### `getImageURL(mangaSlug:chapterNumber:pageIndex:)`

```swift
func getImageURL(mangaSlug: String, chapterNumber: Int, pageIndex: Int) -> URL?
```
Retorna la URL local de una imagen si existe.

---

#### Gestión de Almacenamiento

##### `getStorageSize()`

```swift
func getStorageSize() -> Int64
```
Retorna el tamaño total usado en bytes.

##### `clearAllDownloads()`

```swift
func clearAllDownloads()
```
Elimina todos los capítulos descargados.

##### `formatFileSize(_:)`

```swift
func formatFileSize(_ bytes: Int64) -> String
```
Formatea bytes a string legible (KB, MB, GB).

**Ejemplo:**
```swift
let storage = StorageService.shared

// Obtener tamaño usado
let size = storage.getStorageSize()
let formatted = storage.formatFileSize(size)
print("Almacenamiento usado: \(formatted)")

// Limpiar todo
storage.clearAllDownloads()
```

---

## ViewModels

Los ViewModels coordinan entre Services y Views.

### MangaListViewModel

ViewModel para la lista principal de mangas.

```swift
@MainActor
class MangaListViewModel: ObservableObject
```

#### Propiedades Publicadas

| Propiedad | Tipo | Descripción |
|-----------|------|-------------|
| `mangas` | `[Manga]` | Lista de mangas cargados |
| `isLoading` | `Bool` | Indicador de carga |
| `searchText` | `String` | Texto de búsqueda |
| `filter` | `MangaFilter` | Filtro actual (all/favorites/downloaded) |
| `newMangaSlug` | `String` | Slug del nuevo manga a agregar |

#### Métodos Públicos

##### `loadMangas()`

```swift
func loadMangas() async
```
Carga los mangas guardados en favoritos.

##### `addManga(_:)`

```swift
func addManga(_ slug: String) async
```
Agrega un nuevo manga mediante scraping.

#### Propiedades Computadas

##### `filteredMangas`

```swift
var filteredMangas: [Manga]
```
Retorna mangas filtrados por búsqueda y categoría.

**Ejemplo:**
```swift
@StateObject private var viewModel = MangaListViewModel()

// Cargar mangas
await viewModel.loadMangas()

// Agregar manga
await viewModel.addManga("one-piece_1695365223767")

// Filtrar
viewModel.filter = .favorites
viewModel.searchText = "one"
```

---

### MangaDetailViewModel

ViewModel para el detalle de un manga.

```swift
@MainActor
class MangaDetailViewModel: ObservableObject
```

#### Propiedades Publicadas

| Propiedad | Tipo | Descripción |
|-----------|------|-------------|
| `chapters` | `[Chapter]` | Lista de capítulos |
| `isLoadingChapters` | `Bool` | Indicador de carga |
| `isFavorite` | `Bool` | Estado de favorito |
| `selectedChapter` | `Chapter?` | Capítulo seleccionado |
| `showingDownloadAll` | `Bool` | Mostrar diálogo de descarga |

#### Métodos Públicos

##### `loadChapters()`

```swift
func loadChapters() async
```
Carga los capítulos del manga.

##### `toggleFavorite()`

```swift
func toggleFavorite()
```
Alterna el estado de favorito.

##### `downloadAllChapters()`

```swift
func downloadAllChapters()
```
Inicia descarga de todos los capítulos.

##### `downloadLastChapters(count:)`

```swift
func downloadLastChapters(count: Int)
```
Descarga los últimos N capítulos.

**Ejemplo:**
```swift
@StateObject private var viewModel = MangaDetailViewModel(manga: manga)

// Cargar capítulos
await viewModel.loadChapters()

// Marcar favorito
viewModel.toggleFavorite()

// Descargar últimos 10
viewModel.downloadLastChapters(count: 10)
```

---

### ReaderViewModel

ViewModel para el lector de capítulos.

```swift
@MainActor
class ReaderViewModel: ObservableObject
```

#### Propiedades Publicadas

| Propiedad | Tipo | Descripción |
|-----------|------|-------------|
| `pages` | `[MangaPage]` | Lista de páginas |
| `currentPage` | `Int` | Página actual |
| `isLoading` | `Bool` | Indicador de carga |
| `showError` | `Bool` | Mostrar error |
| `showControls` | `Bool` | Mostrar controles UI |
| `isFavorite` | `Bool` | Estado de favorito |
| `isDownloaded` | `Bool` | Capítulo descargado |
| `backgroundColor` | `Color` | Color de fondo |
| `readingMode` | `ReadingMode` | Modo de lectura |

#### Métodos Públicos

##### `loadPages()`

```swift
func loadPages() async
```
Carga las páginas del capítulo (desde local o web).

##### `cachePage(_:image:)`

```swift
func cachePage(_ page: MangaPage, image: Image) async
```
Cachea una página localmente (TODO: implementar).

##### `toggleFavorite()`

```swift
func toggleFavorite()
```
Alterna favorito del manga actual.

##### `cycleBackgroundColor()`

```swift
func cycleBackgroundColor()
```
Cicla entre colores de fondo (blanco/negro/sepia).

#### Propiedades Computadas

- `currentPageIndex`: `Int` - Índice de página actual
- `totalPages`: `Int` - Total de páginas

**Ejemplo:**
```swift
@StateObject private var viewModel = ReaderViewModel(manga: manga, chapter: chapter)

// Cargar páginas
await viewModel.loadPages()

// Ir a página específica
viewModel.currentPage = 10

// Cambiar fondo
viewModel.cycleBackgroundColor()

// Cambiar modo lectura
viewModel.readingMode = .horizontal
```

---

## Views

Las vistas son componentes SwiftUI que presentan la UI.

### ContentView

Vista principal que muestra la lista de mangas.

**Componentes:**
- `MangaRowView`: Fila individual de manga
- `MangaListViewModel`: ViewModel asociado

**Funcionalidades:**
- Búsqueda de mangas
- Filtros (todos/favoritos/descargados)
- Agregar manga manualmente
- Pull-to-refresh

### MangaDetailView

Vista de detalle de un manga específico.

**Componentes:**
- `ChapterRowView`: Fila de capítulo
- `FlowLayout`: Layout de géneros
- `MangaDetailViewModel`: ViewModel asociado

**Funcionalidades:**
- Mostrar información del manga
- Listar capítulos
- Marcar favorito
- Descargar capítulos

### ReaderView

Vista de lectura de capítulos.

**Componentes:**
- `PageView`: Vista de página individual
- `ReaderViewModel`: ViewModel asociado

**Funcionalidades:**
- Mostrar páginas con zoom/pan
- Navegación entre páginas
- Configurar fondo
- Cambiar modo de lectura
- Slider de navegación

---

## Errores y Manejo de Excepciones

### ScrapingError

Errores específicos del scraper.

```swift
enum ScrapingError: LocalizedError {
    case webViewNotInitialized
    case pageLoadFailed
    case noContentFound
    case parsingError

    var errorDescription: String? { ... }
}
```

#### Casos

| Error | Descripción | Mensaje (Español) |
|-------|-------------|-------------------|
| `webViewNotInitialized` | WKWebView no configurado | "WebView no está inicializado" |
| `pageLoadFailed` | Error cargando página | "Error al cargar la página" |
| `noContentFound` | No se encontró contenido | "No se encontró contenido" |
| `parsingError` | Error procesando datos | "Error al procesar el contenido" |

### Estrategias de Manejo de Errores

1. **ViewModels**: Capturan errores del scraper
2. **Views**: Muestran alertas al usuario
3. **Servicios**: Propagan errores con `throws`

**Ejemplo:**
```swift
do {
    let manga = try await scraper.scrapeMangaInfo(mangaSlug: slug)
    // Manejar éxito
} catch ScrapingError.webViewNotInitialized {
    // Mostrar alerta específica
    errorMessage = "Error de configuración"
} catch {
    // Error genérico
    errorMessage = error.localizedDescription
}
```

---

**Última actualización**: Febrero 2026
**Versión**: 1.0.0
