package agentstate

import (
	"errors"
	"fmt"
	"strings"
	"sync"
	"time"
)

var (
	ErrSnapshotRequired = errors.New("agent state snapshot required")
	ErrEmptyChangeSet   = errors.New("agent state change set is empty")
	ErrInvalidEnvelope  = errors.New("invalid agent state envelope")
)

type ChangeSet struct {
	Execution          *AgentExecutionState
	DeliveryUpserts    []WakeDeliveryState
	DeliveryRemovals   []string
	DiagnosticUpserts  []AgentDiagnostic
	DiagnosticRemovals []string
}

func (changes ChangeSet) empty() bool {
	return changes.Execution == nil &&
		len(changes.DeliveryUpserts) == 0 &&
		len(changes.DeliveryRemovals) == 0 &&
		len(changes.DiagnosticUpserts) == 0 &&
		len(changes.DiagnosticRemovals) == 0
}

type Publisher struct {
	mu sync.Mutex

	agentID        string
	clock          func() time.Time
	sessionEpoch   uint64
	revision       uint64
	snapshotNeeded bool
}

func NewPublisher(agentID string, lastPersistedEpoch uint64, clock func() time.Time) *Publisher {
	if clock == nil {
		clock = time.Now
	}
	return &Publisher{
		agentID:        strings.TrimSpace(agentID),
		clock:          clock,
		sessionEpoch:   nextEpoch(lastPersistedEpoch),
		snapshotNeeded: true,
	}
}

func nextEpoch(last uint64) uint64 {
	if last == ^uint64(0) {
		return last
	}
	return last + 1
}

func (publisher *Publisher) Snapshot(state State) (AgentStateSnapshotV2, error) {
	publisher.mu.Lock()
	defer publisher.mu.Unlock()

	nextRevision := publisher.revision + 1
	if nextRevision == 0 {
		return AgentStateSnapshotV2{}, fmt.Errorf("%w: revision overflow", ErrInvalidEnvelope)
	}
	state = cloneState(state)
	snapshot := AgentStateSnapshotV2{
		EnvelopeBaseV2: EnvelopeBaseV2{
			Version:      ProtocolVersion,
			AgentID:      publisher.agentID,
			SessionEpoch: publisher.sessionEpoch,
			Revision:     nextRevision,
			EmittedAt:    publisher.clock().UTC(),
		},
		Mode:        SnapshotMode,
		Execution:   state.Execution,
		Deliveries:  state.Deliveries,
		Diagnostics: state.Diagnostics,
	}
	if err := snapshot.Validate(); err != nil {
		return AgentStateSnapshotV2{}, fmt.Errorf("%w: %v", ErrInvalidEnvelope, err)
	}
	publisher.revision = nextRevision
	publisher.snapshotNeeded = false
	return snapshot, nil
}

func (publisher *Publisher) Delta(changes ChangeSet) (AgentStateDeltaV2, error) {
	publisher.mu.Lock()
	defer publisher.mu.Unlock()

	if publisher.snapshotNeeded || publisher.revision == 0 {
		return AgentStateDeltaV2{}, ErrSnapshotRequired
	}
	if changes.empty() {
		return AgentStateDeltaV2{}, ErrEmptyChangeSet
	}
	nextRevision := publisher.revision + 1
	if nextRevision == 0 {
		return AgentStateDeltaV2{}, fmt.Errorf("%w: revision overflow", ErrInvalidEnvelope)
	}
	delta := AgentStateDeltaV2{
		EnvelopeBaseV2: EnvelopeBaseV2{
			Version:      ProtocolVersion,
			AgentID:      publisher.agentID,
			SessionEpoch: publisher.sessionEpoch,
			Revision:     nextRevision,
			EmittedAt:    publisher.clock().UTC(),
		},
		Mode:               DeltaMode,
		BaseRevision:       publisher.revision,
		Execution:          changes.Execution,
		DeliveryUpserts:    changes.DeliveryUpserts,
		DeliveryRemovals:   changes.DeliveryRemovals,
		DiagnosticUpserts:  changes.DiagnosticUpserts,
		DiagnosticRemovals: changes.DiagnosticRemovals,
	}
	delta = cloneDelta(delta)
	if err := delta.Validate(); err != nil {
		return AgentStateDeltaV2{}, fmt.Errorf("%w: %v", ErrInvalidEnvelope, err)
	}
	publisher.revision = nextRevision
	return delta, nil
}

func (publisher *Publisher) RequireSnapshot() {
	publisher.mu.Lock()
	publisher.snapshotNeeded = true
	publisher.mu.Unlock()
}

func (publisher *Publisher) NewEpoch(lastPersistedEpoch uint64) {
	publisher.mu.Lock()
	publisher.sessionEpoch = nextEpoch(lastPersistedEpoch)
	publisher.revision = 0
	publisher.snapshotNeeded = true
	publisher.mu.Unlock()
}

func (publisher *Publisher) Revision() uint64 {
	publisher.mu.Lock()
	defer publisher.mu.Unlock()
	return publisher.revision
}

func (publisher *Publisher) SessionEpoch() uint64 {
	publisher.mu.Lock()
	defer publisher.mu.Unlock()
	return publisher.sessionEpoch
}
