package api

// toyotaSource is the first vehicleSource: Toyota Connected Europe, the backend
// behind the MyToyota app (see internal/plugins/builtin/toyota). It contributes
// nothing but wiring — credentials come from the cascade already in
// integrations.go, and the payload walking lives in vehicleproviders.go — which
// is the shape the next manufacturer's adapter should copy.

import (
	"context"
	"encoding/json"
	"strings"
)

// toyotaLabel is what the user sees: the app they know the account by, not the
// corporate service name behind it. It titles the car's provider tab.
const toyotaLabel = "MyToyota"

type toyotaSource struct{}

func (toyotaSource) id() string      { return toyotaPlugin }
func (toyotaSource) label() string   { return toyotaLabel }
func (toyotaSource) service() string { return "Toyota Connected Europe" }

func (toyotaSource) listAction() string { return "vehicles" }

// sections are the plugin's per-VIN read-only capabilities, in the order the tab
// shows them: the live readings first, then the history.
func (toyotaSource) sections() []providerSection {
	return []providerSection{
		{ID: "telemetry", Action: "telemetry"},
		{ID: "electric", Action: "electric"},
		{ID: "status", Action: "status"},
		{ID: "climate", Action: "climate"},
		{ID: "climateSettings", Action: "climate-settings"},
		{ID: "health", Action: "health"},
		{ID: "location", Action: "location"},
		{ID: "serviceHistory", Action: "service-history"},
		{ID: "notifications", Action: "notifications"},
	}
}

// gate resolves the caller's MyToyota credentials through the global → org → user
// cascade and reports what is missing when they cannot be used. It is the single
// place those messages are written: handleToyotaVehicles calls it too.
func (toyotaSource) gate(ctx context.Context, s *Server, who *callerIdentity, userRaw json.RawMessage) (map[string]string, bool, string) {
	res := s.resolveToyota(ctx, who, userRaw)
	switch {
	case !res.available:
		return nil, false, "The Toyota integration is disabled by the administrator"
	case !res.orgEnabled:
		return nil, false, "The Toyota integration is disabled for your organization"
	case !res.enabled:
		return nil, false, "Enable the Toyota integration in Settings to load your vehicles"
	case strings.TrimSpace(res.eff.Username) == "" || strings.TrimSpace(res.eff.Password) == "":
		return nil, false, "Enter your MyToyota email and password to connect"
	}
	return map[string]string{
		"username": res.eff.Username,
		"password": res.eff.Password,
		"brand":    res.eff.Brand,
	}, true, ""
}

// vehicles normalizes the /v2/vehicle/guid payload. Every field is looked up by
// name rather than by path (see findString in vehicleproviders.go), because this
// is an unofficial API: a renamed or moved key costs one blank field instead of a
// broken import, and the whole object still reaches the UI via Fields/Raw.
func (toyotaSource) vehicles(raw json.RawMessage) []providerVehicle {
	objects := vehicleObjects(raw)
	out := make([]providerVehicle, 0, len(objects))

	for _, obj := range objects {
		vin := findString(obj, "vin")
		id := vin
		if id == "" {
			id = findString(obj, "vehicleId", "guid", "id")
		}
		if id == "" {
			continue // nothing stable to address it by; skip rather than guess
		}

		v := providerVehicle{
			ID:                    id,
			VIN:                   vin,
			Make:                  toyotaMake(findString(obj, "brand", "brandName", "make")),
			Model:                 findString(obj, "modelName", "modelDescription", "carModelName", "carLineName", "model"),
			Registration:          findString(obj, "licensePlate", "registrationNumber", "plateNumber", "licencePlate"),
			FuelType:              normalizeProviderFuelType(findString(obj, "fuelType", "fuel", "engineType", "powerTrain", "drivetrain")),
			BuildDate:             isoDateOnly(findString(obj, "productionDate", "manufacturingDate", "buildDate")),
			FirstRegistrationDate: isoDateOnly(findString(obj, "firstRegistrationDate", "initialRegistrationDate", "registrationDate")),
			ImageURL:              findString(obj, "imageUrl", "carImageUrl", "image", "picture"),
		}
		if year, ok := findInt(obj, "modelYear", "productionYear", "year"); ok && year > 1900 && year < 2200 {
			v.Year = year
		}
		v.Name = vehicleDisplayName(obj, v)

		if b, err := json.Marshal(obj); err == nil {
			v.Raw = b
			v.Fields, _ = flattenJSON(obj, maxSectionFields)
		}
		out = append(out, v)
	}
	return out
}

// toyotaMake expands the one-letter brand code the app uses on the wire ("T"/"L")
// and otherwise passes the reported brand through in title case.
func toyotaMake(brand string) string {
	switch strings.ToUpper(strings.TrimSpace(brand)) {
	case "T", "TOYOTA", "":
		return "Toyota"
	case "L", "LEXUS":
		return "Lexus"
	}
	b := strings.TrimSpace(brand)
	return strings.ToUpper(b[:1]) + strings.ToLower(b[1:])
}

// vehicleDisplayName is the name the car gets by default: the nickname the owner
// already gave the vehicle in the app, else make + model, else the VIN.
func vehicleDisplayName(obj map[string]any, v providerVehicle) string {
	if alias := findString(obj, "alias", "nickName", "displayName", "vehicleName"); alias != "" {
		return alias
	}
	if name := strings.TrimSpace(v.Make + " " + v.Model); name != "" {
		return name
	}
	return v.ID
}

// vehicleObjects digs the list of vehicle objects out of a plugin payload,
// accepting the shapes these APIs use interchangeably: a bare array, an
// envelope with a "payload" array, an envelope wrapping a single object, or an
// array nested somewhere else entirely.
func vehicleObjects(raw json.RawMessage) []map[string]any {
	var tree any
	if json.Unmarshal(raw, &tree) != nil {
		return nil
	}

	if node, ok := findNode(tree, "payload", "vehicles", "items", "data"); ok {
		if objs := asObjectSlice(node); objs != nil {
			return objs
		}
	}
	if objs := asObjectSlice(tree); objs != nil {
		return objs
	}
	return firstObjectSlice(tree)
}

// asObjectSlice reads a node as a list of objects, treating a lone object as a
// one-element list.
func asObjectSlice(node any) []map[string]any {
	switch v := node.(type) {
	case []any:
		out := make([]map[string]any, 0, len(v))
		for _, e := range v {
			if m, ok := e.(map[string]any); ok {
				out = append(out, m)
			}
		}
		if len(out) > 0 {
			return out
		}
	case map[string]any:
		return []map[string]any{v}
	}
	return nil
}

// firstObjectSlice finds the outermost array of objects anywhere in a tree — the
// last resort when the envelope key is not one we know.
func firstObjectSlice(root any) []map[string]any {
	queue, visited := []any{root}, 0
	for len(queue) > 0 && visited < maxWalkNodes {
		node := queue[0]
		queue = queue[1:]
		visited++

		if arr, ok := node.([]any); ok {
			if objs := asObjectSlice(arr); objs != nil {
				return objs
			}
		}
		switch v := node.(type) {
		case map[string]any:
			for _, k := range sortedKeys(v) {
				queue = append(queue, v[k])
			}
		case []any:
			queue = append(queue, v...)
		}
	}
	return nil
}

// normalizeProviderFuelType maps whatever a provider calls a powertrain onto the
// app's fuel_type enum. Substring matching on purpose: the same drivetrain
// arrives as "HV", "hybrid", "Hybrid Electric" and "PETROL_HYBRID" depending on
// the endpoint. An unrecognized value yields "" so the field is simply left
// unset rather than written wrong.
func normalizeProviderFuelType(v string) string {
	s := strings.ToLower(strings.TrimSpace(v))
	if s == "" {
		return ""
	}
	switch {
	case strings.Contains(s, "hydrogen"), strings.Contains(s, "fcev"), strings.Contains(s, "fuelcell"):
		return "hydrogen"
	// Plug-in and mild hybrids both run on petrol too, so hybrid is checked
	// before the bare fuels — "petrol hybrid" must not land on "petrol".
	case strings.Contains(s, "hybrid"), s == "hv", s == "phev", s == "mhev", strings.Contains(s, "phv"):
		return "hybrid"
	case strings.Contains(s, "diesel"):
		return "diesel"
	case strings.Contains(s, "electric"), s == "ev", s == "bev":
		return "electric"
	case strings.Contains(s, "petrol"), strings.Contains(s, "gasoline"), s == "gas":
		return "petrol"
	}
	return ""
}

// isoDateOnly reduces a provider timestamp to the YYYY-MM-DD the car's date
// fields store. Anything that is not already an ISO date is dropped, rather than
// guessed at: "01/06/2015" is June or January depending on who wrote it.
func isoDateOnly(v string) string {
	s := strings.TrimSpace(v)
	if len(s) < 10 || s[4] != '-' || s[7] != '-' {
		return ""
	}
	head := s[:10]
	for i, r := range head {
		if i == 4 || i == 7 {
			continue
		}
		if r < '0' || r > '9' {
			return ""
		}
	}
	return head
}
