package eu.kanade.tachiyomi.util

import android.app.Activity
import android.content.Context
import android.content.DialogInterface
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import co.touchlab.kermit.Logger
import com.bluelinelabs.conductor.Controller
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.seriesType
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.domain.manga.models.Manga
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.category.addtolibrary.SetCategoriesSheet
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaDetailsController
import eu.kanade.tachiyomi.ui.migration.MigrationFlags
import eu.kanade.tachiyomi.ui.migration.manga.process.MigrationProcessAdapter
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithTrackServiceTwoWay
import eu.kanade.tachiyomi.util.lang.asButton
import eu.kanade.tachiyomi.util.system.launchIO
import eu.kanade.tachiyomi.util.system.launchUI
import eu.kanade.tachiyomi.util.system.materialAlertDialog
import eu.kanade.tachiyomi.util.system.setCustomTitleAndMessage
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.system.withUIContext
import eu.kanade.tachiyomi.util.view.setAction
import eu.kanade.tachiyomi.util.view.setPositiveButton
import eu.kanade.tachiyomi.util.view.setTitle
import eu.kanade.tachiyomi.util.view.snack
import eu.kanade.tachiyomi.util.view.withFadeTransaction
import eu.kanade.tachiyomi.widget.TriStateCheckBox
import java.util.Date
import java.util.Locale
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import yokai.domain.category.interactor.GetCategories
import yokai.domain.category.interactor.SetMangaCategories
import yokai.domain.chapter.interactor.GetChapter
import yokai.domain.manga.interactor.GetManga
import yokai.domain.manga.interactor.UpdateManga
import yokai.domain.manga.models.MangaUpdate
import yokai.domain.track.interactor.InsertTrack
import yokai.i18n.MR
import yokai.util.lang.getString
import android.R as AR

fun Manga.isLocal() = source == LocalSource.ID

suspend fun Manga.shouldDownloadNewChapters(prefs: PreferencesHelper, getCategories: GetCategories = Injekt.get()): Boolean {
    if (!favorite) return false

    // Boolean to determine if user wants to automatically download new chapters.
    val downloadNewChapters = prefs.downloadNewChapters().get()
    if (!downloadNewChapters) return false

    val includedCategories = prefs.downloadNewChaptersInCategories().get().map(String::toInt)
    val excludedCategories = prefs.excludeCategoriesInDownloadNew().get().map(String::toInt)
    if (includedCategories.isEmpty() && excludedCategories.isEmpty()) return true

    // Get all categories, else default category (0)
    val categoriesForManga = this.id?.let { mangaId ->
        getCategories.awaitByMangaId(mangaId)
            .mapNotNull { it.id }
            .takeUnless { it.isEmpty() }
    } ?: listOf(0)

    if (categoriesForManga.any { it in excludedCategories }) return false

    // Included category not selected
    if (includedCategories.isEmpty()) return true

    return categoriesForManga.any { it in includedCategories }
}

fun Manga.moveCategories(activity: Activity, onMangaMoved: () -> Unit) {
    moveCategories(activity, false, onMangaMoved)
}

fun Manga.moveCategories(
    activity: Activity,
    addingToLibrary: Boolean,
    onMangaMoved: () -> Unit,
) {
    val getCategories: GetCategories = Injekt.get()
    // FIXME: Don't do blocking
    val categories = runBlocking { getCategories.await() }
    val categoriesForManga = runBlocking {
        this@moveCategories.id?.let { mangaId -> getCategories.awaitByMangaId(mangaId) }
            .orEmpty()
    }
    val ids = categoriesForManga.mapNotNull { it.id }.toTypedArray()
    SetCategoriesSheet(
        activity,
        this,
        categories.toMutableList(),
        ids,
        addingToLibrary,
    ) {
        onMangaMoved()
        if (addingToLibrary) {
            autoAddTrack(onMangaMoved)
        }
    }.show()
}

fun List<Manga>.moveCategories(
    activity: Activity,
    onMangaMoved: () -> Unit,
) {
    if (this.isEmpty()) return

    val getCategories: GetCategories = Injekt.get()
    // FIXME: Don't do blocking
    val categories = runBlocking { getCategories.await() }
    val mangaCategories = map { manga ->
        manga.id?.let { mangaId -> runBlocking { getCategories.awaitByMangaId(mangaId) } }.orEmpty()
    }
    val commonCategories = mangaCategories.reduce { set1, set2 -> set1.intersect(set2.toSet()).toMutableList() }.toSet()
    val mixedCategories = mangaCategories.flatten().distinct().subtract(commonCategories).toMutableList()
    SetCategoriesSheet(
        activity,
        this,
        categories.toMutableList(),
        categories.map {
            when (it) {
                in commonCategories -> TriStateCheckBox.State.CHECKED
                in mixedCategories -> TriStateCheckBox.State.IGNORE
                else -> TriStateCheckBox.State.UNCHECKED
            }
        }.toTypedArray(),
        false,
    ) {
        onMangaMoved()
    }.show()
}

fun Manga.addOrRemoveToFavorites(
    preferences: PreferencesHelper,
    view: View,
    activity: Activity,
    sourceManager: SourceManager,
    controller: Controller,
    checkForDupes: Boolean = true,
    onMangaAdded: (Pair<Long, Boolean>?) -> Unit,
    onMangaMoved: () -> Unit,
    onMangaDeleted: () -> Unit,
    getCategories: GetCategories = Injekt.get(),
    setMangaCategories: SetMangaCategories = Injekt.get(),
    getManga: GetManga = Injekt.get(),
    updateManga: UpdateManga = Injekt.get(),
): Snackbar? {
    if (!favorite) {
        if (checkForDupes) {
            val duplicateManga = runBlocking(Dispatchers.IO) {
                getManga.awaitDuplicateFavorite(
                    this@addOrRemoveToFavorites.title,
                    this@addOrRemoveToFavorites.source,
                )
            }
            if (duplicateManga != null) {
                showAddDuplicateDialog(
                    this,
                    duplicateManga,
                    activity,
                    sourceManager,
                    controller,
                    addManga = {
                        addOrRemoveToFavorites(
                            preferences,
                            view,
                            activity,
                            sourceManager,
                            controller,
                            false,
                            onMangaAdded,
                            onMangaMoved,
                            onMangaDeleted,
                        )
                    },
                    migrateManga = { source, faved ->
                        onMangaAdded(source to faved)
                    },
                )
                return null
            }
        }

        // FIXME: Don't do blocking
        val categories = runBlocking { getCategories.await() }
        val defaultCategoryId = preferences.defaultCategory().get()
        val defaultCategory = categories.find { it.id == defaultCategoryId }
        val lastUsedCategories = Category.lastCategoriesAddedTo.mapNotNull { catId ->
            categories.find { it.id == catId }
        }
        when {
            defaultCategory != null -> {
                favorite = true
                date_added = Date().time
                autoAddTrack(onMangaMoved)
                // FIXME: Don't do blocking
                runBlocking {
                    updateManga.await(
                        MangaUpdate(
                            id = this@addOrRemoveToFavorites.id!!,
                            favorite = true,
                            dateAdded = this@addOrRemoveToFavorites.date_added,
                        )
                    )
                    setMangaCategories.await(this@addOrRemoveToFavorites.id!!, listOf(defaultCategory.id!!.toLong()))
                }
                (activity as? MainActivity)?.showNotificationPermissionPrompt()
                onMangaMoved()
                return view.snack(activity.getString(MR.strings.added_to_, defaultCategory.name)) {
                    setAction(MR.strings.change) {
                        moveCategories(activity, onMangaMoved)
                    }
                }
            }
            defaultCategoryId == -2 && (
                lastUsedCategories.isNotEmpty() ||
                    Category.lastCategoriesAddedTo.firstOrNull() == 0
                ) -> { // last used category(s)
                favorite = true
                date_added = Date().time
                autoAddTrack(onMangaMoved)
                // FIXME: Don't do blocking
                runBlocking {
                    updateManga.await(
                        MangaUpdate(
                            id = this@addOrRemoveToFavorites.id!!,
                            favorite = true,
                            dateAdded = this@addOrRemoveToFavorites.date_added,
                        )
                    )
                    setMangaCategories.await(this@addOrRemoveToFavorites.id!!, lastUsedCategories.map { it.id!!.toLong() })
                }
                (activity as? MainActivity)?.showNotificationPermissionPrompt()
                onMangaMoved()
                return view.snack(
                    activity.getString(
                        MR.strings.added_to_,
                        when (lastUsedCategories.size) {
                            0 -> activity.getString(MR.strings.default_category).lowercase(Locale.ROOT)
                            1 -> lastUsedCategories.firstOrNull()?.name ?: ""
                            else -> activity.getString(
                                MR.plurals.category_plural,
                                lastUsedCategories.size,
                                lastUsedCategories.size,
                            )
                        },
                    ),
                ) {
                    setAction(MR.strings.change) {
                        moveCategories(activity, onMangaMoved)
                    }
                }
            }
            defaultCategoryId == 0 || categories.isEmpty() -> { // 'Default' or no category
                favorite = true
                date_added = Date().time
                autoAddTrack(onMangaMoved)
                // FIXME: Don't do blocking
                runBlocking {
                    updateManga.await(
                        MangaUpdate(
                            id = this@addOrRemoveToFavorites.id!!,
                            favorite = true,
                            dateAdded = this@addOrRemoveToFavorites.date_added,
                        )
                    )
                    setMangaCategories.await(this@addOrRemoveToFavorites.id!!, emptyList())
                }
                onMangaMoved()
                (activity as? MainActivity)?.showNotificationPermissionPrompt()
                return if (categories.isNotEmpty()) {
                    view.snack(activity.getString(MR.strings.added_to_, activity.getString(MR.strings.default_value))) {
                        setAction(MR.strings.change) {
                            moveCategories(activity, onMangaMoved)
                        }
                    }
                } else {
                    view.snack(MR.strings.added_to_library)
                }
            }
            else -> { // Always ask
                showSetCategoriesSheet(activity, categories, onMangaAdded, onMangaMoved)
            }
        }
    } else {
        val lastAddedDate = date_added
        favorite = false
        date_added = 0
        // FIXME: Don't do blocking
        runBlocking {
            updateManga.await(
                MangaUpdate(
                    id = this@addOrRemoveToFavorites.id!!,
                    favorite = false,
                    dateAdded = 0,
                )
            )
        }
        onMangaMoved()
        return view.snack(view.context.getString(MR.strings.removed_from_library), Snackbar.LENGTH_INDEFINITE) {
            setAction(MR.strings.undo) {
                favorite = true
                date_added = lastAddedDate
                // FIXME: Don't do blocking
                runBlocking {
                    updateManga.await(
                        MangaUpdate(
                            id = this@addOrRemoveToFavorites.id!!,
                            favorite = true,
                            dateAdded = lastAddedDate,
                        )
                    )
                }
                onMangaMoved()
            }
            addCallback(
                object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
                    override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
                        super.onDismissed(transientBottomBar, event)
                        if (!favorite) {
                            onMangaDeleted()
                        }
                    }
                },
            )
        }
    }
    return null
}

private fun Manga.showSetCategoriesSheet(
    activity: Activity,
    categories: List<Category>,
    onMangaAdded: (Pair<Long, Boolean>?) -> Unit,
    onMangaMoved: () -> Unit,
    getCategories: GetCategories = Injekt.get(),
) {
    // FIXME: Don't do blocking
    val categoriesForManga = runBlocking { getCategories.awaitByMangaId(this@showSetCategoriesSheet.id!!) }
    val ids = categoriesForManga.mapNotNull { it.id }.toTypedArray()

    SetCategoriesSheet(
        activity,
        this,
        categories.toMutableList(),
        ids,
        true,
    ) {
        (activity as? MainActivity)?.showNotificationPermissionPrompt()
        onMangaAdded(null)
        autoAddTrack(onMangaMoved)
    }.show()
}

private fun showAddDuplicateDialog(
    newManga: Manga,
    libraryManga: Manga,
    activity: Activity,
    sourceManager: SourceManager,
    controller: Controller,
    addManga: () -> Unit,
    migrateManga: (Long, Boolean) -> Unit,
) {
    val source = sourceManager.getOrStub(libraryManga.source)

    val titles by lazy { MigrationFlags.titles(activity, libraryManga) }
    fun migrateManga(mDialog: DialogInterface, replace: Boolean) {
        val listView = (mDialog as AlertDialog).listView
        val enabled = titles.indices.map { listView.isItemChecked(it) }.toTypedArray()
        val flags = MigrationFlags.getFlagsFromPositions(enabled, libraryManga)
        val enhancedServices by lazy { Injekt.get<TrackManager>().services.filterIsInstance<EnhancedTrackService>() }
        launchUI {
            MigrationProcessAdapter.migrateMangaInternal(
                flags,
                enhancedServices,
                Injekt.get(),
                Injekt.get(),
                source,
                sourceManager.getOrStub(newManga.source),
                libraryManga,
                newManga,
                replace,
            )
        }
        migrateManga(libraryManga.source, !replace)
    }

    activity.materialAlertDialog().apply {
        setCustomTitleAndMessage(0, activity.getString(MR.strings.confirm_manga_add_duplicate, source.name))
        setItems(
            arrayOf(
                activity.getString(MR.strings.show_, libraryManga.seriesType(activity, sourceManager)).asButton(activity),
                activity.getString(MR.strings.add_to_library).asButton(activity),
                activity.getString(MR.strings.migrate).asButton(activity, !newManga.initialized),
            ),
        ) { dialog, i ->
            when (i) {
                0 -> controller.router.pushController(
                    MangaDetailsController(libraryManga)
                        .withFadeTransaction(),
                )
                1 -> addManga()
                2 -> {
                    if (!newManga.initialized) {
                        activity.toast(MR.strings.must_view_details_before_migration, Toast.LENGTH_LONG)
                        return@setItems
                    }
                    activity.materialAlertDialog().apply {
                        setTitle(MR.strings.migration)
                        setMultiChoiceItems(
                            titles,
                            titles.map { true }.toBooleanArray(),
                            null,
                        )
                        setPositiveButton(MR.strings.migrate) { mDialog, _ ->
                            migrateManga(mDialog, true)
                        }
                        setNegativeButton(AR.string.copy) { mDialog, _ ->
                            migrateManga(mDialog, false)
                        }
                        setNeutralButton(AR.string.cancel, null)
                        setCancelable(true)
                    }.show()
                }
                else -> {}
            }
            dialog.dismiss()
        }
        setNegativeButton(activity.getString(AR.string.cancel)) { _, _ -> }
        setCancelable(true)
    }.create().apply {
        setOnShowListener {
            if (!newManga.initialized) {
                val listView = (it as AlertDialog).listView
                val view = listView.getChildAt(2)
                view?.setOnClickListener {
                    if (!newManga.initialized) {
                        activity.toast(
                            MR.strings.must_view_details_before_migration,
                            Toast.LENGTH_LONG,
                        )
                    }
                }
            }
        }
    }.show()
}

fun Manga.autoAddTrack(onMangaMoved: () -> Unit) {
    val loggedServices = Injekt.get<TrackManager>().services.filter { it.isLogged }
    val source = Injekt.get<SourceManager>().getOrStub(this.source)
    val getChapter = Injekt.get<GetChapter>()
    val insertTrack = Injekt.get<InsertTrack>()
    loggedServices
        .filterIsInstance<EnhancedTrackService>()
        .filter { it.accept(source) }
        .forEach { service ->
            launchIO {
                try {
                    service.match(this@autoAddTrack)?.let { track ->
                        val mangaId = this@autoAddTrack.id!!
                        track.manga_id = mangaId
                        (service as TrackService).bind(track)
                        insertTrack.await(track)

                        syncChaptersWithTrackServiceTwoWay(
                            getChapter.awaitAll(mangaId, false),
                            track,
                            service as TrackService
                        )
                        withUIContext {
                            onMangaMoved()
                        }
                    }
                } catch (e: Exception) {
                    Logger.w(e) { "Could not match manga: ${this@autoAddTrack.title} with service $service" }
                }
            }
        }
}

fun Context.mapStatus(status: Int): String {
    return getString(
        when (status) {
            SManga.ONGOING -> MR.strings.ongoing
            SManga.COMPLETED -> MR.strings.completed
            SManga.LICENSED -> MR.strings.licensed
            SManga.PUBLISHING_FINISHED -> MR.strings.publishing_finished
            SManga.CANCELLED -> MR.strings.cancelled
            SManga.ON_HIATUS -> MR.strings.on_hiatus
            else -> MR.strings.unknown
        },
    )
}

fun Context.mapSeriesType(seriesType: Int): String {
    return getString(
        when (seriesType) {
            Manga.TYPE_MANGA -> MR.strings.manga
            Manga.TYPE_MANHWA -> MR.strings.manhwa
            Manga.TYPE_MANHUA -> MR.strings.manhua
            Manga.TYPE_COMIC -> MR.strings.comic
            Manga.TYPE_WEBTOON -> MR.strings.webtoon
            else -> MR.strings.unknown
        },
    )
}
