package com.drivervault.phoneapp.car

import android.os.Handler
import android.os.Looper
import android.text.SpannableString
import android.text.Spanned
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.model.Action
import androidx.car.app.model.ActionStrip
import androidx.car.app.model.CarColor
import androidx.car.app.model.ForegroundCarColorSpan
import androidx.car.app.model.ListTemplate
import androidx.car.app.model.MessageTemplate
import androidx.car.app.model.Template
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import java.util.concurrent.Executors

/**
 * A car screen that shows something read from the API Server: it loads once the
 * host starts it, renders a loading template while the request is out, and turns
 * a failure into a sentence with a Refresh beside it rather than an empty list.
 *
 * Everything the head unit shows is read-only. A screen you cannot type into is
 * a poor place to edit a car, and a driver is a poor person to ask — the phone
 * keeps every write.
 */
abstract class VaultScreen<T>(carContext: CarContext) : Screen(carContext) {

    protected val strings: CarStrings = CarStrings.of(carContext)
    protected val format: CarFormat = CarFormat(
        strings,
        VaultStore.locale(carContext),
        VaultStore.dateFormat(carContext),
    )

    private var data: T? = null
    private var failure: String? = null
    private var loading = false

    init {
        lifecycle.addObserver(object : DefaultLifecycleObserver {
            override fun onStart(owner: LifecycleOwner) {
                if (data == null && failure == null) load()
            }
        })
    }

    /** The screen's own title, which is also what the loading state is titled. */
    protected abstract val title: String

    /** True for the screen the host opens first: it gets the app icon, not a back arrow. */
    protected open val isRoot: Boolean get() = false

    /** Runs off the main thread. Everything a template needs, in one go. */
    protected abstract fun read(api: VaultApi): T

    /** Renders what [read] returned. */
    protected abstract fun template(data: T): Template

    final override fun onGetTemplate(): Template {
        val current = data
        val message = failure
        return when {
            loading || (current == null && message == null) ->
                ListTemplate.Builder()
                    .setTitle(title)
                    .setHeaderAction(headerAction)
                    .setLoading(true)
                    .build()
            message != null -> MessageTemplate.Builder(message)
                .setTitle(title)
                .setHeaderAction(headerAction)
                .addAction(refreshAction)
                .build()
            else -> template(current!!)
        }
    }

    /** Reads again, from the top: the same thing pulling to refresh does on the phone. */
    protected fun load() {
        val endpoint = VaultStore.endpoint(carContext)
        if (endpoint == null) {
            // Nothing signed in on this phone yet, so there is nothing to read
            // with — and no way to fix it from here.
            data = null
            failure = strings.t("carApp.signIn")
            loading = false
            invalidate()
            return
        }
        loading = true
        failure = null
        invalidate()
        IO.execute {
            val result = runCatching { read(VaultApi(endpoint)) }
            MAIN.post {
                if (lifecycle.currentState == Lifecycle.State.DESTROYED) return@post
                loading = false
                result
                    .onSuccess { data = it; failure = null }
                    .onFailure { data = null; failure = describe(it) }
                invalidate()
            }
        }
    }

    protected val headerAction: Action get() = if (isRoot) Action.APP_ICON else Action.BACK

    protected val refreshAction: Action
        get() = Action.Builder()
            .setTitle(strings.t("carApp.refresh"))
            .setOnClickListener { load() }
            .build()

    protected val refreshStrip: ActionStrip
        get() = ActionStrip.Builder().addAction(refreshAction).build()

    /**
     * How many rows this host will show. A head unit takes a handful and drops
     * the rest, so what is shown has to be the part worth showing.
     */
    protected fun listLimit(): Int =
        carContext.getCarService(ConstraintManager::class.java)
            .getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)

    /** A status sentence in its badge colour — the head unit's version of the pill. */
    protected fun tinted(status: CarStatus): CharSequence {
        val color = when (status.key) {
            StatusKey.OVERDUE -> CarColor.RED
            StatusKey.SOON -> CarColor.YELLOW
            StatusKey.OK -> CarColor.GREEN
            StatusKey.UNKNOWN -> null
        } ?: return status.label
        return SpannableString(status.label).apply {
            setSpan(
                ForegroundCarColorSpan.create(color),
                0,
                length,
                Spanned.SPAN_INCLUSIVE_EXCLUSIVE,
            )
        }
    }

    private fun describe(error: Throwable): String = when (error) {
        // The token the phone holds was refused. Only the phone can mint another.
        is VaultUnauthorized -> strings.t("carApp.expired")
        else -> strings.t("carApp.unreachable")
    }

    private companion object {
        val IO = Executors.newSingleThreadExecutor()
        val MAIN = Handler(Looper.getMainLooper())
    }
}
