package ankersolix

// Cards, at the charger rather than at the account.
//
// The Anker app offers two ways to add an RFID card: type its number, which is
// the account write in rfidcards.go, or hold the card against the reader inside
// a twenty-second window. The second one never touches the REST API. It is three
// messages on the charger's own MQTT topics, captured from the app's traffic:
//
//	0108 a2=7   open the reader          -> 0908, with the UID when a card is
//	                                        tapped and without one when the
//	                                        window closes empty
//	0103 a2=1   write the card           -> 0903, then 0904
//	0103 a2=2   remove it                -> the same pair
//	0104        ask for the card list    -> 0904
//
// Nothing here is inferred: every frame above is one this connector watched the
// app send and the charger answer. What is inferred is the account write those
// three replace, which is why a card written here is checked by reading the
// charger's own list back rather than by trusting the write.

import (
	"context"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"time"
)

const (
	// cardReadWindow is how long the reader stays open. The app counts twenty
	// seconds down; a couple more allow for the answer's trip back through the
	// cloud, and the charger closes the window on its own either way.
	cardReadWindow = 24 * time.Second

	// cardWriteWait is how long a write waits for the charger to republish its
	// list. The answer arrived within a second in every capture.
	cardWriteWait = 8 * time.Second

	// cardWriteAdd and cardWriteRemove are the two values the write takes.
	cardWriteAdd    uint8 = 1
	cardWriteRemove uint8 = 2
)

// cardReadResult is what a scan answers with: the card that was tapped, or the
// plain fact that nothing was.
type cardReadResult struct {
	SN      string `json:"sn"`
	Card    string `json:"card,omitempty"`
	Tapped  bool   `json:"tapped"`
	Seconds int    `json:"windowSeconds"`
}

// mqttReadCard opens the charger's card reader and waits for a card. The
// charger answers either way — with a UID when one is tapped, without when the
// window closes — so a scan that finds nothing is an answer rather than a
// timeout, and says so.
func (p *Plugin) mqttReadCard(ctx context.Context, sn string) (json.RawMessage, error) {
	model, err := p.chargerModel(ctx, sn)
	if err != nil {
		return nil, err
	}
	conn, err := p.mqttClient(ctx)
	if err != nil {
		return nil, err
	}
	// Listen before asking: the charger answers in under a second, and a
	// subscription made afterwards would miss it.
	if err := conn.listen(ctx, model, sn); err != nil {
		return nil, err
	}
	since := conn.cardReadAt(sn)

	// Field for field what the app sends, timestamp included — which is to say
	// not included: the app's own reader-open frame carries none, and this
	// command is copied rather than composed.
	frame, err := encodeFrame(msgEVPowerMode, []cmdField{
		rawField(0xa1, 0x22),
		uintField(0xa2, powerModeReadCard),
	})
	if err != nil {
		return nil, err
	}
	// The same encoding the other power-mode command carries; this is that
	// command with a different value, and the charger expects the field on it.
	if err := conn.publishFrame(ctx, model, sn, frame, mqttEncodingMode); err != nil {
		return nil, fmt.Errorf("anker-solix: opening the card reader on %s: %w", sn, err)
	}

	ok, err := conn.waitFor(ctx, sn, func(st *deviceState) bool {
		return st.cardReadAt.After(since)
	}, cardReadWindow)
	if err != nil {
		return nil, err
	}
	out := cardReadResult{SN: sn, Seconds: int(cardReadWindow / time.Second)}
	if ok {
		out.Card = conn.cardRead(sn)
		out.Tapped = out.Card != ""
	}
	return json.Marshal(out)
}

// cardWriteResult is what a device write answers with: what was asked, and the
// charger's own list afterwards. Present is read from that list — the charger
// says 0903 to everything, and a write is judged by what it changed.
type cardWriteResult struct {
	SN      string   `json:"sn"`
	Action  string   `json:"action"` // save | delete
	Number  string   `json:"cardNumber"`
	Present bool     `json:"present"`
	Cards   []string `json:"cards"`
	Via     string   `json:"via"` // charger
	Detail  string   `json:"detail,omitempty"`
}

// mqttWriteCard adds a card to the charger or removes one, and then reads the
// charger's list back. Both halves are the app's own messages.
func (p *Plugin) mqttWriteCard(ctx context.Context, sn, number string, add bool) (*cardWriteResult, error) {
	number = normalizeCardNumber(number)
	uid, err := hex.DecodeString(number)
	if err != nil || len(uid) == 0 {
		return nil, fmt.Errorf("anker-solix: %q is not a card number: the charger takes the UID as hex", number)
	}
	model, err := p.chargerModel(ctx, sn)
	if err != nil {
		return nil, err
	}
	conn, err := p.mqttClient(ctx)
	if err != nil {
		return nil, err
	}
	if err := conn.listen(ctx, model, sn); err != nil {
		return nil, err
	}
	since := conn.cardsAt(sn)

	action := cardWriteAdd
	name := "save"
	if !add {
		action, name = cardWriteRemove, "delete"
	}
	frame, err := encodeFrame(msgEVCardWrite, []cmdField{
		rawField(0xa1, 0x22),
		uintField(0xa2, action),
		uintField(0xa3, 1),
		bytesField(0xa4, uid),
	})
	if err != nil {
		return nil, err
	}
	if err := conn.publishFrame(ctx, model, sn, frame, 0); err != nil {
		return nil, fmt.Errorf("anker-solix: writing card %s to %s: %w", number, sn, err)
	}

	out := &cardWriteResult{SN: sn, Action: name, Number: number, Via: "charger"}
	cards, err := p.mqttCardList(ctx, conn, model, sn, since)
	if err != nil {
		// The write went out; what it did is simply unknown, which is not the
		// same as it having failed and must not be reported as either.
		out.Detail = err.Error()
		return out, nil
	}
	out.Cards = cards
	for _, c := range cards {
		if normalizeCardNumber(c) == number {
			out.Present = true
			break
		}
	}
	return out, nil
}

// mqttCardList is the charger's own list of the cards it will open for. A write
// republishes it unasked; when it does not, it is asked for.
func (p *Plugin) mqttCardList(ctx context.Context, conn *mqttConn, model, sn string, since time.Time) ([]string, error) {
	ok, err := conn.waitFor(ctx, sn, func(st *deviceState) bool {
		return st.cardsAt.After(since)
	}, cardWriteWait)
	if err != nil {
		return nil, err
	}
	if !ok {
		// Nothing came unasked, so ask.
		frame, err := encodeFrame(msgEVCardListReq, []cmdField{rawField(0xa1, 0x22)})
		if err != nil {
			return nil, err
		}
		if err := conn.publishFrame(ctx, model, sn, frame, 0); err != nil {
			return nil, err
		}
		ok, err = conn.waitFor(ctx, sn, func(st *deviceState) bool {
			return st.cardsAt.After(since)
		}, cardWriteWait)
		if err != nil {
			return nil, err
		}
	}
	if !ok {
		return nil, fmt.Errorf("anker-solix: charger %s did not answer with its card list", sn)
	}
	return conn.cards(sn), nil
}

// mqttChargerCards asks the charger for its list on its own, for a caller that
// wants to know what the device holds rather than what the account does.
func (p *Plugin) mqttChargerCards(ctx context.Context, sn string) (json.RawMessage, error) {
	model, err := p.chargerModel(ctx, sn)
	if err != nil {
		return nil, err
	}
	conn, err := p.mqttClient(ctx)
	if err != nil {
		return nil, err
	}
	if err := conn.listen(ctx, model, sn); err != nil {
		return nil, err
	}
	since := conn.cardsAt(sn)
	frame, err := encodeFrame(msgEVCardListReq, []cmdField{rawField(0xa1, 0x22)})
	if err != nil {
		return nil, err
	}
	if err := conn.publishFrame(ctx, model, sn, frame, 0); err != nil {
		return nil, err
	}
	cards, err := p.mqttCardList(ctx, conn, model, sn, since)
	if err != nil {
		return nil, err
	}
	return json.Marshal(map[string]any{"sn": sn, "cards": cards, "via": "charger"})
}

// ---- the card half of a device's state, read out from under the lock --------

func (c *mqttConn) cardReadAt(sn string) time.Time {
	c.mu.Lock()
	defer c.mu.Unlock()
	if st := c.devices[sn]; st != nil {
		return st.cardReadAt
	}
	return time.Time{}
}

func (c *mqttConn) cardRead(sn string) string {
	c.mu.Lock()
	defer c.mu.Unlock()
	if st := c.devices[sn]; st != nil {
		return st.cardRead
	}
	return ""
}

func (c *mqttConn) cardsAt(sn string) time.Time {
	c.mu.Lock()
	defer c.mu.Unlock()
	if st := c.devices[sn]; st != nil {
		return st.cardsAt
	}
	return time.Time{}
}

func (c *mqttConn) cards(sn string) []string {
	c.mu.Lock()
	defer c.mu.Unlock()
	if st := c.devices[sn]; st != nil {
		return append([]string(nil), st.cards...)
	}
	return nil
}
