package appruntime

import (
	"context"
	"fmt"
	"log/slog"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	"github.com/tidbcloud/AgentTeam/internal/core/runeffect"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
)

const (
	defaultInternalHandlerPollInterval  = 2 * time.Second
	defaultInternalHandlerLeaseDuration = 2 * time.Minute
	defaultInternalHandlerTimeout       = 30 * time.Second
	defaultInternalHandlerLimit         = 10

	InternalHandlerErrorNotRegistered = "internal_handler_not_registered"
	InternalHandlerErrorExecution     = "internal_handler_execution_failed"
	InternalHandlerErrorCompletion    = "internal_handler_completion_failed"
)

var internalDispatcherSequence atomic.Uint64

// InternalHandler receives one durably leased App action delivery inside the
// AgentTeam server process. Implementations must use Delivery.EventID as their
// domain idempotency key and must not perform unbounded work.
type InternalHandler interface {
	HandleInternalDelivery(context.Context, InternalHandlerEnvelope) (InternalHandlerOutcome, error)
}

// InternalHandlerEnvelope is the process-local view of a durable delivery.
type InternalHandlerEnvelope struct {
	HandlerKey string
	serviceactor.DeliveryEnvelope
}

// InternalHandlerOutcome records the effect that lets the common delivery
// ledger acknowledge successful handler execution.
type InternalHandlerOutcome struct {
	Effect     runeffect.Effect
	ResultJSON []byte
}

// InternalHandlerRegistry is the process-local, server-owned mapping from
// stable handler keys to implementations. It is deliberately independent of
// public App manifests.
type InternalHandlerRegistry struct {
	mu       sync.RWMutex
	handlers map[string]InternalHandler
	sealed   bool
}

// NewInternalHandlerRegistry constructs an empty internal handler registry.
func NewInternalHandlerRegistry() *InternalHandlerRegistry {
	return &InternalHandlerRegistry{handlers: map[string]InternalHandler{}}
}

// Register adds one stable handler key. Duplicate registrations are rejected.
func (registry *InternalHandlerRegistry) Register(handlerKey string, handler InternalHandler) error {
	if registry == nil {
		return fmt.Errorf("internal handler registry is nil")
	}
	ref, err := serviceactor.InternalHandlerConnectionRef(handlerKey)
	if err != nil || handler == nil {
		return fmt.Errorf("valid internal handler key and implementation are required")
	}
	key, _ := serviceactor.InternalHandlerKeyFromConnectionRef(ref)
	registry.mu.Lock()
	defer registry.mu.Unlock()
	if registry.sealed {
		return fmt.Errorf("internal handler registry is sealed")
	}
	if _, exists := registry.handlers[key]; exists {
		return fmt.Errorf("internal handler %q is already registered", key)
	}
	registry.handlers[key] = handler
	return nil
}

// Seal prevents runtime mutation after server wiring has completed.
func (registry *InternalHandlerRegistry) Seal() {
	if registry == nil {
		return
	}
	registry.mu.Lock()
	registry.sealed = true
	registry.mu.Unlock()
}

func (registry *InternalHandlerRegistry) lookup(handlerKey string) (InternalHandler, bool) {
	if registry == nil {
		return nil, false
	}
	registry.mu.RLock()
	defer registry.mu.RUnlock()
	handler, ok := registry.handlers[strings.TrimSpace(handlerKey)]
	return handler, ok
}

// InternalDispatcherStore is the durable service-actor surface used by the
// in-process dispatcher.
type InternalDispatcherStore interface {
	ListRuntimeInternalDispatchTargets(context.Context, serviceactor.RuntimeInternalDispatchTargetList) ([]serviceactor.RuntimeInternalDispatchTarget, error)
	ClaimDeliveryEnvelopes(context.Context, serviceactor.DeliveryDispatchClaim) ([]serviceactor.DeliveryEnvelope, error)
	CompleteInternalHandlerDelivery(context.Context, serviceactor.InternalHandlerDeliveryCompletion) (serviceactor.InternalHandlerDeliveryCompletionResult, error)
	ScheduleDeliveryAttemptRetry(context.Context, serviceactor.DeliveryAttemptRetrySchedule) (serviceactor.DeliveryAttemptRetryResult, error)
}

// InternalDispatcher invokes AgentTeam-owned App handlers without callback URLs,
// runtime credentials, sessions, or delivery bearer tokens.
type InternalDispatcher struct {
	store            InternalDispatcherStore
	registry         *InternalHandlerRegistry
	logger           *slog.Logger
	now              func() time.Time
	instanceID       string
	lease            time.Duration
	handlerTimeout   time.Duration
	baseBackoff      time.Duration
	maxBackoff       time.Duration
	limit            int
	targetAllowed    func(string) bool
	workspaceGroupID string
}

// InternalDispatcherOptions configures the AgentTeam-owned handler dispatcher.
type InternalDispatcherOptions struct {
	Store            InternalDispatcherStore
	Registry         *InternalHandlerRegistry
	Logger           *slog.Logger
	Now              func() time.Time
	InstanceID       string
	LeaseDuration    time.Duration
	HandlerTimeout   time.Duration
	BaseBackoff      time.Duration
	MaxBackoff       time.Duration
	ClaimLimit       int
	TargetAllowed    func(workspaceID string) bool
	WorkspaceGroupID string
}

// InternalDispatchParams selects one handler route batch.
type InternalDispatchParams struct {
	WorkspaceID           string
	ServiceInstallationID string
	HandlerKey            string
	Limit                 int
	Now                   time.Time
}

// InternalDispatchResult summarizes one in-process dispatch batch.
type InternalDispatchResult struct {
	Claimed   int
	Completed int
	Retried   int
	Items     []InternalDispatchItem
}

// InternalDispatchItem is the bounded per-delivery dispatcher result.
type InternalDispatchItem struct {
	DeliveryID  string
	AttemptID   string
	Completed   bool
	RetryAt     *time.Time
	ErrorCode   string
	ErrorDetail string
}

// NewInternalDispatcher constructs an in-process App action dispatcher.
func NewInternalDispatcher(options InternalDispatcherOptions) *InternalDispatcher {
	instanceID := strings.TrimSpace(options.InstanceID)
	if instanceID == "" {
		generated, err := identifiers.NewEntityID("bihr")
		if err == nil {
			instanceID = generated
		} else {
			instanceID = fmt.Sprintf("process-%d-%d", time.Now().UTC().UnixNano(), internalDispatcherSequence.Add(1))
		}
	}
	return &InternalDispatcher{
		store: options.Store, registry: options.Registry, logger: options.Logger, now: options.Now,
		instanceID: instanceID, lease: options.LeaseDuration, handlerTimeout: options.HandlerTimeout,
		baseBackoff: options.BaseBackoff, maxBackoff: options.MaxBackoff, limit: options.ClaimLimit,
		targetAllowed:    options.TargetAllowed,
		workspaceGroupID: strings.TrimSpace(options.WorkspaceGroupID),
	}
}

// Dispatch claims and invokes one internal handler route.
func (dispatcher *InternalDispatcher) Dispatch(ctx context.Context, params InternalDispatchParams) (InternalDispatchResult, error) {
	if dispatcher == nil || dispatcher.store == nil || dispatcher.registry == nil {
		return InternalDispatchResult{}, fmt.Errorf("internal handler dispatcher is not configured")
	}
	workspaceID := strings.TrimSpace(params.WorkspaceID)
	installationID := strings.TrimSpace(params.ServiceInstallationID)
	handlerRef, err := serviceactor.InternalHandlerConnectionRef(params.HandlerKey)
	if workspaceID == "" || installationID == "" || err != nil {
		return InternalDispatchResult{}, fmt.Errorf("workspace, installation, and valid internal handler key are required")
	}
	handlerKey, _ := serviceactor.InternalHandlerKeyFromConnectionRef(handlerRef)
	if dispatcher.targetAllowed != nil && !dispatcher.targetAllowed(workspaceID) {
		return InternalDispatchResult{}, nil
	}
	now := params.Now.UTC()
	if now.IsZero() {
		now = dispatcher.nowUTC()
	}
	limit := params.Limit
	if limit <= 0 {
		limit = dispatcher.limit
	}
	if limit <= 0 {
		limit = defaultInternalHandlerLimit
	}
	envelopes, err := dispatcher.store.ClaimDeliveryEnvelopes(ctx, serviceactor.DeliveryDispatchClaim{
		WorkspaceID: workspaceID, ServiceInstallationID: installationID,
		SubscriptionKind:     serviceactor.SubscriptionKindAppNotificationAction,
		DeliveryPolicy:       serviceactor.AppRuntimeDeliveryPolicyInternalHandler,
		ServiceConnectionRef: handlerRef, TransportKind: serviceactor.RuntimeTransportKindInternalHandler,
		LeaseOwner:    "internal:" + dispatcher.instanceID + ":" + handlerKey,
		LeaseDuration: dispatcher.leaseDuration(), Limit: limit, Now: now,
	})
	if err != nil {
		return InternalDispatchResult{}, err
	}
	result := InternalDispatchResult{Claimed: len(envelopes), Items: make([]InternalDispatchItem, 0, len(envelopes))}
	for _, envelope := range envelopes {
		item := dispatcher.dispatchOne(ctx, handlerKey, envelope)
		if item.Completed {
			result.Completed++
		} else if item.RetryAt != nil {
			result.Retried++
		}
		result.Items = append(result.Items, item)
	}
	return result, nil
}

// Run scans ready internal routes and dispatches them until context cancel.
func (dispatcher *InternalDispatcher) Run(ctx context.Context, interval time.Duration) {
	if dispatcher == nil || dispatcher.store == nil || dispatcher.registry == nil {
		return
	}
	if interval <= 0 {
		interval = defaultInternalHandlerPollInterval
	}
	dispatcher.runPass(ctx)
	ticker := time.NewTicker(interval)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			dispatcher.runPass(ctx)
		}
	}
}

func (dispatcher *InternalDispatcher) runPass(ctx context.Context) {
	now := dispatcher.nowUTC()
	targets, err := dispatcher.store.ListRuntimeInternalDispatchTargets(ctx, serviceactor.RuntimeInternalDispatchTargetList{WorkspaceGroupID: dispatcher.workspaceGroupID, Now: now, Limit: 100})
	if err != nil {
		if dispatcher.logger != nil && ctx.Err() == nil {
			dispatcher.logger.WarnContext(ctx, "internal App handler target scan failed", "error", err)
		}
		return
	}
	for _, target := range targets {
		if dispatcher.targetAllowed != nil && !dispatcher.targetAllowed(target.WorkspaceID) {
			continue
		}
		result, err := dispatcher.Dispatch(ctx, InternalDispatchParams{
			WorkspaceID: target.WorkspaceID, ServiceInstallationID: target.ServiceInstallationID,
			HandlerKey: target.HandlerKey, Now: now,
		})
		if err != nil {
			if dispatcher.logger != nil && ctx.Err() == nil {
				dispatcher.logger.WarnContext(ctx, "internal App handler dispatch failed", "workspace_id", target.WorkspaceID, "handler_key", target.HandlerKey, "error", err)
			}
			continue
		}
		if dispatcher.logger != nil && result.Claimed > 0 {
			dispatcher.logger.InfoContext(ctx, "internal App handler dispatch completed", "workspace_id", target.WorkspaceID, "handler_key", target.HandlerKey, "claimed", result.Claimed, "completed", result.Completed, "retried", result.Retried)
		}
	}
}

func (dispatcher *InternalDispatcher) dispatchOne(ctx context.Context, handlerKey string, envelope serviceactor.DeliveryEnvelope) InternalDispatchItem {
	item := InternalDispatchItem{DeliveryID: envelope.Delivery.ID, AttemptID: envelope.Attempt.ID}
	if envelope.DeliveryToken != "" || envelope.Attempt.ServiceRuntimeCredentialID != "" || envelope.Attempt.ServiceRuntimeSessionID != "" {
		return dispatcher.retry(ctx, envelope, InternalHandlerErrorCompletion, "internal handler envelope contains external runtime credentials")
	}
	handler, ok := dispatcher.registry.lookup(handlerKey)
	if !ok {
		return dispatcher.retry(ctx, envelope, InternalHandlerErrorNotRegistered, "internal handler is not registered")
	}
	handlerCtx, cancel := context.WithTimeout(ctx, dispatcher.handlerDeadline())
	outcome, err := handler.HandleInternalDelivery(handlerCtx, InternalHandlerEnvelope{HandlerKey: handlerKey, DeliveryEnvelope: envelope})
	cancel()
	if err != nil {
		return dispatcher.retry(ctx, envelope, InternalHandlerErrorExecution, err.Error())
	}
	now := dispatcher.nowUTC()
	_, err = dispatcher.store.CompleteInternalHandlerDelivery(ctx, serviceactor.InternalHandlerDeliveryCompletion{
		WorkspaceID: envelope.Delivery.WorkspaceID, AttemptID: envelope.Attempt.ID,
		HandlerKey: handlerKey, Effect: outcome.Effect, ResultJSON: outcome.ResultJSON, Now: now,
	})
	if err != nil {
		return dispatcher.retry(ctx, envelope, InternalHandlerErrorCompletion, err.Error())
	}
	item.Completed = true
	return item
}

func (dispatcher *InternalDispatcher) retry(ctx context.Context, envelope serviceactor.DeliveryEnvelope, code string, detail string) InternalDispatchItem {
	item := InternalDispatchItem{DeliveryID: envelope.Delivery.ID, AttemptID: envelope.Attempt.ID, ErrorCode: code, ErrorDetail: strings.TrimSpace(detail)}
	now := dispatcher.nowUTC()
	retry, err := dispatcher.store.ScheduleDeliveryAttemptRetry(ctx, serviceactor.DeliveryAttemptRetrySchedule{
		WorkspaceID: envelope.Delivery.WorkspaceID, AttemptID: envelope.Attempt.ID,
		ErrorCode: code, ErrorDetail: item.ErrorDetail,
		BaseBackoff: dispatcher.baseBackoff, MaxBackoff: dispatcher.maxBackoff, Now: now,
	})
	if err != nil {
		item.ErrorCode = InternalHandlerErrorCompletion
		item.ErrorDetail = strings.TrimSpace(err.Error())
		return item
	}
	retryAt := retry.NextRetryAt
	item.RetryAt = &retryAt
	return item
}

func (dispatcher *InternalDispatcher) nowUTC() time.Time {
	if dispatcher != nil && dispatcher.now != nil {
		if now := dispatcher.now().UTC(); !now.IsZero() {
			return now
		}
	}
	return time.Now().UTC()
}

func (dispatcher *InternalDispatcher) leaseDuration() time.Duration {
	if dispatcher != nil && dispatcher.lease > 0 {
		return dispatcher.lease
	}
	return defaultInternalHandlerLeaseDuration
}

func (dispatcher *InternalDispatcher) handlerDeadline() time.Duration {
	deadline := defaultInternalHandlerTimeout
	if dispatcher != nil && dispatcher.handlerTimeout > 0 {
		deadline = dispatcher.handlerTimeout
	}
	lease := dispatcher.leaseDuration()
	if deadline >= lease {
		deadline = lease / 2
	}
	if deadline <= 0 {
		return time.Second
	}
	return deadline
}
