package api

import (
	"encoding/json"
	"fmt"
)

// MessageType enumerates every kind of envelope that can flow over
// the agent ↔ server WebSocket. Keeping these as string constants
// (not iota ints) makes traffic readable in logs and packet captures.
type MessageType string

// Agent → server message types.
const (
	MsgHello          MessageType = "hello"
	MsgHeartbeat      MessageType = "heartbeat"
	MsgJobStarted     MessageType = "job.started"
	MsgJobProgress    MessageType = "job.progress"
	MsgJobFinished    MessageType = "job.finished"
	MsgSnapshotsRpt   MessageType = "snapshots.report"
	MsgRepoStats      MessageType = "repo.stats"
	MsgLogStream      MessageType = "log.stream"
	MsgScheduleAck    MessageType = "schedule.ack"
	MsgScheduleFire   MessageType = "schedule.fire"    // agent: a local cron entry fired, please dispatch a job
	MsgCommandResult  MessageType = "command.result"   // ack for command.run
	MsgTreeListResult MessageType = "tree.list.result" // reply to a server-driven tree.list
	MsgError          MessageType = "error"
)

// Server → agent message types.
const (
	MsgCommandRun    MessageType = "command.run"
	MsgCommandCancel MessageType = "command.cancel"
	MsgScheduleSet   MessageType = "schedule.set"
	MsgConfigUpdate  MessageType = "config.update"
	MsgCommandUpdate MessageType = "command.update"
	MsgTreeList      MessageType = "tree.list" // sync RPC: list a snapshot's children
)

// Envelope is the framing for every WS message in either direction.
// Payload is parsed into the concrete struct chosen by Type.
//
//	ID is set on RPC-style messages (command.run / command.result) so
//	responses can be correlated. For one-shot pushes (heartbeat,
//	job.progress) it is empty.
type Envelope struct {
	Type    MessageType     `json:"type"`
	ID      string          `json:"id,omitempty"`
	Payload json.RawMessage `json:"payload,omitempty"`
}

// Marshal builds an envelope from a concrete payload struct.
func Marshal(t MessageType, id string, payload any) (Envelope, error) {
	if payload == nil {
		return Envelope{Type: t, ID: id}, nil
	}
	raw, err := json.Marshal(payload)
	if err != nil {
		return Envelope{}, fmt.Errorf("marshal %s payload: %w", t, err)
	}
	return Envelope{Type: t, ID: id, Payload: raw}, nil
}

// UnmarshalPayload decodes the envelope's payload into v.
func (e Envelope) UnmarshalPayload(v any) error {
	if len(e.Payload) == 0 {
		return nil
	}
	return json.Unmarshal(e.Payload, v)
}

// ErrorCode enumerates error reasons surfaced over the wire.
// These are stable identifiers; client code may switch on them.
type ErrorCode string

// Stable ErrorCode values surfaced over the wire. Clients switch on
// these; renaming requires a wire-version bump.
const (
	ErrProtocolTooOld ErrorCode = "protocol_too_old"
	ErrProtocolTooNew ErrorCode = "protocol_too_new"
	ErrUnauthorized   ErrorCode = "unauthorised"
	ErrBadRequest     ErrorCode = "bad_request"
	ErrInternal       ErrorCode = "internal"
)

// ErrorPayload is the body of an `error` envelope.
type ErrorPayload struct {
	Code    ErrorCode `json:"code"`
	Message string    `json:"message"`
	HelpURL string    `json:"help_url,omitempty"`
}
