package api

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

	"drivervault/apiserver/internal/models"
)

// Access levels a user can have on a car. accessNone means no access at all.
const (
	accessNone  = ""
	accessRead  = "read"
	accessWrite = "write"
	accessOwner = "owner"
)

// carAccessLevel reports the requesting user's permission on a car: "owner" if
// they own it, otherwise the permission from any car_shares grant ("read"/
// "write"), otherwise "" (no access). It also returns the car record so callers
// that already need it avoid a second fetch.
func (s *Server) carAccessLevel(ctx context.Context, userID, carID string) (string, *carRecord, error) {
	var rec carRecord
	if err := s.pb.GetOne(ctx, colCars, carID, &rec); err != nil {
		return accessNone, nil, err
	}
	if rec.Owner == userID {
		return accessOwner, &rec, nil
	}
	perm, err := s.sharePermission(ctx, carID, userID)
	if err != nil {
		return accessNone, &rec, err
	}
	return perm, &rec, nil
}

// sharePermission returns the permission ("read"/"write") granted to userID on
// carID via car_shares, or "" if there is no grant.
func (s *Server) sharePermission(ctx context.Context, carID, userID string) (string, error) {
	res, err := s.pb.List(ctx, colShares, url.Values{
		"filter":  {fmt.Sprintf("car='%s' && user='%s'", carID, userID)},
		"perPage": {"1"},
	})
	if err != nil {
		return "", err
	}
	var recs []shareRecord
	if err := json.Unmarshal(res.Items, &recs); err != nil || len(recs) == 0 {
		return "", err
	}
	return recs[0].Permission, nil
}

func canWrite(level string) bool { return level == accessOwner || level == accessWrite }

// requireCarAccess enforces that the current user's access to carID meets the
// minimum `need` (accessRead = any access, accessWrite = write or owner,
// accessOwner = owner only). On failure it writes the HTTP response and returns
// false, so callers can `if !s.requireCarAccess(...) { return }`.
func (s *Server) requireCarAccess(w http.ResponseWriter, r *http.Request, carID, need string) bool {
	if carID == "" {
		writeError(w, http.StatusBadRequest, "car is required")
		return false
	}
	level, _, err := s.carAccessLevel(r.Context(), s.currentUserID(r), carID)
	if err != nil {
		writePBError(w, err)
		return false
	}
	var ok bool
	switch need {
	case accessWrite:
		ok = canWrite(level)
	case accessOwner:
		ok = level == accessOwner
	default: // accessRead / any
		ok = level != accessNone
	}
	if !ok {
		writeError(w, http.StatusForbidden, "you do not have access to this car")
		return false
	}
	return true
}

func (s *Server) listCars(w http.ResponseWriter, r *http.Request) {
	me := s.currentUserID(r)

	// Cars the user owns.
	ownedRes, err := s.pb.List(r.Context(), colCars, url.Values{
		"filter":  {fmt.Sprintf("owner='%s'", me)},
		"sort":    {"name"},
		"perPage": {"200"},
	})
	if err != nil {
		writePBError(w, err)
		return
	}
	var owned []carRecord
	if err := json.Unmarshal(ownedRes.Items, &owned); err != nil {
		writeError(w, http.StatusInternalServerError, err.Error())
		return
	}

	out := make([]models.Car, 0, len(owned))
	for _, rec := range owned {
		m := rec.toModel()
		m.Access = accessOwner
		out = append(out, m)
	}

	// Cars shared with the user (each grant → fetch the car, annotate access).
	sharesRes, err := s.pb.List(r.Context(), colShares, url.Values{
		"filter":  {fmt.Sprintf("user='%s'", me)},
		"perPage": {"200"},
	})
	if err != nil {
		writePBError(w, err)
		return
	}
	var shares []shareRecord
	if err := json.Unmarshal(sharesRes.Items, &shares); err != nil {
		writeError(w, http.StatusInternalServerError, err.Error())
		return
	}
	for _, sh := range shares {
		var rec carRecord
		if err := s.pb.GetOne(r.Context(), colCars, sh.Car, &rec); err != nil {
			continue // grant points at a deleted car; skip defensively
		}
		m := rec.toModel()
		m.Access = sh.Permission
		out = append(out, m)
	}

	// Hand the garage back in the order the user arranged it. Best effort: if the
	// profile can't be read, the default order (owned by name, then shared) still
	// renders a usable garage.
	if rec, err := s.fetchUser(r, me); err == nil {
		applyCarOrder(out, rec.carOrder())
	}

	writeJSON(w, http.StatusOK, out)
}

// applyCarOrder sorts cars into the user's arranged order, in place. Cars the
// arrangement doesn't mention — a car added or shared since the last drag — keep
// their relative order and follow the arranged ones, so a new car shows up at
// the end rather than jumping into the middle.
func applyCarOrder(cars []models.Car, order []string) {
	if len(order) == 0 || len(cars) < 2 {
		return
	}
	rank := make(map[string]int, len(order))
	for i, id := range order {
		rank[id] = i
	}
	sort.SliceStable(cars, func(i, j int) bool {
		ri, oki := rank[cars[i].ID]
		rj, okj := rank[cars[j].ID]
		if oki != okj {
			return oki // an arranged car sorts before an unarranged one
		}
		if !oki {
			return false // both unarranged: leave them as they are
		}
		return ri < rj
	})
}

func (s *Server) getCar(w http.ResponseWriter, r *http.Request) {
	level, rec, err := s.carAccessLevel(r.Context(), s.currentUserID(r), r.PathValue("id"))
	if err != nil {
		writePBError(w, err)
		return
	}
	if level == accessNone {
		writeError(w, http.StatusForbidden, "you do not have access to this car")
		return
	}
	m := rec.toModel()
	m.Access = level
	writeJSON(w, http.StatusOK, m)
}

func (s *Server) createCar(w http.ResponseWriter, r *http.Request) {
	var in models.Car
	if err := decodeJSON(r, &in); err != nil {
		writeError(w, http.StatusBadRequest, err.Error())
		return
	}
	if in.Name == "" {
		writeError(w, http.StatusBadRequest, "name is required")
		return
	}
	buildDate, err := normalizeBuildDate(in.BuildDate)
	if err != nil {
		writeError(w, http.StatusBadRequest, err.Error())
		return
	}
	in.BuildDate = buildDate
	applyCarDefaults(&in)

	// Owner is always the authenticated user; ignore any client-supplied owner.
	payload := carPayload(in)
	payload["owner"] = s.currentUserID(r)

	var rec carRecord
	if err := s.pb.Create(r.Context(), colCars, payload, &rec); err != nil {
		writePBError(w, err)
		return
	}
	m := rec.toModel()
	m.Access = accessOwner
	writeJSON(w, http.StatusCreated, m)
}

func (s *Server) updateCar(w http.ResponseWriter, r *http.Request) {
	var in models.Car
	if err := decodeJSON(r, &in); err != nil {
		writeError(w, http.StatusBadRequest, err.Error())
		return
	}
	level, _, err := s.carAccessLevel(r.Context(), s.currentUserID(r), r.PathValue("id"))
	if err != nil {
		writePBError(w, err)
		return
	}
	if !canWrite(level) {
		writeError(w, http.StatusForbidden, "you cannot edit this car")
		return
	}
	buildDate, err := normalizeBuildDate(in.BuildDate)
	if err != nil {
		writeError(w, http.StatusBadRequest, err.Error())
		return
	}
	in.BuildDate = buildDate
	// carPayload deliberately omits owner, so a PATCH never reassigns ownership.
	var rec carRecord
	if err := s.pb.Update(r.Context(), colCars, r.PathValue("id"), carPayload(in), &rec); err != nil {
		writePBError(w, err)
		return
	}
	m := rec.toModel()
	m.Access = level
	writeJSON(w, http.StatusOK, m)
}

// hideableCarTabs are the car-detail tabs that can be switched off. "info" is
// deliberately absent: it is the car itself, and a page with no tabs left would
// be a dead end.
var hideableCarTabs = map[string]bool{
	"provider": true, "services": true, "technical": true, "maintenance": true,
	"fuel": true, "charging": true, "documents": true, "parts": true, "reminders": true,
}

// arrangeableCarTabs are the tabs a car's page can be rearranged into, which is
// the hideable ones plus Information: it cannot be switched off, but there is no
// reason it has to stay at the front. Derived from hideableCarTabs so the two
// sets cannot drift as tabs are added.
var arrangeableCarTabs = func() map[string]bool {
	out := make(map[string]bool, len(hideableCarTabs)+1)
	for key := range hideableCarTabs {
		out[key] = true
	}
	out["info"] = true
	return out
}()

// hideableCarFields are the Information rows that can be switched off — every
// one of them, since unlike the tabs there is no row the page needs to keep.
// Mirrors the car.info.* labels the web app renders.
var hideableCarFields = map[string]bool{
	"oilSpec": true, "transmissionOil": true, "differentialOil": true,
	"brakeFluid": true, "coolant": true, "odometer": true, "serviceInterval": true,
	"nextDue": true, "registrationPlate": true, "registrationCountry": true,
	"vin": true, "fuelType": true, "buildDate": true, "firstRegistration": true,
}

// hideableServiceColumns are the columns of the Service history table that can
// be switched off. Date is deliberately not among them: every row of that table
// is a service that happened on a day, and a history with the day taken out
// stops being a history. "parts" is the one column covering every part a service
// can have changed — they are a growing list, and one column per part would
// widen the table indefinitely — so the set does not grow when a part is added.
var hideableServiceColumns = map[string]bool{
	"km": true, "nextDate": true, "nextKm": true, "parts": true,
	"notes": true, "file": true,
}

// hideableServiceParts are the parts a service record can say were changed, and
// so the ones a car can take off its list: an EV changes no oil, and offering it
// on every service form is a checkbox nobody there will ever tick. Keys mirror
// SERVICE_PARTS in the web app's lib/serviceParts.js and kServiceParts in the
// phone's service_parts.dart — one per boolean on the service_records
// collection.
//
// Every part is hideable, unlike the tabs and the columns: there is no part the
// form needs, because a service that changed nothing at all is a service with an
// empty Changed parts section, which is a thing that happens.
var hideableServiceParts = map[string]bool{
	"oil": true, "engineFilter": true, "cabinFilter": true,
}

// arrangeableServiceColumns are the columns that table can be rearranged into:
// the hideable ones plus Date, which cannot be switched off but has no reason to
// be stuck at the left. Derived from hideableServiceColumns so the two sets
// cannot drift as columns are added — the same construction arrangeableCarTabs
// uses for Information.
var arrangeableServiceColumns = func() map[string]bool {
	out := make(map[string]bool, len(hideableServiceColumns)+1)
	for key := range hideableServiceColumns {
		out[key] = true
	}
	out["date"] = true
	return out
}()

// arrangeableCarMetrics are the headline readings on the connected-service tab,
// and so the keys a car's arrangement of them may name. Derived from the reading
// specs in vehicleproviders.go rather than written out again, so the set cannot
// drift from what that panel actually shows.
var arrangeableCarMetrics = func() map[string]bool {
	out := make(map[string]bool, len(headlineMetricSpecs)+len(unmeasuredMetricKeys))
	for _, spec := range headlineMetricSpecs {
		out[spec.key] = true
	}
	for _, key := range unmeasuredMetricKeys {
		out[key] = true
	}
	return out
}()

// normalizeKeys validates a list of tab or field keys against the keys that
// exist, trimming blanks and duplicates. Unknown keys are rejected rather than
// ignored: they can only come from a stale or wrong client, and dropping them
// silently would hide the mistake while the page carried on as before.
func normalizeKeys(in []string, allowed map[string]bool, what string) ([]string, error) {
	out := make([]string, 0, len(in))
	seen := make(map[string]bool, len(in))
	for _, key := range in {
		key = strings.TrimSpace(key)
		if key == "" || seen[key] {
			continue
		}
		if !allowed[key] {
			return nil, fmt.Errorf("%q is not a car %s", key, what)
		}
		seen[key] = true
		out = append(out, key)
	}
	return out, nil
}

// PUT /api/cars/{id}/view — choose what this car's page shows: which tabs, which
// rows of the Information tab, which columns of the Service history table, which
// parts its service form offers, and the order the tabs, the Information rows,
// those columns and the connected service's headline readings are laid out in.
// Body: {hiddenTabs?: [...], hiddenFields?: [...], hiddenServiceColumns?: [...],
// hiddenServiceParts?: [...], tabOrder?: [...],
// fieldOrder?: [...], serviceColumnOrder?: [...], metricOrder?: [...]}; only the
// lists present are written, so a client can rearrange one group without
// resending the others. Its own endpoint rather than fields on the car edit, so an ordinary
// save of the car form — which sends every other field — can never reveal
// something somebody deliberately switched off. Needs write access: the choice
// belongs to the car, so it is the same permission as editing it.
func (s *Server) updateCarView(w http.ResponseWriter, r *http.Request) {
	var in struct {
		HiddenTabs           *[]string `json:"hiddenTabs"`
		HiddenFields         *[]string `json:"hiddenFields"`
		HiddenServiceColumns *[]string `json:"hiddenServiceColumns"`
		HiddenServiceParts   *[]string `json:"hiddenServiceParts"`
		TabOrder             *[]string `json:"tabOrder"`
		FieldOrder           *[]string `json:"fieldOrder"`
		ServiceColumnOrder   *[]string `json:"serviceColumnOrder"`
		MetricOrder          *[]string `json:"metricOrder"`
	}
	if err := decodeJSON(r, &in); err != nil {
		writeError(w, http.StatusBadRequest, err.Error())
		return
	}
	level, _, err := s.carAccessLevel(r.Context(), s.currentUserID(r), r.PathValue("id"))
	if err != nil {
		writePBError(w, err)
		return
	}
	if !canWrite(level) {
		writeError(w, http.StatusForbidden, "you cannot edit this car")
		return
	}

	payload := map[string]any{}
	if in.HiddenTabs != nil {
		tabs, err := normalizeKeys(*in.HiddenTabs, hideableCarTabs, "tab")
		if err != nil {
			writeError(w, http.StatusBadRequest, err.Error())
			return
		}
		payload["hidden_tabs"] = tabs
	}
	if in.HiddenFields != nil {
		fields, err := normalizeKeys(*in.HiddenFields, hideableCarFields, "field")
		if err != nil {
			writeError(w, http.StatusBadRequest, err.Error())
			return
		}
		payload["hidden_fields"] = fields
	}
	if in.HiddenServiceColumns != nil {
		columns, err := normalizeKeys(*in.HiddenServiceColumns, hideableServiceColumns, "service column")
		if err != nil {
			writeError(w, http.StatusBadRequest, err.Error())
			return
		}
		payload["hidden_service_columns"] = columns
	}
	if in.HiddenServiceParts != nil {
		parts, err := normalizeKeys(*in.HiddenServiceParts, hideableServiceParts, "service part")
		if err != nil {
			writeError(w, http.StatusBadRequest, err.Error())
			return
		}
		payload["hidden_service_parts"] = parts
	}
	if in.TabOrder != nil {
		// A wider set than the hidden tabs: Information is arrangeable although it
		// cannot be switched off. A partial list is accepted, and the tabs it
		// leaves out follow the arranged ones — which is what puts a tab added in
		// a later release at the end rather than in the middle of somebody's bar.
		order, err := normalizeKeys(*in.TabOrder, arrangeableCarTabs, "tab")
		if err != nil {
			writeError(w, http.StatusBadRequest, err.Error())
			return
		}
		payload["tab_order"] = order
	}
	if in.FieldOrder != nil {
		// The same key set as the hidden fields, since every Information row can
		// be moved. A partial list is accepted rather than demanding all 14: the
		// rows it leaves out follow the arranged ones, which is also what makes a
		// row added in a later release land at the end instead of the middle.
		order, err := normalizeKeys(*in.FieldOrder, hideableCarFields, "field")
		if err != nil {
			writeError(w, http.StatusBadRequest, err.Error())
			return
		}
		payload["field_order"] = order
	}
	if in.ServiceColumnOrder != nil {
		// A wider set than the hidden columns, for the same reason the tab order
		// is: Date is arrangeable although it cannot be switched off. A partial
		// list is accepted, and the columns it leaves out follow the arranged
		// ones, so a column added in a later release lands at the right-hand end
		// rather than in the middle of somebody's table.
		order, err := normalizeKeys(*in.ServiceColumnOrder, arrangeableServiceColumns, "service column")
		if err != nil {
			writeError(w, http.StatusBadRequest, err.Error())
			return
		}
		payload["service_column_order"] = order
	}
	if in.MetricOrder != nil {
		// A partial list again, and here it is the normal case: the client can
		// only arrange the readings the provider actually reported, so one it
		// reports later — an EV range on a car that was parked unplugged — joins
		// at the end rather than displacing the arrangement.
		order, err := normalizeKeys(*in.MetricOrder, arrangeableCarMetrics, "reading")
		if err != nil {
			writeError(w, http.StatusBadRequest, err.Error())
			return
		}
		payload["metric_order"] = order
	}
	if len(payload) == 0 {
		writeError(w, http.StatusBadRequest, "no changes provided")
		return
	}

	var rec carRecord
	if err := s.pb.Update(r.Context(), colCars, r.PathValue("id"), payload, &rec); err != nil {
		writePBError(w, err)
		return
	}
	m := rec.toModel()
	m.Access = level
	writeJSON(w, http.StatusOK, m)
}

func (s *Server) deleteCar(w http.ResponseWriter, r *http.Request) {
	level, _, err := s.carAccessLevel(r.Context(), s.currentUserID(r), r.PathValue("id"))
	if err != nil {
		writePBError(w, err)
		return
	}
	if level != accessOwner {
		writeError(w, http.StatusForbidden, "only the owner can delete this car")
		return
	}
	if err := s.pb.Delete(r.Context(), colCars, r.PathValue("id")); err != nil {
		writePBError(w, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// applyCarDefaults fills the spreadsheet's default maintenance intervals when
// the client didn't specify them.
// normalizeBuildDate checks a build date and hands back the value to store.
//
// It is an ISO 8601 reduced-precision date: a year, a year and a month, or a
// full date. A car's build date is often only half known — the VIN plate
// carries a month, the papers a day, a grey import neither — and a field that
// insisted on all three would be answered either with an invented day or with
// nothing. The three shapes sort and compare as strings in date order, which is
// why the prefix is stored rather than a date plus a precision beside it.
//
// Validated rather than taken as typed, because the column is free text: the
// month has to be a month and the day has to exist, or the stored value is
// something no reader can print.
func normalizeBuildDate(v string) (string, error) {
	v = strings.TrimSpace(v)
	if v == "" {
		return "", nil
	}
	var layout string
	switch len(v) {
	case len("2006"):
		layout = "2006"
	case len("2006-01"):
		layout = "2006-01"
	case len("2006-01-02"):
		layout = "2006-01-02"
	default:
		return "", fmt.Errorf("build date %q must be a year, a year and month, or a full date", v)
	}
	// time.Parse rejects month 13 and 31 February for us, so the stored value is
	// always a date that happened.
	if _, err := time.Parse(layout, v); err != nil {
		return "", fmt.Errorf("build date %q must be a year, a year and month, or a full date", v)
	}
	return v, nil
}

func applyCarDefaults(c *models.Car) {
	if c.ServiceIntervalDays <= 0 {
		c.ServiceIntervalDays = 365
	}
	if c.ServiceIntervalKm <= 0 {
		c.ServiceIntervalKm = 15000
	}
	if c.TechnicalCheckIntervalDays <= 0 {
		c.TechnicalCheckIntervalDays = models.DefaultTechnicalCheckIntervalDays
	}
}
