package api

// Charger providers are the garage's import aimed at the wall instead of the
// driveway: a charger already on a service the user has connected becomes a
// charger in DriverVault, without anyone copying a serial off a label.
//
//	GET  /api/charger-providers                      — providers, with connect state
//	GET  /api/charger-providers/{provider}/chargers  — the caller's chargers there
//	POST /api/charger-providers/{provider}/import    — create a home charger from one
//
// The shape deliberately follows vehicleproviders.go — a chargerSource is a
// small adapter over an existing plugin plus its per-user credential cascade, so
// the next charger service is one adapter appended to chargerSources() and
// nothing else. What differs is how little is imported: a car pulls identity,
// dates and an odometer from several capabilities, whereas a charger is a name,
// a serial and the hardware behind it, all of which the list already carries. So
// there is no include selection here and no second round of calls.
//
// Credentials are the caller's own, resolved through the same global → org →
// user cascade the Settings page edits. Nothing borrows another user's account.

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

	"drivervault/apiserver/internal/models"
)

// chargerSource adapts one plugin that can enumerate the caller's chargers.
type chargerSource interface {
	// id is the URL segment and the value persisted on home_charger.provider.
	id() string
	// label names the provider to the user ("Anker Solix").
	label() string
	// service is the upstream service behind it ("Anker Solix cloud").
	service() string
	// gate resolves the caller's effective config from the integration cascade.
	// When ok is false nothing is called and detail says, in one sentence, what
	// the user has to do about it. userRaw is the caller's pluginSettings blob,
	// passed in so one request reads it once.
	gate(ctx context.Context, s *Server, who *callerIdentity, userRaw json.RawMessage) (cfg map[string]string, ok bool, detail string)
	// listAction is the plugin capability that enumerates chargers.
	listAction() string
	// chargers maps that capability's payload onto normalized entries.
	chargers(raw json.RawMessage) []providerCharger
}

// chargerSources are the registered providers, in menu order.
func chargerSources() []chargerSource {
	return []chargerSource{ankerChargerSource{}, greencellChargerSource{}}
}

func chargerSourceByID(id string) (chargerSource, bool) {
	for _, src := range chargerSources() {
		if src.id() == id {
			return src, true
		}
	}
	return nil, false
}

// providerCharger is one charger on the caller's provider account, normalized
// into the fields a home charger is built from.
type providerCharger struct {
	ID       string `json:"id"` // the provider's own id — the serial, for both
	Name     string `json:"name"`
	Vendor   string `json:"vendor,omitempty"`
	Model    string `json:"model,omitempty"`
	Firmware string `json:"firmware,omitempty"`
	SiteID   string `json:"siteId,omitempty"`
	SiteName string `json:"siteName,omitempty"`
	Status   string `json:"status,omitempty"` // the service's own word for its state
	Online   *bool  `json:"online,omitempty"`

	// How the charger is registered on the account — standalone, inside a
	// system (site), or merely bound to it. A charger can be several at once,
	// and which ones it is decides how much the service says about it.
	Sources []string `json:"sources,omitempty"`

	// What it is doing right now, when the service knows: the charge power as
	// the service words it (the unit is upstream's, so it is relayed verbatim)
	// and the charger's OCPP connector state as the service sees it.
	Power          string `json:"power,omitempty"`
	OcppStatus     *int   `json:"ocppStatus,omitempty"`
	OcppStatusDesc string `json:"ocppStatusDesc,omitempty"`

	// What the service knows about the box on the wall rather than the charging:
	// the networks it is on, where it thinks it is, when the account bound it,
	// and the picture the service shows for the model.
	WifiName  string   `json:"wifiName,omitempty"`
	WifiMac   string   `json:"wifiMac,omitempty"`
	WifiRSSI  *int     `json:"wifiRssi,omitempty"`
	BleMac    string   `json:"bleMac,omitempty"`
	TimeZone  string   `json:"timeZone,omitempty"`
	LinkedAt  *float64 `json:"linkedAt,omitempty"` // unix seconds
	ImageURL  string   `json:"imageUrl,omitempty"`
	RelatedBy []string `json:"relatedBy,omitempty"`

	// Attrs is everything else the service said about this charger, under the
	// service's own field names. The fields above are the ones DriverVault has a
	// name for; this is the remainder, relayed so a card can show what the
	// account actually knows rather than only the part we modelled.
	Attrs map[string]string `json:"attrs,omitempty"`

	// LinkedChargerID is set when this one is already in DriverVault, so the UI
	// never offers to import the same charger twice.
	LinkedChargerID string `json:"linkedChargerId,omitempty"`
}

// --- the providers ------------------------------------------------------------

// ankerChargerSource imports from the Anker Solix cloud. The chargers capability
// merges the cloud's several views of an account (see the plugin's chargers.go),
// so a charger arrives here whether it stands alone or belongs to a system.
type ankerChargerSource struct{}

func (ankerChargerSource) id() string         { return ankerPlugin }
func (ankerChargerSource) label() string      { return "Anker Solix" }
func (ankerChargerSource) service() string    { return "Anker Solix cloud" }
func (ankerChargerSource) listAction() string { return "chargers" }

func (ankerChargerSource) gate(ctx context.Context, s *Server, who *callerIdentity, userRaw json.RawMessage) (map[string]string, bool, string) {
	res := s.resolveAnker(ctx, who, userRaw)
	if reason := ankerGate(res, true); reason != "" {
		return nil, false, reason
	}
	return map[string]string{
		"email":    res.eff.Email,
		"password": res.eff.Password,
		"country":  res.eff.Country,
	}, true, ""
}

func (ankerChargerSource) chargers(raw json.RawMessage) []providerCharger {
	var env struct {
		Chargers []struct {
			SN             string   `json:"sn"`
			Name           string   `json:"name"`
			Model          string   `json:"model"`
			Firmware       string   `json:"firmware"`
			SiteID         string   `json:"siteId"`
			SiteName       string   `json:"siteName"`
			Sources        []string `json:"sources"`
			StatusDesc     string   `json:"statusDesc"`
			Online         *bool    `json:"online"`
			Power          string   `json:"power"`
			OcppStatus     *int     `json:"ocppStatus"`
			OcppStatusDesc string   `json:"ocppStatusDesc"`

			WifiName  string   `json:"wifiName"`
			WifiMac   string   `json:"wifiMac"`
			WifiRSSI  *int     `json:"wifiRssi"`
			BleMac    string   `json:"bleMac"`
			TimeZone  string   `json:"timeZone"`
			LinkedAt  *float64 `json:"linkedAt"`
			ImageURL  string   `json:"imageUrl"`
			RelatedBy []string `json:"relatedBy"`

			Attrs map[string]string `json:"attrs"`
		} `json:"chargers"`
	}
	if json.Unmarshal(raw, &env) != nil {
		return nil
	}
	out := make([]providerCharger, 0, len(env.Chargers))
	for _, c := range env.Chargers {
		if c.SN == "" {
			continue
		}
		out = append(out, providerCharger{
			ID: c.SN, Name: c.Name, Vendor: "Anker Solix", Model: c.Model,
			Firmware: c.Firmware, SiteID: c.SiteID, SiteName: c.SiteName,
			Sources: c.Sources, Status: c.StatusDesc, Online: c.Online,
			Power: c.Power, OcppStatus: c.OcppStatus, OcppStatusDesc: c.OcppStatusDesc,
			WifiName: c.WifiName, WifiMac: c.WifiMac, WifiRSSI: c.WifiRSSI, BleMac: c.BleMac,
			TimeZone: c.TimeZone, LinkedAt: c.LinkedAt, ImageURL: c.ImageURL, RelatedBy: c.RelatedBy,
			Attrs: c.Attrs,
		})
	}
	return out
}

// greencellChargerSource imports from a Greencell wallbox on the user's own MQTT
// broker. There is no cloud account behind it — the charger announces itself on
// the broker, which is why this provider can be connected while offering nothing
// until a charger answers.
type greencellChargerSource struct{}

func (greencellChargerSource) id() string         { return greencellPlugin }
func (greencellChargerSource) label() string      { return "Greencell" }
func (greencellChargerSource) service() string    { return "Greencell (MQTT broker)" }
func (greencellChargerSource) listAction() string { return "chargers" }

func (greencellChargerSource) gate(ctx context.Context, s *Server, who *callerIdentity, userRaw json.RawMessage) (map[string]string, bool, string) {
	res := s.resolveGreencell(ctx, who, userRaw)
	if reason := greencellGate(res, true); reason != "" {
		return nil, false, reason
	}
	return greencellPluginConfig(res), true, ""
}

func (greencellChargerSource) chargers(raw json.RawMessage) []providerCharger {
	var env struct {
		Chargers []struct {
			SN    string `json:"sn"`
			Name  string `json:"name"`
			Model string `json:"model"`
		} `json:"chargers"`
	}
	if json.Unmarshal(raw, &env) != nil {
		return nil
	}
	out := make([]providerCharger, 0, len(env.Chargers))
	for _, c := range env.Chargers {
		if c.SN == "" {
			continue
		}
		out = append(out, providerCharger{ID: c.SN, Name: c.Name, Vendor: "Greencell", Model: c.Model})
	}
	return out
}

// --- handlers -----------------------------------------------------------------

// GET /api/charger-providers — every provider with whether the caller can use it
// and, when they cannot, the one sentence that says what to do about it. Never
// an error: an unconnected provider is a normal state with an answer.
func (s *Server) handleListChargerProviders(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)

	out := make([]map[string]any, 0, len(chargerSources()))
	for _, src := range chargerSources() {
		_, ok, detail := src.gate(r.Context(), s, who, userRaw)
		out = append(out, map[string]any{
			"id":        src.id(),
			"label":     src.label(),
			"service":   src.service(),
			"connected": ok,
			"detail":    detail,
		})
	}
	writeJSON(w, http.StatusOK, map[string]any{"providers": out})
}

// resolveChargerSource looks up the provider named in the path and gates it for
// the caller. On any failure it writes the response and returns ok=false.
//
// softGate picks how a closed gate is reported, exactly as the vehicle side does:
// a listing answers 200 with an empty list and a reason, so the UI can say
// "connect this in Settings"; a write answers 400, because there the caller asked
// for something that did not happen.
func (s *Server) resolveChargerSource(w http.ResponseWriter, r *http.Request, softGate bool) (chargerSource, map[string]string, bool) {
	who := caller(r)
	if who == nil {
		writeError(w, http.StatusUnauthorized, "not authenticated")
		return nil, nil, false
	}
	src, found := chargerSourceByID(r.PathValue("provider"))
	if !found {
		writeError(w, http.StatusNotFound, "unknown charger provider")
		return nil, nil, false
	}
	userRaw := s.userPluginSettings(r.Context(), who.ID)
	cfg, ok, detail := src.gate(r.Context(), s, who, userRaw)
	if !ok {
		if softGate {
			writeJSON(w, http.StatusOK, map[string]any{
				"provider":    src.id(),
				"label":       src.label(),
				"service":     src.service(),
				"chargers":    []any{},
				"unavailable": true,
				"detail":      detail,
			})
		} else {
			writeError(w, http.StatusBadRequest, detail)
		}
		return nil, nil, false
	}
	return src, cfg, true
}

// GET /api/charger-providers/{provider}/chargers — the chargers on that account,
// each annotated with the DriverVault charger it is already linked to.
func (s *Server) handleProviderChargers(w http.ResponseWriter, r *http.Request) {
	src, cfg, ok := s.resolveChargerSource(w, r, true)
	if !ok {
		return
	}
	chargers, err := s.fetchProviderChargers(r.Context(), src, cfg)
	if err != nil {
		writeJSON(w, http.StatusBadGateway, map[string]any{"error": err.Error()})
		return
	}
	linked := s.linkedChargers(r.Context(), s.currentUserID(r), src.id())
	for i := range chargers {
		chargers[i].LinkedChargerID = linked[chargers[i].ID]
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"provider": src.id(),
		"label":    src.label(),
		"service":  src.service(),
		"chargers": chargers,
	})
}

// fetchProviderChargers invokes the provider's list capability and normalizes it.
func (s *Server) fetchProviderChargers(ctx context.Context, src chargerSource, cfg map[string]string) ([]providerCharger, error) {
	raw, err := s.plugins.InvokeWith(ctx, src.id(), cfg, src.listAction(), nil)
	if err != nil {
		return nil, err
	}
	return src.chargers(raw), nil
}

// linkedChargers maps provider charger id -> home charger id for one user and
// provider. Best effort: an unreachable PocketBase yields an empty map and the
// UI simply shows nothing as linked.
func (s *Server) linkedChargers(ctx context.Context, userID, provider string) map[string]string {
	out := map[string]string{}
	if userID == "" || provider == "" {
		return out
	}
	res, err := s.pb.List(ctx, colHomeChargers, url.Values{
		"filter":  {"owner='" + userID + "' && provider='" + provider + "'"},
		"perPage": {"200"},
	})
	if err != nil {
		return out
	}
	var recs []homeChargerRecord
	if json.Unmarshal(res.Items, &recs) != nil {
		return out
	}
	for _, rec := range recs {
		if rec.ProviderChargerID != "" {
			out[rec.ProviderChargerID] = rec.ID
		}
	}
	return out
}

// POST /api/charger-providers/{provider}/import — create a home charger from one
// on the account. Body: {chargerId, name?}. The name defaults to what the service
// calls it, then to the provider's own label, so a charger is never nameless.
func (s *Server) handleChargerImport(w http.ResponseWriter, r *http.Request) {
	var body struct {
		ChargerID string `json:"chargerId"`
		Name      string `json:"name"`
	}
	if err := decodeJSON(r, &body); err != nil {
		writeError(w, http.StatusBadRequest, err.Error())
		return
	}
	src, cfg, ok := s.resolveChargerSource(w, r, false)
	if !ok {
		return
	}
	chargers, err := s.fetchProviderChargers(r.Context(), src, cfg)
	if err != nil {
		writeJSON(w, http.StatusBadGateway, map[string]any{"error": err.Error()})
		return
	}
	charger, found := findProviderCharger(chargers, body.ChargerID)
	if !found {
		writeError(w, http.StatusNotFound, "that charger is not on your "+src.label()+" account")
		return
	}

	me := s.currentUserID(r)
	if existing := s.linkedChargers(r.Context(), me, src.id())[charger.ID]; existing != "" {
		writeJSON(w, http.StatusConflict, map[string]any{
			"error":     "this charger is already in DriverVault",
			"chargerId": existing,
		})
		return
	}

	hc := models.HomeCharger{
		Name:     strings.TrimSpace(body.Name),
		Serial:   charger.ID,
		Vendor:   charger.Vendor,
		Model:    charger.Model,
		SiteName: charger.SiteName,
	}
	if hc.Name == "" {
		hc.Name = strings.TrimSpace(charger.Name)
	}
	if hc.Name == "" {
		hc.Name = src.label() + " charger"
	}

	payload := homeChargerPayload(hc)
	payload["owner"] = me
	payload["provider"] = src.id()
	payload["provider_charger_id"] = charger.ID

	var rec homeChargerRecord
	if err := s.pb.Create(r.Context(), colHomeChargers, payload, &rec); err != nil {
		writePBError(w, err)
		return
	}
	writeJSON(w, http.StatusCreated, map[string]any{"charger": rec.toModel()})
}

// findProviderCharger locates a charger by the provider's id, matched case
// insensitively — a serial is often typed or pasted in the case it was printed.
func findProviderCharger(list []providerCharger, id string) (providerCharger, bool) {
	id = strings.TrimSpace(id)
	for _, c := range list {
		if strings.EqualFold(c.ID, id) {
			return c, true
		}
	}
	return providerCharger{}, false
}
