package com.drivervault.phoneapp.car

import java.text.NumberFormat
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
import java.util.TimeZone

/**
 * How urgent a badge is. The four states of lib/format.dart's StatusKey,
 * declared worst last so the order they are written in is the order they rank.
 */
enum class StatusKey {
    UNKNOWN, OK, SOON, OVERDUE;

    /** How bad this state is beside another one — _rank() in format.dart. */
    val severity: Int get() = ordinal
}

/** A badge: how bad it is, and the sentence that says so. */
data class CarStatus(val key: StatusKey, val label: String)

/**
 * Dates, distances and the due badges, formatted the way the phone formats them
 * — the twin of lib/format.dart, reading the same settings (the account's locale
 * and date format) and, through [CarStrings], the same translated wording. Only
 * what a car screen shows is here; the fuel and money figures have no head unit
 * to reach.
 *
 * The maths is the phone's too: a service falls due on a date OR an odometer
 * reading, and the badge takes the worse of the two.
 */
class CarFormat(
    private val strings: CarStrings,
    localeTag: String,
    private val dateFormat: String,
) {
    private val locale: Locale =
        Locale.forLanguageTag(localeTag).takeIf { it.language.isNotEmpty() } ?: Locale.US

    /** Grouped by the account's region, so the odometer agrees with the dates beside it. */
    fun number(value: Int): String = NumberFormat.getIntegerInstance(locale).format(value)

    /** 0 km is a reading — a car collected new — not a blank. */
    fun km(value: Int?): String = if (value == null) EMPTY else number(value) + " km"

    /** The account's chosen date pattern, with month names in its language. */
    fun date(iso: String?): String {
        val day = parse(iso) ?: return EMPTY
        val pattern = when (dateFormat) {
            "DMY_NUM" -> "dd-MM-yyyy"
            "DMY" -> "dd MMM yyyy"
            "MDY" -> "MMM dd, yyyy"
            else -> "yyyy-MM-dd"
        }
        return SimpleDateFormat(pattern, locale).format(day)
    }

    /**
     * The due badge for a car: the worse of its date and odometer signals, worded
     * the way serviceStatus() words it in format.dart — one headline for the
     * severity, then each trigger as a bare quantity.
     */
    fun serviceStatus(service: VaultService?, currentKm: Int): CarStatus {
        val date = dateSignal(service?.nextServiceDate)
        val km = kmSignal(currentKm, service?.nextServiceKm)
        if (date.key == StatusKey.UNKNOWN && km.key == StatusKey.UNKNOWN) {
            return CarStatus(StatusKey.UNKNOWN, strings.t("status.noData"))
        }
        // With one signal to go on, that signal's own sentence says it best.
        if (date.key == StatusKey.UNKNOWN) return CarStatus(km.key, km.label)
        if (km.key == StatusKey.UNKNOWN) return CarStatus(date.key, date.label)
        val worse = if (km.key.severity > date.key.severity) km else date
        return CarStatus(worse.key, bothSignals(date, km, worse.key))
    }

    /**
     * The badge for one reminder. The server has already picked the worse of its
     * two triggers; this only chooses the wording.
     */
    fun reminderStatus(reminder: VaultReminder): CarStatus {
        val key = when (reminder.status) {
            "overdue" -> StatusKey.OVERDUE
            "due_soon" -> StatusKey.SOON
            "upcoming" -> StatusKey.OK
            else -> StatusKey.UNKNOWN // done | no_trigger
        }
        if (reminder.status == "done") return CarStatus(key, strings.t("status.done"))
        if (reminder.status == "no_trigger") return CarStatus(key, strings.t("status.noTrigger"))

        val days = reminder.daysLeft
        val km = reminder.kmLeft
        val parts = mutableListOf<String>()
        if (reminder.status == "overdue") {
            if (days != null && days < 0) parts += strings.t("status.days", mapOf("days" to -days))
            if (km != null && km < 0) parts += strings.t("status.km", mapOf("km" to number(-km)))
            val label = if (parts.isEmpty()) {
                strings.t("status.overdue")
            } else {
                strings.t("status.overdueBy", mapOf("parts" to parts.joinToString(SEPARATOR)))
            }
            return CarStatus(key, label)
        }
        if (days != null && days >= 0) {
            parts += if (days == 0) {
                strings.t("status.today")
            } else {
                strings.t("status.days", mapOf("days" to days))
            }
        }
        if (km != null && km >= 0) parts += strings.t("status.km", mapOf("km" to number(km)))
        val label = if (parts.isEmpty()) {
            strings.t("status.upcoming")
        } else {
            strings.t("status.dueIn", mapOf("parts" to parts.joinToString(SEPARATOR)))
        }
        return CarStatus(key, label)
    }

    // --- the two signals a service is due on ---------------------------------

    private data class Signal(val key: StatusKey, val label: String, val value: Int?)

    private fun dateSignal(iso: String?): Signal {
        val due = parse(iso) ?: return Signal(StatusKey.UNKNOWN, strings.t("status.noData"), null)
        val days = daysUntil(due)
        return when {
            days < 0 -> Signal(
                StatusKey.OVERDUE,
                strings.t("status.serviceOverdueDays", mapOf("days" to -days)),
                days,
            )
            days <= DAYS_SOON -> Signal(
                StatusKey.SOON,
                strings.t("status.dueInDays", mapOf("days" to days)),
                days,
            )
            else -> Signal(StatusKey.OK, strings.t("status.okDays", mapOf("days" to days)), days)
        }
    }

    private fun kmSignal(currentKm: Int, nextKm: Int?): Signal {
        if (nextKm == null) return Signal(StatusKey.UNKNOWN, strings.t("status.noKm"), null)
        val remaining = nextKm - currentKm
        return when {
            remaining < 0 -> Signal(
                StatusKey.OVERDUE,
                strings.t("status.serviceOverdueKm", mapOf("km" to number(-remaining))),
                remaining,
            )
            remaining <= KM_SOON -> Signal(
                StatusKey.SOON,
                strings.t("status.inKm", mapOf("km" to number(remaining))),
                remaining,
            )
            else -> Signal(
                StatusKey.OK,
                strings.t("status.kmLeft", mapOf("km" to number(remaining))),
                remaining,
            )
        }
    }

    /**
     * Words a badge watching both triggers. Overdue quotes only what has actually
     * passed: the other trigger is not late, and its comfortable remainder under
     * an "Overdue" headline would read as one.
     */
    private fun bothSignals(date: Signal, km: Signal, key: StatusKey): String {
        val parts = mutableListOf<String>()
        if (key == StatusKey.OVERDUE) {
            if (date.key == StatusKey.OVERDUE) {
                parts += strings.t("status.days", mapOf("days" to -date.value!!))
            }
            if (km.key == StatusKey.OVERDUE) {
                parts += strings.t("status.km", mapOf("km" to number(-km.value!!)))
            }
            return strings.t(
                "status.serviceOverdueBy",
                mapOf("parts" to parts.joinToString(SEPARATOR)),
            )
        }
        parts += strings.t("status.days", mapOf("days" to date.value))
        parts += strings.t("status.km", mapOf("km" to number(km.value!!)))
        val both = mapOf("parts" to parts.joinToString(SEPARATOR))
        // Each key spelled out where it is used, rather than picked into a
        // variable: it is what lets the phone's tests see which keys these
        // screens ask for.
        return if (key == StatusKey.SOON) {
            strings.t("status.dueIn", both)
        } else {
            strings.t("status.okIn", both)
        }
    }

    // --- dates ---------------------------------------------------------------

    /**
     * Whole days between today and [due], both taken as local calendar days. The
     * rounding is what keeps a clock change from turning a 23-hour day into a day
     * that never elapsed.
     */
    private fun daysUntil(due: Date): Int {
        val target = midnight(due)
        val today = midnight(Date())
        return Math.round((target - today) / MILLIS_PER_DAY.toDouble()).toInt()
    }

    private fun midnight(date: Date): Long {
        val calendar = Calendar.getInstance()
        calendar.time = date
        calendar.set(Calendar.HOUR_OF_DAY, 0)
        calendar.set(Calendar.MINUTE, 0)
        calendar.set(Calendar.SECOND, 0)
        calendar.set(Calendar.MILLISECOND, 0)
        return calendar.timeInMillis
    }

    /**
     * An instant as the API writes it (RFC 3339, out of Go's time.Time), read
     * into the phone's own time zone the way DateTime.parse().toLocal() reads it
     * — so the day printed here is the day printed there. A bare date is taken as
     * a local one, which is what it means.
     */
    private fun parse(iso: String?): Date? {
        val value = iso?.trim().orEmpty()
        if (!DATE_HEAD.containsMatchIn(value)) return null
        if (value.length <= 10) return parseWith("yyyy-MM-dd", value, null)
        // "...T10:30:00.123456789Z" and "...+02:00" -> "...T10:30:00+0000", the
        // one shape SimpleDateFormat's Z can read.
        val normalized = value
            .let { FRACTION.replace(it, "") }
            .let { if (it.endsWith("Z")) it.dropLast(1) + "+0000" else it }
            .let { OFFSET_COLON.replace(it, "$1$2") }
        return parseWith("yyyy-MM-dd'T'HH:mm:ssZ", normalized, null)
            ?: parseWith("yyyy-MM-dd'T'HH:mm:ss", value.take(19), TimeZone.getTimeZone("UTC"))
            ?: parseWith("yyyy-MM-dd", value.take(10), null)
    }

    private fun parseWith(pattern: String, value: String, zone: TimeZone?): Date? = try {
        SimpleDateFormat(pattern, Locale.US).apply { zone?.let { timeZone = it } }.parse(value)
    } catch (_: ParseException) {
        null
    }

    private companion object {
        const val EMPTY = "—" // an em dash, the same blank common.empty is
        const val SEPARATOR = " · "
        const val DAYS_SOON = 30
        const val KM_SOON = 1000
        const val MILLIS_PER_DAY = 24L * 60 * 60 * 1000
        val DATE_HEAD = Regex("^\\d{4}-\\d{2}-\\d{2}")
        val FRACTION = Regex("\\.\\d+")
        val OFFSET_COLON = Regex("([+-]\\d{2}):(\\d{2})$")
    }
}
