package api

import (
	"context"
	"encoding/json"
	"net/http"
	"net/url"
	"strings"
)

// Integrations exposes the Toyota Connected Europe plugin to end users under a
// three-layer cascade so that everyone can run it under their own MyToyota
// account, while a superadmin (and, in an organization, an org admin) can impose
// settings from above.
//
// Resolution is a cascade: the top layer wins, and a lower layer only fills a
// field the layers above left blank.
//
//   - global (L1): pluginSettings on the app_settings singleton, set in the API
//     Server panel by a superadmin. This is the top of the cascade for everyone.
//   - org    (L2): pluginSettings.toyota on the caller's organization record,
//     editable by an org admin. Present only for users who belong to an org.
//   - user   (L3): pluginSettings.toyota on the caller's own user record.
//
// So the effective cascade is:
//   - org user:      global → org → user
//   - org-less user: global → user
//
// The MyToyota username + password resolve together as a *pair* from the highest
// layer that supplies a username, so credential halves are never mixed across
// layers. The brand resolves on its own. Enablement is strictly per-user (L3),
// gated by the global master switch (the plugin being enabled in the panel) and,
// for org users, by the org gate.
//
// Secrets (and inherited usernames) are never returned to a lower-privileged
// client: the effective config is resolved server-side and only masked values
// leave the API. The live probe runs server-side against the resolved config.

const (
	toyotaPlugin     = "toyota"
	toyotaSecretMask = "••••••••"
)

// toyotaConfig is one layer's Toyota settings.
type toyotaConfig struct {
	Username string `json:"username"`
	Password string `json:"password"`
	Brand    string `json:"brand"`
}

// toyotaStored is what we persist per user/org under pluginSettings.toyota.
type toyotaStored struct {
	Config toyotaConfig `json:"config"`
	// Enabled is the personal per-user opt-in (user layer). Default false.
	Enabled bool `json:"enabled"`
	// Disabled is the organization layer's off switch, stored inverted so that
	// absent == enabled. Only meaningful on the org record; ignored on user records.
	Disabled bool `json:"disabled,omitempty"`
}

// toyotaSettingsDoc is the pluginSettings JSON shape (only toyota today).
type toyotaSettingsDoc struct {
	Toyota toyotaStored `json:"toyota"`
}

// toyotaFieldView is one field's resolved state for the UI.
type toyotaFieldView struct {
	Effective string `json:"effective"` // resolved value in force (secrets/inherited creds masked)
	Own       string `json:"own"`       // the caller's own editable-layer value (secret masked)
	Source    string `json:"source"`    // global | org | user | unset
	Locked    bool   `json:"locked"`    // set above the caller's editable layer
}

// toyotaResolution is the fully-resolved Toyota state for one caller. It captures
// the effective cascade plus both editable layers (personal + organization), so
// an org admin can manage each independently.
type toyotaResolution struct {
	eff        toyotaConfig      // effective (unmasked) — used only server-side (probes)
	userOwn    toyotaConfig      // caller's personal (L3) values (unmasked)
	orgOwn     toyotaConfig      // organization (L2) values (unmasked)
	source     map[string]string // field -> layer name (global|org|user|unset)
	isSuper    bool              // superadmin: manages the global layer in the panel
	canOrg     bool              // caller may edit the organization layer (org admin)
	available  bool              // global master switch (plugin enabled in the panel)
	orgEnabled bool              // org gate (default true; gates the org's users)
	enabled    bool              // caller's personal enable flag
}

// toyotaLayerRank orders the cascade layers; a higher number is lower priority.
var toyotaLayerRank = map[string]int{"global": 1, "org": 2, "user": 3}

// resolveToyota computes the cascade for a caller. userRaw is the caller's
// pluginSettings blob (read from their user record).
func (s *Server) resolveToyota(ctx context.Context, who *callerIdentity, userRaw json.RawMessage) toyotaResolution {
	g, masterEnabled, _ := s.plugins.RawConfig(toyotaPlugin)
	gc := toyotaConfig{Username: g["username"], Password: g["password"], Brand: g["brand"]}

	var oStored toyotaStored
	if who.OrgID != "" {
		oStored, _ = s.orgToyota(ctx, who.OrgID)
	}
	oc := oStored.Config

	var uStored toyotaStored
	if len(userRaw) > 0 {
		var d toyotaSettingsDoc
		_ = json.Unmarshal(userRaw, &d)
		uStored = d.Toyota
	}
	uc := uStored.Config

	res := toyotaResolution{
		source:  map[string]string{},
		userOwn: uc,
		orgOwn:  oc,
		isSuper: who.isSuperadmin(),
		// An org admin may edit the organization layer in addition to their own
		// personal layer. Requires the service account (org writes go through it).
		canOrg:     who.isManager() && !who.isSuperadmin() && who.OrgID != "" && s.pb.Configured(),
		available:  masterEnabled,
		orgEnabled: !oStored.Disabled, // default true; off only when the org disabled it
		enabled:    uStored.Enabled,
	}

	// Ordered layers, top (highest priority) first.
	type layer struct {
		name string
		c    toyotaConfig
	}
	layers := []layer{{"global", gc}}
	if who.OrgID != "" {
		layers = append(layers, layer{"org", oc})
	}
	layers = append(layers, layer{"user", uc})

	// Brand resolves independently: the highest layer that sets it wins.
	res.source["brand"] = "unset"
	for _, l := range layers {
		if v := strings.TrimSpace(l.c.Brand); v != "" {
			res.eff.Brand, res.source["brand"] = v, l.name
			break
		}
	}

	// Credentials resolve as a pair from the highest layer with a username, so
	// the username and password never come from different layers.
	credSrc := "unset"
	for _, l := range layers {
		if u := strings.TrimSpace(l.c.Username); u != "" {
			res.eff.Username, res.eff.Password, credSrc = u, l.c.Password, l.name
			break
		}
	}
	res.source["username"] = credSrc
	res.source["password"] = credSrc
	return res
}

// toyotaLockedFor reports whether a field whose value comes from source is locked
// for a caller whose editable layer is editable (i.e. the value is set above them).
func toyotaLockedFor(source, editable string) bool {
	if editable == "none" {
		return true // superadmin edits the global layer in the panel, not here
	}
	sr, ok := toyotaLayerRank[source]
	if !ok {
		return false // unset — the caller may be the first to set it
	}
	return sr < toyotaLayerRank[editable]
}

// toyotaMaskPresent returns the secret mask when v is non-empty, else "".
func toyotaMaskPresent(v string) string {
	if strings.TrimSpace(v) != "" {
		return toyotaSecretMask
	}
	return ""
}

// orgToyota reads an organization's stored Toyota settings (config + the org
// gate) and its raw pluginSettings blob via the service account. Best effort:
// zero values on any miss so callers proceed as if the org layer were empty.
func (s *Server) orgToyota(ctx context.Context, orgID string) (toyotaStored, json.RawMessage) {
	if orgID == "" || !s.pb.Configured() {
		return toyotaStored{}, nil
	}
	data, status, err := s.pb.Raw(ctx, http.MethodGet,
		"/api/collections/"+colOrgs+"/records/"+url.PathEscape(orgID)+"?fields=pluginSettings", nil)
	if err != nil || status != http.StatusOK {
		return toyotaStored{}, nil
	}
	var rec struct {
		PluginSettings json.RawMessage `json:"pluginSettings"`
	}
	_ = json.Unmarshal(data, &rec)
	var doc toyotaSettingsDoc
	if len(rec.PluginSettings) > 0 {
		_ = json.Unmarshal(rec.PluginSettings, &doc)
	}
	return doc.Toyota, rec.PluginSettings
}

// userPluginSettings reads a user's raw pluginSettings blob via the service
// account. Best effort: nil on any miss.
func (s *Server) userPluginSettings(ctx context.Context, id string) json.RawMessage {
	if id == "" || !s.pb.Configured() {
		return nil
	}
	var rec struct {
		PluginSettings json.RawMessage `json:"pluginSettings"`
	}
	if err := s.pb.GetOne(ctx, s.usersCollection(), id, &rec); err != nil {
		return nil
	}
	return rec.PluginSettings
}

// mergeToyota applies a mutation to the toyota entry of a pluginSettings blob,
// preserving any other plugin keys, and returns the new blob.
func mergeToyota(existing json.RawMessage, apply func(*toyotaStored)) json.RawMessage {
	doc := map[string]json.RawMessage{}
	if len(existing) > 0 {
		_ = json.Unmarshal(existing, &doc)
	}
	if doc == nil {
		doc = map[string]json.RawMessage{} // existing was JSON null
	}
	var ts toyotaStored
	if raw, ok := doc["toyota"]; ok {
		_ = json.Unmarshal(raw, &ts)
	}
	apply(&ts)
	b, _ := json.Marshal(ts)
	doc["toyota"] = b
	out, _ := json.Marshal(doc)
	return out
}

// toyotaScopeView builds the masked field set for one editable scope. editable is
// the layer the caller edits in this scope ("user" | "org" | "none"); a field is
// locked when its effective value is set above that layer.
func (s *Server) toyotaScopeView(res toyotaResolution, editable string) map[string]any {
	own := res.userOwn
	if editable == "org" {
		own = res.orgOwn
	}
	field := func(key, eff, ownv string, secret bool) toyotaFieldView {
		src := res.source[key]
		locked := toyotaLockedFor(src, editable)
		fv := toyotaFieldView{Source: src, Locked: locked}
		switch {
		case secret:
			// Never expose a secret; show only presence.
			fv.Effective, fv.Own = toyotaMaskPresent(eff), toyotaMaskPresent(ownv)
		case key == "username" && locked:
			// Inherited username — hide the concrete value from a lower layer.
			fv.Effective, fv.Own = toyotaMaskPresent(eff), toyotaMaskPresent(ownv)
		default:
			fv.Effective, fv.Own = eff, ownv
		}
		return fv
	}
	return map[string]any{
		"editableLayer": editable,
		"fields": map[string]toyotaFieldView{
			"username": field("username", res.eff.Username, own.Username, false),
			"password": field("password", res.eff.Password, own.Password, true),
			"brand":    field("brand", res.eff.Brand, own.Brand, false),
		},
	}
}

// toyotaView builds the masked, client-safe response body from a resolution. It
// exposes a "user" scope for everyone plus, for org admins, an "org" scope — each
// with its own locked-field state — so the UI can present the two independently.
func (s *Server) toyotaView(who *callerIdentity, res toyotaResolution) map[string]any {
	out := map[string]any{
		"available":    res.available,
		"orgEnabled":   res.orgEnabled,
		"enabled":      res.enabled,
		"role":         who.Role,
		"orgId":        who.OrgID,
		"canEditOrg":   res.canOrg,
		"isSuperadmin": res.isSuper,
	}
	if res.isSuper {
		// Superadmin manages the global layer in the panel; here it is read-only.
		out["editableLayer"] = "none"
		out["scopes"] = map[string]any{"user": s.toyotaScopeView(res, "none")}
		return out
	}
	scopes := map[string]any{"user": s.toyotaScopeView(res, "user")}
	if res.canOrg {
		scopes["org"] = s.toyotaScopeView(res, "org")
	}
	out["scopes"] = scopes
	return out
}

// GET /api/integrations/toyota — resolved Toyota view for the caller.
func (s *Server) handleGetToyota(w http.ResponseWriter, r *http.Request) {
	who := caller(r)
	if who == nil {
		writeError(w, http.StatusUnauthorized, "not authenticated")
		return
	}
	userRaw := s.userPluginSettings(r.Context(), who.ID)
	res := s.resolveToyota(r.Context(), who, userRaw)
	writeJSON(w, http.StatusOK, s.toyotaView(who, res))
}

// PUT /api/integrations/toyota — save the caller's editable layer. Body:
// {enabled?: bool, scope?: "user"|"org", config?: {username, password, brand}}.
// Fields locked above the caller are ignored; a password left at the mask is
// preserved.
func (s *Server) handlePutToyota(w http.ResponseWriter, r *http.Request) {
	who := caller(r)
	if who == nil {
		writeError(w, http.StatusUnauthorized, "not authenticated")
		return
	}
	if !s.pb.Configured() {
		writeError(w, http.StatusServiceUnavailable, "integration settings not configured on the server")
		return
	}
	var body struct {
		Enabled *bool             `json:"enabled"`
		Scope   string            `json:"scope"` // "user" (default) | "org" (admins only)
		Config  map[string]string `json:"config"`
	}
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		writeError(w, http.StatusBadRequest, "invalid json")
		return
	}

	userRaw := s.userPluginSettings(r.Context(), who.ID)
	res := s.resolveToyota(r.Context(), who, userRaw)

	// Resolve which layer this write targets. Everyone edits their own personal
	// (user) layer by default; an org admin may target the organization layer by
	// asking for scope "org". Superadmins are read-only here (they manage global
	// in the panel) and may only toggle their personal enable flag.
	editable := "user"
	switch {
	case res.isSuper:
		editable = "none"
	case strings.EqualFold(strings.TrimSpace(body.Scope), "org"):
		if !res.canOrg {
			writeError(w, http.StatusForbidden, "only an organization admin can edit organization settings")
			return
		}
		editable = "org"
	}

	// Build the new target-layer config from its current own values, overlaying
	// only fields the caller is allowed to change in this scope.
	newOwn := res.userOwn
	if editable == "org" {
		newOwn = res.orgOwn
	}
	applyField := func(key string, set func(*toyotaConfig, string)) {
		v, ok := body.Config[key]
		if !ok || toyotaLockedFor(res.source[key], editable) {
			return
		}
		if key == "password" && v == toyotaSecretMask {
			return // keep current secret
		}
		set(&newOwn, strings.TrimSpace(v))
	}
	applyField("username", func(c *toyotaConfig, v string) { c.Username = v })
	applyField("password", func(c *toyotaConfig, v string) { c.Password = v })
	applyField("brand", func(c *toyotaConfig, v string) { c.Brand = v })

	// Persist the organization layer (admins) via the service account.
	if editable == "org" {
		_, orgRaw := s.orgToyota(r.Context(), who.OrgID)
		newDoc := mergeToyota(orgRaw, func(ts *toyotaStored) {
			ts.Config = newOwn
			// In the org scope the enable flag is the org master switch, stored
			// inverted (disabled) so absent means enabled.
			if body.Enabled != nil {
				ts.Disabled = !*body.Enabled
			}
		})
		_, st, err := s.pb.Raw(r.Context(), http.MethodPatch,
			"/api/collections/"+colOrgs+"/records/"+url.PathEscape(who.OrgID),
			map[string]json.RawMessage{"pluginSettings": newDoc})
		if err != nil {
			writeUpstreamDown(w, err)
			return
		}
		if st != http.StatusOK {
			writeError(w, http.StatusBadGateway, "could not save organization settings")
			return
		}
	}

	// Persist the user record: the personal enable flag lives here (user/
	// superadmin scope — in the org scope it targets the org gate instead), and
	// so does the personal config layer when this write targets the user scope.
	personalEnable := body.Enabled != nil && editable != "org"
	if personalEnable || editable == "user" {
		newDoc := mergeToyota(userRaw, func(ts *toyotaStored) {
			if personalEnable {
				ts.Enabled = *body.Enabled
			}
			if editable == "user" {
				ts.Config = newOwn
			}
		})
		if err := s.pb.Update(r.Context(), s.usersCollection(), who.ID,
			map[string]any{"pluginSettings": newDoc}, nil); err != nil {
			writePBError(w, err)
			return
		}
	}

	// Re-resolve and return the fresh view.
	fresh := s.userPluginSettings(r.Context(), who.ID)
	res2 := s.resolveToyota(r.Context(), who, fresh)
	writeJSON(w, http.StatusOK, s.toyotaView(who, res2))
}

// POST /api/integrations/toyota/health — live probe using the caller's resolved
// config. Never returns secrets.
func (s *Server) handleToyotaHealth(w http.ResponseWriter, r *http.Request) {
	who := caller(r)
	if who == nil {
		writeError(w, http.StatusUnauthorized, "not authenticated")
		return
	}
	userRaw := s.userPluginSettings(r.Context(), who.ID)
	res := s.resolveToyota(r.Context(), who, userRaw)

	down := func(detail string) {
		writeJSON(w, http.StatusOK, map[string]any{"health": map[string]any{"status": "down", "detail": detail}})
	}
	switch {
	case !res.available:
		down("The Toyota integration is disabled by the administrator")
		return
	case !res.orgEnabled:
		down("The Toyota integration is disabled for your organization")
		return
	case strings.TrimSpace(res.eff.Username) == "" || strings.TrimSpace(res.eff.Password) == "":
		down("Enter your MyToyota email and password to connect")
		return
	}

	cfg := map[string]string{
		"username": res.eff.Username,
		"password": res.eff.Password,
		"brand":    res.eff.Brand,
	}
	h, err := s.plugins.HealthCheckWith(r.Context(), toyotaPlugin, cfg)
	if err != nil {
		writeJSON(w, http.StatusBadGateway, map[string]any{"error": err.Error()})
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"health": h})
}

// GET /api/integrations/toyota/vehicles — the caller's Toyota vehicles as the
// upstream returned them, fetched server-side under their resolved credentials.
// Gated by the same switches as the settings view (global master, org gate,
// personal opt-in, credentials present); when any gate is off it returns 200 with
// an empty list plus a reason, so the UI can degrade quietly rather than error.
//
// The gate and its wording live in toyotaSource (vehicleproviders_toyota.go), so
// this endpoint and the generic vehicle-provider endpoints cannot drift apart.
// This one stays because it relays the raw payload; /api/vehicle-providers/
// toyota/vehicles returns the normalized, importable shape.
func (s *Server) handleToyotaVehicles(w http.ResponseWriter, r *http.Request) {
	who := caller(r)
	if who == nil {
		writeError(w, http.StatusUnauthorized, "not authenticated")
		return
	}
	userRaw := s.userPluginSettings(r.Context(), who.ID)
	cfg, ok, detail := toyotaSource{}.gate(r.Context(), s, who, userRaw)
	if !ok {
		writeJSON(w, http.StatusOK, map[string]any{"vehicles": []any{}, "unavailable": true, "detail": detail})
		return
	}

	raw, err := s.plugins.InvokeWith(r.Context(), toyotaPlugin, cfg, "vehicles", nil)
	if err != nil {
		writeJSON(w, http.StatusBadGateway, map[string]any{"error": err.Error()})
		return
	}
	// Relay the upstream JSON verbatim under "vehicles".
	writeJSON(w, http.StatusOK, map[string]any{"vehicles": json.RawMessage(raw)})
}
