package domain

import (
	"fmt"
	"strings"
)

// MessageScopeKind is the persisted kind of a concrete message scope.
//
// Unlike MessageTargetKind, this type contains no author-facing pseudo-target
// such as Team and names reply-thread scopes as "thread", matching storage.
type MessageScopeKind string

const (
	MessageScopeKindChannel MessageScopeKind = "channel"
	MessageScopeKindThread  MessageScopeKind = "thread"
	MessageScopeKindDM      MessageScopeKind = "dm"
	MessageScopeKindTask    MessageScopeKind = "task"

	// MaxWorkflowTaskWatchHandoffPredecessors is the shared kernel, effect, and
	// message-store bound for the distinct predecessor Agent IDs replaced by one
	// same-Task handoff. It does not bound Plan branches or frontier tokens.
	MaxWorkflowTaskWatchHandoffPredecessors = 256
)

// MessageScopeValue is the canonical portable identity of one existing
// message scope. Authorization and workspace lookup remain the caller's
// responsibility.
type MessageScopeValue struct {
	Kind    MessageScopeKind `json:"kind"`
	ScopeID string           `json:"scopeId"`
}

// MessageScopeValueFromResolvedTarget converts one already-resolved authored
// target into its portable concrete scope identity. Pseudo-target kinds are
// rejected rather than leaking into scope-valued workflow data.
func MessageScopeValueFromResolvedTarget(target ResolvedMessageTarget) (MessageScopeValue, error) {
	var kind MessageScopeKind
	switch target.Kind {
	case MessageTargetKindChannel:
		kind = MessageScopeKindChannel
	case MessageTargetKindReplyThread:
		kind = MessageScopeKindThread
	case MessageTargetKindDM:
		kind = MessageScopeKindDM
	case MessageTargetKindTask:
		kind = MessageScopeKindTask
	default:
		return MessageScopeValue{}, invalidArgument(fmt.Sprintf("resolved message target kind %q is not a concrete message scope", target.Kind))
	}
	return NormalizeMessageScopeValue(MessageScopeValue{Kind: kind, ScopeID: target.ScopeID})
}

// NormalizeMessageScopeValue accepts the canonical typed value or its exact
// JSON-object representation. It deliberately does not coerce target strings,
// channel values, aliases, unknown fields, or non-canonical whitespace/casing.
func NormalizeMessageScopeValue(value any) (MessageScopeValue, error) {
	var scope MessageScopeValue
	switch typed := value.(type) {
	case MessageScopeValue:
		scope = typed
	case map[string]any:
		if len(typed) != 2 {
			return MessageScopeValue{}, invalidArgument("message scope must contain exactly kind and scopeId")
		}
		kind, kindOK := typed["kind"].(string)
		scopeID, scopeIDOK := typed["scopeId"].(string)
		if !kindOK || !scopeIDOK {
			return MessageScopeValue{}, invalidArgument("message scope kind and scopeId must be strings")
		}
		scope = MessageScopeValue{Kind: MessageScopeKind(kind), ScopeID: scopeID}
	default:
		return MessageScopeValue{}, invalidArgument("message scope must be an object with kind and scopeId")
	}
	if err := ValidateMessageScopeValue(scope); err != nil {
		return MessageScopeValue{}, err
	}
	return scope, nil
}

// ValidateMessageScopeValue validates an already-typed canonical scope value.
func ValidateMessageScopeValue(scope MessageScopeValue) error {
	if strings.TrimSpace(string(scope.Kind)) != string(scope.Kind) {
		return invalidArgument("message scope kind must not have surrounding whitespace")
	}
	switch scope.Kind {
	case MessageScopeKindChannel, MessageScopeKindThread, MessageScopeKindDM, MessageScopeKindTask:
	default:
		return invalidArgument(fmt.Sprintf("unsupported message scope kind %q", scope.Kind))
	}
	if strings.TrimSpace(scope.ScopeID) != scope.ScopeID {
		return invalidArgument("message scope id must not have surrounding whitespace")
	}
	if !rawScopeIDPattern.MatchString(scope.ScopeID) {
		return invalidArgument(fmt.Sprintf("invalid message scope id %q", scope.ScopeID))
	}
	return nil
}
