package net.sergeych.toread.storage

enum class BookFileStorageKind {
    EXTERNAL_URI,
    MANAGED_FILE,
    DATABASE_BLOB,
    INDEXEDDB_BLOB,
    REMOTE_URL,
    MISSING,
}

enum class BookImportPolicy {
    LINK,
    COPY,
    STORE_BLOB,
}

enum class BookReadingStatus {
    NEW,
    TO_READ,
    READING,
    READ,
    NOT_INTERESTED,
}

data class BookRecord(
    val id: String,
    val title: String? = null,
    val subtitle: String? = null,
    val authors: List<String> = emptyList(),
    val language: String? = null,
    val date: String? = null,
    val description: String? = null,
    val keywords: String? = null,
    val coverImage: ByteArray? = null,
    val coverImageMimeType: String? = null,
    val createdAt: Long,
    val updatedAt: Long,
) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is BookRecord) return false

        return id == other.id &&
            title == other.title &&
            subtitle == other.subtitle &&
            authors == other.authors &&
            language == other.language &&
            date == other.date &&
            description == other.description &&
            keywords == other.keywords &&
            coverImage.contentEquals(other.coverImage) &&
            coverImageMimeType == other.coverImageMimeType &&
            createdAt == other.createdAt &&
            updatedAt == other.updatedAt
    }

    override fun hashCode(): Int {
        var result = id.hashCode()
        result = 31 * result + (title?.hashCode() ?: 0)
        result = 31 * result + (subtitle?.hashCode() ?: 0)
        result = 31 * result + authors.hashCode()
        result = 31 * result + (language?.hashCode() ?: 0)
        result = 31 * result + (date?.hashCode() ?: 0)
        result = 31 * result + (description?.hashCode() ?: 0)
        result = 31 * result + (keywords?.hashCode() ?: 0)
        result = 31 * result + (coverImage?.contentHashCode() ?: 0)
        result = 31 * result + (coverImageMimeType?.hashCode() ?: 0)
        result = 31 * result + createdAt.hashCode()
        result = 31 * result + updatedAt.hashCode()
        return result
    }
}

data class BookCoverRecord(
    val image: ByteArray,
    val mimeType: String?,
)

data class BookBodyRecord(
    val id: String,
    val exactTextHash: String,
    val canonicalizationVersion: Int,
    val nearSignature: String? = null,
    val wordCount: Int? = null,
    val language: String? = null,
    val createdAt: Long,
)

data class BodyClusterRecord(
    val id: String,
    val representativeBodyId: String,
    val createdAt: Long,
)

data class BookFileRecord(
    val id: String,
    val bookId: String? = null,
    val bodyId: String? = null,
    val bodyClusterId: String? = null,
    val duplicateOfFileId: String? = null,
    val rawSha256: String,
    val format: String? = null,
    val mimeType: String? = null,
    val sizeBytes: Long? = null,
    val originalFilename: String? = null,
    val storageKind: BookFileStorageKind,
    val storageUri: String? = null,
    val contentObjectId: String? = null,
    val lastModifiedMillis: Long? = null,
    val lastSeenAt: Long? = null,
    val readingStatus: BookReadingStatus = BookReadingStatus.NEW,
    val favorite: Boolean = false,
    val lastReadAt: Long? = null,
    val createdAt: Long,
    val updatedAt: Long,
    val importedAt: Long = createdAt,
)

data class LibraryFileRecord(
    val fileId: String,
    val bookId: String? = null,
    val title: String? = null,
    val authors: List<String> = emptyList(),
    val language: String? = null,
    val date: String? = null,
    val format: String? = null,
    val sizeBytes: Long? = null,
    val originalFilename: String? = null,
    val storageUri: String? = null,
    val lastSeenAt: Long? = null,
    val readingStatus: BookReadingStatus = BookReadingStatus.NEW,
    val favorite: Boolean = false,
    val lastReadAt: Long? = null,
    val importedAt: Long,
)

data class ContentAnchor(
    val version: Int = 1,
    val canonicalCharOffset: Long? = null,
    val canonicalTokenOffset: Long? = null,
    val progress: Double? = null,
    val exact: String? = null,
    val prefix: String? = null,
    val suffix: String? = null,
    val fingerprintsJson: String? = null,
    val formatHintsJson: String? = null,
)

data class ReadingStateRecord(
    val id: String,
    val bodyClusterId: String,
    val bodyId: String? = null,
    val anchor: ContentAnchor,
    val updatedAt: Long,
)

data class BookmarkRecord(
    val id: String,
    val bodyClusterId: String,
    val bodyId: String? = null,
    val anchor: ContentAnchor,
    val title: String? = null,
    val selectedTextSnapshot: String? = null,
    val color: String? = null,
    val createdAt: Long,
    val updatedAt: Long,
)

data class NoteRecord(
    val id: String,
    val bookmarkId: String? = null,
    val bodyClusterId: String,
    val bodyId: String? = null,
    val anchor: ContentAnchor,
    val text: String,
    val createdAt: Long,
    val updatedAt: Long,
)

interface BookRepository {
    fun upsert(book: BookRecord)
    fun get(id: String): BookRecord?
    fun getCover(id: String): BookCoverRecord?
    fun list(limit: Int = 100, offset: Int = 0): List<BookRecord>
    fun delete(id: String): Boolean
}

interface BookBodyRepository {
    fun upsert(body: BookBodyRecord)
    fun get(id: String): BookBodyRecord?
    fun findByExactTextHash(exactTextHash: String, canonicalizationVersion: Int): BookBodyRecord?
}

interface BodyClusterRepository {
    fun upsert(cluster: BodyClusterRecord)
    fun get(id: String): BodyClusterRecord?
    fun findByRepresentativeBodyId(bodyId: String): BodyClusterRecord?
}

interface BookFileRepository {
    fun upsert(file: BookFileRecord)
    fun get(id: String): BookFileRecord?
    fun getLibraryFile(id: String): LibraryFileRecord?
    fun findByRawSha256(rawSha256: String): List<BookFileRecord>
    fun findByOriginalFilenameSizeAndModified(originalFilename: String, sizeBytes: Long, lastModifiedMillis: Long): List<BookFileRecord>
    fun findByOriginalFilenameSizeAndRawSha256(originalFilename: String, sizeBytes: Long, rawSha256: String): List<BookFileRecord>
    fun findPrimaryDuplicateTarget(bodyClusterId: String?, bodyId: String?, rawSha256: String): BookFileRecord?
    fun markDuplicateFiles(): Int
    fun listLibraryFiles(limit: Int = 100, offset: Int = 0): List<LibraryFileRecord>
    fun listRecentlyAddedLibraryFiles(sinceImportedAt: Long, limit: Int = 50): List<LibraryFileRecord>
    fun searchLibraryFiles(prefixes: List<String>, limit: Int = 100): List<LibraryFileRecord>
    fun list(limit: Int = 500, offset: Int = 0): List<BookFileRecord>
    fun listForBook(bookId: String): List<BookFileRecord>
    fun updateReadingStatus(id: String, status: BookReadingStatus): Boolean
    fun updateFavorite(id: String, favorite: Boolean): Boolean
    fun touchLastReadAt(id: String, lastReadAt: Long): Boolean
    fun delete(id: String): Boolean
}

interface ReadingStateRepository {
    fun upsert(state: ReadingStateRecord)
    fun get(id: String): ReadingStateRecord?
    fun getForBodyCluster(bodyClusterId: String): ReadingStateRecord?
    fun deleteForBodyCluster(bodyClusterId: String): Int
    fun delete(id: String): Boolean
}

interface BookmarkRepository {
    fun upsert(bookmark: BookmarkRecord)
    fun get(id: String): BookmarkRecord?
    fun listForBodyCluster(bodyClusterId: String): List<BookmarkRecord>
    fun delete(id: String): Boolean
}

interface NoteRepository {
    fun upsert(note: NoteRecord)
    fun get(id: String): NoteRecord?
    fun listForBookmark(bookmarkId: String): List<NoteRecord>
    fun listForBodyCluster(bodyClusterId: String): List<NoteRecord>
    fun delete(id: String): Boolean
}

interface LibraryDatabase {
    val books: BookRepository
    val bodies: BookBodyRepository
    val clusters: BodyClusterRepository
    val files: BookFileRepository
    val readingStates: ReadingStateRepository
    val bookmarks: BookmarkRepository
    val notes: NoteRepository

    fun <T> transaction(block: LibraryDatabase.() -> T): T
    fun close()
}
