package api

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

	"drivervault/apiserver/internal/models"
)

// Reminders: what the user wants to be told about, and when.
//
// A reminder fires on a date, an odometer reading, or both (whichever arrives
// first). Two kinds are returned side by side:
//
//   - Stored reminders, which the user creates and completes.
//   - Auto-derived reminders, synthesised on read from data the user has
//     already entered — a document's expiry date, the next service due from the
//     latest service record. These are read-only and carry no row of their own,
//     so a renewal date never has to be typed twice and can never drift out of
//     step with the document it came from. Their ids are namespaced ("auto:…")
//     and every write endpoint rejects them.

// autoPrefix marks a reminder id as derived rather than stored.
const autoPrefix = "auto:"

var reminderTypes = map[string]bool{
	"maintenance": true,
	"document":    true,
	"service":     true,
	"inspection":  true,
	"other":       true,
}

// documentTypeLabels give auto-derived document reminders a title that reads
// like something a person would write.
var documentTypeLabels = map[string]string{
	"insurance":    "Insurance",
	"pollution":    "Pollution certificate",
	"registration": "Registration",
	"inspection":   "Inspection",
	"roadTax":      "Road tax",
	"warranty":     "Warranty",
	"other":        "Document",
}

// listCarReminders serves GET /api/cars/{id}/reminders.
func (s *Server) listCarReminders(w http.ResponseWriter, r *http.Request) {
	carID := r.PathValue("id")
	if !s.requireCarAccess(w, r, carID, accessRead) {
		return
	}
	s.respondReminderList(w, r, carID)
}

// listReminders serves GET /api/reminders?car={id}.
func (s *Server) listReminders(w http.ResponseWriter, r *http.Request) {
	carID := r.URL.Query().Get("car")
	if !s.requireCarAccess(w, r, carID, accessRead) {
		return
	}
	s.respondReminderList(w, r, carID)
}

func (s *Server) respondReminderList(w http.ResponseWriter, r *http.Request, carID string) {
	car, err := s.carModel(r.Context(), carID)
	if err != nil {
		writePBError(w, err)
		return
	}

	stored, err := s.fetchReminders(r, carID, car.CurrentKm)
	if err != nil {
		writePBError(w, err)
		return
	}
	// A failure to derive extras must not take down the user's own reminders,
	// which are the part they actually rely on.
	out := append(stored, s.autoReminders(r, car)...)

	now := time.Now()
	sortReminders(out, now)
	writeJSON(w, http.StatusOK, out)
}

// fetchReminders loads a car's stored reminders with their status resolved
// against today and the car's odometer.
func (s *Server) fetchReminders(r *http.Request, carID string, currentKm int) ([]models.Reminder, error) {
	res, err := s.pb.List(r.Context(), colReminders, url.Values{
		"filter":  {fmt.Sprintf("car='%s'", carID)},
		"perPage": {"500"},
	})
	if err != nil {
		return nil, err
	}
	var recs []reminderRecord
	if err := json.Unmarshal(res.Items, &recs); err != nil {
		return nil, err
	}
	now := time.Now()
	out := make([]models.Reminder, 0, len(recs))
	for _, rec := range recs {
		m := rec.toModel()
		m.ComputeReminderDerived(now, currentKm)
		out = append(out, m)
	}
	return out, nil
}

// autoReminders synthesises the read-only reminders implied by a car's
// documents and its service schedule. Errors are swallowed: these are a
// convenience layered on top of the stored list, and losing them is better than
// failing the request.
func (s *Server) autoReminders(r *http.Request, car *models.Car) []models.Reminder {
	now := time.Now()
	out := []models.Reminder{}

	// One per document that has a renewal date.
	if docs, err := s.fetchDocuments(r, car.ID); err == nil {
		for _, d := range docs {
			if d.ExpiryDate == nil {
				continue
			}
			label := documentTypeLabels[d.Type]
			if label == "" {
				label = "Document"
			}
			rem := models.Reminder{
				ID:        autoPrefix + "doc:" + d.ID,
				Car:       car.ID,
				Title:     label + " renewal — " + d.Title,
				Type:      "document",
				DueDate:   d.ExpiryDate,
				Auto:      true,
				SourceRef: d.ID,
			}
			if d.Provider != "" {
				rem.Notes = d.Provider
			}
			rem.ComputeReminderDerived(now, car.CurrentKm)
			out = append(out, rem)
		}
	}

	// One for the next service due, from the most recent service record.
	if latest, err := s.latestServiceRecord(r, car); err == nil && latest != nil {
		rem := models.Reminder{
			ID:        autoPrefix + "service:" + latest.ID,
			Car:       car.ID,
			Title:     "Service due",
			Type:      "service",
			DueDate:   latest.NextServiceDate,
			Auto:      true,
			SourceRef: latest.ID,
			Notes: fmt.Sprintf("Based on the service on %s",
				latest.Date.Format("2006-01-02")),
		}
		if latest.NextServiceKm != nil {
			rem.DueKm = *latest.NextServiceKm
		}
		if rem.DueDate != nil || rem.DueKm > 0 {
			rem.ComputeReminderDerived(now, car.CurrentKm)
			out = append(out, rem)
		}
	}

	return out
}

// latestServiceRecord returns the car's most recent service record with its
// next-due fields computed, or nil when the car has no service history.
func (s *Server) latestServiceRecord(r *http.Request, car *models.Car) (*models.ServiceRecord, error) {
	res, err := s.pb.List(r.Context(), colServices, url.Values{
		"filter":  {fmt.Sprintf("car='%s'", car.ID)},
		"sort":    {"-date"},
		"perPage": {"1"},
	})
	if err != nil {
		return nil, err
	}
	var recs []serviceRecord
	if err := json.Unmarshal(res.Items, &recs); err != nil {
		return nil, err
	}
	if len(recs) == 0 {
		return nil, nil
	}
	m := recs[0].toModel()
	m.ComputeDerived(car)
	return &m, nil
}

// sortReminders orders the list the way it needs to be acted on: everything
// outstanding first, soonest deadline at the top, with completed reminders
// pushed to the bottom.
func sortReminders(rs []models.Reminder, now time.Time) {
	sort.SliceStable(rs, func(i, j int) bool {
		a, b := rs[i], rs[j]
		if a.Done != b.Done {
			return !a.Done
		}
		ad, bd := a.DueDate != nil, b.DueDate != nil
		if ad != bd {
			return ad // dated reminders before open-ended ones
		}
		if ad && bd && !a.DueDate.Equal(*b.DueDate) {
			return a.DueDate.Before(*b.DueDate)
		}
		return a.Title < b.Title
	})
}

func (s *Server) getReminder(w http.ResponseWriter, r *http.Request) {
	rec, ok := s.loadStoredReminder(w, r, accessRead)
	if !ok {
		return
	}
	car, err := s.carModel(r.Context(), rec.Car)
	if err != nil {
		writePBError(w, err)
		return
	}
	m := rec.toModel()
	m.ComputeReminderDerived(time.Now(), car.CurrentKm)
	writeJSON(w, http.StatusOK, m)
}

func (s *Server) createReminder(w http.ResponseWriter, r *http.Request) {
	var in models.Reminder
	if err := decodeJSON(r, &in); err != nil {
		writeError(w, http.StatusBadRequest, err.Error())
		return
	}
	if in.Type == "" {
		in.Type = "other"
	}
	if msg := validateReminder(in); msg != "" {
		writeError(w, http.StatusBadRequest, msg)
		return
	}
	if !s.requireCarAccess(w, r, in.Car, accessWrite) {
		return
	}
	var rec reminderRecord
	if err := s.pb.Create(r.Context(), colReminders, reminderPayload(in), &rec); err != nil {
		writePBError(w, err)
		return
	}
	s.respondReminder(w, r, rec)
}

func (s *Server) updateReminder(w http.ResponseWriter, r *http.Request) {
	var in models.Reminder
	if err := decodeJSON(r, &in); err != nil {
		writeError(w, http.StatusBadRequest, err.Error())
		return
	}
	existing, ok := s.loadStoredReminder(w, r, accessWrite)
	if !ok {
		return
	}
	in.Car = existing.Car // the reminder's car is not reassignable via PATCH
	if in.Type == "" {
		in.Type = "other"
	}
	if msg := validateReminder(in); msg != "" {
		writeError(w, http.StatusBadRequest, msg)
		return
	}
	// Completing via PATCH should still stamp when it happened, matching what
	// the complete endpoint records.
	if in.Done && in.DoneAt == nil {
		now := time.Now()
		in.DoneAt = &now
	}
	if !in.Done {
		in.DoneAt = nil
	}
	var rec reminderRecord
	if err := s.pb.Update(r.Context(), colReminders, existing.ID, reminderPayload(in), &rec); err != nil {
		writePBError(w, err)
		return
	}
	s.respondReminder(w, r, rec)
}

// handleCompleteReminder serves POST /api/reminders/{id}/complete.
//
// A one-off reminder is simply closed. A recurring one (repeatDays/repeatKm)
// instead rolls its triggers forward and stays open — the next oil change is
// due a year after this one was done, not a year after it was first scheduled,
// so the roll is measured from completion.
func (s *Server) handleCompleteReminder(w http.ResponseWriter, r *http.Request) {
	existing, ok := s.loadStoredReminder(w, r, accessWrite)
	if !ok {
		return
	}
	m := existing.toModel()
	now := time.Now()

	payload := map[string]any{}
	if m.RepeatDays > 0 || m.RepeatKm > 0 {
		if m.RepeatDays > 0 {
			next := now.AddDate(0, 0, m.RepeatDays)
			payload["due_date"] = formatPBDate(next)
		}
		if m.RepeatKm > 0 {
			car, err := s.carModel(r.Context(), m.Car)
			if err != nil {
				writePBError(w, err)
				return
			}
			// Roll from where the car actually is: the work was done now, so the
			// next one is due RepeatKm from this reading, whether it was done
			// early or late. Rolling from the old target instead would let an
			// early completion drift the schedule forward for good.
			//
			// A reading of 0 rolls from 0 like any other: a car collected new is
			// genuinely there, and CurrentKm + RepeatKm is ahead of the car by
			// construction, so this cannot land a target in the past.
			payload["due_km"] = car.CurrentKm + m.RepeatKm
		}
		payload["done"] = false
		payload["done_at"] = ""
	} else {
		payload["done"] = true
		payload["done_at"] = formatPBDate(now)
	}

	var rec reminderRecord
	if err := s.pb.Update(r.Context(), colReminders, existing.ID, payload, &rec); err != nil {
		writePBError(w, err)
		return
	}
	s.respondReminder(w, r, rec)
}

func (s *Server) deleteReminder(w http.ResponseWriter, r *http.Request) {
	existing, ok := s.loadStoredReminder(w, r, accessWrite)
	if !ok {
		return
	}
	if err := s.pb.Delete(r.Context(), colReminders, existing.ID); err != nil {
		writePBError(w, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// loadStoredReminder fetches the {id} reminder and checks car access at `need`.
// Auto-derived ids are rejected up front: they have no row behind them, so any
// write against one is a client bug rather than a 404 from PocketBase.
func (s *Server) loadStoredReminder(w http.ResponseWriter, r *http.Request, need string) (reminderRecord, bool) {
	id := r.PathValue("id")
	if strings.HasPrefix(id, autoPrefix) {
		writeError(w, http.StatusBadRequest,
			"this reminder is derived from a document or service record — edit that instead")
		return reminderRecord{}, false
	}
	var rec reminderRecord
	if err := s.pb.GetOne(r.Context(), colReminders, id, &rec); err != nil {
		writePBError(w, err)
		return reminderRecord{}, false
	}
	if !s.requireCarAccess(w, r, rec.Car, need) {
		return reminderRecord{}, false
	}
	return rec, true
}

// respondReminder writes a stored reminder with its derived status resolved.
func (s *Server) respondReminder(w http.ResponseWriter, r *http.Request, rec reminderRecord) {
	m := rec.toModel()
	currentKm := 0
	if car, err := s.carModel(r.Context(), rec.Car); err == nil {
		currentKm = car.CurrentKm
	}
	m.ComputeReminderDerived(time.Now(), currentKm)
	writeJSON(w, http.StatusOK, m)
}

func validateReminder(rm models.Reminder) string {
	switch {
	case rm.Car == "":
		return "car is required"
	case strings.TrimSpace(rm.Title) == "":
		return "title is required"
	case !reminderTypes[rm.Type]:
		return "type must be one of: maintenance, document, service, inspection, other"
	case rm.DueKm < 0 || rm.RepeatDays < 0 || rm.RepeatKm < 0:
		return "due km and repeat values cannot be negative"
	}
	// A reminder with neither trigger would never come due, which is not what
	// anyone means by "remind me".
	if (rm.DueDate == nil || rm.DueDate.IsZero()) && rm.DueKm == 0 {
		return "set a due date, a due odometer reading, or both"
	}
	return ""
}
