package message

import (
	"strings"
)

const (
	ContributionSourceKindHumanCompose      = "human_compose"
	ContributionSourceKindAgentRuntime      = "agent_runtime"
	ContributionSourceKindProviderEvent     = "provider_event"
	ContributionSourceKindScheduleRun       = "schedule_run"
	ContributionSourceKindWorkflowRun       = "workflow_run"
	ContributionSourceKindReviewComment     = "review_comment"
	ContributionSourceKindServiceDirectPost = "service_direct_post"

	ContributionSourceRefTypeMessage = DeliverySourceRefMessage
	// ContributionSourceRefTypeTaskActivity identifies a non-message typed task
	// activity row as the source reference for task timeline facts. It is used
	// by task read DTOs only; message writes must continue to use message,
	// service-event, or automation-run source refs.
	ContributionSourceRefTypeTaskActivity = "task_activity"
	// ContributionSourceRefTypeServiceEventDelivery is the native source
	// reference for provider/service callback events that created a visible
	// contribution through the service event delivery ledger.
	ContributionSourceRefTypeServiceEventDelivery = "service_event_delivery"
	// ContributionSourceRefTypeAutomationRun is the native source reference for
	// AgentTeam-owned automation such as schedules.
	ContributionSourceRefTypeAutomationRun = "automation_run"
	// ContributionSourceRefTypeWorkflowRun identifies the first-class
	// Plan/Binding/Invocation/Run ledger that produced a visible message.
	ContributionSourceRefTypeWorkflowRun = "workflow_run"
)

type ContributionSource struct {
	Kind      string
	RefType   string
	RefID     string
	RunID     string
	EventID   string
	EventType string
	Label     string
	// SkipWorkflowRunMessageBinding is trusted runtime-only write control.
	SkipWorkflowRunMessageBinding bool `json:"-"`
}

func (source ContributionSource) Normalize() ContributionSource {
	source.Kind = strings.TrimSpace(source.Kind)
	source.RefType = strings.TrimSpace(source.RefType)
	source.RefID = strings.TrimSpace(source.RefID)
	source.RunID = strings.TrimSpace(source.RunID)
	source.EventID = strings.TrimSpace(source.EventID)
	source.EventType = strings.TrimSpace(source.EventType)
	source.Label = strings.TrimSpace(source.Label)
	return source
}

func (source ContributionSource) IsZero() bool {
	source = source.Normalize()
	return source.Kind == "" && source.RefType == "" && source.RefID == "" && source.RunID == "" && source.EventID == "" && source.EventType == "" && source.Label == ""
}

// ValidateForWrite verifies that explicit source facts use one of the current
// contribution source kinds and the matching reference shape. Empty source is
// allowed so ordinary message writers can rely on read-time default sources.
func (source ContributionSource) ValidateForWrite() error {
	source = source.Normalize()
	if source.IsZero() {
		return nil
	}
	switch source.Kind {
	case ContributionSourceKindHumanCompose, ContributionSourceKindAgentRuntime:
		if source.RefType != ContributionSourceRefTypeMessage {
			return invalidArgument("invalid contribution source ref type")
		}
		if source.RefID == "" {
			return invalidArgument("contribution source ref id is required")
		}
	case ContributionSourceKindServiceDirectPost:
		if source.RefType != ContributionSourceRefTypeMessage {
			return invalidArgument("invalid contribution source ref type")
		}
		if source.RefID == "" {
			return invalidArgument("contribution source ref id is required")
		}
	case ContributionSourceKindReviewComment:
		if source.RefType != ContributionSourceRefTypeMessage {
			return invalidArgument("invalid contribution source ref type")
		}
		// Review comment messages are ordinary scope messages with review
		// metadata. During writes the generated message id is not known until
		// after source validation, so an empty ref id is allowed temporarily and
		// bound to the generated message id before fanout/read payloads are
		// produced. Read-time projections always include the concrete ref id.
	case ContributionSourceKindProviderEvent:
		if source.RefType != ContributionSourceRefTypeServiceEventDelivery {
			return invalidArgument("invalid contribution source ref type")
		}
		return validateLedgerContributionSourceRef(source)
	case ContributionSourceKindScheduleRun:
		if source.RefType != ContributionSourceRefTypeAutomationRun {
			return invalidArgument("invalid contribution source ref type")
		}
		return validateLedgerContributionSourceRef(source)
	case ContributionSourceKindWorkflowRun:
		if source.RefType != ContributionSourceRefTypeWorkflowRun {
			return invalidArgument("invalid contribution source ref type")
		}
		if err := validateLedgerContributionSourceRef(source); err != nil {
			return err
		}
		if source.EventID == "" {
			return invalidArgument("workflow contribution source effect id is required")
		}
		return nil
	default:
		return invalidArgument("unsupported contribution source kind")
	}
	return nil
}

func validateLedgerContributionSourceRef(source ContributionSource) error {
	if source.RefID == "" {
		return invalidArgument("contribution source ref id is required")
	}
	if source.RunID != "" && source.RunID != source.RefID {
		return invalidArgument("contribution source run ref mismatch")
	}
	return nil
}

// NormalizeForWrite validates and trims a source for persistence.
func (source ContributionSource) NormalizeForWrite() (ContributionSource, error) {
	source = source.Normalize()
	if source.RunID == "" && (source.RefType == ContributionSourceRefTypeServiceEventDelivery || source.RefType == ContributionSourceRefTypeAutomationRun || source.RefType == ContributionSourceRefTypeWorkflowRun) {
		source.RunID = source.RefID
	}
	if err := source.ValidateForWrite(); err != nil {
		return ContributionSource{}, err
	}
	return source, nil
}

func defaultContributionSourceForAuthor(messageID string, profile AuthorProfile) ContributionSource {
	profile = profile.Normalize()
	actorType := profile.ActorType
	kind := ""
	switch actorType {
	case AuthorActorTypeHuman:
		kind = ContributionSourceKindHumanCompose
	case AuthorActorTypeAgent:
		kind = ContributionSourceKindAgentRuntime
	case AuthorActorTypeService:
		if profile.ServiceKind == AuthorServiceKindServiceAccount {
			kind = ContributionSourceKindServiceDirectPost
		}
	}
	if kind == "" {
		return ContributionSource{}
	}
	return ContributionSource{
		Kind:    kind,
		RefType: ContributionSourceRefTypeMessage,
		RefID:   strings.TrimSpace(messageID),
		Label:   profile.DisplayName,
	}.Normalize()
}

func DefaultContributionSourceForAppMessage(appMessage AppMessage) ContributionSource {
	return defaultContributionSourceForAuthor(appMessage.ID, appMessage.EffectiveAuthorProfile())
}

func ApplyDefaultContributionSourceToAppMessage(appMessage *AppMessage) {
	if appMessage == nil || !appMessage.Source.IsZero() {
		return
	}
	appMessage.Source = DefaultContributionSourceForAppMessage(*appMessage)
}

func DefaultContributionSourceForHistoryMessage(historyMessage HistoryMessage) ContributionSource {
	return defaultContributionSourceForAuthor(historyMessage.ID, historyMessage.AuthorProfile)
}

func ApplyDefaultContributionSourceToHistoryMessage(historyMessage *HistoryMessage) {
	if historyMessage == nil || !historyMessage.Source.IsZero() {
		return
	}
	historyMessage.Source = DefaultContributionSourceForHistoryMessage(*historyMessage)
}

func ContributionSourceForReviewComment(messageID string, label ...string) ContributionSource {
	return ContributionSource{
		Kind:    ContributionSourceKindReviewComment,
		RefType: ContributionSourceRefTypeMessage,
		RefID:   strings.TrimSpace(messageID),
		Label:   firstNonEmptyString(label...),
	}.Normalize()
}

func (source ContributionSource) WithDefaultMessageRefID(messageID string) ContributionSource {
	source = source.Normalize()
	if source.RefType == ContributionSourceRefTypeMessage && source.RefID == "" {
		source.RefID = strings.TrimSpace(messageID)
	}
	return source.Normalize()
}

type ServiceEventDeliveryContributionSourceParams struct {
	DeliveryID  string
	Label       string
	EventID     string
	EventType   string
	EventAction string
}

func ContributionSourceForServiceEventDelivery(params ServiceEventDeliveryContributionSourceParams) ContributionSource {
	deliveryID := strings.TrimSpace(params.DeliveryID)
	return ContributionSource{
		Kind:      ContributionSourceKindProviderEvent,
		RefType:   ContributionSourceRefTypeServiceEventDelivery,
		RefID:     deliveryID,
		RunID:     deliveryID,
		EventID:   strings.TrimSpace(params.EventID),
		EventType: firstNonEmptyString(params.EventAction, params.EventType),
		Label:     strings.TrimSpace(params.Label),
	}.Normalize()
}

type AutomationRunContributionSourceParams struct {
	RunID               string
	AutomationID        string
	AutomationName      string
	TimeMode            string
	SourceType          string
	SourceOccurrenceRef string
	EventID             string
	EventType           string
}

type WorkflowRunContributionSourceParams struct {
	RunID        string
	EffectID     string
	PlanID       string
	WorkflowName string
	SourceKind   string
}

func ContributionSourceForWorkflowRun(params WorkflowRunContributionSourceParams) ContributionSource {
	runID := strings.TrimSpace(params.RunID)
	return ContributionSource{
		Kind:      ContributionSourceKindWorkflowRun,
		RefType:   ContributionSourceRefTypeWorkflowRun,
		RefID:     runID,
		RunID:     runID,
		EventID:   strings.TrimSpace(params.EffectID),
		EventType: strings.TrimSpace(params.SourceKind),
		Label:     firstNonEmptyString(params.WorkflowName, params.PlanID),
	}.Normalize()
}

func ContributionSourceForAutomationRun(params AutomationRunContributionSourceParams) ContributionSource {
	runID := strings.TrimSpace(params.RunID)
	return ContributionSource{
		Kind:      ContributionSourceKindScheduleRun,
		RefType:   ContributionSourceRefTypeAutomationRun,
		RefID:     runID,
		RunID:     runID,
		EventID:   firstNonEmptyString(params.EventID, params.SourceOccurrenceRef),
		EventType: firstNonEmptyString(params.EventType, params.TimeMode, params.SourceType),
		Label:     firstNonEmptyString(params.AutomationName, params.AutomationID),
	}.Normalize()
}

func firstNonEmptyString(values ...string) string {
	for _, value := range values {
		if trimmed := strings.TrimSpace(value); trimmed != "" {
			return trimmed
		}
	}
	return ""
}
