import "package:flutter/material.dart";
import "package:intl/intl.dart";

import "i18n.dart";
import "main.dart";
import "models.dart";
import "theme.dart";

/// The languages intl actually ships symbols for. It throws rather than falling
/// back on the rest, and the API only validates a locale's *shape*
/// (`^[a-z]{2}-[A-Z]{2}$`), so an unsupported tag can legitimately arrive here —
/// set from the web, which has the browser's full ICU data behind it. Guarding
/// once keeps that from throwing out of every date on screen.
final _supportedLanguages = DateFormat.allLocalesWithSymbols().toSet();

/// The user's locale when intl can render it, else a safe default. Only the
/// language subtag is checked: intl resolves an unknown *region* by falling back
/// to the language ("en-PL" formats as "en"), but an unknown language throws.
String get _locale =>
    _supportedLanguages.contains(appSettings.language) ? appSettings.locale : "en-US";

/// Every number we render goes through here so the grouping separator follows
/// the user's chosen region rather than the device's own locale — otherwise the
/// odometer disagrees with the dates and costs beside it.
String _num(num value) => NumberFormat.decimalPattern(_locale).format(value);

/// Formats a date per the signed-in user's chosen date format (appSettings),
/// mirroring the web app's format.js. Month names follow the locale's language.
String formatDate(DateTime? d) {
  if (d == null) return "—";
  final pattern = switch (appSettings.dateFormat) {
    "DMY_NUM" => "dd-MM-yyyy",
    "DMY" => "dd MMM yyyy",
    "MDY" => "MMM dd, yyyy",
    _ => "yyyy-MM-dd", // YMD
  };
  return DateFormat(pattern, _locale).format(d);
}

/// A date somebody may only half know, as an ISO 8601 reduced-precision date:
/// "2015", "2015-03" or "2015-03-10". Mirrors formatPartialDate in the web
/// app's format.js, which is where such a value is entered — a car's build date
/// is often only a year, and this app must not print a day nobody supplied.
///
/// Taken apart as a string rather than parsed: DateTime.parse rejects "2015"
/// and "2015-03" outright, which is exactly why the value has to be handled
/// here at all.
String formatPartialDate(String iso) {
  final value = iso.trim();
  if (value.isEmpty) return "—";
  final match = RegExp(r"^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$").firstMatch(value);
  if (match == null) return formatDate(DateTime.tryParse(value));
  final year = match.group(1)!;
  final month = match.group(2);
  if (match.group(3) != null) return formatDate(DateTime.tryParse(value));
  if (month == null) return year;

  // Month and year: the month's name where the user's format spells months out,
  // its number where the format is numeric, in the app's usual order either way.
  final when = DateTime(int.parse(year), int.parse(month));
  return switch (appSettings.dateFormat) {
    "DMY_NUM" => "$month-$year",
    "DMY" || "MDY" => DateFormat("MMM yyyy", _locale).format(when),
    _ => "$year-$month",
  };
}

/// A timestamp: the user's chosen date format with the wall clock beside it.
/// Only the provider snapshot needs one — everything else in the app is
/// date-only — but it follows the same settings as [formatDate] so the two never
/// disagree on screen.
String formatDateTime(DateTime? d) {
  if (d == null) return "—";
  return "${formatDate(d)} ${DateFormat.Hm(_locale).format(d)}";
}

// 0 km is a reading — a car collected new — not a blank. See format.js.
String formatKm(int? km) => km == null ? "—" : "${_num(km)} km";

// The fuel figures. The server sends null for anything it could not derive (a
// window with a missed fill, a first-ever tank), which reads as "—" rather than
// a misleading zero.
String formatLiters(double? value) => value == null ? "—" : "${value.toStringAsFixed(2)} L";

/// Amounts are stored as plain numbers; the user's currency setting only decides
/// how they are displayed. Nothing is converted — a figure entered as 40 reads as
/// 40 in whichever currency is selected.
///
/// No explicit fraction digits: the currency's own minor unit pins them, which
/// keeps the 2 decimals the fuel figures were written for while still rendering
/// yen without phantom sen.
String formatMoney(double? value) {
  if (value == null) return "—";
  return NumberFormat.simpleCurrency(locale: _locale, name: appSettings.currency).format(value);
}

/// One decimal: the interesting differences between tanks live in tenths, and
/// rounding to whole litres collapses a best of 6.8 and a worst of 7.0 into the
/// same number.
String formatConsumption(double? value) =>
    value == null ? "—" : "${value.toStringAsFixed(1)} L/100km";

String formatKmPerLiter(double? value) =>
    value == null ? "—" : "${value.toStringAsFixed(2)} km/L";

/// The charging figures. Same rule as the fuel ones: null is "could not be
/// derived", which reads as "—" rather than a misleading zero.
String formatKwh(double? value) => value == null ? "—" : "${value.toStringAsFixed(2)} kWh";

String formatKwhConsumption(double? value) =>
    value == null ? "—" : "${value.toStringAsFixed(1)} kWh/100km";

String formatKmPerKwh(double? value) =>
    value == null ? "—" : "${value.toStringAsFixed(2)} km/kWh";

const int _kmSoon = 1000;

enum StatusKey { unknown, ok, soon, overdue }

class Status {
  final StatusKey key;
  final String label;
  const Status(this.key, this.label);

  /// Soft tint background for the status pill — DriverVault semantic colours,
  /// re-cut for dark surfaces.
  Color bg(bool dark) => switch (key) {
        StatusKey.overdue => dark ? DriverVault.dangerSoftDark : DriverVault.dangerSoft,
        StatusKey.soon => dark ? DriverVault.warningSoftDark : DriverVault.warningSoft,
        StatusKey.ok => dark ? DriverVault.successSoftDark : DriverVault.successSoft,
        StatusKey.unknown => dark ? DriverVault.darkSunken : DriverVault.ink50,
      };
  Color fg(bool dark) => switch (key) {
        StatusKey.overdue => DriverVault.danger,
        StatusKey.soon => DriverVault.warning,
        StatusKey.ok => DriverVault.success,
        StatusKey.unknown => dark ? DriverVault.darkTextMuted : DriverVault.ink500,
      };
}

int _rank(StatusKey k) => switch (k) {
      StatusKey.unknown => 0,
      StatusKey.ok => 1,
      StatusKey.soon => 2,
      StatusKey.overdue => 3,
    };

/// One of the two service triggers: its badge state and its own wording, plus
/// the number behind that wording so a badge holding both can quote the two
/// side by side.
typedef _Signal = ({StatusKey key, String label, int? value});

_Signal _dateSignal(DateTime? nextDate) {
  if (nextDate == null) return (key: StatusKey.unknown, label: t("status.noData"), value: null);
  final today = DateTime.now();
  final days = DateTime(nextDate.year, nextDate.month, nextDate.day)
      .difference(DateTime(today.year, today.month, today.day))
      .inDays;
  if (days < 0) {
    return (key: StatusKey.overdue, label: t("status.serviceOverdueDays", params: {"days": days.abs()}), value: days);
  }
  if (days <= 30) {
    return (key: StatusKey.soon, label: t("status.dueInDays", params: {"days": days}), value: days);
  }
  return (key: StatusKey.ok, label: t("status.okDays", params: {"days": days}), value: days);
}

_Signal _kmSignal(int currentKm, int? nextKm) {
  if (nextKm == null) return (key: StatusKey.unknown, label: t("status.noKm"), value: null);
  final remaining = nextKm - currentKm;
  if (remaining < 0) {
    return (
      key: StatusKey.overdue,
      label: t("status.serviceOverdueKm", params: {"km": _num(remaining.abs())}),
      value: remaining,
    );
  }
  if (remaining <= _kmSoon) {
    return (key: StatusKey.soon, label: t("status.inKm", params: {"km": _num(remaining)}), value: remaining);
  }
  return (key: StatusKey.ok, label: t("status.kmLeft", params: {"km": _num(remaining)}), value: remaining);
}

/// Maps the server's expiry/reminder state names onto the badge palette. The
/// client never re-derives the date maths — it only chooses the wording.
StatusKey _expiryKey(String state) => switch (state) {
      "expired" => StatusKey.overdue,
      "expiring_soon" => StatusKey.soon,
      "valid" => StatusKey.ok,
      _ => StatusKey.unknown, // no_expiry
    };

/// Renewal badge for a dated document or certificate, driven by the server's
/// expiry assessment.
Status expiryStatus(ExpiryAssessment e) {
  final days = e.days;
  final label = switch (e.state) {
    "expired" => t("status.expiredAgo", params: {"days": (days ?? 0).abs()}),
    "expiring_soon" => days == 0 ? t("status.expiresToday") : t("status.renewInDays", params: {"days": days}),
    "valid" => t("status.validDays", params: {"days": days}),
    _ => t("status.noExpiry"),
  };
  return Status(_expiryKey(e.state), label);
}

/// Reminder badge. The server has already picked the worse of the date and
/// odometer signals; this only chooses the wording, leading with whichever
/// trigger is actually closest to firing.
Status reminderStatus(Reminder r) {
  final key = switch (r.status) {
    "overdue" => StatusKey.overdue,
    "due_soon" => StatusKey.soon,
    "upcoming" => StatusKey.ok,
    _ => StatusKey.unknown, // done | no_trigger
  };

  final days = r.daysLeft;
  final km = r.kmLeft;
  if (r.status == "done") return Status(StatusKey.unknown, t("status.done"));
  if (r.status == "no_trigger") return Status(StatusKey.unknown, t("status.noTrigger"));

  final parts = <String>[];
  if (r.status == "overdue") {
    if (days != null && days < 0) parts.add(t("status.days", params: {"days": days.abs()}));
    if (km != null && km < 0) parts.add(t("status.km", params: {"km": _num(km.abs())}));
    return Status(key, parts.isEmpty ? t("status.overdue") : t("status.overdueBy", params: {"parts": parts.join(" · ")}));
  }

  if (days != null && days >= 0) parts.add(days == 0 ? t("status.today") : t("status.days", params: {"days": days}));
  if (km != null && km >= 0) parts.add(t("status.km", params: {"km": _num(km)}));
  return Status(key, parts.isEmpty ? t("status.upcoming") : t("status.dueIn", params: {"parts": parts.join(" · ")}));
}

/// Warranty badge for a maintenance entry. Unlike the others this one has no
/// server-side assessment — only the raw active/days-left pair — so the wording
/// is chosen from those directly.
Status? warrantyStatus(MaintenanceEntry m) {
  final days = m.warrantyDaysLeft;
  if (m.warrantyActive == null || days == null) return null;
  if (!m.warrantyActive!) return Status(StatusKey.unknown, t("status.warrantyExpiredAgo", params: {"days": days.abs()}));
  if (days <= 30) return Status(StatusKey.soon, t("status.warrantyEndsIn", params: {"days": days}));
  return Status(StatusKey.ok, t("status.underWarranty", params: {"days": days}));
}

/// Combines the date- and km-based signals, returning the worse of the two —
/// the same logic as the web app and the spreadsheet idea.
Status serviceStatus(ServiceRecord? latest, Car car) {
  final date = _dateSignal(latest?.nextServiceDate);
  final km = _kmSignal(car.currentKm, latest?.nextServiceKm);
  final worse = _rank(km.key) > _rank(date.key) ? km : date;

  if (date.key == StatusKey.unknown && km.key == StatusKey.unknown) {
    return Status(StatusKey.unknown, t("status.noData"));
  }
  // With one signal to go on, that signal's own sentence says it best.
  if (date.key == StatusKey.unknown) return Status(km.key, km.label);
  if (km.key == StatusKey.unknown) return Status(date.key, date.label);
  return Status(worse.key, _bothSignals(date, km, worse.key));
}

/// Words a badge that has a due date AND an odometer target. A service falls
/// due on whichever arrives first, so "OK · 354d" on its own left out half of
/// what the badge is watching: the distance still to run belongs beside the
/// days. One headline for the severity, then each trigger as a bare quantity -
/// the shape reminderStatus already uses, it having had the two-trigger
/// problem first.
String _bothSignals(_Signal date, _Signal km, StatusKey key) {
  final parts = <String>[];
  if (key == StatusKey.overdue) {
    // Only what has actually passed. The other trigger is not late, and its
    // comfortable remainder under an "Overdue" headline would read as one.
    if (date.key == StatusKey.overdue) parts.add(t("status.days", params: {"days": date.value!.abs()}));
    if (km.key == StatusKey.overdue) parts.add(t("status.km", params: {"km": _num(km.value!.abs())}));
    return t("status.serviceOverdueBy", params: {"parts": parts.join(" · ")});
  }
  parts.add(t("status.days", params: {"days": date.value}));
  parts.add(t("status.km", params: {"km": _num(km.value!)}));
  return t(key == StatusKey.soon ? "status.dueIn" : "status.okIn", params: {"parts": parts.join(" · ")});
}
