package api

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

// This file mirrors integrations.go and integrations_ankersolix.go for the
// Greencell HabuDen EV charger: the same three-layer cascade (global → org →
// user) lets everyone point the integration at their own MQTT broker while a
// superadmin (and, in an organization, an org admin) can impose settings from
// above. See integrations.go for the full rationale; only the fields differ.
//
//   - global (L1): pluginSettings on the app_settings singleton, set in the API
//     Server panel by a superadmin — the top of the cascade for everyone.
//   - org    (L2): pluginSettings.greencell on the caller's organization record.
//   - user   (L3): pluginSettings.greencell on the caller's own user record.
//
// Unlike the cloud connectors, what resolves here is a *broker*, not an account:
// host, port, TLS and the broker credentials describe one endpoint and therefore
// resolve together as a unit from the highest layer that supplies a host. Mixing
// a host from the organization with a password from a user would address a broker
// with credentials that were never meant for it. The charger serial, the QUERY
// command topic and the listen window resolve on their own, because each
// describes the charger rather than the endpoint.
//
// 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.

const (
	greencellPlugin     = "greencell"
	greencellSecretMask = "••••••••"
)

// greencellConfig is one layer's Greencell settings.
type greencellConfig struct {
	// Broker identity — these five resolve together (see the file comment).
	Host     string `json:"host"`
	Port     string `json:"port"`
	TLS      string `json:"tls"` // "on" | "off"; empty means unset so the cascade continues
	Username string `json:"username"`
	Password string `json:"password"`

	// Serial is the charger to read; blank means "discover whatever is there".
	Serial string `json:"serial"`
	// CommandTopic is the optional MQTT topic a QUERY is sent on to make the
	// charger publish at once. Greencell does not document it, so it is left to
	// whoever found theirs; blank means read-only listening.
	CommandTopic string `json:"commandTopic"`
	// Timeout is the listen window in seconds — how long a read waits for the
	// charger to publish before answering with what it has.
	Timeout string `json:"timeout"`
}

// greencellStored is what we persist per user/org under pluginSettings.greencell.
type greencellStored struct {
	Config greencellConfig `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"`
}

// greencellSettingsDoc is the pluginSettings JSON shape for the greencell key.
type greencellSettingsDoc struct {
	Greencell greencellStored `json:"greencell"`
}

// greencellFieldView is one field's resolved state for the UI.
type greencellFieldView struct {
	Effective string `json:"effective"` // resolved value in force (secrets/inherited identity 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
}

// greencellResolution is the fully-resolved Greencell state for one caller.
type greencellResolution struct {
	eff        greencellConfig   // effective (unmasked) — used only server-side (probes)
	userOwn    greencellConfig   // caller's personal (L3) values (unmasked)
	orgOwn     greencellConfig   // 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
}

// greencellBrokerFields are the fields that resolve as one unit with the host.
var greencellBrokerFields = []string{"host", "port", "tls", "username", "password"}

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

// normalizeGreencellTLS maps a raw TLS value to "on"/"off", or "" when unset or
// unrecognized so the cascade continues to the next layer.
func normalizeGreencellTLS(v string) string {
	switch strings.ToLower(strings.TrimSpace(v)) {
	case "on", "true", "yes", "1", "tls", "mqtts":
		return "on"
	case "off", "false", "no", "0", "plain":
		return "off"
	default:
		return ""
	}
}

// normalizeGreencellPort validates a broker port, returning "" for anything that
// is not a usable TCP port so a typo does not silently address port 0.
func normalizeGreencellPort(v string) string {
	v = strings.TrimSpace(v)
	if v == "" {
		return ""
	}
	n, err := strconv.Atoi(v)
	if err != nil || n < 1 || n > 65535 {
		return ""
	}
	return strconv.Itoa(n)
}

// normalizeGreencellTimeout validates the listen window in seconds. The plugin
// clamps the value it is given as well; rejecting nonsense here keeps a bad entry
// from being stored and shown back as if it had taken effect.
func normalizeGreencellTimeout(v string) string {
	v = strings.TrimSpace(v)
	if v == "" {
		return ""
	}
	n, err := strconv.Atoi(v)
	if err != nil || n < 1 || n > 60 {
		return ""
	}
	return strconv.Itoa(n)
}

// resolveGreencell computes the cascade for a caller. userRaw is the caller's
// pluginSettings blob (read from their user record).
func (s *Server) resolveGreencell(ctx context.Context, who *callerIdentity, userRaw json.RawMessage) greencellResolution {
	g, masterEnabled, _ := s.plugins.RawConfig(greencellPlugin)
	gc := greencellConfig{
		Host: g["host"], Port: g["port"], TLS: g["tls"],
		Username: g["username"], Password: g["password"],
		Serial: g["serial"], CommandTopic: g["commandTopic"], Timeout: g["timeout"],
	}

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

	var uStored greencellStored
	if len(userRaw) > 0 {
		var d greencellSettingsDoc
		_ = json.Unmarshal(userRaw, &d)
		uStored = d.Greencell
	}
	uc := uStored.Config

	res := greencellResolution{
		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    greencellConfig
	}
	layers := []layer{{"global", gc}}
	if who.OrgID != "" {
		layers = append(layers, layer{"org", oc})
	}
	layers = append(layers, layer{"user", uc})

	// The broker resolves as a unit from the highest layer that names a host, so
	// an address is never combined with credentials from a different layer.
	brokerSrc := "unset"
	for _, l := range layers {
		if h := strings.TrimSpace(l.c.Host); h != "" {
			res.eff.Host = h
			res.eff.Port = normalizeGreencellPort(l.c.Port)
			res.eff.TLS = normalizeGreencellTLS(l.c.TLS)
			res.eff.Username = strings.TrimSpace(l.c.Username)
			res.eff.Password = l.c.Password
			brokerSrc = l.name
			break
		}
	}
	for _, f := range greencellBrokerFields {
		res.source[f] = brokerSrc
	}

	// The serial and the listen window say nothing about the broker, so each
	// resolves on its own: the highest layer that sets it wins.
	res.source["serial"] = "unset"
	for _, l := range layers {
		if v := strings.TrimSpace(l.c.Serial); v != "" {
			res.eff.Serial, res.source["serial"] = v, l.name
			break
		}
	}
	res.source["commandTopic"] = "unset"
	for _, l := range layers {
		if v := strings.TrimSpace(l.c.CommandTopic); v != "" {
			res.eff.CommandTopic, res.source["commandTopic"] = v, l.name
			break
		}
	}
	res.source["timeout"] = "unset"
	for _, l := range layers {
		if v := normalizeGreencellTimeout(l.c.Timeout); v != "" {
			res.eff.Timeout, res.source["timeout"] = v, l.name
			break
		}
	}
	return res
}

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

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

// orgGreencell reads an organization's stored Greencell 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) orgGreencell(ctx context.Context, orgID string) (greencellStored, json.RawMessage) {
	if orgID == "" || !s.pb.Configured() {
		return greencellStored{}, 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 greencellStored{}, nil
	}
	var rec struct {
		PluginSettings json.RawMessage `json:"pluginSettings"`
	}
	_ = json.Unmarshal(data, &rec)
	var doc greencellSettingsDoc
	if len(rec.PluginSettings) > 0 {
		_ = json.Unmarshal(rec.PluginSettings, &doc)
	}
	return doc.Greencell, rec.PluginSettings
}

// mergeGreencell applies a mutation to the greencell entry of a pluginSettings
// blob, preserving any other plugin keys, and returns the new blob.
func mergeGreencell(existing json.RawMessage, apply func(*greencellStored)) 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 gs greencellStored
	if raw, ok := doc["greencell"]; ok {
		_ = json.Unmarshal(raw, &gs)
	}
	apply(&gs)
	b, _ := json.Marshal(gs)
	doc["greencell"] = b
	out, _ := json.Marshal(doc)
	return out
}

// greencellScopeView 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) greencellScopeView(res greencellResolution, editable string) map[string]any {
	own := res.userOwn
	if editable == "org" {
		own = res.orgOwn
	}
	field := func(key, eff, ownv string, secret bool) greencellFieldView {
		src := res.source[key]
		locked := greencellLockedFor(src, editable)
		fv := greencellFieldView{Source: src, Locked: locked}
		switch {
		case secret:
			// Never expose a secret; show only presence.
			fv.Effective, fv.Own = greencellMaskPresent(eff), greencellMaskPresent(ownv)
		case (key == "host" || key == "username") && locked:
			// An inherited broker address or account belongs to the layer above;
			// show only that it is set.
			fv.Effective, fv.Own = greencellMaskPresent(eff), greencellMaskPresent(ownv)
		default:
			fv.Effective, fv.Own = eff, ownv
		}
		return fv
	}
	return map[string]any{
		"editableLayer": editable,
		"fields": map[string]greencellFieldView{
			"host":         field("host", res.eff.Host, own.Host, false),
			"port":         field("port", res.eff.Port, normalizeGreencellPort(own.Port), false),
			"tls":          field("tls", res.eff.TLS, normalizeGreencellTLS(own.TLS), false),
			"username":     field("username", res.eff.Username, own.Username, false),
			"password":     field("password", res.eff.Password, own.Password, true),
			"serial":       field("serial", res.eff.Serial, own.Serial, false),
			"timeout":      field("timeout", res.eff.Timeout, normalizeGreencellTimeout(own.Timeout), false),
			"commandTopic": field("commandTopic", res.eff.CommandTopic, own.CommandTopic, false),
		},
	}
}

// greencellView builds the masked, client-safe response body from a resolution.
func (s *Server) greencellView(who *callerIdentity, res greencellResolution) 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.greencellScopeView(res, "none")}
		return out
	}
	scopes := map[string]any{"user": s.greencellScopeView(res, "user")}
	if res.canOrg {
		scopes["org"] = s.greencellScopeView(res, "org")
	}
	out["scopes"] = scopes
	return out
}

// greencellPluginConfig turns a resolution into the config map the plugin takes.
func greencellPluginConfig(res greencellResolution) map[string]string {
	return map[string]string{
		"host":         res.eff.Host,
		"port":         res.eff.Port,
		"tls":          res.eff.TLS,
		"username":     res.eff.Username,
		"password":     res.eff.Password,
		"serial":       res.eff.Serial,
		"commandTopic": res.eff.CommandTopic,
		"timeout":      res.eff.Timeout,
	}
}

// greencellGate returns the reason the integration cannot run for this caller, or
// "" when it can. requireOptIn additionally demands the personal enable flag,
// which a live probe deliberately does not (the probe is how you check settings
// before turning it on).
func greencellGate(res greencellResolution, requireOptIn bool) string {
	switch {
	case !res.available:
		return "The Greencell integration is disabled by the administrator"
	case !res.orgEnabled:
		return "The Greencell integration is disabled for your organization"
	case requireOptIn && !res.enabled:
		return "Enable the Greencell integration in Settings to load your chargers"
	case strings.TrimSpace(res.eff.Host) == "":
		return "Enter the address of the MQTT broker your charger publishes to"
	}
	return ""
}

// GET /api/integrations/greencell — resolved Greencell view for the caller.
func (s *Server) handleGetGreencell(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.resolveGreencell(r.Context(), who, userRaw)
	writeJSON(w, http.StatusOK, s.greencellView(who, res))
}

// PUT /api/integrations/greencell — save the caller's editable layer. Body:
// {enabled?: bool, scope?: "user"|"org", config?: {host, port, tls, username,
// password, serial, commandTopic, timeout}}. Fields locked above the caller are
// ignored; a password left at the mask is preserved.
func (s *Server) handlePutGreencell(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.resolveGreencell(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(*greencellConfig, string)) {
		v, ok := body.Config[key]
		if !ok || greencellLockedFor(res.source[key], editable) {
			return
		}
		if key == "password" && v == greencellSecretMask {
			return // keep current secret
		}
		set(&newOwn, strings.TrimSpace(v))
	}
	applyField("host", func(c *greencellConfig, v string) { c.Host = v })
	applyField("port", func(c *greencellConfig, v string) { c.Port = normalizeGreencellPort(v) })
	applyField("tls", func(c *greencellConfig, v string) { c.TLS = normalizeGreencellTLS(v) })
	applyField("username", func(c *greencellConfig, v string) { c.Username = v })
	applyField("password", func(c *greencellConfig, v string) { c.Password = v })
	applyField("serial", func(c *greencellConfig, v string) { c.Serial = strings.ToUpper(v) })
	applyField("commandTopic", func(c *greencellConfig, v string) { c.CommandTopic = v })
	applyField("timeout", func(c *greencellConfig, v string) { c.Timeout = normalizeGreencellTimeout(v) })

	// Persist the organization layer (admins) via the service account.
	if editable == "org" {
		_, orgRaw := s.orgGreencell(r.Context(), who.OrgID)
		newDoc := mergeGreencell(orgRaw, func(gs *greencellStored) {
			gs.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 {
				gs.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 := mergeGreencell(userRaw, func(gs *greencellStored) {
			if personalEnable {
				gs.Enabled = *body.Enabled
			}
			if editable == "user" {
				gs.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.resolveGreencell(r.Context(), who, fresh)
	writeJSON(w, http.StatusOK, s.greencellView(who, res2))
}

// POST /api/integrations/greencell/health — live probe using the caller's
// resolved config: connect to the broker and ask any charger to announce itself.
// Never returns secrets.
func (s *Server) handleGreencellHealth(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.resolveGreencell(r.Context(), who, userRaw)

	if reason := greencellGate(res, false); reason != "" {
		writeJSON(w, http.StatusOK, map[string]any{"health": map[string]any{"status": "down", "detail": reason}})
		return
	}
	h, err := s.plugins.HealthCheckWith(r.Context(), greencellPlugin, greencellPluginConfig(res))
	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/greencell/chargers — the chargers that answer a discovery
// broadcast on the caller's resolved broker. Gated by the same switches as the
// settings view; when any gate is off it returns 200 with an empty list plus a
// reason, so the UI can degrade quietly rather than error.
func (s *Server) handleGreencellChargers(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.resolveGreencell(r.Context(), who, userRaw)

	if reason := greencellGate(res, true); reason != "" {
		writeJSON(w, http.StatusOK, map[string]any{"chargers": []any{}, "unavailable": true, "detail": reason})
		return
	}
	raw, err := s.plugins.InvokeWith(r.Context(), greencellPlugin, greencellPluginConfig(res), "chargers", nil)
	if err != nil {
		writeJSON(w, http.StatusBadGateway, map[string]any{"error": err.Error()})
		return
	}
	// The plugin already answers {"chargers": [...]}; relay it verbatim.
	writeJSON(w, http.StatusOK, json.RawMessage(raw))
}

// GET /api/integrations/greencell/chargers/{sn}/state — one charger's live state,
// read off the broker under the caller's resolved config.
func (s *Server) handleGreencellChargerState(w http.ResponseWriter, r *http.Request) {
	who := caller(r)
	if who == nil {
		writeError(w, http.StatusUnauthorized, "not authenticated")
		return
	}
	sn := strings.TrimSpace(r.PathValue("sn"))
	if sn == "" {
		writeError(w, http.StatusBadRequest, "charger serial is required")
		return
	}

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

	if reason := greencellGate(res, true); reason != "" {
		writeJSON(w, http.StatusOK, map[string]any{"unavailable": true, "detail": reason})
		return
	}
	params, _ := json.Marshal(map[string]string{"sn": sn})
	raw, err := s.plugins.InvokeWith(r.Context(), greencellPlugin, greencellPluginConfig(res), "charger-state", params)
	if err != nil {
		writeJSON(w, http.StatusBadGateway, map[string]any{"error": err.Error()})
		return
	}
	writeJSON(w, http.StatusOK, json.RawMessage(raw))
}
