// Package runeffect defines the shared run/effect vocabulary implemented by
// AgentTeam-owned automation runs and service event deliveries.
package runeffect

import (
	"fmt"
	"strings"
	"time"
)

// LedgerRefType identifies the durable ledger that owns a run-effect record.
// It is intentionally not a table abstraction: service event deliveries and
// automation runs remain separate domain ledgers while sharing these semantics.
type LedgerRefType string

const (
	LedgerRefTypeAutomationRun        LedgerRefType = "automation_run"
	LedgerRefTypeServiceEventDelivery LedgerRefType = "service_event_delivery"
)

// Status is the common lifecycle vocabulary for ledgers that reserve work and
// record an effect.
type Status string

const (
	StatusRunning      Status = "running"
	StatusRetryWaiting Status = "retry_waiting"
	StatusSucceeded    Status = "succeeded"
	StatusFailed       Status = "failed"
	StatusCanceled     Status = "canceled"
	StatusSkipped      Status = "skipped"
	StatusDuplicate    Status = "duplicate"
)

// NormalizeStatus trims and validates a persisted status value.
func NormalizeStatus(value string) (Status, error) {
	status := Status(strings.TrimSpace(value))
	switch status {
	case StatusRunning, StatusRetryWaiting, StatusSucceeded, StatusFailed, StatusCanceled, StatusSkipped, StatusDuplicate:
		return status, nil
	default:
		return "", fmt.Errorf("unknown run effect status %q", value)
	}
}

// IsTerminal reports whether the status represents a final ledger outcome.
func (status Status) IsTerminal() bool {
	switch status {
	case StatusSucceeded, StatusFailed, StatusCanceled, StatusSkipped, StatusDuplicate:
		return true
	default:
		return false
	}
}

// IsActive reports whether a ledger still expects retry or completion work.
func (status Status) IsActive() bool {
	switch status {
	case StatusRunning, StatusRetryWaiting:
		return true
	default:
		return false
	}
}

// EffectKind describes the visible or domain effect produced by a successful
// ledger. A no-op outcome must use EffectKindNone explicitly.
type EffectKind string

const (
	EffectKindNone                 EffectKind = "none"
	EffectKindMessage              EffectKind = "message"
	EffectKindTaskDiscussion       EffectKind = "task_discussion"
	EffectKindAutomationRun        EffectKind = "automation_run"
	EffectKindMessageUpdate        EffectKind = "message_update"
	EffectKindNotificationResolved EffectKind = "notification_resolved"
	EffectKindMessageWithdrawal    EffectKind = "message_withdrawal"
)

// NormalizeEffectKind trims and validates an effect kind value.
func NormalizeEffectKind(value string) (EffectKind, error) {
	kind := EffectKind(strings.TrimSpace(value))
	switch kind {
	case "":
		return EffectKindNone, nil
	case EffectKindNone, EffectKindMessage, EffectKindTaskDiscussion, EffectKindAutomationRun,
		EffectKindMessageUpdate, EffectKindNotificationResolved, EffectKindMessageWithdrawal:
		return kind, nil
	default:
		return "", fmt.Errorf("unknown run effect kind %q", value)
	}
}

// Effect is the common persisted meaning of the output produced by a run or
// delivery. MessageID is populated only when the effect is a transcript message;
// task discussion entries use Ref because they are not scope_messages rows.
type Effect struct {
	Kind      EffectKind
	Ref       string
	MessageID string
}

// NoEffect records an explicit no-op effect.
func NoEffect() Effect {
	return Effect{Kind: EffectKindNone}
}

// MessageEffect records a transcript message effect.
func MessageEffect(messageID string) Effect {
	trimmed := strings.TrimSpace(messageID)
	return Effect{Kind: EffectKindMessage, Ref: trimmed, MessageID: trimmed}
}

// TaskDiscussionEffect records a task discussion effect.
func TaskDiscussionEffect(entryID string) Effect {
	return Effect{Kind: EffectKindTaskDiscussion, Ref: strings.TrimSpace(entryID)}
}

// AutomationRunEffect records an automation run effect.
func AutomationRunEffect(runID string) Effect {
	return Effect{Kind: EffectKindAutomationRun, Ref: strings.TrimSpace(runID)}
}

// MessageUpdateEffect records an in-place App notification message mutation.
func MessageUpdateEffect(notificationID string, messageID string) Effect {
	return Effect{Kind: EffectKindMessageUpdate, Ref: strings.TrimSpace(notificationID), MessageID: strings.TrimSpace(messageID)}
}

// NotificationResolvedEffect records a terminal App notification resolution.
func NotificationResolvedEffect(notificationID string, messageID string) Effect {
	return Effect{Kind: EffectKindNotificationResolved, Ref: strings.TrimSpace(notificationID), MessageID: strings.TrimSpace(messageID)}
}

// MessageWithdrawalEffect records a App notification message withdrawal.
func MessageWithdrawalEffect(notificationID string, messageID string) Effect {
	return Effect{Kind: EffectKindMessageWithdrawal, Ref: strings.TrimSpace(notificationID), MessageID: strings.TrimSpace(messageID)}
}

// Normalize returns a canonical representation, filling the message effect ref
// from MessageID when older callers only supplied MessageID.
func (effect Effect) Normalize() (Effect, error) {
	kind, err := NormalizeEffectKind(string(effect.Kind))
	if err != nil {
		return Effect{}, err
	}
	normalized := Effect{
		Kind:      kind,
		Ref:       strings.TrimSpace(effect.Ref),
		MessageID: strings.TrimSpace(effect.MessageID),
	}
	if normalized.Kind == EffectKindMessage {
		if normalized.Ref == "" {
			normalized.Ref = normalized.MessageID
		}
		if normalized.MessageID == "" {
			normalized.MessageID = normalized.Ref
		}
	}
	if normalized.Kind == EffectKindNone {
		normalized.Ref = ""
		normalized.MessageID = ""
	}
	return normalized, nil
}

// Validate checks the effect fields without assuming a specific ledger table.
func (effect Effect) Validate() error {
	normalized, err := effect.Normalize()
	if err != nil {
		return err
	}
	switch normalized.Kind {
	case EffectKindNone:
		return nil
	case EffectKindMessage:
		if normalized.Ref == "" || normalized.MessageID == "" {
			return fmt.Errorf("message effect requires ref and message id")
		}
		if normalized.Ref != normalized.MessageID {
			return fmt.Errorf("message effect ref %q must match message id %q", normalized.Ref, normalized.MessageID)
		}
	case EffectKindTaskDiscussion, EffectKindAutomationRun:
		if normalized.Ref == "" {
			return fmt.Errorf("%s effect requires ref", normalized.Kind)
		}
		if normalized.MessageID != "" {
			return fmt.Errorf("%s effect must not set message id", normalized.Kind)
		}
	case EffectKindMessageUpdate, EffectKindNotificationResolved, EffectKindMessageWithdrawal:
		if normalized.Ref == "" || normalized.MessageID == "" {
			return fmt.Errorf("%s effect requires ref and message id", normalized.Kind)
		}
	}
	return nil
}

// Error records stable operator/user diagnostics for a failed ledger.
type Error struct {
	Code   string
	Detail string
}

// Normalize returns a trimmed diagnostic payload.
func (failure Error) Normalize() Error {
	return Error{Code: strings.TrimSpace(failure.Code), Detail: strings.TrimSpace(failure.Detail)}
}

// Empty reports whether no failure diagnostic is present.
func (failure Error) Empty() bool {
	failure = failure.Normalize()
	return failure.Code == "" && failure.Detail == ""
}

// Contract is the store-neutral run/effect shape shared by automation runs and
// service event deliveries. The owning ledger decides its domain-specific keys;
// this contract keeps status, idempotency, attempt, completion, effect, and
// diagnostics consistent.
type Contract struct {
	Status         Status
	IdempotencyKey string
	AttemptCount   int
	AttemptedAt    *time.Time
	CompletedAt    *time.Time
	Effect         Effect
	Error          Error
}

// Validate enforces the shared run/effect invariants that both ledgers must
// satisfy before projecting rows into read models or diagnostics.
func (contract Contract) Validate() error {
	status, err := NormalizeStatus(string(contract.Status))
	if err != nil {
		return err
	}
	if strings.TrimSpace(contract.IdempotencyKey) == "" {
		return fmt.Errorf("run effect contract requires idempotency key")
	}
	if contract.AttemptCount < 0 {
		return fmt.Errorf("run effect contract attempt count must be non-negative")
	}
	if contract.AttemptCount > 0 && contract.AttemptedAt == nil {
		return fmt.Errorf("run effect contract with attempts requires attempted_at")
	}
	effect, err := contract.Effect.Normalize()
	if err != nil {
		return err
	}
	if err := effect.Validate(); err != nil {
		return err
	}
	failure := contract.Error.Normalize()
	switch status {
	case StatusRunning:
		if contract.CompletedAt != nil {
			return fmt.Errorf("running run effect contract must not set completed_at")
		}
		if effect.Kind != EffectKindNone {
			return fmt.Errorf("running run effect contract must not set effect")
		}
	case StatusRetryWaiting:
		if contract.CompletedAt != nil {
			return fmt.Errorf("retry-waiting run effect contract must not set completed_at")
		}
		if effect.Kind != EffectKindNone {
			return fmt.Errorf("retry-waiting run effect contract must not set effect")
		}
	case StatusSucceeded:
		if contract.CompletedAt == nil {
			return fmt.Errorf("succeeded run effect contract requires completed_at")
		}
		if effect.Kind == EffectKindNone {
			return fmt.Errorf("succeeded run effect contract requires effect")
		}
		if !failure.Empty() {
			return fmt.Errorf("succeeded run effect contract must not set error")
		}
	case StatusFailed:
		if contract.CompletedAt == nil {
			return fmt.Errorf("failed run effect contract requires completed_at")
		}
		if failure.Code == "" {
			return fmt.Errorf("failed run effect contract requires error code")
		}
		if effect.Kind != EffectKindNone {
			return fmt.Errorf("failed run effect contract must not set effect")
		}
	case StatusCanceled:
		if contract.CompletedAt == nil {
			return fmt.Errorf("canceled run effect contract requires completed_at")
		}
		if effect.Kind != EffectKindNone {
			return fmt.Errorf("canceled run effect contract must not set effect")
		}
	case StatusSkipped, StatusDuplicate:
		if effect.Kind != EffectKindNone {
			return fmt.Errorf("%s run effect contract must not set effect", status)
		}
	}
	return nil
}
