package api

import (
	"encoding/json"
	"strings"
	"time"

	"drivervault/apiserver/internal/models"
)

// PocketBase stores datetimes as e.g. "2015-06-12 00:00:00.000Z". These layouts
// are tried (in order) when parsing values coming back from PocketBase.
var pbDateLayouts = []string{
	"2006-01-02 15:04:05.000Z",
	"2006-01-02 15:04:05Z",
	time.RFC3339,
	"2006-01-02",
}

func parsePBDate(s string) time.Time {
	s = strings.TrimSpace(s)
	if s == "" {
		return time.Time{}
	}
	for _, l := range pbDateLayouts {
		if t, err := time.Parse(l, s); err == nil {
			return t
		}
	}
	return time.Time{}
}

// formatPBDate renders a date in the format PocketBase expects on write.
func formatPBDate(t time.Time) string {
	if t.IsZero() {
		return ""
	}
	return t.UTC().Format("2006-01-02 15:04:05.000Z")
}

// --- cars ---

// carRecord is the PocketBase-facing shape of a car (snake_case fields).
type carRecord struct {
	ID                         string `json:"id"`
	Name                       string `json:"name"`
	Make                       string `json:"make"`
	Model                      string `json:"model"`
	Year                       int    `json:"year"`
	Registration               string `json:"registration"`
	RegistrationCountry        string `json:"registration_country"`
	VIN                        string `json:"vin"`
	ServiceIntervalDays        int    `json:"service_interval_days"`
	ServiceIntervalKm          int    `json:"service_interval_km"`
	TechnicalCheckIntervalDays int    `json:"technical_check_interval_days"`
	OilSpec                    string `json:"oil_spec"`
	TransmissionOilSpec        string `json:"transmission_oil_spec"`
	DifferentialOilSpec        string `json:"differential_oil_spec"`
	BrakeFluidSpec             string `json:"brake_fluid_spec"`
	CoolantSpec                string `json:"coolant_spec"`
	CurrentKm                  int    `json:"current_km"`
	FuelType                   string `json:"fuel_type"`
	BuildDate                  string `json:"build_date"`
	FirstRegistrationDate      string `json:"first_registration_date"`
	Provider                   string `json:"provider"`
	ProviderVehicleID          string `json:"provider_vehicle_id"`
	Owner                      string `json:"owner"`
	Created                    string `json:"created"`
	Updated                    string `json:"updated"`

	// Switched-off tabs, Information fields, Service history columns and service
	// parts, plus
	// the arrangements of the tabs, the Information rows, those columns and the
	// connected service's readings. Raw
	// because PocketBase hands back whatever a json field holds — null on a car
	// nobody has configured — which is not a []string.
	HiddenTabs           json.RawMessage `json:"hidden_tabs"`
	HiddenFields         json.RawMessage `json:"hidden_fields"`
	HiddenServiceColumns json.RawMessage `json:"hidden_service_columns"`
	HiddenServiceParts   json.RawMessage `json:"hidden_service_parts"`
	TabOrder             json.RawMessage `json:"tab_order"`
	FieldOrder           json.RawMessage `json:"field_order"`
	ServiceColumnOrder   json.RawMessage `json:"service_column_order"`
	MetricOrder          json.RawMessage `json:"metric_order"`
}

func (rec carRecord) toModel() models.Car {
	return models.Car{
		ID:                         rec.ID,
		Name:                       rec.Name,
		Make:                       rec.Make,
		Model:                      rec.Model,
		Year:                       rec.Year,
		Registration:               rec.Registration,
		RegistrationCountry:        rec.RegistrationCountry,
		VIN:                        rec.VIN,
		ServiceIntervalDays:        rec.ServiceIntervalDays,
		ServiceIntervalKm:          rec.ServiceIntervalKm,
		TechnicalCheckIntervalDays: rec.TechnicalCheckIntervalDays,
		OilSpec:                    rec.OilSpec,
		TransmissionOilSpec:        rec.TransmissionOilSpec,
		DifferentialOilSpec:        rec.DifferentialOilSpec,
		BrakeFluidSpec:             rec.BrakeFluidSpec,
		CoolantSpec:                rec.CoolantSpec,
		CurrentKm:                  rec.CurrentKm,
		FuelType:                   rec.FuelType,
		BuildDate:                  rec.BuildDate,
		FirstRegistrationDate:      rec.FirstRegistrationDate,
		Provider:                   rec.Provider,
		ProviderVehicleID:          rec.ProviderVehicleID,
		HiddenTabs:                 decodeStringList(rec.HiddenTabs),
		HiddenFields:               decodeStringList(rec.HiddenFields),
		HiddenServiceColumns:       decodeStringList(rec.HiddenServiceColumns),
		HiddenServiceParts:         decodeStringList(rec.HiddenServiceParts),
		TabOrder:                   decodeStringList(rec.TabOrder),
		FieldOrder:                 decodeStringList(rec.FieldOrder),
		ServiceColumnOrder:         decodeStringList(rec.ServiceColumnOrder),
		MetricOrder:                decodeStringList(rec.MetricOrder),
		Owner:                      rec.Owner,
		Created:                    rec.Created,
		Updated:                    rec.Updated,
	}
}

// carPayload builds the write payload for create/update from a domain Car. It
// deliberately omits owner and the provider link: a car edit must not reassign
// ownership, and it must not touch the connected-service link either (that is
// carProviderPayload's job, reached only through the provider endpoints).
func carPayload(c models.Car) map[string]any {
	return map[string]any{
		"name":                          c.Name,
		"make":                          c.Make,
		"model":                         c.Model,
		"year":                          c.Year,
		"registration":                  c.Registration,
		"registration_country":          c.RegistrationCountry,
		"vin":                           c.VIN,
		"service_interval_days":         c.ServiceIntervalDays,
		"service_interval_km":           c.ServiceIntervalKm,
		"technical_check_interval_days": c.TechnicalCheckIntervalDays,
		"oil_spec":                      c.OilSpec,
		"transmission_oil_spec":         c.TransmissionOilSpec,
		"differential_oil_spec":         c.DifferentialOilSpec,
		"brake_fluid_spec":              c.BrakeFluidSpec,
		"coolant_spec":                  c.CoolantSpec,
		"current_km":                    c.CurrentKm,
		"fuel_type":                     c.FuelType,
		"build_date":                    c.BuildDate,
		"first_registration_date":       c.FirstRegistrationDate,
	}
}

// carProviderPayload is the connected-service link on its own, so linking and
// unlinking is a one-field write that leaves the rest of the car alone. An empty
// provider clears both fields (unlink).
func carProviderPayload(provider, vehicleID string) map[string]any {
	if provider == "" {
		return map[string]any{"provider": "", "provider_vehicle_id": ""}
	}
	return map[string]any{"provider": provider, "provider_vehicle_id": vehicleID}
}

// --- service records ---

type serviceRecord struct {
	ID                     string `json:"id"`
	Car                    string `json:"car"`
	Date                   string `json:"date"`
	Km                     int    `json:"km"`
	ChangedOil             bool   `json:"changed_oil"`
	ChangedEngineAirFilter bool   `json:"changed_engine_air_filter"`
	ChangedCabinAirFilter  bool   `json:"changed_cabin_air_filter"`
	Notes                  string `json:"notes"`
	File                   string `json:"file"`
	Created                string `json:"created"`
	Updated                string `json:"updated"`
}

func (rec serviceRecord) toModel() models.ServiceRecord {
	return models.ServiceRecord{
		ID:                     rec.ID,
		Car:                    rec.Car,
		Date:                   parsePBDate(rec.Date),
		Km:                     rec.Km,
		ChangedOil:             rec.ChangedOil,
		ChangedEngineAirFilter: rec.ChangedEngineAirFilter,
		ChangedCabinAirFilter:  rec.ChangedCabinAirFilter,
		Notes:                  rec.Notes,
		Attachment:             attachmentOf(rec.File),
		Created:                rec.Created,
		Updated:                rec.Updated,
	}
}

// servicePayload omits the file field — see attachmentOf.
func servicePayload(r models.ServiceRecord) map[string]any {
	return map[string]any{
		"car":                       r.Car,
		"date":                      formatPBDate(r.Date),
		"km":                        r.Km,
		"changed_oil":               r.ChangedOil,
		"changed_engine_air_filter": r.ChangedEngineAirFilter,
		"changed_cabin_air_filter":  r.ChangedCabinAirFilter,
		"notes":                     r.Notes,
	}
}

// --- technical checks ---

type technicalCheckRecord struct {
	ID         string  `json:"id"`
	Car        string  `json:"car"`
	Date       string  `json:"date"`
	Result     string  `json:"result"`
	Cost       float64 `json:"cost"`
	Station    string  `json:"station"`
	ValidUntil string  `json:"valid_until"`
	Notes      string  `json:"notes"`
	File       string  `json:"file"`
	Created    string  `json:"created"`
	Updated    string  `json:"updated"`
}

func (rec technicalCheckRecord) toModel() models.TechnicalCheck {
	return models.TechnicalCheck{
		ID:         rec.ID,
		Car:        rec.Car,
		Date:       parsePBDate(rec.Date),
		Result:     rec.Result,
		Cost:       rec.Cost,
		Station:    rec.Station,
		ValidUntil: parsePBDatePtr(rec.ValidUntil),
		Notes:      rec.Notes,
		Attachment: attachmentOf(rec.File),
		Created:    rec.Created,
		Updated:    rec.Updated,
	}
}

// technicalCheckPayload omits the file field — see attachmentOf.
func technicalCheckPayload(t models.TechnicalCheck) map[string]any {
	return map[string]any{
		"car":         t.Car,
		"date":        formatPBDate(t.Date),
		"result":      t.Result,
		"cost":        t.Cost,
		"station":     t.Station,
		"valid_until": formatPBDatePtr(t.ValidUntil),
		"notes":       t.Notes,
	}
}

// --- attachments ---

// attachmentOf renders a PocketBase file field into the model's attachment pair.
//
// It has no counterpart on the write side on purpose: attachments move over
// multipart via their own endpoint (attachments.go), never as JSON, so a
// metadata write must not carry a file field that would blank an existing
// upload.
func attachmentOf(file string) models.Attachment {
	return models.Attachment{FileName: file, HasFile: file != ""}
}

// --- parts ---

type partRecord struct {
	ID         string `json:"id"`
	Car        string `json:"car"`
	Name       string `json:"name"`
	PartNumber string `json:"part_number"`
	Category   string `json:"category"`
	Notes      string `json:"notes"`
	File       string `json:"file"`
	Created    string `json:"created"`
	Updated    string `json:"updated"`
}

func (rec partRecord) toModel() models.Part {
	return models.Part{
		ID:         rec.ID,
		Car:        rec.Car,
		Name:       rec.Name,
		PartNumber: rec.PartNumber,
		Category:   rec.Category,
		Notes:      rec.Notes,
		Attachment: attachmentOf(rec.File),
		Created:    rec.Created,
		Updated:    rec.Updated,
	}
}

// partPayload omits the file field — see attachmentOf.
func partPayload(p models.Part) map[string]any {
	return map[string]any{
		"car":         p.Car,
		"name":        p.Name,
		"part_number": p.PartNumber,
		"category":    p.Category,
		"notes":       p.Notes,
	}
}

// parsePBDatePtr is parsePBDate for optional dates: a blank or unparseable
// value yields nil rather than the zero time, so "no expiry" stays
// distinguishable from "expired in year zero".
func parsePBDatePtr(s string) *time.Time {
	t := parsePBDate(s)
	if t.IsZero() {
		return nil
	}
	return &t
}

// formatPBDatePtr is formatPBDate for optional dates; nil writes an empty value,
// which is how PocketBase stores "unset".
func formatPBDatePtr(t *time.Time) string {
	if t == nil {
		return ""
	}
	return formatPBDate(*t)
}

// --- fuel entries ---

type fuelRecord struct {
	ID         string  `json:"id"`
	Car        string  `json:"car"`
	Date       string  `json:"date"`
	Km         int     `json:"km"`
	Liters     float64 `json:"liters"`
	Cost       float64 `json:"cost"`
	FullTank   bool    `json:"full_tank"`
	MissedFill bool    `json:"missed_fill"`
	Station    string  `json:"station"`
	Notes      string  `json:"notes"`
	File       string  `json:"file"`
	Created    string  `json:"created"`
	Updated    string  `json:"updated"`
}

func (rec fuelRecord) toModel() models.FuelEntry {
	return models.FuelEntry{
		ID:         rec.ID,
		Car:        rec.Car,
		Date:       parsePBDate(rec.Date),
		Km:         rec.Km,
		Liters:     rec.Liters,
		Cost:       rec.Cost,
		FullTank:   rec.FullTank,
		MissedFill: rec.MissedFill,
		Station:    rec.Station,
		Notes:      rec.Notes,
		Attachment: attachmentOf(rec.File),
		Created:    rec.Created,
		Updated:    rec.Updated,
	}
}

// fuelPayload omits the file field — see attachmentOf.
func fuelPayload(f models.FuelEntry) map[string]any {
	return map[string]any{
		"car":         f.Car,
		"date":        formatPBDate(f.Date),
		"km":          f.Km,
		"liters":      f.Liters,
		"cost":        f.Cost,
		"full_tank":   f.FullTank,
		"missed_fill": f.MissedFill,
		"station":     f.Station,
		"notes":       f.Notes,
	}
}

// --- charging sessions ---

type chargingRecord struct {
	ID            string  `json:"id"`
	Car           string  `json:"car"`
	Date          string  `json:"date"`
	Km            int     `json:"km"`
	Kwh           float64 `json:"kwh"`
	Cost          float64 `json:"cost"`
	FullCharge    bool    `json:"full_charge"`
	MissedSession bool    `json:"missed_session"`
	Location      string  `json:"location"`
	Notes         string  `json:"notes"`
	File          string  `json:"file"`
	Created       string  `json:"created"`
	Updated       string  `json:"updated"`
}

func (rec chargingRecord) toModel() models.ChargingSession {
	return models.ChargingSession{
		ID:            rec.ID,
		Car:           rec.Car,
		Date:          parsePBDate(rec.Date),
		Km:            rec.Km,
		Kwh:           rec.Kwh,
		Cost:          rec.Cost,
		FullCharge:    rec.FullCharge,
		MissedSession: rec.MissedSession,
		Location:      rec.Location,
		Notes:         rec.Notes,
		Attachment:    attachmentOf(rec.File),
		Created:       rec.Created,
		Updated:       rec.Updated,
	}
}

// chargingPayload omits the file field — see attachmentOf.
func chargingPayload(c models.ChargingSession) map[string]any {
	return map[string]any{
		"car":            c.Car,
		"date":           formatPBDate(c.Date),
		"km":             c.Km,
		"kwh":            c.Kwh,
		"cost":           c.Cost,
		"full_charge":    c.FullCharge,
		"missed_session": c.MissedSession,
		"location":       c.Location,
		"notes":          c.Notes,
	}
}

// --- maintenance entries ---

type maintenanceRecord struct {
	ID            string  `json:"id"`
	Car           string  `json:"car"`
	Date          string  `json:"date"`
	Km            int     `json:"km"`
	Type          string  `json:"type"`
	Status        string  `json:"status"`
	Workshop      string  `json:"workshop"`
	Location      string  `json:"location"`
	Description   string  `json:"description"`
	PartsUsed     string  `json:"parts_used"`
	LaborCost     float64 `json:"labor_cost"`
	PartsCost     float64 `json:"parts_cost"`
	InvoiceNumber string  `json:"invoice_number"`
	WarrantyUntil string  `json:"warranty_until"`
	Notes         string  `json:"notes"`
	File          string  `json:"file"`
	Created       string  `json:"created"`
	Updated       string  `json:"updated"`
}

func (rec maintenanceRecord) toModel() models.MaintenanceEntry {
	return models.MaintenanceEntry{
		ID:            rec.ID,
		Car:           rec.Car,
		Date:          parsePBDate(rec.Date),
		Km:            rec.Km,
		Type:          rec.Type,
		Status:        rec.Status,
		Workshop:      rec.Workshop,
		Location:      rec.Location,
		Description:   rec.Description,
		PartsUsed:     rec.PartsUsed,
		LaborCost:     rec.LaborCost,
		PartsCost:     rec.PartsCost,
		InvoiceNumber: rec.InvoiceNumber,
		WarrantyUntil: parsePBDatePtr(rec.WarrantyUntil),
		Notes:         rec.Notes,
		Attachment:    attachmentOf(rec.File),
		Created:       rec.Created,
		Updated:       rec.Updated,
	}
}

// maintenancePayload omits the file field — see attachmentOf.
func maintenancePayload(m models.MaintenanceEntry) map[string]any {
	return map[string]any{
		"car":            m.Car,
		"date":           formatPBDate(m.Date),
		"km":             m.Km,
		"type":           m.Type,
		"status":         m.Status,
		"workshop":       m.Workshop,
		"location":       m.Location,
		"description":    m.Description,
		"parts_used":     m.PartsUsed,
		"labor_cost":     m.LaborCost,
		"parts_cost":     m.PartsCost,
		"invoice_number": m.InvoiceNumber,
		"warranty_until": formatPBDatePtr(m.WarrantyUntil),
		"notes":          m.Notes,
	}
}

// --- car documents ---

type documentRecord struct {
	ID         string  `json:"id"`
	Car        string  `json:"car"`
	Type       string  `json:"type"`
	Title      string  `json:"title"`
	Provider   string  `json:"provider"`
	Reference  string  `json:"reference"`
	IssueDate  string  `json:"issue_date"`
	ExpiryDate string  `json:"expiry_date"`
	Cost       float64 `json:"cost"`
	Notes      string  `json:"notes"`
	File       string  `json:"file"`
	Created    string  `json:"created"`
	Updated    string  `json:"updated"`
}

func (rec documentRecord) toModel() models.CarDocument {
	return models.CarDocument{
		ID:         rec.ID,
		Car:        rec.Car,
		Type:       rec.Type,
		Title:      rec.Title,
		Provider:   rec.Provider,
		Reference:  rec.Reference,
		IssueDate:  parsePBDatePtr(rec.IssueDate),
		ExpiryDate: parsePBDatePtr(rec.ExpiryDate),
		Cost:       rec.Cost,
		Notes:      rec.Notes,
		Attachment: attachmentOf(rec.File),
		Created:    rec.Created,
		Updated:    rec.Updated,
	}
}

// documentPayload omits the file field — see attachmentOf.
func documentPayload(d models.CarDocument) map[string]any {
	return map[string]any{
		"car":         d.Car,
		"type":        d.Type,
		"title":       d.Title,
		"provider":    d.Provider,
		"reference":   d.Reference,
		"issue_date":  formatPBDatePtr(d.IssueDate),
		"expiry_date": formatPBDatePtr(d.ExpiryDate),
		"cost":        d.Cost,
		"notes":       d.Notes,
	}
}

// --- reminders ---

type reminderRecord struct {
	ID         string `json:"id"`
	Car        string `json:"car"`
	Title      string `json:"title"`
	Type       string `json:"type"`
	DueDate    string `json:"due_date"`
	DueKm      int    `json:"due_km"`
	RepeatDays int    `json:"repeat_days"`
	RepeatKm   int    `json:"repeat_km"`
	Done       bool   `json:"done"`
	DoneAt     string `json:"done_at"`
	Notes      string `json:"notes"`
	Created    string `json:"created"`
	Updated    string `json:"updated"`
}

func (rec reminderRecord) toModel() models.Reminder {
	return models.Reminder{
		ID:         rec.ID,
		Car:        rec.Car,
		Title:      rec.Title,
		Type:       rec.Type,
		DueDate:    parsePBDatePtr(rec.DueDate),
		DueKm:      rec.DueKm,
		RepeatDays: rec.RepeatDays,
		RepeatKm:   rec.RepeatKm,
		Done:       rec.Done,
		DoneAt:     parsePBDatePtr(rec.DoneAt),
		Notes:      rec.Notes,
		Created:    rec.Created,
		Updated:    rec.Updated,
	}
}

func reminderPayload(r models.Reminder) map[string]any {
	return map[string]any{
		"car":         r.Car,
		"title":       r.Title,
		"type":        r.Type,
		"due_date":    formatPBDatePtr(r.DueDate),
		"due_km":      r.DueKm,
		"repeat_days": r.RepeatDays,
		"repeat_km":   r.RepeatKm,
		"done":        r.Done,
		"done_at":     formatPBDatePtr(r.DoneAt),
		"notes":       r.Notes,
	}
}
