package com.drivervault.phoneapp.car

import org.json.JSONArray
import org.json.JSONObject
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL

/** The server rejected the token. Only the phone can mint a new one. */
class VaultUnauthorized : IOException("unauthorized")

/** A car, as much of it as a head unit has room for. Mirrors models.dart's Car. */
data class VaultCar(
    val id: String,
    val name: String,
    val subtitle: String,
    val registration: String,
    val currentKm: Int,
    val access: String,
) {
    val isOwner: Boolean get() = access == "owner"
    val isReadOnly: Boolean get() = access == "read"
}

/** A service record's two derived due fields, plus the day it was done. */
data class VaultService(
    val date: String,
    val nextServiceDate: String,
    val nextServiceKm: Int?,
)

/**
 * One thing that wants doing — stored or derived by the server from a document's
 * expiry or the service schedule. [status] and the two "left" figures are the
 * server's; nothing here re-derives them.
 */
data class VaultReminder(
    val title: String,
    val status: String,
    val daysLeft: Int?,
    val kmLeft: Int?,
)

/**
 * The car screens' read side of the API Server. Deliberately a handful of GETs
 * over HttpURLConnection rather than a second copy of api.dart: the head unit
 * only reads, and a client with no writes in it cannot make one by accident.
 */
class VaultApi(private val endpoint: VaultStore.Endpoint) {

    fun cars(): List<VaultCar> {
        val items = getArray("/cars")
        return (0 until items.length()).mapNotNull { i ->
            items.optJSONObject(i)?.let { car ->
                VaultCar(
                    id = car.optString("id"),
                    name = car.optString("name"),
                    subtitle = listOf(
                        car.optString("make"),
                        car.optString("model"),
                        car.optInt("year").takeIf { it > 0 }?.toString().orEmpty(),
                    ).filter { it.isNotEmpty() }.joinToString(" "),
                    registration = car.optString("registration"),
                    currentKm = car.optInt("currentKm"),
                    access = car.optString("access", "owner"),
                )
            }
        }
    }

    /**
     * The car's most recent service, which is what the due status is read from.
     * The list comes back newest first, the same order the dashboard takes its
     * own "latest" from.
     */
    fun latestService(carId: String): VaultService? {
        val items = getArray("/cars/$carId/service-records")
        val record = items.optJSONObject(0) ?: return null
        return VaultService(
            date = record.optString("date"),
            nextServiceDate = record.optString("nextServiceDate"),
            nextServiceKm = if (record.isNull("nextServiceKm")) null else record.optInt("nextServiceKm"),
        )
    }

    /** A car's reminders, already sorted by the server with the outstanding first. */
    fun reminders(carId: String): List<VaultReminder> {
        val items = getArray("/cars/$carId/reminders")
        return (0 until items.length()).mapNotNull { i ->
            items.optJSONObject(i)?.let { rem ->
                VaultReminder(
                    title = rem.optString("title"),
                    status = rem.optString("status"),
                    daysLeft = if (rem.isNull("daysLeft")) null else rem.optInt("daysLeft"),
                    kmLeft = if (rem.isNull("kmLeft")) null else rem.optInt("kmLeft"),
                )
            }
        }
    }

    private fun getArray(path: String): JSONArray {
        val body = get(path)
        return runCatching { JSONArray(body) }.getOrElse { JSONArray() }
    }

    private fun get(path: String): String {
        val connection = URL(endpoint.base + path).openConnection() as HttpURLConnection
        connection.requestMethod = "GET"
        connection.setRequestProperty("Authorization", "Bearer " + endpoint.token)
        connection.setRequestProperty("Accept", "application/json")
        connection.connectTimeout = 10_000
        connection.readTimeout = 15_000
        try {
            val status = connection.responseCode
            if (status == HttpURLConnection.HTTP_UNAUTHORIZED) throw VaultUnauthorized()
            val stream = if (status in 200..299) connection.inputStream else connection.errorStream
            val body = stream?.use { it.readBytes().toString(Charsets.UTF_8) }.orEmpty()
            if (status !in 200..299) throw IOException(errorMessage(body, status))
            return body
        } finally {
            connection.disconnect()
        }
    }

    /** The server's own {"error": …}, so a rejection reads as more than a number. */
    private fun errorMessage(body: String, status: Int): String {
        val message = runCatching { JSONObject(body).optString("error") }.getOrDefault("")
        return if (message.isNullOrEmpty()) "HTTP $status" else message
    }
}
