package message

import "strings"

// Automation service author helpers.
//
// Workflow send_message and Scheduled Message effects write their visible
// message under a frozen Human owner for authorization/audit, so
// scope_messages.author_member_id and the member-derived AuthorProfile are the
// Human. Those effects are automation, not the Human, and the visible service
// author must be a recognizable Workflow or Schedule service author while the
// Human stays the separate authorization/audit principal. These helpers build
// and resolve that automation service author from the typed contribution
// source so every projection carries one stable identity.

// automationServiceSourceKind is one of the durable automation source kinds
// that can safely identify an automation service author.
type automationServiceSourceKind string

const (
	automationServiceSourceWorkflow automationServiceSourceKind = "workflow"
	automationServiceSourceSchedule automationServiceSourceKind = "schedule"
)

// automationServiceSourceIdentity holds the stable durable identity facts
// extracted from a source that satisfies the native write contract. RefID is the
// stable run id; Workflow additionally requires EventID (the send_message effect
// identity).
type automationServiceSourceIdentity struct {
	Kind    automationServiceSourceKind
	RefID   string
	EventID string
}

// resolveAutomationServiceSourceIdentity is the single centralized predicate
// for whether a resolved source safely identifies an automation service author.
// It mirrors ContributionSource.ValidateForWrite / validateLedgerContributionSourceRef:
//
//   - Workflow requires kind=workflow_run, refType=workflow_run, non-empty RefID,
//     RunID empty or exactly RefID, and non-empty EventID (the send_message effect
//     identity).
//   - Schedule requires kind=schedule_run, refType=automation_run, non-empty RefID,
//     and RunID empty or exactly RefID. Optional schedule EventID is not required
//     because the current write validator does not require it.
//
// Rejected: ID-less or RunID-only sources with missing RefID, RefID/RunID
// mismatch, blank IDs, wrong ref types, and Workflow without an effect ID. Both
// AutomationServiceAuthorForSource and AutomationSourceIdentifiesRun use this
// predicate so they cannot drift.
func resolveAutomationServiceSourceIdentity(source ContributionSource) (automationServiceSourceIdentity, bool) {
	source = source.Normalize()
	// Approval feedback is authored and displayed as the approving Human even
	// though its provenance is a Workflow Run. Its source is an audit/fanout
	// identity, not a canonical automation-author overlay.
	if source.EventType == "approval_feedback" {
		return automationServiceSourceIdentity{}, false
	}
	switch source.Kind {
	case ContributionSourceKindWorkflowRun:
		if source.RefType != ContributionSourceRefTypeWorkflowRun {
			return automationServiceSourceIdentity{}, false
		}
		refID := strings.TrimSpace(source.RefID)
		if refID == "" {
			return automationServiceSourceIdentity{}, false
		}
		if runID := strings.TrimSpace(source.RunID); runID != "" && runID != refID {
			return automationServiceSourceIdentity{}, false
		}
		effectID := strings.TrimSpace(source.EventID)
		if effectID == "" {
			return automationServiceSourceIdentity{}, false
		}
		return automationServiceSourceIdentity{
			Kind:    automationServiceSourceWorkflow,
			RefID:   refID,
			EventID: effectID,
		}, true
	case ContributionSourceKindScheduleRun:
		if source.RefType != ContributionSourceRefTypeAutomationRun {
			return automationServiceSourceIdentity{}, false
		}
		refID := strings.TrimSpace(source.RefID)
		if refID == "" {
			return automationServiceSourceIdentity{}, false
		}
		if runID := strings.TrimSpace(source.RunID); runID != "" && runID != refID {
			return automationServiceSourceIdentity{}, false
		}
		return automationServiceSourceIdentity{
			Kind:    automationServiceSourceSchedule,
			RefID:   refID,
			EventID: strings.TrimSpace(source.EventID),
		}, true
	default:
		return automationServiceSourceIdentity{}, false
	}
}

// AutomationServiceAuthorForSource returns the service author
// profile for a workflow_run or schedule_run contribution source. It returns
// false for every other source or for a source that does not satisfy the durable
// native write contract, so ordinary Human/Agent/App messages keep their normal
// author derivation. The profile intentionally carries no Human member id:
// automation authors must not inherit Human UI identity (self-message
// suppression, avatar grouping, mention routing).
func AutomationServiceAuthorForSource(source ContributionSource) (AuthorProfile, bool) {
	identity, ok := resolveAutomationServiceSourceIdentity(source)
	if !ok {
		return AuthorProfile{}, false
	}
	switch identity.Kind {
	case automationServiceSourceWorkflow:
		return workflowAutomationAuthorProfile(source, identity), true
	case automationServiceSourceSchedule:
		return scheduleAutomationAuthorProfile(source, identity), true
	default:
		return AuthorProfile{}, false
	}
}

func workflowAutomationAuthorProfile(source ContributionSource, identity automationServiceSourceIdentity) AuthorProfile {
	runID := identity.RefID
	effectID := identity.EventID
	if effectID == "" {
		effectID = runID
	}
	displayName := strings.TrimSpace(source.Label)
	if displayName == "" {
		displayName = "Workflow"
	}
	return AuthorProfile{
		ActorType:   AuthorActorTypeService,
		PrincipalID: runID,
		TargetID:    effectID,
		DisplayName: displayName,
		ServiceKind: AuthorServiceKindWorkflow,
		ServiceSlug: "workflow",
		Badge:       "workflow",
	}.Normalize()
}

func scheduleAutomationAuthorProfile(source ContributionSource, identity automationServiceSourceIdentity) AuthorProfile {
	runID := identity.RefID
	targetID := strings.TrimSpace(source.EventID)
	if targetID == "" {
		targetID = runID
	}
	displayName := strings.TrimSpace(source.Label)
	if displayName == "" {
		displayName = "Scheduled message"
	}
	return AuthorProfile{
		ActorType:   AuthorActorTypeService,
		PrincipalID: runID,
		TargetID:    targetID,
		DisplayName: displayName,
		ServiceKind: AuthorServiceKindSchedule,
		ServiceSlug: "schedule",
		Badge:       "schedule",
	}.Normalize()
}

// AutomationSourceIdentifiesRun reports whether the source facts identify a
// workflow_run or schedule_run automation run satisfying the durable native
// write contract. It is used to decide when a message should present its
// automation service author instead of the frozen Human owner.
func AutomationSourceIdentifiesRun(source ContributionSource) bool {
	_, ok := resolveAutomationServiceSourceIdentity(source)
	return ok
}

// isWorkflowApprovalFeedbackSource keeps the real Human author visible for
// approval feedback. The Workflow run remains typed provenance, but this
// ordinary comment is intentionally not an automation-authored post.
func isWorkflowApprovalFeedbackSource(source ContributionSource) bool {
	source = source.Normalize()
	return source.Kind == ContributionSourceKindWorkflowRun && source.EventType == "approval_feedback"
}

// IsAutomationServiceAuthor reports whether a profile is one of the
// automation service authors resolved from a workflow/schedule source.
func IsAutomationServiceAuthor(profile AuthorProfile) bool {
	profile = profile.Normalize()
	return profile.ActorType == AuthorActorTypeService &&
		(profile.ServiceKind == AuthorServiceKindWorkflow || profile.ServiceKind == AuthorServiceKindSchedule)
}

// ApplyAutomationServiceAuthorToAppMessage overlays the automation service
// author on an AppMessage whose resolved source identifies a workflow_run or
// schedule_run run. Ordinary Human/Agent/App messages are left unchanged. The
// overlay is authoritative only when the current author is the frozen Human
// owner (automation rows never present the Human as primary).
func ApplyAutomationServiceAuthorToAppMessage(message *AppMessage) {
	if message == nil || isWorkflowApprovalFeedbackSource(message.Source) {
		return
	}
	service, ok := AutomationServiceAuthorForSource(message.Source)
	if !ok {
		return
	}
	// Never demote an already-applied service author, and only replace a
	// Human-owned row (the frozen Human execution principal). An Agent/App/Team
	// author is not an automation-owner row and stays as-is.
	if IsAutomationServiceAuthor(message.AuthorProfile) {
		return
	}
	if strings.TrimSpace(message.AuthorProfile.ActorType) != AuthorActorTypeHuman {
		return
	}
	message.ApplyAuthorProfile(service)
}

// ApplyAutomationServiceAuthorToHistoryMessage applies the same overlay to a
// HistoryMessage read model used by history/search/read-check paths.
func ApplyAutomationServiceAuthorToHistoryMessage(message *HistoryMessage) {
	if message == nil || isWorkflowApprovalFeedbackSource(message.Source) {
		return
	}
	service, ok := AutomationServiceAuthorForSource(message.Source)
	if !ok {
		return
	}
	if IsAutomationServiceAuthor(message.AuthorProfile) {
		return
	}
	if strings.TrimSpace(message.AuthorProfile.ActorType) != AuthorActorTypeHuman {
		return
	}
	message.AuthorProfile = service
}

// ApplyAutomationServiceAuthorToReceiveMessage applies the same overlay to a
// ReceiveMessage agent/browser wake payload. It is used by the fanout boundary
// so every live and replayed delivery presents the same automation service
// author.
func ApplyAutomationServiceAuthorToReceiveMessage(message *ReceiveMessage) {
	if message == nil || isWorkflowApprovalFeedbackSource(message.Source) {
		return
	}
	service, ok := AutomationServiceAuthorForSource(message.Source)
	if !ok {
		return
	}
	if IsAutomationServiceAuthor(message.AuthorProfile) {
		return
	}
	if strings.TrimSpace(message.AuthorProfile.ActorType) != AuthorActorTypeHuman {
		return
	}
	message.AuthorProfile = service
}
