package com.aletheia.app.ui.qbooks

import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.aletheia.app.AletheiaApplication
import com.aletheia.app.R
import com.aletheia.app.data.QBooksUrlPolicy
import com.aletheia.app.data.Result
import com.aletheia.app.data.SettingsRepository
import com.aletheia.app.databinding.FragmentQbooksLibraryBinding
import com.aletheia.app.model.Book
import com.aletheia.app.model.QBooksBook
import com.aletheia.app.ui.main.MainActivity
import com.aletheia.app.ui.reader.ReaderActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.io.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class QBooksLibraryFragment : Fragment() {
    private var _binding: FragmentQbooksLibraryBinding? = null
    private val binding get() = _binding!!
    private val app by lazy { requireActivity().application as AletheiaApplication }
    private lateinit var booksAdapter: QBooksCatalogAdapter

    private var currentPage = 0
    private var nextPageUrl: String? = null
    private var isConfigured = false
    private var isLoading = false
    private var hasMoreBooks = true
    private var searchJob: Job? = null
    private var loadJob: Job? = null
    private val coverJobs = mutableListOf<Job>()
    private val activeDownloads = mutableSetOf<String>()
    private var requestGeneration = 0

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, state: Bundle?): View {
        _binding = FragmentQbooksLibraryBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, state: Bundle?) {
        super.onViewCreated(view, state)
        booksAdapter = QBooksCatalogAdapter(::openBookDetails, ::downloadBook)
        binding.qbooksRecycler.layoutManager = LinearLayoutManager(requireContext())
        binding.qbooksRecycler.adapter = booksAdapter
        binding.qbooksRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                if (dy <= 0 || !isConfigured || !hasMoreBooks || isLoading) return
                val manager = recyclerView.layoutManager as? LinearLayoutManager ?: return
                if (manager.findLastVisibleItemPosition() >= booksAdapter.itemCount - 4) loadBooks(reset = false)
            }
        })

        binding.searchInput.doAfterTextChanged {
            searchJob?.cancel()
            searchJob = viewLifecycleOwner.lifecycleScope.launch {
                delay(350)
                loadBooks(reset = true)
            }
        }
        binding.searchInput.setOnEditorActionListener { _, _, _ ->
            loadBooks(reset = true)
            true
        }

        binding.openSettingsButton.setOnClickListener { openProfile() }
        binding.errorSettingsButton.setOnClickListener { openProfile() }
        binding.catalogSettingsButton.setOnClickListener { openProfile() }
        binding.retryButton.setOnClickListener { loadBooks(reset = true, forceRefresh = true) }
        binding.refreshCatalogButton.setOnClickListener { loadBooks(reset = true, forceRefresh = true) }
        binding.swipeRefresh.setOnRefreshListener { loadBooks(reset = true, forceRefresh = true) }

        val genreChips = mapOf(
            binding.genreFantasy to R.string.genre_fantasy,
            binding.genreDetective to R.string.genre_detective,
            binding.genreRomance to R.string.genre_romance,
            binding.genreFantasyMagic to R.string.genre_fantasy_magic,
            binding.genreHistory to R.string.genre_history,
            binding.genreScience to R.string.genre_science
        )
        genreChips.forEach { (chip, title) ->
            chip.setOnClickListener { binding.searchInput.setText(getString(title)) }
        }
    }

    override fun onResume() {
        super.onResume()
        initializeCatalog()
    }

    override fun onDestroyView() {
        searchJob?.cancel()
        loadJob?.cancel()
        coverJobs.forEach { it.cancel() }
        coverJobs.clear()
        _binding = null
        super.onDestroyView()
    }

    private fun initializeCatalog() {
        var url = app.settingsRepository.getCatalogUrl()
        var validation = QBooksUrlPolicy.validate(url)
        if (!validation.isAllowed) {
            url = SettingsRepository.DEFAULT_CATALOG_URL
            app.settingsRepository.setString(SettingsRepository.KEY_QBOOKS_URL, url)
            validation = QBooksUrlPolicy.validate(url)
        }
        if (!validation.isAllowed) {
            isConfigured = false
            renderError(getString(R.string.status_qbooks_invalid_url))
            return
        }
        app.qBooksService.configure(url, null, null)
        isConfigured = true
        binding.notConfiguredCard.visibility = View.GONE
        binding.errorCard.visibility = View.GONE
        binding.searchCard.visibility = View.VISIBLE
        binding.qbooksActionsRow.visibility = View.VISIBLE
        binding.swipeRefresh.visibility = View.VISIBLE
        if (booksAdapter.itemCount == 0) loadBooks(reset = true)
    }

    private fun loadBooks(reset: Boolean, forceRefresh: Boolean = false) {
        if (!isConfigured || (!reset && (isLoading || !hasMoreBooks))) {
            binding.swipeRefresh.isRefreshing = false
            return
        }
        if (reset) {
            requestGeneration++
            loadJob?.cancel()
            coverJobs.forEach { it.cancel() }
            coverJobs.clear()
            isLoading = false
            currentPage = 0
            nextPageUrl = null
            hasMoreBooks = true
            binding.swipeRefresh.isRefreshing = true
        }

        val generation = requestGeneration
        val pageToLoad = currentPage
        val pageUrl = if (reset) null else nextPageUrl
        val query = currentSearchQuery()
        isLoading = true
        setOverview(
            getString(
                if (reset) R.string.qbooks_catalog_overview_loading_title
                else R.string.qbooks_catalog_overview_loading_more_title
            ),
            getString(if (reset) R.string.qbooks_catalog_loading else R.string.qbooks_catalog_loading_more),
            query.takeIf(String::isNotBlank)?.let { getString(R.string.qbooks_catalog_query_meta, it) }
        )

        loadJob = viewLifecycleOwner.lifecycleScope.launch {
            val cached = app.qBooksService.getCachedCatalogPage(query, pageToLoad, PAGE_SIZE, pageUrl)
            if (_binding == null || generation != requestGeneration) return@launch
            val hasCachedPage = cached != null
            if (cached != null) {
                applyCatalogPage(cached.page, reset, pageToLoad, generation)
                binding.swipeRefresh.isRefreshing = false
                if (!forceRefresh && cached.isFresh(SEARCH_CACHE_MAX_AGE_MS)) {
                    isLoading = false
                    return@launch
                }
            }

            val result = withContext(Dispatchers.IO) {
                app.qBooksService.getCatalogPage(query, pageToLoad, PAGE_SIZE, pageUrl)
            }
            if (_binding == null || generation != requestGeneration) return@launch

            when (result) {
                is Result.Success -> {
                    applyCatalogPage(result.value, reset, pageToLoad, generation)
                }
                is Result.Failure -> if (hasCachedPage) {
                    renderLoaded()
                    if (forceRefresh) showMessage(result.message)
                } else if (reset) renderError(result.message) else {
                    renderLoaded()
                    showMessage(result.message)
                }
            }
            binding.swipeRefresh.isRefreshing = false
            isLoading = false
        }
    }

    private fun applyCatalogPage(
        page: com.aletheia.app.model.CatalogPage,
        reset: Boolean,
        pageIndex: Int,
        generation: Int
    ) {
        if (reset) booksAdapter.submitList(page.books) else booksAdapter.appendOrUpdate(page.books)
        loadMetadataAndCovers(page.books, generation)
        nextPageUrl = page.nextPageUrl
        hasMoreBooks = !page.nextPageUrl.isNullOrBlank()
        currentPage = pageIndex + 1
        renderLoaded()
    }

    private fun renderLoaded() {
        binding.errorCard.visibility = View.GONE
        binding.notConfiguredCard.visibility = View.GONE
        binding.searchCard.visibility = View.VISIBLE
        binding.qbooksActionsRow.visibility = View.VISIBLE
        binding.swipeRefresh.visibility = View.VISIBLE
        val count = booksAdapter.itemCount
        val query = currentSearchQuery()
        binding.emptyResultsCard.visibility = if (count == 0) View.VISIBLE else View.GONE
        binding.emptyResultsDetailText.text = if (query.isBlank()) {
            getString(R.string.status_empty_qbooks_subtitle)
        } else {
            getString(R.string.qbooks_catalog_empty_query, query)
        }
        val detail = if (query.isBlank()) {
            resources.getQuantityString(R.plurals.qbooks_catalog_summary, count, count)
        } else {
            resources.getQuantityString(R.plurals.qbooks_catalog_search_summary, count, count, query)
        }
        val hint = if (hasMoreBooks) R.string.qbooks_catalog_more_hint else R.string.qbooks_catalog_end_hint
        setOverview(
            getString(R.string.qbooks_catalog_overview_title),
            detail,
            if (count == 0) null else getString(hint)
        )
    }

    private fun renderError(message: String) {
        booksAdapter.submitList(emptyList())
        binding.searchCard.visibility = if (isConfigured) View.VISIBLE else View.GONE
        binding.qbooksActionsRow.visibility = View.GONE
        binding.swipeRefresh.visibility = View.GONE
        binding.notConfiguredCard.visibility = if (isConfigured) View.GONE else View.VISIBLE
        binding.errorCard.visibility = if (isConfigured) View.VISIBLE else View.GONE
        binding.errorText.text = message
        binding.catalogOverviewCard.visibility = View.GONE
        binding.emptyResultsCard.visibility = View.GONE
        binding.swipeRefresh.isRefreshing = false
        isLoading = false
    }

    private fun setOverview(title: String, detail: String, meta: String?) {
        binding.catalogOverviewCard.visibility = View.VISIBLE
        binding.catalogOverviewTitleText.text = title
        binding.catalogStatusText.text = detail
        binding.catalogOverviewMetaText.text = meta.orEmpty()
        binding.catalogOverviewMetaText.visibility = if (meta.isNullOrBlank()) View.GONE else View.VISIBLE
    }

    private fun loadMetadataAndCovers(books: List<QBooksBook>, generation: Int) {
        books.forEach { book ->
            coverJobs += viewLifecycleOwner.lifecycleScope.launch {
                var enriched = app.qBooksService.enrichBookMetadata(book)
                if (_binding != null && generation == requestGeneration && enriched != book) {
                    booksAdapter.updateBook(enriched)
                }
                val cover = if (enriched.coverImage == null && !enriched.coverUrl.isNullOrBlank()) {
                    app.qBooksService.fetchCoverImage(enriched)
                } else {
                    enriched.coverImage
                }
                if (cover != null && _binding != null && generation == requestGeneration) {
                    enriched = enriched.copy(coverImage = cover)
                    booksAdapter.updateBook(enriched)
                }
            }
        }
    }

    private fun downloadBook(book: QBooksBook) {
        if (!activeDownloads.add(book.id)) return
        setDownloadStatus(getString(R.string.qbooks_download_status, book.title), true)
        viewLifecycleOwner.lifecycleScope.launch {
            val result = withContext(Dispatchers.IO) {
                runCatching {
                    app.bookRepository.getBookByRemoteId(book.id)?.let { return@runCatching it }
                    val progress: (Int) -> Unit = { value ->
                        view?.post { setDownloadStatus(getString(R.string.qbooks_download_progress, value.toDouble()), true) }
                    }
                    val temp = when (val download = app.qBooksService.downloadBook(
                        book,
                        app.cacheDir,
                        progress
                    )) {
                        is Result.Success -> download.value
                        is Result.Failure -> error(download.message)
                    }
                    try {
                        app.bookRepository.importDownloadedBook(
                            temp,
                            buildDownloadFileName(book),
                            book.id,
                            book.coverImage
                        )
                    } finally {
                        temp.takeIf(File::exists)?.delete()
                    }
                }
            }
            activeDownloads.remove(book.id)
            if (_binding == null) return@launch
            result.onSuccess { downloaded ->
                setDownloadStatus(getString(R.string.qbooks_download_complete), false)
                MaterialAlertDialogBuilder(requireContext())
                    .setTitle(R.string.dialog_ready_title)
                    .setMessage(getString(R.string.dialog_book_added_message, book.title))
                    .setPositiveButton(R.string.action_read) { _, _ -> openDownloadedBook(downloaded) }
                    .setNegativeButton(R.string.action_ok, null)
                    .show()
            }.onFailure {
                setDownloadStatus("", false)
                showMessage(getString(R.string.dialog_download_book_failed, it.message ?: it.javaClass.simpleName))
            }
        }
    }

    private fun openBookDetails(book: QBooksBook) {
        startActivity(BookDetailActivity.createIntent(requireContext(), book))
    }

    private fun openDownloadedBook(book: Book) {
        startActivity(Intent(requireContext(), ReaderActivity::class.java).putExtra(ReaderActivity.EXTRA_BOOK_ID, book.id))
    }

    private fun openProfile() {
        (activity as? MainActivity)?.selectTab(R.id.nav_profile)
    }

    private fun currentSearchQuery(): String = binding.searchInput.text?.toString().orEmpty().trim()

    private fun setDownloadStatus(status: String, inProgress: Boolean) {
        binding.downloadStatusCard.visibility = if (status.isBlank()) View.GONE else View.VISIBLE
        binding.downloadStatusText.text = status
        binding.downloadStatusProgress.visibility = if (inProgress) View.VISIBLE else View.GONE
    }

    private fun showMessage(message: String) {
        MaterialAlertDialogBuilder(requireContext())
            .setTitle(R.string.dialog_error_title)
            .setMessage(message)
            .setPositiveButton(R.string.action_ok, null)
            .show()
    }

    private fun buildDownloadFileName(book: QBooksBook): String {
        val safeTitle = book.title.replace(Regex("[\\\\/:*?\"<>|]"), "_")
        return "$safeTitle.${book.format.lowercase()}"
    }

    companion object {
        private const val PAGE_SIZE = 20
        private const val SEARCH_CACHE_MAX_AGE_MS = 60 * 60 * 1000L
        fun newInstance() = QBooksLibraryFragment()
    }
}
