// Package models defines the domain types for the car maintenance tracker.
//
// The shapes mirror the original "Car Service.xlsx": one Car per sheet, a log
// of ServiceRecords (date + km, plus which parts were changed), and a per-car
// catalog of Parts (cols M/N). Derived fields follow the spreadsheet formulas:
//
//	Next Service Date = Service Date + ServiceIntervalDays   (Excel: A + 365)
//	Next Service Km   = Service Km   + ServiceIntervalKm     (Excel: B + 15000)
package models

import "time"

// Attachment is the single optional file a record carries — a scan, a receipt, a
// workshop invoice, a photo of a part's box. Embedded by every type that can
// hold one.
//
// FileName is the name PocketBase stored it under. The bytes are not in here:
// they are served from GET /api/{records}/{id}/file, which re-checks access on
// every request, so an attachment is never a public URL.
type Attachment struct {
	FileName string `json:"fileName,omitempty"`
	HasFile  bool   `json:"hasFile"`
}

// Car corresponds to one worksheet in the original spreadsheet.
type Car struct {
	ID                  string `json:"id"`
	Name                string `json:"name"`                // e.g. "Toyota Yaris"
	Make                string `json:"make"`                // e.g. "Toyota"
	Model               string `json:"model"`               // e.g. "Yaris"
	Year                int    `json:"year"`                // optional
	Registration        string `json:"registration"`        // optional plate
	RegistrationCountry string `json:"registrationCountry"` // optional (country of registration)
	VIN                 string `json:"vin"`                 // optional

	// Maintenance intervals, configurable per car. The spreadsheet hard-coded
	// 365 days and 15000 km; here they are stored so each car can differ.
	ServiceIntervalDays int `json:"serviceIntervalDays"`
	ServiceIntervalKm   int `json:"serviceIntervalKm"`

	// TechnicalCheckIntervalDays is the roadworthiness inspection cycle. It only
	// prefills the next date — the interval is set by law, not by the car, and
	// changes as the car ages, so any check can override it with the date its
	// certificate actually carries.
	TechnicalCheckIntervalDays int `json:"technicalCheckIntervalDays"`

	// CurrentKm is the car's present odometer reading, updated by the user. Used
	// to flag km-based overdue service (current_km >= last service km + interval).
	CurrentKm int `json:"currentKm"`

	OilSpec             string `json:"oilSpec"`             // e.g. "Toyota Advanced Fuel Economy 0W20"
	TransmissionOilSpec string `json:"transmissionOilSpec"` // e.g. "Toyota WS"
	DifferentialOilSpec string `json:"differentialOilSpec"` // e.g. "SAE 75W-90 GL-5"
	BrakeFluidSpec      string `json:"brakeFluidSpec"`      // e.g. "DOT 4"
	CoolantSpec         string `json:"coolantSpec"`         // e.g. "Toyota Super Long Life Coolant"

	FuelType              string `json:"fuelType"`              // petrol | petrol_lpg | diesel | diesel_lpg | hybrid | electric | hydrogen
	BuildDate             string `json:"buildDate"`             // ISO 8601, precision as known: "2015" | "2015-03" | "2015-03-10"
	FirstRegistrationDate string `json:"firstRegistrationDate"` // ISO YYYY-MM-DD (date-only)

	// Provider links this car to the manufacturer service it came from — the name
	// of the plugin ("toyota"), plus that plugin's own id for the vehicle
	// (ProviderVehicleID; the VIN, for Toyota). Both are blank for a hand-entered
	// car. They are set by the import/link endpoints only, never by an ordinary
	// car edit, so saving the form cannot silently break the link.
	Provider          string `json:"provider,omitempty"`
	ProviderVehicleID string `json:"providerVehicleId,omitempty"`

	// HiddenTabs and HiddenFields are what this car's page does not show: tabs
	// (["fuel"] on an EV) and Information fields (["differentialOil"] on a car
	// without one). Properties of the car, so everyone it is shared with sees the
	// same page. The hidden sets, not the visible ones, so anything added in a
	// later release is on by default. Set through the view endpoint only, never by
	// an ordinary car edit, so saving the form cannot silently reveal them again.
	HiddenTabs   []string `json:"hiddenTabs"`
	HiddenFields []string `json:"hiddenFields"`

	// FieldOrder is the arrangement of the Information rows, as the field keys in
	// the order they are laid out. Also a property of the car, and it covers the
	// hidden rows too, so a row switched back on returns to where it was. Empty
	// on a car nobody has rearranged, which means the page's own default order;
	// a key it doesn't mention — a row added in a later release — follows the
	// arranged ones rather than appearing in the middle.
	FieldOrder []string `json:"fieldOrder"`

	// HiddenServiceColumns is what the Service history table does not show, as
	// column keys (["parts", "file"] for somebody who keeps only dates and
	// distances). The hidden set like the two above, so a column added later is
	// on by default, and Date is not among the keys it may name: a service
	// record is its date, and a table of them without it reads as a list of
	// nothing.
	HiddenServiceColumns []string `json:"hiddenServiceColumns"`

	// HiddenServiceParts is the parts this car's services never change, as part
	// keys (["oil"] on an EV, which has none to change). They come off the
	// Changed parts section of the service form and out of the history's parts
	// column together: a part nobody records is one nobody wants offered either.
	// The hidden set like the ones above, so a part added later is on by
	// default, and there is no order beside it — the parts are a checkbox list
	// inside one column, and their position says nothing a tab's or a column's
	// does.
	HiddenServiceParts []string `json:"hiddenServiceParts"`

	// ServiceColumnOrder is the arrangement of those columns, covering the
	// hidden ones so a column switched back on returns to where it was. It does
	// include "date", which cannot be switched off but can be moved off the
	// front — the same rule TabOrder applies to Information.
	ServiceColumnOrder []string `json:"serviceColumnOrder"`

	// MetricOrder is the same thing for the headline readings on the connected
	// service's tab, as the reading keys ("odometer", "evRange", …). A reading
	// the provider didn't report at the time it was arranged simply isn't in the
	// list, and joins the end when it does turn up.
	MetricOrder []string `json:"metricOrder"`

	// TabOrder is the order of the tabs themselves, as tab keys. It covers the
	// hidden tabs too, like FieldOrder, so a tab switched back on returns to
	// where it was, and it includes "info" — Information cannot be switched off
	// but it can be moved off the front.
	TabOrder []string `json:"tabOrder"`

	// Owner is the user id that owns this car. Access is the requesting user's
	// permission on it — "owner", "write", or "read" — computed by the API at
	// read time and never persisted (omitempty; not part of the write payload).
	Owner  string `json:"owner,omitempty"`
	Access string `json:"access,omitempty"`

	Created string `json:"created,omitempty"`
	Updated string `json:"updated,omitempty"`
}

// HomeCharger is a charger the user owns — their own wallbox, not a station on
// the public network. It belongs to the person rather than to a car: it charges
// whichever car is plugged into it, and it outlives any of them.
type HomeCharger struct {
	ID        string  `json:"id"`
	Name      string  `json:"name"`
	Serial    string  `json:"serial,omitempty"`
	Vendor    string  `json:"vendor,omitempty"`   // "Anker Solix", "Greencell"
	Model     string  `json:"model,omitempty"`    // "A5191"
	SiteName  string  `json:"siteName,omitempty"` // the system it belongs to, where it has one
	PowerKw   float64 `json:"powerKw,omitempty"`
	Connector string  `json:"connector,omitempty"`

	// Provider links this charger to the service it was imported from — the
	// charger-provider name ("anker-solix") plus that provider's own id for it
	// (the serial). Both blank for one added by hand. Set by the import endpoint
	// only, never by an ordinary edit, so a rename cannot break the link.
	Provider          string `json:"provider,omitempty"`
	ProviderChargerID string `json:"providerChargerId,omitempty"`

	Created string `json:"created,omitempty"`
}

// ServiceRecord is one row of the Service log for a car.
type ServiceRecord struct {
	ID   string    `json:"id"`
	Car  string    `json:"car"`  // relation -> Car.ID
	Date time.Time `json:"date"` // service date (Excel col A)
	Km   int       `json:"km"`   // odometer at service (Excel col B)

	// "Changed Parts" checkboxes (Excel cols E/F/G).
	ChangedOil             bool `json:"changedOil"`             // Oil & Oil Filter
	ChangedEngineAirFilter bool `json:"changedEngineAirFilter"` // Engine Air Filter
	ChangedCabinAirFilter  bool `json:"changedCabinAirFilter"`  // Cabin Air Filter

	Notes string `json:"notes,omitempty"`

	// The workshop receipt or stamped service-book page for this visit.
	Attachment

	// Derived (not stored): filled in by the API on read.
	NextServiceDate *time.Time `json:"nextServiceDate,omitempty"` // Excel col C
	NextServiceKm   *int       `json:"nextServiceKm,omitempty"`   // Excel col D

	Created string `json:"created,omitempty"`
	Updated string `json:"updated,omitempty"`
}

// TechnicalCheck is one mandatory roadworthiness inspection in a car's history —
// przegląd techniczny, MOT, TÜV, contrôle technique, depending on where the car
// is registered.
//
// It is shaped like a ServiceRecord, but recurs on time alone: an inspection
// falls due on a date whatever the odometer says. The cycle is a legal one
// rather than a property of the car, and it is not constant — a new car's first
// check falls due years later than its third — so the car's interval is only a
// default, and ValidUntil overrides it per record.
type TechnicalCheck struct {
	ID   string    `json:"id"`
	Car  string    `json:"car"`  // relation -> Car.ID
	Date time.Time `json:"date"` // date of the inspection

	Result  string  `json:"result"` // passed | failed
	Cost    float64 `json:"cost"`
	Station string  `json:"station,omitempty"` // inspection station / inspector
	Notes   string  `json:"notes,omitempty"`

	// ValidUntil is the expiry printed on the certificate. When set it wins over
	// the car's interval, because it is the date that actually governs.
	ValidUntil *time.Time `json:"validUntil,omitempty"`

	// The certificate itself.
	Attachment

	// Derived (not stored): filled in by the API on read.
	NextCheckDate *time.Time       `json:"nextCheckDate,omitempty"`
	Expiry        ExpiryAssessment `json:"expiry"`

	Created string `json:"created,omitempty"`
	Updated string `json:"updated,omitempty"`
}

// Part is one entry in a car's parts catalog (Excel cols M/N).
type Part struct {
	ID         string `json:"id"`
	Car        string `json:"car"`        // relation -> Car.ID
	Name       string `json:"name"`       // e.g. "Oil Filter"
	PartNumber string `json:"partNumber"` // e.g. "04152-YZZA7"
	Category   string `json:"category"`   // optional: oil|filter|wiper|other
	Notes      string `json:"notes,omitempty"`

	// A photo of the box, or the spec sheet for the part.
	Attachment

	Created string `json:"created,omitempty"`
	Updated string `json:"updated,omitempty"`
}

// FuelEntry is one refuelling stop for a car.
//
// Efficiency is derived by the full-tank method rather than stored: a tank
// filled to the brim is a known reference point, so the fuel burned between two
// consecutive full tanks is exactly what was poured in over that span. Partial
// fills in between are not measurable on their own — they roll into the next
// full tank's window. See ComputeFuelDerived.
type FuelEntry struct {
	ID     string    `json:"id"`
	Car    string    `json:"car"`  // relation -> Car.ID
	Date   time.Time `json:"date"` // date of the refill
	Km     int       `json:"km"`   // odometer at the pump
	Liters float64   `json:"liters"`
	Cost   float64   `json:"cost"` // total paid for this fill

	// FullTank marks a fill to the brim — the reference point efficiency windows
	// are measured between.
	FullTank bool `json:"fullTank"`

	// MissedFill records that a refill happened before this one without being
	// logged. The odometer span is then not accounted for by the litres on
	// record, so any window containing it is left uncomputed rather than
	// reported as an implausibly good figure.
	MissedFill bool `json:"missedFill"`

	Station string `json:"station,omitempty"`
	Notes   string `json:"notes,omitempty"`

	// The pump receipt.
	Attachment

	// Derived (not stored): filled in by the API on read.
	PricePerLiter   *float64 `json:"pricePerLiter,omitempty"`
	DistanceKm      *int     `json:"distanceKm,omitempty"`      // since the previous full tank
	LitersUsed      *float64 `json:"litersUsed,omitempty"`      // litres burned over that distance
	ConsumptionL100 *float64 `json:"consumptionL100,omitempty"` // litres per 100 km
	KmPerLiter      *float64 `json:"kmPerLiter,omitempty"`
	CostPerKm       *float64 `json:"costPerKm,omitempty"`

	Created string `json:"created,omitempty"`
	Updated string `json:"updated,omitempty"`
}

// FuelStats summarises a car's whole refill history.
type FuelStats struct {
	Entries     int     `json:"entries"`
	TotalLiters float64 `json:"totalLiters"`
	TotalCost   float64 `json:"totalCost"`

	// TrackedDistanceKm is the distance covered by computable full-tank windows,
	// which is less than the odometer span whenever the history starts or ends
	// on a partial fill. The averages below describe exactly this distance.
	TrackedDistanceKm int `json:"trackedDistanceKm"`

	AvgConsumptionL100   *float64 `json:"avgConsumptionL100,omitempty"`
	BestConsumptionL100  *float64 `json:"bestConsumptionL100,omitempty"`
	WorstConsumptionL100 *float64 `json:"worstConsumptionL100,omitempty"`
	AvgKmPerLiter        *float64 `json:"avgKmPerLiter,omitempty"`
	AvgPricePerLiter     *float64 `json:"avgPricePerLiter,omitempty"`
	CostPerKm            *float64 `json:"costPerKm,omitempty"`

	FirstDate *time.Time `json:"firstDate,omitempty"`
	LastDate  *time.Time `json:"lastDate,omitempty"`
}

// ChargingSession is one charge of an electric car — the EV counterpart of
// FuelEntry, and it works the same way: energy and money are recorded, and the
// efficiency is derived by the same reference-point method (see
// ComputeChargingDerived).
//
// A charge to the car's usual full point plays the role of the full tank. It has
// to, for the same reason: a session only says how much energy went in, not how
// much was left in the battery, so the distance a given number of kWh covered is
// only knowable between two charges that ended at the same state.
type ChargingSession struct {
	ID   string    `json:"id"`
	Car  string    `json:"car"`  // relation -> Car.ID
	Date time.Time `json:"date"` // date of the charge
	Km   int       `json:"km"`   // odometer when plugging in
	Kwh  float64   `json:"kwh"`  // energy delivered
	Cost float64   `json:"cost"` // total paid for this charge

	// FullCharge marks a charge taken to the car's usual full point — the
	// reference the efficiency windows are measured between.
	FullCharge bool `json:"fullCharge"`

	// MissedSession records that the car was charged before this without being
	// logged — a top-up at a friend's socket, say. The odometer span is then not
	// accounted for by the kWh on record, so any window containing it is left
	// uncomputed rather than reported as implausibly efficient.
	MissedSession bool `json:"missedSession"`

	// Location is where it was charged ("Home", "Ionity Køge"). Free text: an
	// operator list would go stale and this is only ever read by a person.
	Location string `json:"location,omitempty"`
	Notes    string `json:"notes,omitempty"`

	// The charge point's receipt.
	Attachment

	// Derived (not stored): filled in by the API on read.
	PricePerKwh       *float64 `json:"pricePerKwh,omitempty"`
	DistanceKm        *int     `json:"distanceKm,omitempty"`        // since the previous full charge
	KwhUsed           *float64 `json:"kwhUsed,omitempty"`           // energy used over that distance
	ConsumptionKwh100 *float64 `json:"consumptionKwh100,omitempty"` // kWh per 100 km
	KmPerKwh          *float64 `json:"kmPerKwh,omitempty"`
	CostPerKm         *float64 `json:"costPerKm,omitempty"`

	Created string `json:"created,omitempty"`
	Updated string `json:"updated,omitempty"`
}

// ChargingStats summarises a car's whole charging history.
type ChargingStats struct {
	Entries   int     `json:"entries"`
	TotalKwh  float64 `json:"totalKwh"`
	TotalCost float64 `json:"totalCost"`

	// TrackedDistanceKm is the distance covered by computable windows, which is
	// less than the odometer span whenever the history starts or ends on a
	// partial charge. The averages below describe exactly this distance.
	TrackedDistanceKm int `json:"trackedDistanceKm"`

	AvgConsumptionKwh100   *float64 `json:"avgConsumptionKwh100,omitempty"`
	BestConsumptionKwh100  *float64 `json:"bestConsumptionKwh100,omitempty"`
	WorstConsumptionKwh100 *float64 `json:"worstConsumptionKwh100,omitempty"`
	AvgKmPerKwh            *float64 `json:"avgKmPerKwh,omitempty"`
	AvgPricePerKwh         *float64 `json:"avgPricePerKwh,omitempty"`
	CostPerKm              *float64 `json:"costPerKm,omitempty"`

	FirstDate *time.Time `json:"firstDate,omitempty"`
	LastDate  *time.Time `json:"lastDate,omitempty"`
}

// MaintenanceEntry is one workshop visit or repair — work done on the car
// outside the routine service schedule (which lives in ServiceRecord). A broken
// alternator replaced at a garage belongs here; the annual oil change does not.
type MaintenanceEntry struct {
	ID   string    `json:"id"`
	Car  string    `json:"car"`  // relation -> Car.ID
	Date time.Time `json:"date"` // date of the visit
	Km   int       `json:"km"`   // odometer at the visit

	Type        string `json:"type"`        // repair|inspection|bodywork|tyres|diagnostics|recall|warranty|other
	Status      string `json:"status"`      // scheduled|in_progress|completed
	Workshop    string `json:"workshop"`    // garage/workshop name
	Location    string `json:"location"`    // optional: city or address
	Description string `json:"description"` // what was done
	PartsUsed   string `json:"partsUsed"`   // free-text list of parts replaced

	LaborCost float64 `json:"laborCost"`
	PartsCost float64 `json:"partsCost"`

	InvoiceNumber string     `json:"invoiceNumber,omitempty"`
	WarrantyUntil *time.Time `json:"warrantyUntil,omitempty"`
	Notes         string     `json:"notes,omitempty"`

	// The workshop's invoice.
	Attachment

	// Derived (not stored): filled in by the API on read.
	TotalCost        float64 `json:"totalCost"`
	WarrantyActive   *bool   `json:"warrantyActive,omitempty"`
	WarrantyDaysLeft *int    `json:"warrantyDaysLeft,omitempty"`

	Created string `json:"created,omitempty"`
	Updated string `json:"updated,omitempty"`
}

// CarDocument is a piece of paperwork tied to a car — insurance policies,
// pollution/emissions certificates, registration papers, and so on. The renewal
// date is the point of the whole record: an expired policy is a car that cannot
// legally be driven, so Expiry is computed live on every read.
type CarDocument struct {
	ID    string `json:"id"`
	Car   string `json:"car"`   // relation -> Car.ID
	Type  string `json:"type"`  // insurance|pollution|registration|inspection|roadTax|warranty|other
	Title string `json:"title"` // e.g. "Third-party liability 2026"

	Provider  string `json:"provider,omitempty"`  // insurer / issuing authority
	Reference string `json:"reference,omitempty"` // policy or certificate number

	IssueDate  *time.Time `json:"issueDate,omitempty"`
	ExpiryDate *time.Time `json:"expiryDate,omitempty"` // blank = never expires
	Cost       float64    `json:"cost"`
	Notes      string     `json:"notes,omitempty"`

	// The scan or photo of the paperwork itself.
	Attachment

	// Derived (not stored): filled in by the API on read.
	Expiry ExpiryAssessment `json:"expiry"`

	Created string `json:"created,omitempty"`
	Updated string `json:"updated,omitempty"`
}

// ExpiryAssessment is the server-computed lifecycle state of a dated document.
type ExpiryAssessment struct {
	State string `json:"state"`           // no_expiry | valid | expiring_soon | expired
	Days  *int   `json:"daysUntilExpiry"` // nil when there is no expiry date
}

// Reminder is something the user wants to be told about: a booked workshop slot,
// an insurance renewal, a tyre swap. A reminder fires on a date, an odometer
// reading, or both — whichever comes first.
type Reminder struct {
	ID    string `json:"id"`
	Car   string `json:"car"` // relation -> Car.ID
	Title string `json:"title"`
	Type  string `json:"type"` // maintenance|document|service|inspection|other

	DueDate *time.Time `json:"dueDate,omitempty"`
	DueKm   int        `json:"dueKm,omitempty"`

	// RepeatDays/RepeatKm turn a reminder into a recurring one: completing it
	// rolls the trigger forward by this much instead of closing it out.
	RepeatDays int `json:"repeatDays,omitempty"`
	RepeatKm   int `json:"repeatKm,omitempty"`

	Done   bool       `json:"done"`
	DoneAt *time.Time `json:"doneAt,omitempty"`
	Notes  string     `json:"notes,omitempty"`

	// Derived (not stored): filled in by the API on read.
	Status    string `json:"status"`              // done | overdue | due_soon | upcoming | no_trigger
	DaysLeft  *int   `json:"daysLeft,omitempty"`  // nil when there is no due date
	KmLeft    *int   `json:"kmLeft,omitempty"`    // nil when there is no due km / no odometer
	Auto      bool   `json:"auto"`                // true = derived from a document/service, read-only
	SourceRef string `json:"sourceRef,omitempty"` // id of the record an auto reminder came from

	Created string `json:"created,omitempty"`
	Updated string `json:"updated,omitempty"`
}

// User is the authenticated account's profile, covering the Settings panel's
// Account/Profile/Appearance sections.
type User struct {
	ID       string `json:"id"`
	Email    string `json:"email"`
	Verified bool   `json:"verified"`
	Name     string `json:"name"`
	Bio      string `json:"bio"`

	HasAvatar bool `json:"hasAvatar"`

	Theme      string `json:"theme"`      // light | dark | system
	Locale     string `json:"locale"`     // e.g. "en-US"
	DateFormat string `json:"dateFormat"` // YMD | DMY | MDY
	TimeFormat string `json:"timeFormat"` // auto (the region's own) | 24 | 12
	Currency   string `json:"currency"`   // ISO 4217 code, e.g. "EUR"
	FontSize   string `json:"fontSize"`   // small | medium | large
	Role       string `json:"role"`       // user | admin

	// DragLocked holds every arrangement on this account's pages still: the
	// garage, a car's tabs, its Information rows, the provider's readings. A
	// guard against nudging a layout while reading it, not a permission — it is
	// the user's own setting and says nothing about what they may edit. False
	// (draggable) on an account that has never set it.
	DragLocked bool `json:"dragLocked"`

	// Organization membership. Empty when the user belongs to no organization —
	// the clients use that to offer creating one (which makes them its admin).
	Organization     string `json:"organization"`
	OrganizationName string `json:"organizationName,omitempty"`

	// CarOrder is the garage arrangement: car ids in the order this user dragged
	// them into. The car list is already returned in this order, so a client only
	// needs it to send an updated arrangement back.
	CarOrder         []string `json:"carOrder"`
	ChargerTabOrder  []string `json:"chargerTabOrder"`
	ChargerCardOrder []string `json:"chargerCardOrder"`

	// DefaultTabs is which tab each tabbed page opens on, keyed by page:
	// {"charging": "home", "car": "info", "settings": "personal"}. A page absent
	// from the map opens on whichever tab leads its bar — the arrangement above —
	// so this only carries the cases where landing tab and reading order differ.
	DefaultTabs map[string]string `json:"defaultTabs"`

	// Non-empty while an account-deletion request is pending its cooldown.
	DeletionRequestedAt *time.Time `json:"deletionRequestedAt,omitempty"`

	Created string `json:"created,omitempty"`
}

// Session is one active login (device) for the current user.
type Session struct {
	ID          string    `json:"id"`
	DeviceLabel string    `json:"deviceLabel"`
	IP          string    `json:"ip"`
	Current     bool      `json:"current"`
	Created     time.Time `json:"created"`
	ExpiresAt   time.Time `json:"expiresAt"`
}

// ComputeDerived fills NextServiceDate / NextServiceKm from the car's intervals,
// reproducing the spreadsheet formulas. Intervals of 0 fall back to the
// spreadsheet defaults (365 days, 15000 km).
//
// Note the asymmetry in what counts as "no reading": a zero date is genuinely
// absent, but a zero odometer is a reading. A car collected new sits at 0 km
// and its first service is still due 15000 km later, so 0 has to produce a
// next-due figure like any other number would.
func (r *ServiceRecord) ComputeDerived(c *Car) {
	days := c.ServiceIntervalDays
	if days <= 0 {
		days = 365
	}
	km := c.ServiceIntervalKm
	if km <= 0 {
		km = 15000
	}
	if !r.Date.IsZero() {
		d := r.Date.AddDate(0, 0, days)
		r.NextServiceDate = &d
	}
	if r.Km >= 0 {
		n := r.Km + km
		r.NextServiceKm = &n
	}
}

// DefaultTechnicalCheckIntervalDays is the annual cycle most of Europe settles
// into once a car is a few years old. It is only a starting point: see
// Car.TechnicalCheckIntervalDays.
const DefaultTechnicalCheckIntervalDays = 365

// ComputeTechnicalCheckDerived fills NextCheckDate and the expiry assessment.
//
// The date on the certificate wins over the car's interval when present; the
// interval only covers records entered without one. A failed inspection
// certifies nothing, so it yields no next date at all — reading one off a
// failure would put a reassuring "valid until" on a car that just flunked.
func (t *TechnicalCheck) ComputeTechnicalCheckDerived(c *Car, now time.Time) {
	t.NextCheckDate = nil
	t.Expiry = ExpiryAssessment{State: "no_expiry"}
	if t.Result == "failed" {
		return
	}

	switch {
	case t.ValidUntil != nil && !t.ValidUntil.IsZero():
		d := *t.ValidUntil
		t.NextCheckDate = &d
	case !t.Date.IsZero():
		days := DefaultTechnicalCheckIntervalDays
		if c != nil && c.TechnicalCheckIntervalDays > 0 {
			days = c.TechnicalCheckIntervalDays
		}
		d := t.Date.AddDate(0, 0, days)
		t.NextCheckDate = &d
	default:
		return
	}

	days := daysBetween(now, *t.NextCheckDate)
	state := "valid"
	switch {
	case days < 0:
		state = "expired"
	case days <= SoonDays:
		state = "expiring_soon"
	}
	t.Expiry = ExpiryAssessment{State: state, Days: &days}
}

// SoonDays is the window within which an upcoming expiry or reminder is
// surfaced as "due soon" rather than merely upcoming.
const SoonDays = 30

// soonKm mirrors SoonDays for odometer-triggered reminders.
const soonKm = 1000

// ComputeFuelDerived fills the derived efficiency fields on a car's refill
// history. `entries` must be ordered oldest-first by odometer.
//
// The full-tank method: between two consecutive full tanks the car burned
// exactly the fuel added over that span, because both endpoints are the same
// known level. Everything poured in after the earlier full tank up to and
// including the later one counts, which is what folds partial fills into the
// window that closes them. A window is left uncomputed when a fill inside it is
// flagged MissedFill, when the odometer did not advance, or when no litres were
// recorded — reporting a figure there would be fiction.
func ComputeFuelDerived(entries []FuelEntry) {
	for i := range entries {
		if entries[i].Liters > 0 && entries[i].Cost > 0 {
			p := entries[i].Cost / entries[i].Liters
			entries[i].PricePerLiter = &p
		}
	}

	lastFull := -1
	for i := range entries {
		if !entries[i].FullTank {
			continue
		}
		if lastFull < 0 {
			// First full tank: nothing before it to measure against.
			lastFull = i
			continue
		}

		dist := entries[i].Km - entries[lastFull].Km
		liters, cost := 0.0, 0.0
		usable := true
		for j := lastFull + 1; j <= i; j++ {
			if entries[j].MissedFill {
				usable = false
			}
			liters += entries[j].Liters
			cost += entries[j].Cost
		}

		if usable && dist > 0 && liters > 0 {
			d, l := dist, liters
			entries[i].DistanceKm = &d
			entries[i].LitersUsed = &l

			l100 := liters / float64(dist) * 100
			entries[i].ConsumptionL100 = &l100

			kmpl := float64(dist) / liters
			entries[i].KmPerLiter = &kmpl

			if cost > 0 {
				cpk := cost / float64(dist)
				entries[i].CostPerKm = &cpk
			}
		}
		lastFull = i
	}
}

// ComputeFuelStats summarises a refill history whose derived fields have already
// been filled in by ComputeFuelDerived. `entries` must be ordered oldest-first.
//
// Averages are distance-weighted — total litres over total distance across every
// computable window — rather than a mean of the per-window figures, so a long
// motorway run counts for more than a short trip across town, which is what
// actually happened to the fuel.
func ComputeFuelStats(entries []FuelEntry) FuelStats {
	st := FuelStats{Entries: len(entries)}
	if len(entries) == 0 {
		return st
	}

	var windowLiters, windowCost float64
	for i := range entries {
		e := &entries[i]
		st.TotalLiters += e.Liters
		st.TotalCost += e.Cost

		if e.ConsumptionL100 == nil {
			continue
		}
		st.TrackedDistanceKm += *e.DistanceKm
		windowLiters += *e.LitersUsed
		if e.CostPerKm != nil {
			windowCost += *e.CostPerKm * float64(*e.DistanceKm)
		}
		if st.BestConsumptionL100 == nil || *e.ConsumptionL100 < *st.BestConsumptionL100 {
			v := *e.ConsumptionL100
			st.BestConsumptionL100 = &v
		}
		if st.WorstConsumptionL100 == nil || *e.ConsumptionL100 > *st.WorstConsumptionL100 {
			v := *e.ConsumptionL100
			st.WorstConsumptionL100 = &v
		}
	}

	if st.TrackedDistanceKm > 0 && windowLiters > 0 {
		avg := windowLiters / float64(st.TrackedDistanceKm) * 100
		st.AvgConsumptionL100 = &avg
		kmpl := float64(st.TrackedDistanceKm) / windowLiters
		st.AvgKmPerLiter = &kmpl
		if windowCost > 0 {
			cpk := windowCost / float64(st.TrackedDistanceKm)
			st.CostPerKm = &cpk
		}
	}
	if st.TotalLiters > 0 && st.TotalCost > 0 {
		ppl := st.TotalCost / st.TotalLiters
		st.AvgPricePerLiter = &ppl
	}

	first, last := entries[0].Date, entries[len(entries)-1].Date
	if !first.IsZero() {
		st.FirstDate = &first
	}
	if !last.IsZero() {
		st.LastDate = &last
	}
	return st
}

// ComputeChargingDerived fills the derived efficiency fields on a car's charging
// history. `entries` must be ordered oldest-first by odometer.
//
// The same window method as ComputeFuelDerived, with charges to the usual full
// point as the endpoints: between two of them the car used exactly the energy
// put in over that span, since both ends are the same battery state. Partial
// charges in between fold into the window that closes them. A window is left
// uncomputed when a session inside it is flagged MissedSession, when the
// odometer did not advance, or when no energy was recorded.
func ComputeChargingDerived(entries []ChargingSession) {
	for i := range entries {
		if entries[i].Kwh > 0 && entries[i].Cost > 0 {
			p := entries[i].Cost / entries[i].Kwh
			entries[i].PricePerKwh = &p
		}
	}

	lastFull := -1
	for i := range entries {
		if !entries[i].FullCharge {
			continue
		}
		if lastFull < 0 {
			// First full charge: nothing before it to measure against.
			lastFull = i
			continue
		}

		dist := entries[i].Km - entries[lastFull].Km
		kwh, cost := 0.0, 0.0
		usable := true
		for j := lastFull + 1; j <= i; j++ {
			if entries[j].MissedSession {
				usable = false
			}
			kwh += entries[j].Kwh
			cost += entries[j].Cost
		}

		if usable && dist > 0 && kwh > 0 {
			d, k := dist, kwh
			entries[i].DistanceKm = &d
			entries[i].KwhUsed = &k

			kwh100 := kwh / float64(dist) * 100
			entries[i].ConsumptionKwh100 = &kwh100

			kmpkwh := float64(dist) / kwh
			entries[i].KmPerKwh = &kmpkwh

			if cost > 0 {
				cpk := cost / float64(dist)
				entries[i].CostPerKm = &cpk
			}
		}
		lastFull = i
	}
}

// ComputeChargingStats summarises a charging history whose derived fields have
// already been filled in by ComputeChargingDerived. `entries` must be ordered
// oldest-first.
//
// Averages are distance-weighted, as with fuel: total energy over total distance
// across every computable window, so a long motorway run counts for more than a
// short trip across town — which is what actually happened to the battery.
func ComputeChargingStats(entries []ChargingSession) ChargingStats {
	st := ChargingStats{Entries: len(entries)}
	if len(entries) == 0 {
		return st
	}

	var windowKwh, windowCost float64
	for i := range entries {
		e := &entries[i]
		st.TotalKwh += e.Kwh
		st.TotalCost += e.Cost

		if e.ConsumptionKwh100 == nil {
			continue
		}
		st.TrackedDistanceKm += *e.DistanceKm
		windowKwh += *e.KwhUsed
		if e.CostPerKm != nil {
			windowCost += *e.CostPerKm * float64(*e.DistanceKm)
		}
		if st.BestConsumptionKwh100 == nil || *e.ConsumptionKwh100 < *st.BestConsumptionKwh100 {
			v := *e.ConsumptionKwh100
			st.BestConsumptionKwh100 = &v
		}
		if st.WorstConsumptionKwh100 == nil || *e.ConsumptionKwh100 > *st.WorstConsumptionKwh100 {
			v := *e.ConsumptionKwh100
			st.WorstConsumptionKwh100 = &v
		}
	}

	if st.TrackedDistanceKm > 0 && windowKwh > 0 {
		avg := windowKwh / float64(st.TrackedDistanceKm) * 100
		st.AvgConsumptionKwh100 = &avg
		kmpkwh := float64(st.TrackedDistanceKm) / windowKwh
		st.AvgKmPerKwh = &kmpkwh
		if windowCost > 0 {
			cpk := windowCost / float64(st.TrackedDistanceKm)
			st.CostPerKm = &cpk
		}
	}
	if st.TotalKwh > 0 && st.TotalCost > 0 {
		ppk := st.TotalCost / st.TotalKwh
		st.AvgPricePerKwh = &ppk
	}

	first, last := entries[0].Date, entries[len(entries)-1].Date
	if !first.IsZero() {
		st.FirstDate = &first
	}
	if !last.IsZero() {
		st.LastDate = &last
	}
	return st
}

// ComputeMaintenanceDerived fills the derived cost and warranty fields.
func (m *MaintenanceEntry) ComputeMaintenanceDerived(now time.Time) {
	m.TotalCost = m.LaborCost + m.PartsCost
	if m.WarrantyUntil == nil || m.WarrantyUntil.IsZero() {
		return
	}
	days := daysBetween(now, *m.WarrantyUntil)
	active := days >= 0
	m.WarrantyActive = &active
	m.WarrantyDaysLeft = &days
}

// ComputeExpiry classifies a document by its expiry date relative to `now`.
func (d *CarDocument) ComputeExpiry(now time.Time) {
	if d.ExpiryDate == nil || d.ExpiryDate.IsZero() {
		d.Expiry = ExpiryAssessment{State: "no_expiry"}
		return
	}
	days := daysBetween(now, *d.ExpiryDate)
	state := "valid"
	switch {
	case days < 0:
		state = "expired"
	case days <= SoonDays:
		state = "expiring_soon"
	}
	d.Expiry = ExpiryAssessment{State: state, Days: &days}
}

// ComputeReminderDerived resolves a reminder's status against today's date and
// the car's current odometer. A reminder with both triggers fires on whichever
// arrives first, so the worse of the two signals wins.
func (r *Reminder) ComputeReminderDerived(now time.Time, currentKm int) {
	if r.Done {
		r.Status = "done"
		return
	}

	rank := map[string]int{"no_trigger": 0, "upcoming": 1, "due_soon": 2, "overdue": 3}
	status := "no_trigger"
	worsen := func(s string) {
		if rank[s] > rank[status] {
			status = s
		}
	}

	if r.DueDate != nil && !r.DueDate.IsZero() {
		days := daysBetween(now, *r.DueDate)
		r.DaysLeft = &days
		switch {
		case days < 0:
			worsen("overdue")
		case days <= SoonDays:
			worsen("due_soon")
		default:
			worsen("upcoming")
		}
	}

	// currentKm == 0 is a real odometer, not a missing one — see ComputeDerived.
	// Without this a brand-new car's first service reminder shows a due date but
	// never the distance left to run.
	if r.DueKm > 0 && currentKm >= 0 {
		left := r.DueKm - currentKm
		r.KmLeft = &left
		switch {
		case left < 0:
			worsen("overdue")
		case left <= soonKm:
			worsen("due_soon")
		default:
			worsen("upcoming")
		}
	}

	r.Status = status
}

// daysBetween returns whole days from `now` to `target`, both truncated to the
// day, so a deadline later today reads as 0 rather than a fraction.
func daysBetween(now, target time.Time) int {
	today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
	t := time.Date(target.Year(), target.Month(), target.Day(), 0, 0, 0, 0, time.UTC)
	return int(t.Sub(today).Hours() / 24)
}
