// Package envelope defines the message envelope — the only message shape that
// flows between agents and the orchestrator. The envelope is the wire format
// for transport and the row shape in the messages table (payload_json is the
// serialized Payload struct).
//
// Envelope is the harness ACP. See plan §7. Adding a new message type is two
// edits: add it to TypeAll + add validation if it carries new payload fields.
package envelope

import (
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"time"
)

// Type is the message type discriminator. The set is small on purpose; adding
// kinds should be a deliberate decision (see plan §8.3 — avoid option explosion).
type Type string

const (
	TypeDelegate      Type = "delegate"
	TypeReport        Type = "report"
	TypeQuery         Type = "query"
	TypeClarify       Type = "clarify"
	TypeAnswer        Type = "answer"
	TypeAck           Type = "ack"
	TypeNack          Type = "nack"
	TypeHeartbeat     Type = "heartbeat"
	TypeSteering      Type = "steering"
	TypeInterrupt     Type = "interrupt"
	TypeDraft         Type = "action.draft"
	TypeDraftApprove  Type = "action.draft.approve"
	TypeDraftReject   Type = "action.draft.reject"
)

// All known message types. Order is significant only for JSON Schema enum
// stability.
var TypeAll = []Type{
	TypeDelegate, TypeReport, TypeQuery, TypeClarify, TypeAnswer,
	TypeAck, TypeNack, TypeHeartbeat, TypeSteering, TypeInterrupt,
	TypeDraft, TypeDraftApprove, TypeDraftReject,
}

// Expects encodes whether the sender requires a reply within ttl_ms.
type Expects string

const (
	ExpectsNone   Expects = "none"
	ExpectsAck    Expects = "ack"
	ExpectsAnswer Expects = "answer"
	ExpectsReport Expects = "report"
)

// Payload is the inner intent + refs of a message. Stored in payload_json.
type Payload struct {
	Intent      string   `json:"intent,omitempty"`
	ContextRefs []string `json:"context_refs,omitempty"`
	Expects     Expects  `json:"expects,omitempty"`
	Deadline    string   `json:"deadline,omitempty"` // ISO-8601, optional override
	// Reason carries either a kind code (e.g. "stop", "redirect", "cost_ceiling")
	// or a free-text explanation, depending on the message type.
	Reason string `json:"reason,omitempty"`
	// Extra is the escape hatch for type-specific fields. Use sparingly; if a
	// type needs structured fields often, promote them to top-level Payload.
	Extra map[string]any `json:"extra,omitempty"`
}

// Envelope is the message shape on the wire and in storage.
type Envelope struct {
	ID         string  `json:"id"`
	RunID      string  `json:"run_id"`
	From       string  `json:"from"`
	To         string  `json:"to"`
	Type       Type    `json:"type"`
	TaskID     string  `json:"task_id,omitempty"`
	InReplyTo  string  `json:"in_reply_to,omitempty"`
	TTLMs      int64   `json:"ttl_ms"`
	Priority   int     `json:"priority,omitempty"` // higher = sooner; interrupts default to 100
	CreatedAt  string  `json:"created_at,omitempty"`
	Payload    Payload `json:"payload"`
}

// Validate returns the first violation of envelope rules, or nil. Validation
// is strict for required fields and lenient on payload contents — the
// orchestrator decides what payload fields are meaningful per type.
func (e *Envelope) Validate() error {
	if e == nil {
		return errors.New("envelope: nil")
	}
	if strings.TrimSpace(e.ID) == "" {
		return errors.New("envelope: id required")
	}
	if strings.TrimSpace(e.RunID) == "" {
		return errors.New("envelope: run_id required")
	}
	if strings.TrimSpace(e.From) == "" {
		return errors.New("envelope: from required")
	}
	if strings.TrimSpace(e.To) == "" {
		return errors.New("envelope: to required")
	}
	if !isKnownType(e.Type) {
		return fmt.Errorf("envelope: unknown type %q", e.Type)
	}
	if e.TTLMs < 0 {
		return errors.New("envelope: ttl_ms must be >= 0 (0 = infinite)")
	}
	// Interrupt requires an ack; failure of clear semantics here costs us.
	if e.Type == TypeInterrupt && e.Payload.Expects != ExpectsAck {
		return errors.New("envelope: interrupt must have expects=ack")
	}
	// Steering must reference a target task and require ack.
	if e.Type == TypeSteering {
		if e.TaskID == "" {
			return errors.New("envelope: steering requires task_id")
		}
		if e.Payload.Expects != ExpectsAck {
			return errors.New("envelope: steering must have expects=ack")
		}
	}
	return nil
}

// Marshal returns the canonical JSON for the envelope.
func (e *Envelope) Marshal() ([]byte, error) { return json.Marshal(e) }

// Unmarshal parses raw JSON into an envelope.
func Unmarshal(raw []byte) (*Envelope, error) {
	var e Envelope
	if err := json.Unmarshal(raw, &e); err != nil {
		return nil, err
	}
	return &e, nil
}

// PriorityFor returns the default routing priority for a message type. The
// transport may override per-message via Envelope.Priority.
func PriorityFor(t Type) int {
	switch t {
	case TypeInterrupt:
		return 100
	case TypeSteering, TypeNack:
		return 20
	case TypeAck, TypeAnswer:
		return 10
	default:
		return 0
	}
}

// Deadline returns the absolute deadline derived from CreatedAt + TTLMs. If
// CreatedAt is missing or unparseable, returns now + TTL.
func (e *Envelope) Deadline(now time.Time) time.Time {
	base := now
	if e.CreatedAt != "" {
		if t, err := time.Parse("2006-01-02T15:04:05.000Z", e.CreatedAt); err == nil {
			base = t
		}
	}
	return base.Add(time.Duration(e.TTLMs) * time.Millisecond)
}

func isKnownType(t Type) bool {
	for _, k := range TypeAll {
		if k == t {
			return true
		}
	}
	return false
}
