package appruntime

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

const (
	defaultRunnerClaimLimit    = 10
	defaultRunnerLeaseDuration = 2 * time.Minute
	maximumRunnerClaimLimit    = 100
	maximumRunnerLeaseDuration = 15 * time.Minute
	minimumRunnerLeaseDuration = 5 * time.Second
)

// RunnerStore is the service actor runtime persistence needed by the App
// runner stream transport.
type RunnerStore interface {
	AuthenticateRuntimeCredential(ctx context.Context, token string) (serviceactor.RuntimeCredential, error)
	UpsertRuntimeSession(ctx context.Context, params serviceactor.RuntimeSessionUpsert) (serviceactor.RuntimeSession, error)
	LoadRuntimeSession(ctx context.Context, workspaceID string, sessionID string) (serviceactor.RuntimeSession, error)
	SetRuntimeSessionStatus(ctx context.Context, workspaceID string, sessionID string, status string, now time.Time) (serviceactor.RuntimeSession, error)
	ClaimDeliveryEnvelopes(ctx context.Context, params serviceactor.DeliveryDispatchClaim) ([]serviceactor.DeliveryEnvelope, error)
}

// RunnerAuthority owns the App-specific credential, installation, grant, and
// subscription checks required by the runner transport.
type RunnerAuthority interface {
	ValidateAppRunnerCredential(ctx context.Context, credential serviceactor.RuntimeCredential) (serviceactor.RuntimeCredential, error)
	LoadCurrentAppRunnerCredential(ctx context.Context, workspaceID string, credentialID string) (serviceactor.RuntimeCredential, error)
	AppRunnerClaimSubscriptionKinds(ctx context.Context, workspaceID string, installationID string) ([]string, error)
}

// RunnerService manages runner-stream runtime sessions and delivery claims.
type RunnerService struct {
	store     RunnerStore
	authority RunnerAuthority
	now       func() time.Time
	lease     time.Duration
	limit     int
}

// RunnerServiceOptions configures the App runner stream service.
type RunnerServiceOptions struct {
	Store         RunnerStore
	Authority     RunnerAuthority
	Now           func() time.Time
	LeaseDuration time.Duration
	ClaimLimit    int
}

// RunnerAuthentication is the authenticated runner credential context used by
// heartbeat, claim, and later reply/ack/fail runtime APIs.
type RunnerAuthentication struct {
	WorkspaceID                string
	ServiceDefinitionID        string
	ServiceInstallationID      string
	ServiceMemberID            string
	ServiceRuntimeCredentialID string
}

// RunnerHeartbeatParams records one runner session heartbeat.
type RunnerHeartbeatParams struct {
	RunnerID      string
	RunnerVersion string
	Capabilities  map[string]any
	Metadata      map[string]any
}

// RunnerClaimParams records one runner delivery claim request.
type RunnerClaimParams struct {
	RunnerID      string
	RunnerVersion string
	Capabilities  map[string]any
	Metadata      map[string]any
	Limit         int
	LeaseDuration time.Duration
}

// RunnerSessionView is the service-owned App runner session contract.
type RunnerSessionView struct {
	ID                         string
	WorkspaceID                string
	ServiceDefinitionID        string
	ServiceInstallationID      string
	ServiceMemberID            string
	ServiceRuntimeCredentialID string
	RunnerID                   string
	RunnerVersion              string
	Status                     string
	Capabilities               map[string]any
	LastHeartbeatAt            *time.Time
	LastClaimAt                *time.Time
	CreatedAt                  time.Time
	UpdatedAt                  time.Time
	Metadata                   map[string]any
}

// RunnerDeliveryEnvelopeView is the service-owned claimed delivery contract.
type RunnerDeliveryEnvelopeView struct {
	EnvelopeID    string
	Delivery      serviceactor.EventDelivery
	Attempt       serviceactor.DeliveryAttempt
	DeliveryToken string
	EventType     string
	EventAction   string
	EventPayload  []byte
	CreatedAt     time.Time
}

// RunnerClaimResult returns the refreshed session and claimed envelopes.
type RunnerClaimResult struct {
	Session   RunnerSessionView
	Envelopes []RunnerDeliveryEnvelopeView
}

// NewRunnerService constructs a App runner stream service.
func NewRunnerService(options RunnerServiceOptions) *RunnerService {
	return &RunnerService{
		store:     options.Store,
		authority: options.Authority,
		now:       options.Now,
		lease:     options.LeaseDuration,
		limit:     options.ClaimLimit,
	}
}

// AuthenticateRunner validates a bearer token as an active runner-stream App
// runtime credential.
func (service *RunnerService) AuthenticateRunner(ctx context.Context, bearerToken string) (RunnerAuthentication, error) {
	if service == nil || service.store == nil || service.authority == nil {
		return RunnerAuthentication{}, fmt.Errorf("app runner stream store is not configured")
	}
	credential, err := service.store.AuthenticateRuntimeCredential(ctx, strings.TrimSpace(bearerToken))
	if err != nil {
		if errors.Is(err, serviceactor.ErrNotFound) || errors.Is(err, serviceactor.ErrBadRequest) {
			return RunnerAuthentication{}, storeerr.ErrUnauthorized
		}
		return RunnerAuthentication{}, err
	}
	if err := validateRunnerCredentialShape(credential); err != nil {
		return RunnerAuthentication{}, err
	}
	credential, err = service.authority.ValidateAppRunnerCredential(ctx, credential)
	if err != nil {
		return RunnerAuthentication{}, err
	}
	return runnerAuthentication(credential), nil
}

// Heartbeat records the runner as online and returns the current runtime
// session. The same session identity is reused by later claim/reply/ack/fail
// calls for this runner id.
func (service *RunnerService) Heartbeat(ctx context.Context, auth RunnerAuthentication, params RunnerHeartbeatParams) (RunnerSessionView, error) {
	credential, err := service.currentCredential(ctx, auth)
	if err != nil {
		return RunnerSessionView{}, err
	}
	capabilitiesJSON, err := model.MarshalJSONObject("app runner capabilities", cloneStringMap(params.Capabilities))
	if err != nil {
		return RunnerSessionView{}, err
	}
	metadata := cloneStringMap(params.Metadata)
	metadata["transport"] = serviceactor.RuntimeTransportKindRunnerStream
	metadataJSON, err := model.MarshalJSONObject("app runner session metadata", metadata)
	if err != nil {
		return RunnerSessionView{}, err
	}
	session, err := service.heartbeat(ctx, credential, params, service.nowUTC(), capabilitiesJSON, metadataJSON)
	if err != nil {
		return RunnerSessionView{}, err
	}
	return runnerSessionView(session), nil
}

func (service *RunnerService) heartbeat(ctx context.Context, credential serviceactor.RuntimeCredential, params RunnerHeartbeatParams, now time.Time, capabilitiesJSON []byte, metadataJSON []byte) (serviceactor.RuntimeSession, error) {
	runnerID := strings.TrimSpace(params.RunnerID)
	if runnerID == "" {
		return serviceactor.RuntimeSession{}, fmt.Errorf("runner id is required")
	}
	now = now.UTC()
	heartbeatAt := now
	return service.store.UpsertRuntimeSession(ctx, serviceactor.RuntimeSessionUpsert{
		WorkspaceID:                credential.WorkspaceID,
		ServiceDefinitionID:        credential.ServiceDefinitionID,
		ServiceInstallationID:      credential.ServiceInstallationID,
		ServiceMemberID:            credential.ServiceMemberID,
		ServiceRuntimeCredentialID: credential.ID,
		RunnerID:                   runnerID,
		RunnerVersion:              strings.TrimSpace(params.RunnerVersion),
		Status:                     serviceactor.RuntimeSessionStatusOnline,
		CapabilitiesJSON:           capabilitiesJSON,
		LastHeartbeatAt:            &heartbeatAt,
		Now:                        now,
		Metadata:                   metadataJSON,
	})
}

// ClaimDeliveries refreshes the runner heartbeat and leases ready App runtime
// deliveries for this runner session. The dispatcher applies the closed App
// runtime kind allowlist, so unrelated service events cannot enter this stream.
func (service *RunnerService) ClaimDeliveries(ctx context.Context, auth RunnerAuthentication, params RunnerClaimParams) (RunnerClaimResult, error) {
	credential, err := service.currentCredential(ctx, auth)
	if err != nil {
		return RunnerClaimResult{}, err
	}
	capabilitiesJSON, err := model.MarshalJSONObject("app runner capabilities", cloneStringMap(params.Capabilities))
	if err != nil {
		return RunnerClaimResult{}, err
	}
	metadata := cloneStringMap(params.Metadata)
	metadata["transport"] = serviceactor.RuntimeTransportKindRunnerStream
	metadataJSON, err := model.MarshalJSONObject("app runner session metadata", metadata)
	if err != nil {
		return RunnerClaimResult{}, err
	}
	authorizedKinds, err := service.authority.AppRunnerClaimSubscriptionKinds(ctx, credential.WorkspaceID, credential.ServiceInstallationID)
	if err != nil {
		return RunnerClaimResult{}, err
	}
	subscriptionKinds, err := normalizeRunnerSubscriptionKinds(authorizedKinds)
	if err != nil {
		return RunnerClaimResult{}, err
	}
	now := service.nowUTC()
	session, err := service.heartbeat(ctx, credential, RunnerHeartbeatParams{RunnerID: params.RunnerID, RunnerVersion: params.RunnerVersion}, now, capabilitiesJSON, metadataJSON)
	if err != nil {
		return RunnerClaimResult{}, err
	}
	if len(subscriptionKinds) == 0 {
		return RunnerClaimResult{Session: runnerSessionView(session), Envelopes: []RunnerDeliveryEnvelopeView{}}, nil
	}
	envelopes, err := service.store.ClaimDeliveryEnvelopes(ctx, serviceactor.DeliveryDispatchClaim{
		WorkspaceID:                credential.WorkspaceID,
		ServiceInstallationID:      credential.ServiceInstallationID,
		SubscriptionKinds:          subscriptionKinds,
		DeliveryPolicy:             serviceactor.AppRuntimeDeliveryPolicyExternal,
		ServiceConnectionRef:       serviceactor.AppRuntimeConnectionRefExternal,
		TransportKind:              serviceactor.RuntimeTransportKindRunnerStream,
		ServiceRuntimeCredentialID: credential.ID,
		ServiceRuntimeSessionID:    session.ID,
		LeaseOwner:                 "runner:" + session.ID,
		LeaseDuration:              service.normalizeLeaseDuration(params.LeaseDuration),
		Limit:                      service.normalizeLimit(params.Limit),
		Now:                        now,
	})
	if err != nil {
		return RunnerClaimResult{}, err
	}
	claimedSession := session
	if len(envelopes) > 0 {
		reloaded, err := service.store.LoadRuntimeSession(ctx, credential.WorkspaceID, session.ID)
		if err != nil {
			return RunnerClaimResult{}, err
		}
		claimedSession = reloaded
	}
	return RunnerClaimResult{Session: runnerSessionView(claimedSession), Envelopes: runnerEnvelopeViews(envelopes)}, nil
}

// MarkSessionOffline records an orderly runner disconnect.
func (service *RunnerService) MarkSessionOffline(ctx context.Context, auth RunnerAuthentication, sessionID string) (RunnerSessionView, error) {
	credential, err := service.currentCredential(ctx, auth)
	if err != nil {
		return RunnerSessionView{}, err
	}
	trimmedSessionID := strings.TrimSpace(sessionID)
	if trimmedSessionID == "" {
		return RunnerSessionView{}, fmt.Errorf("runtime session id is required")
	}
	loaded, err := service.store.LoadRuntimeSession(ctx, credential.WorkspaceID, trimmedSessionID)
	if err != nil {
		return RunnerSessionView{}, err
	}
	if strings.TrimSpace(loaded.ServiceInstallationID) != strings.TrimSpace(credential.ServiceInstallationID) || strings.TrimSpace(loaded.ServiceRuntimeCredentialID) != strings.TrimSpace(credential.ID) {
		return RunnerSessionView{}, storeerr.ErrForbidden
	}
	session, err := service.store.SetRuntimeSessionStatus(ctx, credential.WorkspaceID, trimmedSessionID, serviceactor.RuntimeSessionStatusOffline, service.nowUTC())
	if err != nil {
		return RunnerSessionView{}, err
	}
	return runnerSessionView(session), nil
}

func (service *RunnerService) currentCredential(ctx context.Context, auth RunnerAuthentication) (serviceactor.RuntimeCredential, error) {
	if service == nil || service.store == nil || service.authority == nil {
		return serviceactor.RuntimeCredential{}, fmt.Errorf("app runner stream store is not configured")
	}
	if strings.TrimSpace(auth.WorkspaceID) == "" || strings.TrimSpace(auth.ServiceRuntimeCredentialID) == "" {
		return serviceactor.RuntimeCredential{}, storeerr.ErrUnauthorized
	}
	credential, err := service.authority.LoadCurrentAppRunnerCredential(ctx, auth.WorkspaceID, auth.ServiceRuntimeCredentialID)
	if err != nil {
		return serviceactor.RuntimeCredential{}, err
	}
	if err := validateRunnerCredentialShape(credential); err != nil {
		return serviceactor.RuntimeCredential{}, err
	}
	return credential, nil
}

func validateRunnerCredentialShape(credential serviceactor.RuntimeCredential) error {
	if strings.TrimSpace(credential.ID) == "" || strings.TrimSpace(credential.WorkspaceID) == "" || strings.TrimSpace(credential.ServiceInstallationID) == "" || strings.TrimSpace(credential.ServiceMemberID) == "" {
		return storeerr.ErrUnauthorized
	}
	if strings.TrimSpace(credential.ServiceKind) != serviceactor.KindApp || strings.TrimSpace(credential.CredentialKind) != serviceactor.RuntimeCredentialKindRunnerStreamToken || strings.TrimSpace(credential.TransportKind) != serviceactor.RuntimeTransportKindRunnerStream || strings.TrimSpace(credential.Status) != serviceactor.RuntimeCredentialStatusActive {
		return storeerr.ErrUnauthorized
	}
	return nil
}

func normalizeRunnerSubscriptionKinds(values []string) ([]string, error) {
	seen := make(map[string]struct{}, len(values))
	normalized := make([]string, 0, len(values))
	for _, value := range values {
		kind := strings.TrimSpace(value)
		if kind == "" {
			continue
		}
		if !serviceactor.IsAppRuntimeSubscriptionKind(kind) {
			return nil, fmt.Errorf("app runner subscription kind %q is not supported", kind)
		}
		if _, ok := seen[kind]; ok {
			continue
		}
		seen[kind] = struct{}{}
		normalized = append(normalized, kind)
	}
	return normalized, nil
}

func runnerAuthentication(credential serviceactor.RuntimeCredential) RunnerAuthentication {
	return RunnerAuthentication{
		WorkspaceID:                credential.WorkspaceID,
		ServiceDefinitionID:        credential.ServiceDefinitionID,
		ServiceInstallationID:      credential.ServiceInstallationID,
		ServiceMemberID:            credential.ServiceMemberID,
		ServiceRuntimeCredentialID: credential.ID,
	}
}

func runnerSessionView(session serviceactor.RuntimeSession) RunnerSessionView {
	return RunnerSessionView{
		ID:                         session.ID,
		WorkspaceID:                session.WorkspaceID,
		ServiceDefinitionID:        session.ServiceDefinitionID,
		ServiceInstallationID:      session.ServiceInstallationID,
		ServiceMemberID:            session.ServiceMemberID,
		ServiceRuntimeCredentialID: session.ServiceRuntimeCredentialID,
		RunnerID:                   session.RunnerID,
		RunnerVersion:              session.RunnerVersion,
		Status:                     session.Status,
		Capabilities:               decodeRunnerJSONObject(session.CapabilitiesJSON),
		LastHeartbeatAt:            session.LastHeartbeatAt,
		LastClaimAt:                session.LastClaimAt,
		CreatedAt:                  session.CreatedAt,
		UpdatedAt:                  session.UpdatedAt,
		Metadata:                   decodeRunnerJSONObject(session.Metadata),
	}
}

func runnerEnvelopeViews(envelopes []serviceactor.DeliveryEnvelope) []RunnerDeliveryEnvelopeView {
	if envelopes == nil {
		return nil
	}
	views := make([]RunnerDeliveryEnvelopeView, 0, len(envelopes))
	for _, envelope := range envelopes {
		views = append(views, RunnerDeliveryEnvelopeView{
			EnvelopeID:    envelope.EnvelopeID,
			Delivery:      envelope.Delivery,
			Attempt:       envelope.Attempt,
			DeliveryToken: envelope.DeliveryToken,
			EventType:     envelope.EventType,
			EventAction:   envelope.EventAction,
			EventPayload:  envelope.EventPayload,
			CreatedAt:     envelope.CreatedAt,
		})
	}
	return views
}

func cloneStringMap(input map[string]any) map[string]any {
	out := make(map[string]any, len(input)+1)
	for key, value := range input {
		out[strings.TrimSpace(key)] = value
	}
	return out
}

func decodeRunnerJSONObject(payload []byte) map[string]any {
	out := map[string]any{}
	if len(payload) == 0 {
		return out
	}
	if err := model.UnmarshalJSONObject("app runner JSON object", payload, &out); err != nil || out == nil {
		return map[string]any{}
	}
	return out
}

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

func (service *RunnerService) normalizeLimit(limit int) int {
	if limit <= 0 {
		limit = service.limit
	}
	if limit <= 0 {
		return defaultRunnerClaimLimit
	}
	if limit > maximumRunnerClaimLimit {
		return maximumRunnerClaimLimit
	}
	return limit
}

func (service *RunnerService) normalizeLeaseDuration(lease time.Duration) time.Duration {
	if lease <= 0 {
		lease = service.lease
	}
	if lease <= 0 {
		return defaultRunnerLeaseDuration
	}
	if lease < minimumRunnerLeaseDuration {
		return minimumRunnerLeaseDuration
	}
	if lease > maximumRunnerLeaseDuration {
		return maximumRunnerLeaseDuration
	}
	return lease
}
