package ankersolix

// The cards that open the charger, and the two writes that change them.
//
// get_device_cards is well behaved: every card comes back as alias_name,
// card_number and create_time, and nothing else. There is no card id anywhere in
// that payload, which is why both writes below address a card by its number —
// it is the only handle the account ever gives out.
//
// save_device_card and delete_device_card are a different matter. Neither is
// documented, by Anker or by the reference implementation: both names were read
// out of the app package, and the bodies here are inferred from the field names
// the read view answers with. That is a guess, and it is treated as one:
//
//   - a write never reports its own success. After the call the card list is
//     read again and the answer says whether the card is on the charger now, so
//     a caller never has to take an ack's word for what happened;
//   - the cloud's own response is relayed alongside it, because an endpoint
//     nobody has documented is one whose reply is worth reading;
//   - nothing here deletes by pattern, by index, or in bulk. One card, named in
//     full, per call.

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

const (
	epRfidSaveCard   = "power_service/v1/rfid/save_device_card"   // add or rename one card — payload inferred, see above
	epRfidDeleteCard = "power_service/v1/rfid/delete_device_card" // remove one card — payload inferred, see above
)

// rfidCard is one authorised card, under the names get_device_cards uses for it.
// create_time is relayed rather than parsed: it is upstream's own epoch and the
// UI already knows how to read one.
type rfidCard struct {
	Name   string          `json:"alias_name,omitempty"`
	Number string          `json:"card_number,omitempty"`
	Added  json.RawMessage `json:"create_time,omitempty"`
}

// rfidWriteResult is what a write answers with: what was asked, what the cloud
// said, and — the part that matters — the list as it stands afterwards.
type rfidWriteResult struct {
	SN      string     `json:"sn"`
	Action  string     `json:"action"`     // save | delete
	Number  string     `json:"cardNumber"` // as it was sent, normalized
	Present bool       `json:"present"`    // whether the account holds that card now
	Cards   []rfidCard `json:"cards"`
	// What the charger itself did, when it could be reached: the write the Anker
	// app makes, and the only one of the two that is not inferred.
	Charger  *cardWriteResult `json:"charger,omitempty"`
	Response json.RawMessage  `json:"response,omitempty"`
	Detail   string           `json:"detail,omitempty"` // why the list could not be read back
}

// normalizeCardNumber puts a card number in the form the account stores it in.
// The numbers arrive as bare uppercase hex; people type them with spaces, dashes
// or colons between the bytes, and a number that differs from the stored one
// only in punctuation would delete nothing and add a duplicate.
func normalizeCardNumber(s string) string {
	var b strings.Builder
	for _, r := range s {
		switch {
		case r >= '0' && r <= '9', r >= 'A' && r <= 'Z':
			b.WriteRune(r)
		case r >= 'a' && r <= 'z':
			b.WriteRune(r - 'a' + 'A')
		}
	}
	return b.String()
}

// rfidCardLabel is the name a card gets when it is added without one — the same
// shape the Anker app writes, so a card added here does not stand out in it.
func rfidCardLabel(number string) string {
	if len(number) > 4 {
		number = number[len(number)-4:]
	}
	return "RFID " + number
}

// parseRfidCards reads the card list out of get_device_cards' envelope. A
// response that carries no list is an empty charger, not an error: an account
// with no cards answers exactly that way.
func parseRfidCards(body []byte) ([]rfidCard, error) {
	var env struct {
		Data struct {
			List []rfidCard `json:"list"`
		} `json:"data"`
	}
	if err := json.Unmarshal(body, &env); err != nil {
		return nil, err
	}
	return env.Data.List, nil
}

// cardPresent says whether a number is among the cards, comparing them the way
// normalizeCardNumber writes them so punctuation cannot answer for the account.
func cardPresent(cards []rfidCard, number string) bool {
	want := normalizeCardNumber(number)
	for _, c := range cards {
		if normalizeCardNumber(c.Number) == want {
			return true
		}
	}
	return false
}

// rfidList is the cards authorised on one charger, as the account holds them.
func (p *Plugin) rfidList(ctx context.Context, sn string) ([]rfidCard, error) {
	body, err := p.apiRequest(ctx, epRfidCards, map[string]any{"device_sn": sn})
	if err != nil {
		return nil, err
	}
	return parseRfidCards(body)
}

// rfidSaveCard adds a card, both places it has to be added.
//
// The charger is written first, with the app's own message: it is the device
// that decides who may start a charge, and that write is the one this connector
// watched the app make rather than inferred. The account write follows because
// it is the half that carries a name — the charger's message has no name field —
// and because the list people read is the account's. Either may fail on its own
// and the answer says which; only both failing is an error.
func (p *Plugin) rfidSaveCard(ctx context.Context, sn, number, name string) (json.RawMessage, error) {
	number = normalizeCardNumber(number)
	if number == "" {
		return nil, fmt.Errorf("anker-solix: a card number is required")
	}
	name = strings.TrimSpace(name)
	if name == "" {
		name = rfidCardLabel(number)
	}
	dev, devErr := p.mqttWriteCard(ctx, sn, number, true)
	body, err := p.apiRequest(ctx, epRfidSaveCard, map[string]any{
		"device_sn":   sn,
		"card_number": number,
		"alias_name":  name,
	})
	if err != nil && devErr != nil {
		return nil, fmt.Errorf("anker-solix: saving card %s: charger: %v; account: %v", number, devErr, err)
	}
	return p.rfidAfterWrite(ctx, sn, "save", number, body, dev, devErr)
}

// rfidDeleteCard removes one card from the charger. The caller names the whole
// number: there is no "delete the third one" here, because an index into a list
// that was read a minute ago is not a card.
func (p *Plugin) rfidDeleteCard(ctx context.Context, sn, number string) (json.RawMessage, error) {
	number = normalizeCardNumber(number)
	if number == "" {
		return nil, fmt.Errorf("anker-solix: a card number is required")
	}
	dev, devErr := p.mqttWriteCard(ctx, sn, number, false)
	body, err := p.apiRequest(ctx, epRfidDeleteCard, map[string]any{
		"device_sn":   sn,
		"card_number": number,
	})
	if err != nil && devErr != nil {
		return nil, fmt.Errorf("anker-solix: deleting card %s: charger: %v; account: %v", number, devErr, err)
	}
	return p.rfidAfterWrite(ctx, sn, "delete", number, body, dev, devErr)
}

// rfidAfterWrite reads the list back and answers with it. A list that cannot be
// read is not a failed write — the write already happened — so it is reported as
// the detail beside an answer that says nothing about presence rather than
// guessing at one.
func (p *Plugin) rfidAfterWrite(ctx context.Context, sn, action, number string, response []byte,
	dev *cardWriteResult, devErr error) (json.RawMessage, error) {
	out := rfidWriteResult{SN: sn, Action: action, Number: number, Response: json.RawMessage(response)}
	if dev != nil {
		out.Charger = dev
	} else if devErr != nil {
		// The charger could not be reached or would not answer. The account write
		// may still have landed, so this is said beside the answer rather than
		// instead of it.
		out.Charger = &cardWriteResult{SN: sn, Action: action, Number: number, Via: "charger", Detail: devErr.Error()}
	}
	cards, err := p.rfidList(ctx, sn)
	if err != nil {
		out.Detail = err.Error()
	} else {
		out.Cards = cards
		out.Present = cardPresent(cards, number)
	}
	return json.Marshal(out)
}
