/**
 * Eigene Service-Prognosen (Ölwechsel, Inspektion). Portiert aus
 * `oelwechselPrognose()` im alten Panel; die Inspektion kam am 2026-08-17
 * hinzu (§ AGENTS.md, "Inspektion: voraussichtliches Datum ergänzen").
 *
 * Die Rechnung beginnt bewusst neu ab dem letzten tatsächlich durchgeführten
 * Eintrag dieser Art — der allgemeine Langzeitdurchschnitt seit dem
 * allerersten Servicebucheintrag wäre dafür zu träge. Aus km und Tagen seit
 * dem letzten Eintrag ergibt sich die aktuelle Fahrleistung pro Tag;
 * hochgerechnet auf die verbleibenden km bis zum Intervall ergibt das ein
 * Datum, gedeckelt auf spätestens das Zeitlimit des Intervalls.
 */

import type { Fahrzeug } from "./profilAdapter"

const TAG_MS = 86_400_000

export interface Oelprognose {
  /** ISO-Tagesdatum. */
  datum: string
  restKm: number
  kmProTag: number
  /** Die Zeitgrenze hat gegriffen, nicht die Kilometergrenze. */
  durchZeitlimit: boolean
}

interface Buchhaltung {
  datum?: string
  km?: number
  art?: string
}

function monatePlus(isoDatum: string, monate: number): string {
  const d = new Date(isoDatum)
  if (Number.isNaN(d.getTime())) return isoDatum
  d.setMonth(d.getMonth() + monate)
  return d.toISOString().slice(0, 10)
}

/** Der jüngste Servicebucheintrag, dessen Art auf das gesuchte Stichwort
    passt (z. B. "öl"/"oel" oder "inspektion"). */
function letzterEintrag(buch: readonly Buchhaltung[], stichwort: RegExp): Buchhaltung | null {
  const treffer = buch
    .filter((e) => typeof e.art === "string" && stichwort.test(e.art) && e.datum && e.km != null)
    .sort((a, b) => String(b.datum).localeCompare(String(a.datum)))
  return treffer[0] ?? null
}

/** Der jüngste Servicebucheintrag, dessen Art auf einen Ölwechsel hindeutet. */
export function letzterOelwechsel(buch: readonly Buchhaltung[]): Buchhaltung | null {
  return letzterEintrag(buch, /öl|oel/i)
}

/** Der jüngste Servicebucheintrag, dessen Art auf eine Inspektion hindeutet. */
export function letzterInspektion(buch: readonly Buchhaltung[]): Buchhaltung | null {
  return letzterEintrag(buch, /inspektion/i)
}

/** Gemeinsamer Kern für Ölwechsel- und Inspektionsprognose - beide rechnen
    identisch, nur mit anderem Intervall und anderer Suche im Servicebuch. */
function servicePrognose(
  fahrzeug: Fahrzeug,
  letzter: Buchhaltung | null,
  intervallKm: number,
  intervallMonate: number,
  jetzt: Date,
): Oelprognose | null {
  if (!fahrzeug.odoBekannt) return null
  if (!letzter?.datum || letzter.km == null) return null
  if (!Number.isFinite(intervallKm) || intervallKm <= 0) return null

  const zielKm = letzter.km + intervallKm
  const restKm = Math.max(0, zielKm - fahrzeug.odo)
  const kappDatum = monatePlus(letzter.datum, Number.isFinite(intervallMonate) ? intervallMonate : 24)

  if (restKm <= 0) {
    return { datum: jetzt.toISOString().slice(0, 10), restKm: 0, kmProTag: 0, durchZeitlimit: false }
  }

  const tageSeit = (jetzt.getTime() - new Date(letzter.datum).getTime()) / TAG_MS
  const kmSeit = fahrzeug.odo - letzter.km

  // Ohne belastbare Fahrleistung (direkt nach dem Eintrag) gibt es keine
  // seriöse Kilometerprognose — dann gilt das Zeitlimit.
  if (tageSeit <= 0 || kmSeit <= 0) {
    return { datum: kappDatum, restKm, kmProTag: 0, durchZeitlimit: true }
  }

  const kmProTag = kmSeit / tageSeit
  const tageBisZiel = restKm / kmProTag
  const ausKm = new Date(jetzt.getTime() + tageBisZiel * TAG_MS).toISOString().slice(0, 10)

  const durchZeitlimit = kappDatum < ausKm
  return {
    datum: durchZeitlimit ? kappDatum : ausKm,
    restKm,
    kmProTag,
    durchZeitlimit,
  }
}

export function oelwechselPrognose(
  fahrzeug: Fahrzeug,
  buch: readonly Buchhaltung[],
  jetzt = new Date(),
): Oelprognose | null {
  const oel = fahrzeug.oel
  const intervallKm =
    oel.modus === "hersteller"
      ? (oel.herstellerKm ?? 0)
      : Math.min(oel.km ?? Infinity, oel.herstellerKm ?? Infinity)
  const intervallMonate =
    oel.modus === "hersteller"
      ? (oel.herstellerMonate ?? 0)
      : Math.min(oel.monate ?? Infinity, oel.herstellerMonate ?? Infinity)
  return servicePrognose(fahrzeug, letzterOelwechsel(buch), intervallKm, intervallMonate, jetzt)
}

/** Inspektion: feste 30.000 km / 24 Monate, wie auch das Fahrzeug selbst und
    das alte Panel rechnen - kein einstellbares Intervall wie beim Ölwechsel. */
export function inspektionPrognose(
  fahrzeug: Fahrzeug,
  buch: readonly Buchhaltung[],
  jetzt = new Date(),
): Oelprognose | null {
  return servicePrognose(fahrzeug, letzterEintrag(buch, /inspektion/i), 30000, 24, jetzt)
}

interface Fahrtdaten {
  ts_start: string
  distance_km: number | null
  status?: string
}

/** Fahrleistung pro Tag aus dem tatsächlichen Fahrtenlog, unabhängig vom
    Servicebuch - Grundlage für meldungsPrognose() unten, die auch ganz ohne
    Servicebucheintrag funktionieren soll. Nur abgeschlossene Fahrten mit
    bekannter Distanz zählen; die Rate ist der Durchschnitt seit der ältesten
    erfassten Fahrt, kein kurzes Zeitfenster - damit bleibt sie robust gegen
    einzelne fahrtenarme Tage. null ohne belastbare Datenlage. */
export function kmProTagAusFahrten(fahrten: readonly Fahrtdaten[], jetzt = new Date()): number | null {
  const gueltig = fahrten.filter((f) => f.status !== "offen" && f.distance_km != null)
  if (!gueltig.length) return null
  const km = gueltig.reduce((s, f) => s + (f.distance_km ?? 0), 0)
  const aeltesteZeit = Math.min(...gueltig.map((f) => new Date(f.ts_start).getTime()))
  const tage = (jetzt.getTime() - aeltesteZeit) / TAG_MS
  return tage >= 1 ? km / tage : null
}

export interface Meldungsprognose {
  /** ISO-Tagesdatum. */
  datum: string
  /** Die vom Fahrzeug gemeldete Zeitgrenze hat gegriffen, nicht die
      hochgerechnete Kilometergrenze - der Nutzer fährt zu wenig, um die
      km-Grenze rechtzeitig zu erreichen. */
  durchZeitlimit: boolean
  /** Restkilometer bis zur Fälligkeit, falls bekannt. */
  restKm: number | null
}

/** Prognose rein aus der Fahrzeugmeldung, für den Fall ganz ohne
    Servicebucheintrag (siehe Aufrufer unten und in Service.tsx) - ein echter
    Servicebucheintrag bleibt vorrangig, näher am tatsächlichen letzten
    Service. Restkilometer aus der Meldung, hochgerechnet mit der echten
    Fahrleistung aus dem Fahrtenlog statt einer Servicebuch-Rate; gedeckelt
    auf das vom Fahrzeug selbst gemeldete Datum als Zeitgrenze. */
export function meldungsPrognose(
  faelligTs: string | null,
  faelligKm: number | null,
  fahrten: readonly Fahrtdaten[],
  jetzt = new Date(),
): Meldungsprognose | null {
  const zeitDatum = faelligTs ? faelligTs.slice(0, 10) : null
  if (faelligKm == null) return zeitDatum ? { datum: zeitDatum, durchZeitlimit: true, restKm: null } : null
  const restKm = Math.abs(faelligKm)
  if (restKm <= 0) return { datum: jetzt.toISOString().slice(0, 10), durchZeitlimit: false, restKm }
  const rate = kmProTagAusFahrten(fahrten, jetzt)
  if (!rate) return zeitDatum ? { datum: zeitDatum, durchZeitlimit: true, restKm } : null
  const kmDatum = new Date(jetzt.getTime() + (restKm / rate) * TAG_MS).toISOString().slice(0, 10)
  const durchZeitlimit = !!zeitDatum && zeitDatum < kmDatum
  return { datum: durchZeitlimit ? (zeitDatum as string) : kmDatum, durchZeitlimit, restKm }
}

/** Eigenes (kürzeres) Intervall ganz ohne Servicebucheintrag: die
    Fahrzeugmeldung kennt nur das Herstellerintervall, nicht die in
    "Einrichten" gewählte kürzere Vorgabe. Ein angenommener letzter Service
    wird aus der gemeldeten Fälligkeit und der bekannten Herstellerintervall-
    Länge zurückgerechnet ("Fälligkeitspunkt minus Herstellerintervall"), und
    das eigene Intervall ab demselben angenommenen Punkt neu hochgerechnet -
    eine Näherung (der wahre letzte Service kann näher oder ferner liegen),
    aber ohne sie würde die eigene Intervallwahl bis zum ersten
    Servicebucheintrag komplett ignoriert (siehe oelMeldungsPrognose()). */
export function meldungsPrognoseEigenesIntervall(
  faelligTs: string | null,
  faelligKm: number | null,
  herstellerKm: number | null,
  herstellerMonate: number | null,
  eigenKm: number | null,
  eigenMonate: number | null,
  fahrten: readonly Fahrtdaten[],
  jetzt = new Date(),
): Meldungsprognose | null {
  if (faelligKm == null || !herstellerKm || eigenKm == null) return null
  const restKm = Math.max(0, Math.abs(faelligKm) - herstellerKm + eigenKm)
  const zeitDatum =
    faelligTs && herstellerMonate != null && eigenMonate != null
      ? monatePlus(faelligTs, eigenMonate - herstellerMonate)
      : null
  if (restKm <= 0) return { datum: jetzt.toISOString().slice(0, 10), durchZeitlimit: false, restKm }
  const rate = kmProTagAusFahrten(fahrten, jetzt)
  if (!rate) return zeitDatum ? { datum: zeitDatum, durchZeitlimit: true, restKm } : null
  const kmDatum = new Date(jetzt.getTime() + (restKm / rate) * TAG_MS).toISOString().slice(0, 10)
  const durchZeitlimit = !!zeitDatum && zeitDatum < kmDatum
  return { datum: durchZeitlimit ? (zeitDatum as string) : kmDatum, durchZeitlimit, restKm }
}

/** Fällt ganz ohne Servicebucheintrag auf die Fahrzeugmeldung zurück - bei
    Herstellervorgabe unverändert (die Meldung IST die Herstellervorgabe),
    bei eigenem (kürzerem) Intervall über meldungsPrognoseEigenesIntervall()
    hochgerechnet, damit die Intervallwahl in "Einrichten" auch ganz ohne
    Servicebucheintrag etwas bewirkt. Fehlen die Herstellerwerte selbst
    (kein Sensor dafür), fällt das sanft auf die reine Meldung zurück. */
export function oelMeldungsPrognose(
  fahrzeug: Fahrzeug,
  fahrten: readonly Fahrtdaten[],
  jetzt = new Date(),
): Meldungsprognose | null {
  const oel = fahrzeug.oel
  const roh = () => meldungsPrognose(fahrzeug.oelwechselFaelligTs, fahrzeug.oelwechselFaelligKm, fahrten, jetzt)
  if (oel.modus === "hersteller") return roh()
  const eigen = meldungsPrognoseEigenesIntervall(
    fahrzeug.oelwechselFaelligTs,
    fahrzeug.oelwechselFaelligKm,
    oel.herstellerKm,
    oel.herstellerMonate,
    oel.km,
    oel.monate,
    fahrten,
    jetzt,
  )
  return eigen ?? roh()
}

/* ------------------------------------------------------ Nächster Service */

export type Serviceart = "Ölwechsel" | "Inspektion" | "Hauptuntersuchung"

export interface NaechsterService {
  art: Serviceart
  /** Restkilometer bis zur Fälligkeit. null bei der Hauptuntersuchung — die
      hat kein Kilometerziel, nur einen Termin. */
  restKm: number | null
  /** ISO-Tagesdatum der voraussichtlichen Fälligkeit. */
  datum: string
}

/**
 * Der zeitlich nächste anstehende Service über alle drei Arten hinweg —
 * Gegenstück zu `naechsterTermin()` im Panel.
 *
 * Ölwechsel und Inspektion kommen vorrangig aus der eigenen Prognose
 * (Servicebuch plus tatsächliche Fahrleistung); ohne Servicebucheintrag dazu
 * fällt jede der beiden einzeln auf meldungsPrognose() zurück - die vom
 * Fahrzeug selbst gemeldete Fälligkeit, hochgerechnet mit der Fahrleistung
 * aus dem Fahrtenlog. Ein frisch eingerichtetes Fahrzeug mit zugeordnetem
 * Sensor, aber noch ganz ohne Servicebuch, zeigte hier vorher "kein Eintrag
 * im Servicebuch", obwohl die Fälligkeit längst bekannt war. Die
 * Hauptuntersuchung kommt weiterhin nur aus dem im Profil gepflegten
 * Termin: sie hat kein Kilometerziel, das Fahrzeug meldet sie nicht, und
 * hochrechnen lässt sie sich auch nicht.
 */
export function naechsterService(
  fahrzeug: Fahrzeug,
  buch: readonly Buchhaltung[],
  fahrten: readonly Fahrtdaten[] = [],
  jetzt = new Date(),
): NaechsterService | null {
  const kandidaten: NaechsterService[] = []

  const oel = oelwechselPrognose(fahrzeug, buch, jetzt)
  if (oel) {
    kandidaten.push({ art: "Ölwechsel", restKm: oel.restKm, datum: oel.datum })
  } else {
    const m = oelMeldungsPrognose(fahrzeug, fahrten, jetzt)
    if (m) kandidaten.push({ art: "Ölwechsel", restKm: m.restKm, datum: m.datum })
  }

  const insp = inspektionPrognose(fahrzeug, buch, jetzt)
  if (insp) {
    kandidaten.push({ art: "Inspektion", restKm: insp.restKm, datum: insp.datum })
  } else {
    const m = meldungsPrognose(fahrzeug.inspektionFaelligTs, fahrzeug.inspektionFaelligKm, fahrten, jetzt)
    if (m) kandidaten.push({ art: "Inspektion", restKm: m.restKm, datum: m.datum })
  }

  // fahrzeug.hu wird von Hand gepflegt und kann leer oder unlesbar sein.
  const hu = new Date(fahrzeug.hu)
  if (fahrzeug.hu && !Number.isNaN(hu.getTime())) {
    kandidaten.push({
      art: "Hauptuntersuchung",
      restKm: null,
      datum: hu.toISOString().slice(0, 10),
    })
  }

  if (kandidaten.length === 0) return null
  // ISO-Tagesdaten, deshalb reicht der Zeichenkettenvergleich.
  return kandidaten.sort((a, b) => a.datum.localeCompare(b.datum))[0] ?? null
}
