package app

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"sort"
	"strings"
	"time"
	"unicode"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
)

const (
	AppNotificationIngressRuntime  = "runtime"
	AppNotificationIngressInternal = "internal"

	appNotificationExecutionActorAppRuntime      = "app_runtime"
	appNotificationExecutionActorInternalService = "internal_service"

	// AppNotificationRuntimeAuthenticationRunner identifies a principal
	// authenticated by a long-lived App runner credential. The credential is
	// still required for runner-stream delivery operations, but is only one of
	// the credentials accepted by the transport-independent notification API.
	AppNotificationRuntimeAuthenticationRunner = "runner_credential"
	// AppNotificationRuntimeAuthenticationCapability identifies a principal
	// authenticated by the short-lived capability token issued to an App
	// callback invocation.
	AppNotificationRuntimeAuthenticationCapability = "app_capability"

	AppNotificationMutationUpdate   = "update"
	AppNotificationMutationResolve  = "resolve"
	AppNotificationMutationWithdraw = "withdraw"
	AppNotificationMutationExpire   = "expire"
)

// AppNotificationRuntimePrincipal is the transport-independent identity used
// by ordinary App notification resource operations. Delivery ack/fail and
// delivery-bound mutations intentionally continue to use AppRuntimeDeliveryAuth
// so a capability token can never stand in for a current delivery lease token.
type AppNotificationRuntimePrincipal struct {
	WorkspaceID           string
	ServiceDefinitionID   string
	ServiceInstallationID string
	AuthenticationKind    string
	AuthenticationID      string
}

func (principal AppNotificationRuntimePrincipal) executionActor() AppNotificationExecutionActor {
	// AuthenticationKind records how the request reached AgentTeam, while the
	// durable execution role remains the transport-independent App runtime.
	return AppNotificationExecutionActor{
		Type: appNotificationExecutionActorAppRuntime,
		ID:   strings.TrimSpace(principal.AuthenticationID),
	}
}

// AppNotificationTarget is the ingress-independent target command.
type AppNotificationTarget struct {
	Kind     string
	MemberID string
	ScopeID  string
}

// AppNotificationContent is the visible message/card projection requested by
// a App. CardJSON contains the registered card component document.
type AppNotificationContent struct {
	Kind          string
	Text          string
	FallbackText  string
	CardJSON      []byte
	AttachmentIDs []string
	Metadata      map[string]any
}

// AppNotificationBusinessObject identifies the owning-domain object and the
// version used for this projection.
type AppNotificationBusinessObject struct {
	Namespace string
	Type      string
	ID        string
	Version   int64
	URL       string
}

func (object AppNotificationBusinessObject) IsZero() bool {
	return strings.TrimSpace(object.Namespace) == "" && strings.TrimSpace(object.Type) == "" && strings.TrimSpace(object.ID) == "" && object.Version == 0 && strings.TrimSpace(object.URL) == ""
}

// AppNotificationRecipientBasis points at an owning-domain recipient fact.
type AppNotificationRecipientBasis struct {
	Kind string
	Ref  string
}

// AppNotificationActorRef records business causation independently from the
// authenticated execution actor.
type AppNotificationActorRef struct {
	Type string
	ID   string
}

// AppNotificationExecutionActor is projected from runtime or trusted internal
// authentication and is never accepted from request JSON.
type AppNotificationExecutionActor struct {
	Type string
	ID   string
}

// AppNotificationCommand is the canonical create command shared by both
// ingress adapters.
type AppNotificationCommand struct {
	WorkspaceID           string
	ServiceInstallationID string
	Ingress               string
	ExecutionActor        AppNotificationExecutionActor
	ClientNotificationID  string
	Target                AppNotificationTarget
	Content               AppNotificationContent
	BusinessObject        AppNotificationBusinessObject
	RecipientBasis        AppNotificationRecipientBasis
	CausedByActor         AppNotificationActorRef
	ExpiresAt             *time.Time
	CorrelationID         string
}

// AppNotificationMutationCommand updates, resolves, withdraws, or expires an
// existing notification under App ownership and optimistic revision control.
type AppNotificationMutationCommand struct {
	WorkspaceID            string
	ServiceInstallationID  string
	Ingress                string
	ExecutionActor         AppNotificationExecutionActor
	NotificationID         string
	Kind                   string
	ExpectedRevision       int64
	ClientMutationID       string
	Content                *AppNotificationContent
	BusinessObjectVersion  *int64
	Reason                 string
	CausedByActor          AppNotificationActorRef
	CorrelationID          string
	ServiceEventDeliveryID string
	DeliveryAttemptID      string
	DeliveryBearerToken    string
	// CurrentInternalDeliveryID and CurrentInternalAttemptID identify the
	// in-process App action lease whose handler is applying this mutation.
	// They are populated only by MutateAppNotificationForBuiltinDelivery and
	// let terminal mutations retire the action subscription without cancelling
	// the delivery that still has to record its completion effect.
	CurrentInternalDeliveryID string
	CurrentInternalAttemptID  string
}

// AppNotificationActionCommand is projected from an authenticated Human
// request. ActorMemberID is resolved from the session, never from JSON.
type AppNotificationActionCommand struct {
	WorkspaceID    string
	MessageID      string
	ActionKey      string
	ActorMemberID  string
	CardRevision   int64
	ClientActionID string
	Input          map[string]any
	CorrelationID  string
}

// AppNotificationResult is the authoritative resource projection returned by
// create/get/mutation operations.
type AppNotificationResult struct {
	Accepted             bool
	WorkspaceID          string
	AlreadyCreated       bool
	AlreadyApplied       bool
	NotificationID       string
	NotificationRevision int64
	MessageID            string
	ScopeID              string
	MessageSeq           int64
	Status               string
	ContentKind          string
	CorrelationID        string
	ExpiresAt            *time.Time
	ResolvedAt           *time.Time
	WithdrawnAt          *time.Time
	CreatedAt            time.Time
	UpdatedAt            time.Time
	// MessageMutation is populated for update/resolve/withdraw operations so
	// adapters can publish the authoritative post-commit realtime projection.
	MessageMutation                *message.AppMessage
	MessageMutationKind            string
	MessageMutationIsLatestVisible bool
}

// AppNotificationActionResult is the durable Human invocation projection.
type AppNotificationActionResult struct {
	Accepted               bool
	AlreadyAccepted        bool
	ActionInvocationID     string
	ServiceEventDeliveryID string
	NotificationID         string
	MessageID              string
	CardRevision           int64
	NotificationRevision   int64
	State                  string
	CorrelationID          string
}

// AppNotificationCardView is the authenticated Human-facing projection for
// one interactive-card message. The render payload remains the canonical
// registered component document; lifecycle and revision fields come from the
// authoritative notification/card rows rather than client metadata.
type AppNotificationCardView struct {
	NotificationID       string
	NotificationRevision int64
	NotificationStatus   string
	MessageID            string
	ScopeID              string
	SchemaVersion        string
	CardRevision         int64
	RenderPayloadJSON    json.RawMessage
	State                string
	ExpiresAt            *time.Time
	CreatedAt            time.Time
	UpdatedAt            time.Time
}

// AppNotificationRecipientFact is returned by a registered owning-domain
// resolver. Status is interpreted by the resolver: Active authorizes new
// delivery/action acceptance; a terminal fact remains available for existing
// notification convergence.
type AppNotificationRecipientFact struct {
	Kind               string
	Ref                string
	WorkspaceID        string
	RecipientMemberID  string
	Version            int64
	Status             string
	Active             bool
	ExpiresAt          *time.Time
	PermissionBasisRef string
}

// AppNotificationRecipientFactRequest provides typed target and business
// context to an owning-domain recipient resolver. The resolver returns the
// current canonical fact for this exact basis/object/member tuple, including a
// still-addressable terminal or deactivated fact. Callers decide whether they
// require Active=true (new delivery) or only stable identity and a monotonic
// version (existing notification convergence).
type AppNotificationRecipientFactRequest struct {
	WorkspaceID      string
	InstallationID   string
	Basis            AppNotificationRecipientBasis
	BusinessObject   AppNotificationBusinessObject
	ExpectedMemberID string
	Now              time.Time
}

// AppNotificationActionAuthorizationRequest asks the same owning-domain
// resolver to confirm that the current Human can invoke an action on the
// current recipient fact. The resolver must use indexed, current facts.
type AppNotificationActionAuthorizationRequest struct {
	WorkspaceID    string
	Basis          AppNotificationRecipientBasis
	BusinessObject AppNotificationBusinessObject
	ActorMemberID  string
	ActionKey      string
	CardRevision   int64
	Input          map[string]any
	Now            time.Time
}

// AppNotificationRecipientResolver is the registered seam between notification
// delivery and an owning business domain such as approval assignments.
// ResolveAppNotificationRecipient must return not-found when the typed fact no
// longer exists; it must not silently substitute another recipient, basis, or
// business object.
type AppNotificationRecipientResolver interface {
	ResolveAppNotificationRecipient(context.Context, coredb.QueryRower, AppNotificationRecipientFactRequest) (AppNotificationRecipientFact, error)
	AuthorizeAppNotificationAction(context.Context, coredb.QueryRower, AppNotificationActionAuthorizationRequest) error
}

// AppNotificationInternalServiceRegistration binds one trusted first-party
// identity to a App definition and bounded business/target/content namespaces.
// Secret is hashed in memory during Store construction.
type AppNotificationInternalServiceRegistration struct {
	IdentityID                string
	Secret                    string
	ServiceDefinitionSlug     string
	AllowedBusinessNamespaces []string
	AllowedTargetKinds        []string
	AllowedContentKinds       []string
}

// AppNotificationBuiltinServiceRegistration binds a AgentTeam-owned domain to an
// in-process action handler. It intentionally carries no HTTP credential.
type AppNotificationBuiltinServiceRegistration struct {
	IdentityID                string
	ServiceDefinitionSlug     string
	ActionHandlerKey          string
	AllowedBusinessNamespaces []string
	AllowedTargetKinds        []string
	AllowedContentKinds       []string
}

type appNotificationInternalService struct {
	IdentityID                string
	SecretDigest              [sha256.Size]byte
	HasSecret                 bool
	ServiceDefinitionSlug     string
	ActionHandlerKey          string
	AllowedBusinessNamespaces map[string]struct{}
	AllowedTargetKinds        map[string]struct{}
	AllowedContentKinds       map[string]struct{}
}

func normalizeAppNotificationInternalService(registration AppNotificationInternalServiceRegistration) (appNotificationInternalService, error) {
	identityID := strings.TrimSpace(registration.IdentityID)
	secret := strings.TrimSpace(registration.Secret)
	definitionSlug := strings.TrimSpace(registration.ServiceDefinitionSlug)
	if identityID == "" || secret == "" || definitionSlug == "" {
		return appNotificationInternalService{}, fmt.Errorf("internal App notification identity, secret, and service definition slug are required")
	}
	if strings.IndexFunc(secret, func(value rune) bool { return unicode.IsSpace(value) || unicode.IsControl(value) }) >= 0 {
		return appNotificationInternalService{}, fmt.Errorf("internal App notification secret must be a single HTTP token")
	}
	service := appNotificationInternalService{
		IdentityID:                identityID,
		SecretDigest:              sha256.Sum256([]byte(secret)),
		HasSecret:                 true,
		ServiceDefinitionSlug:     definitionSlug,
		AllowedBusinessNamespaces: normalizedStringSet(registration.AllowedBusinessNamespaces),
		AllowedTargetKinds:        normalizedStringSet(registration.AllowedTargetKinds),
		AllowedContentKinds:       normalizedStringSet(registration.AllowedContentKinds),
	}
	if len(service.AllowedTargetKinds) == 0 || len(service.AllowedContentKinds) == 0 {
		return appNotificationInternalService{}, fmt.Errorf("internal App notification target and content allowlists are required")
	}
	return service, nil
}

func normalizeAppNotificationBuiltinService(registration AppNotificationBuiltinServiceRegistration) (appNotificationInternalService, error) {
	identityID := strings.TrimSpace(registration.IdentityID)
	definitionSlug := strings.TrimSpace(registration.ServiceDefinitionSlug)
	handlerKey := strings.TrimSpace(registration.ActionHandlerKey)
	if identityID == "" || definitionSlug == "" || handlerKey == "" {
		return appNotificationInternalService{}, fmt.Errorf("built-in App notification identity, definition slug, and action handler key are required")
	}
	if _, err := serviceactor.InternalHandlerConnectionRef(handlerKey); err != nil {
		return appNotificationInternalService{}, fmt.Errorf("built-in App notification action handler key is invalid: %w", err)
	}
	service := appNotificationInternalService{
		IdentityID:                identityID,
		ServiceDefinitionSlug:     definitionSlug,
		ActionHandlerKey:          handlerKey,
		AllowedBusinessNamespaces: normalizedStringSet(registration.AllowedBusinessNamespaces),
		AllowedTargetKinds:        normalizedStringSet(registration.AllowedTargetKinds),
		AllowedContentKinds:       normalizedStringSet(registration.AllowedContentKinds),
	}
	if len(service.AllowedTargetKinds) == 0 || len(service.AllowedContentKinds) == 0 {
		return appNotificationInternalService{}, fmt.Errorf("built-in App notification target and content allowlists are required")
	}
	return service, nil
}

func normalizedStringSet(values []string) map[string]struct{} {
	result := make(map[string]struct{}, len(values))
	for _, value := range values {
		if trimmed := strings.TrimSpace(value); trimmed != "" {
			result[trimmed] = struct{}{}
		}
	}
	return result
}

func sortedStringSet(values map[string]struct{}) []string {
	result := make([]string, 0, len(values))
	for value := range values {
		result = append(result, value)
	}
	sort.Strings(result)
	return result
}

func appNotificationDeterministicJSON(value any) ([]byte, error) {
	payload, err := json.Marshal(value)
	if err != nil {
		return nil, fmt.Errorf("marshal App notification canonical payload: %w", err)
	}
	return payload, nil
}

func appNotificationDigest(value any) (string, error) {
	payload, err := appNotificationDeterministicJSON(value)
	if err != nil {
		return "", err
	}
	digest := sha256.Sum256(payload)
	return hex.EncodeToString(digest[:]), nil
}

func deterministicAppNotificationID(installationID, clientNotificationID string) string {
	digest := sha256.Sum256([]byte("app-notification\x00" + strings.TrimSpace(installationID) + "\x00" + strings.TrimSpace(clientNotificationID)))
	return "btn_" + hex.EncodeToString(digest[:16])
}

func deterministicAppNotificationMessageID(notificationID string) string {
	digest := sha256.Sum256([]byte("app-notification-message\x00" + strings.TrimSpace(notificationID)))
	return hex.EncodeToString(digest[:16])
}

func deterministicAppNotificationActionID(messageID string, cardRevision int64, actorMemberID, clientActionID string) string {
	digest := sha256.Sum256([]byte(fmt.Sprintf("app-notification-action\x00%s\x00%d\x00%s\x00%s", strings.TrimSpace(messageID), cardRevision, strings.TrimSpace(actorMemberID), strings.TrimSpace(clientActionID))))
	return "bna_" + hex.EncodeToString(digest[:16])
}
