package com.pixelized.biblib.repository.book


import android.util.Log
import androidx.paging.DataSource
import com.pixelized.biblib.database.BibLibDatabase
import com.pixelized.biblib.database.crossref.BookAuthorCrossRef
import com.pixelized.biblib.database.crossref.BookGenreCrossRef
import com.pixelized.biblib.database.data.*
import com.pixelized.biblib.database.factory.*
import com.pixelized.biblib.model.book.*
import com.pixelized.biblib.network.client.IBibLibClient
import com.pixelized.biblib.network.data.response.BookListResponse
import com.pixelized.biblib.network.data.response.GenreListResponse
import com.pixelized.biblib.network.data.response.SeriesListResponse
import com.pixelized.biblib.network.factory.BookFactory
import com.pixelized.biblib.network.factory.GenreFactory
import com.pixelized.biblib.network.factory.SeriesFactory
import com.pixelized.biblib.repository.apiCache.IAPICacheRepository
import com.pixelized.biblib.utils.exception.BookFetchException
import com.pixelized.biblib.utils.exception.NewBookFetchException
import kotlinx.coroutines.*

class BookRepository constructor(
    private val client: IBibLibClient,
    private val cache: IAPICacheRepository,
    private val database: BibLibDatabase,
    private val bookFactory: BookFactory,
    private val seriesFactory: SeriesFactory,
    private val genresFactory: GenreFactory,
) : IBookRepository {

    override fun getAll(): List<Book> =
        database.bookDao().getAll().map { it.toBook() }

    override fun getBook(id: Int): Book =
        database.bookDao().getBook(id = id).toBook()

    override fun getBookCount(): Int =
        database.bookDao().count()

    override fun getBooksSource(): DataSource.Factory<Int, Book> =
        database.bookDao().getBook().map { it.toBook() }

    override fun getNewsSource(): DataSource.Factory<Int, Book> =
        database.bookDao().getNews().map { it.toBook() }

    override suspend fun getBookDetail(id: Int): Book {
        val response = client.service.detail(id)
        return bookFactory.fromDetailResponseToBook(response)
    }

    override suspend fun update() = withContext(Dispatchers.IO) {
        if (fetchNewBooks() || getBookCount() <= 0) {
            fetchAllBooks()
        }
    }

    /**
     * This method will fetch all new book from [client] and save them into the [cache]
     * @return return true if the data have been loaded and book need to be updated.
     */
    private suspend fun fetchNewBooks(): Boolean {
        val cached = cache.new
        val response: BookListResponse = client.service.new()

        return when {
            response.isError -> {
                Log.e("loadNewBooks", response.message ?: "")
                throw NewBookFetchException(response.message)
            }
            cached != response -> {
                cache.new = response
                true
            }
            else -> {
                false
            }
        }
    }


    /**
     * This method will fetch all book from [client] and save them into the [repository]
     * @return this method will return true the books have been correctly fetch, otherwise false.
     */
    private suspend fun fetchAllBooks(): Boolean = runBlocking {
        val seriesAsync = async { client.service.series() }
        val listAsync = async { client.service.list() }
        val genreAsync = async { client.service.genre() }
        val seriesResponse: SeriesListResponse = seriesAsync.await()
        val bookResponse: BookListResponse = listAsync.await()
        val genreResponse: GenreListResponse = genreAsync.await()

        when {
            bookResponse.isError -> {
                Log.e("loadAllBooks", bookResponse.message ?: "")
                throw BookFetchException(bookResponse.message)
            }
            seriesResponse.isError -> {
                Log.e("loadAllBooks", seriesResponse.message ?: "")
                throw BookFetchException(seriesResponse.message)
            }
            genreResponse.isError -> {
                Log.e("loadAllBooks", genreResponse.message ?: "")
                throw BookFetchException(genreResponse.message)
            }
            else -> {
                // prepare a list of new book ids.
                val newIds = cache.new?.data?.map { it.book_id } ?: listOf()
                // prepare a hash of series by books ids.
                val series = seriesFactory.fromListResponseToSeriesHash(response = seriesResponse)
                // prepare a hash of genre by books ids.
                val genres = genresFactory.fromListResponseToGenreHash(response = genreResponse)
                // parse books.
                val books = bookResponse.data?.map { dto ->
                    val isNew = newIds.contains(dto.book_id)
                    val index = newIds.indexOf(dto.book_id)
                    return@map bookFactory.fromListResponseToBook(
                        response = dto,
                        seriesHash = series,
                        genreHash = genres,
                        isNew = isNew,
                        newOrder = index
                    )
                }
                // check if data is valid
                if (books.isNullOrEmpty()) {
                    Log.e("loadAllBooks", "Book list is empty")
                    throw BookFetchException("Book list is empty")
                }
                // update the database.
                save(books)
                true
            }
        }
    }

    private fun save(data: List<Book>) {
        val authors = mutableSetOf<AuthorDbo>()
        val genres = mutableSetOf<GenreDbo>()
        val series = mutableSetOf<SeriesDbo>()
        val languages = mutableSetOf<LanguageDbo>()
        val books = mutableListOf<BookDbo>()

        val bookAuthorCrossRef = mutableListOf<BookAuthorCrossRef>()
        val bookGenreCrossRef = mutableListOf<BookGenreCrossRef>()

        data.forEach { book ->
            book.author.forEach {
                authors.add(it.toDbo())
                bookAuthorCrossRef.add(BookAuthorCrossRef(bookId = book.id, it.id))
            }
            book.genre?.forEach {
                genres.add(it.toDbo())
                bookGenreCrossRef.add(BookGenreCrossRef(bookId = book.id, it.id))
            }
            book.language?.let {
                languages.add(it.toDbo())
            }
            book.series?.let {
                series.add(it.toDbo(it.id))
            }
            books.add(book.toDbo())
        }

        database.authorDao().insert(*authors.toTypedArray())
        database.genreDao().insert(*genres.toTypedArray())
        database.languageDao().insert(*languages.toTypedArray())
        database.seriesDao().insert(*series.toTypedArray())
        database.bookDao().insert(*books.toTypedArray())

        database.crossRefDao().insert(*bookAuthorCrossRef.toTypedArray())
        database.crossRefDao().insert(*bookGenreCrossRef.toTypedArray())
    }
}