package api

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"sort"

	"drivervault/apiserver/internal/models"
)

// Charging tracking: a log of charges per car, plus the efficiency derived from
// it. The EV counterpart of fuel.go, and deliberately the same shape — a car
// that runs on electrons still has a cost per km, and the two tabs read alike.
//
// Nothing about consumption is stored — it is recomputed from the whole history
// on every read (models.ComputeChargingDerived), so correcting a session three
// months back fixes every window it touches with no rows to migrate.

// fetchChargingSessions loads a car's charges oldest-first and fills in the
// derived efficiency fields. Ordering is by odometer rather than date because
// the windows are spans of distance, and a session logged with the wrong date
// would otherwise scramble the chain.
func (s *Server) fetchChargingSessions(r *http.Request, carID string) ([]models.ChargingSession, error) {
	res, err := s.pb.List(r.Context(), colCharging, url.Values{
		"filter":  {fmt.Sprintf("car='%s'", carID)},
		"sort":    {"km"},
		"perPage": {"1000"},
	})
	if err != nil {
		return nil, err
	}
	var recs []chargingRecord
	if err := json.Unmarshal(res.Items, &recs); err != nil {
		return nil, err
	}
	out := make([]models.ChargingSession, 0, len(recs))
	for _, rec := range recs {
		out = append(out, rec.toModel())
	}
	// PocketBase sorts numerically here, but re-sorting locally keeps the
	// invariant ComputeChargingDerived depends on explicit and cheap.
	sort.SliceStable(out, func(i, j int) bool { return out[i].Km < out[j].Km })
	models.ComputeChargingDerived(out)
	return out, nil
}

// listCarChargingSessions serves GET /api/cars/{id}/charging-sessions, newest first.
func (s *Server) listCarChargingSessions(w http.ResponseWriter, r *http.Request) {
	carID := r.PathValue("id")
	if !s.requireCarAccess(w, r, carID, accessRead) {
		return
	}
	entries, err := s.fetchChargingSessions(r, carID)
	if err != nil {
		writePBError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, reverseCharging(entries))
}

// listChargingSessions serves GET /api/charging-sessions?car={id}.
func (s *Server) listChargingSessions(w http.ResponseWriter, r *http.Request) {
	carID := r.URL.Query().Get("car")
	if !s.requireCarAccess(w, r, carID, accessRead) {
		return
	}
	entries, err := s.fetchChargingSessions(r, carID)
	if err != nil {
		writePBError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, reverseCharging(entries))
}

// listCarChargingStats serves GET /api/cars/{id}/charging-stats.
func (s *Server) listCarChargingStats(w http.ResponseWriter, r *http.Request) {
	carID := r.PathValue("id")
	if !s.requireCarAccess(w, r, carID, accessRead) {
		return
	}
	entries, err := s.fetchChargingSessions(r, carID)
	if err != nil {
		writePBError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, models.ComputeChargingStats(entries))
}

// reverseCharging flips the oldest-first working order into the newest-first
// order clients display.
func reverseCharging(in []models.ChargingSession) []models.ChargingSession {
	out := make([]models.ChargingSession, len(in))
	for i, e := range in {
		out[len(in)-1-i] = e
	}
	return out
}

// withChargingDerived recomputes the whole history and returns the one session
// the caller just wrote, so a create/update response carries the same derived
// figures the list would show.
func (s *Server) withChargingDerived(r *http.Request, carID, id string) (models.ChargingSession, error) {
	entries, err := s.fetchChargingSessions(r, carID)
	if err != nil {
		return models.ChargingSession{}, err
	}
	for _, e := range entries {
		if e.ID == id {
			return e, nil
		}
	}
	return models.ChargingSession{}, fmt.Errorf("charging session %s not found after write", id)
}

func (s *Server) getChargingSession(w http.ResponseWriter, r *http.Request) {
	var rec chargingRecord
	if err := s.pb.GetOne(r.Context(), colCharging, r.PathValue("id"), &rec); err != nil {
		writePBError(w, err)
		return
	}
	if !s.requireCarAccess(w, r, rec.Car, accessRead) {
		return
	}
	entry, err := s.withChargingDerived(r, rec.Car, rec.ID)
	if err != nil {
		writePBError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, entry)
}

func (s *Server) createChargingSession(w http.ResponseWriter, r *http.Request) {
	var in models.ChargingSession
	if err := decodeJSON(r, &in); err != nil {
		writeError(w, http.StatusBadRequest, err.Error())
		return
	}
	if err := validateChargingSession(in); err != "" {
		writeError(w, http.StatusBadRequest, err)
		return
	}
	if !s.requireCarAccess(w, r, in.Car, accessWrite) {
		return
	}
	var rec chargingRecord
	if err := s.pb.Create(r.Context(), colCharging, chargingPayload(in), &rec); err != nil {
		writePBError(w, err)
		return
	}
	// A charge is also the freshest odometer reading there is, exactly as a
	// refill is; keeping the car in step means the km-based service and reminder
	// status stay honest without the user retyping the number on the car itself.
	s.advanceOdometer(r, in.Car, in.Km)

	entry, err := s.withChargingDerived(r, rec.Car, rec.ID)
	if err != nil {
		writePBError(w, err)
		return
	}
	writeJSON(w, http.StatusCreated, entry)
}

func (s *Server) updateChargingSession(w http.ResponseWriter, r *http.Request) {
	var in models.ChargingSession
	if err := decodeJSON(r, &in); err != nil {
		writeError(w, http.StatusBadRequest, err.Error())
		return
	}
	var existing chargingRecord
	if err := s.pb.GetOne(r.Context(), colCharging, r.PathValue("id"), &existing); err != nil {
		writePBError(w, err)
		return
	}
	if !s.requireCarAccess(w, r, existing.Car, accessWrite) {
		return
	}
	// The car is fixed by the record being edited; a body claiming another car
	// must not move the session across the access boundary just checked.
	in.Car = existing.Car
	if err := validateChargingSession(in); err != "" {
		writeError(w, http.StatusBadRequest, err)
		return
	}
	var rec chargingRecord
	if err := s.pb.Update(r.Context(), colCharging, r.PathValue("id"), chargingPayload(in), &rec); err != nil {
		writePBError(w, err)
		return
	}
	s.advanceOdometer(r, rec.Car, in.Km)

	entry, err := s.withChargingDerived(r, rec.Car, rec.ID)
	if err != nil {
		writePBError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, entry)
}

func (s *Server) deleteChargingSession(w http.ResponseWriter, r *http.Request) {
	var existing chargingRecord
	if err := s.pb.GetOne(r.Context(), colCharging, r.PathValue("id"), &existing); err != nil {
		writePBError(w, err)
		return
	}
	if !s.requireCarAccess(w, r, existing.Car, accessWrite) {
		return
	}
	if err := s.pb.Delete(r.Context(), colCharging, r.PathValue("id")); err != nil {
		writePBError(w, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// validateChargingSession returns a human-readable reason the session is
// unusable, or "" when it is fine.
func validateChargingSession(c models.ChargingSession) string {
	switch {
	case c.Car == "":
		return "car is required"
	case c.Date.IsZero():
		return "date is required"
	case c.Km < 0:
		return "odometer (km) cannot be negative"
	case c.Kwh <= 0:
		return "kwh must be greater than zero"
	case c.Cost < 0:
		return "cost cannot be negative"
	}
	return ""
}
