package ocpp

import (
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
)

// OCPP-J message types (OCPP 1.6 §4.2). Every frame is a JSON array whose first
// element is one of these.
const (
	MessageTypeCall       = 2 // [2, id, action, payload]
	MessageTypeCallResult = 3 // [3, id, payload]
	MessageTypeCallError  = 4 // [4, id, errorCode, errorDescription, errorDetails]
)

// Standard OCPP-J error codes used when rejecting a CALL.
const (
	ErrNotImplemented              = "NotImplemented"
	ErrNotSupported                = "NotSupported"
	ErrInternalError               = "InternalError"
	ErrProtocolError               = "ProtocolError"
	ErrSecurityError               = "SecurityError"
	ErrFormationViolation          = "FormationViolation"
	ErrPropertyConstraintViolation = "PropertyConstraintViolation"
	ErrGenericError                = "GenericError"
)

// Message is a decoded OCPP-J frame; only the fields relevant to Type are set.
type Message struct {
	Type    int
	ID      string
	Action  string          // CALL only
	Payload json.RawMessage // CALL and CALLRESULT

	ErrorCode        string          // CALLERROR only
	ErrorDescription string          // CALLERROR only
	ErrorDetails     json.RawMessage // CALLERROR only
}

// DecodeMessage parses one OCPP-J frame.
func DecodeMessage(b []byte) (Message, error) {
	var arr []json.RawMessage
	if err := json.Unmarshal(b, &arr); err != nil {
		return Message{}, fmt.Errorf("ocpp: not a JSON array: %w", err)
	}
	if len(arr) < 3 {
		return Message{}, errors.New("ocpp: message array too short")
	}
	var typ int
	if err := json.Unmarshal(arr[0], &typ); err != nil {
		return Message{}, fmt.Errorf("ocpp: bad message type: %w", err)
	}
	var id string
	if err := json.Unmarshal(arr[1], &id); err != nil {
		return Message{}, fmt.Errorf("ocpp: bad message id: %w", err)
	}
	m := Message{Type: typ, ID: id}

	switch typ {
	case MessageTypeCall:
		if len(arr) != 4 {
			return Message{}, errors.New("ocpp: CALL must have 4 elements")
		}
		if err := json.Unmarshal(arr[2], &m.Action); err != nil {
			return Message{}, fmt.Errorf("ocpp: bad action: %w", err)
		}
		m.Payload = arr[3]
	case MessageTypeCallResult:
		m.Payload = arr[2]
	case MessageTypeCallError:
		if len(arr) != 5 {
			return Message{}, errors.New("ocpp: CALLERROR must have 5 elements")
		}
		_ = json.Unmarshal(arr[2], &m.ErrorCode)
		_ = json.Unmarshal(arr[3], &m.ErrorDescription)
		m.ErrorDetails = arr[4]
	default:
		return Message{}, fmt.Errorf("ocpp: unknown message type %d", typ)
	}
	return m, nil
}

// EncodeCall builds a CALL frame. A nil/empty payload is encoded as {} because
// OCPP requires the payload to be a JSON object.
func EncodeCall(id, action string, payload any) ([]byte, error) {
	p, err := payloadObject(payload)
	if err != nil {
		return nil, err
	}
	return json.Marshal([]any{MessageTypeCall, id, action, p})
}

// EncodeCallResult builds a CALLRESULT frame answering the CALL with id.
func EncodeCallResult(id string, payload any) ([]byte, error) {
	p, err := payloadObject(payload)
	if err != nil {
		return nil, err
	}
	return json.Marshal([]any{MessageTypeCallResult, id, p})
}

// EncodeCallError builds a CALLERROR frame rejecting the CALL with id.
func EncodeCallError(id, code, description string, details any) ([]byte, error) {
	d, err := payloadObject(details)
	if err != nil {
		return nil, err
	}
	return json.Marshal([]any{MessageTypeCallError, id, code, description, d})
}

// payloadObject normalizes any payload to a JSON object RawMessage, mapping
// nil/null to the empty object {}.
func payloadObject(payload any) (json.RawMessage, error) {
	switch v := payload.(type) {
	case nil:
		return json.RawMessage("{}"), nil
	case json.RawMessage:
		if len(v) == 0 || string(v) == "null" {
			return json.RawMessage("{}"), nil
		}
		return v, nil
	default:
		b, err := json.Marshal(payload)
		if err != nil {
			return nil, err
		}
		if len(b) == 0 || string(b) == "null" {
			return json.RawMessage("{}"), nil
		}
		return b, nil
	}
}

// newMessageID returns a fresh unique id for an outbound CALL.
func newMessageID() string {
	var b [8]byte
	_, _ = rand.Read(b[:])
	return hex.EncodeToString(b[:])
}
