package app

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"strings"
	"time"

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

// ListRuntimeInternalDispatchTargets exposes the durable in-process App routes
// through the app store so completion can also converge action state.
func (store *Store) ListRuntimeInternalDispatchTargets(ctx context.Context, params serviceactor.RuntimeInternalDispatchTargetList) ([]serviceactor.RuntimeInternalDispatchTarget, error) {
	return store.serviceActorStore().ListRuntimeInternalDispatchTargets(ctx, params)
}

func (store *Store) ClaimDeliveryEnvelopes(ctx context.Context, params serviceactor.DeliveryDispatchClaim) ([]serviceactor.DeliveryEnvelope, error) {
	return store.serviceActorStore().ClaimDeliveryEnvelopes(ctx, params)
}

// MarkDeliveryAttemptDelivered keeps callback transport state and the durable
// Human action projection in one transaction.
func (store *Store) MarkDeliveryAttemptDelivered(ctx context.Context, params serviceactor.DeliveryAttemptDeliveredMark) (serviceactor.DeliveryAttempt, error) {
	var result serviceactor.DeliveryAttempt
	err := coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		snapshot, err := loadInternalAppNotificationActionDeliverySnapshot(ctx, tx, params.WorkspaceID, params.AttemptID)
		if err != nil {
			return err
		}
		result, err = store.serviceActorStore().InTx(tx).MarkDeliveryAttemptDelivered(ctx, params)
		if err != nil {
			return err
		}
		if snapshot.SubscriptionKind == serviceactor.SubscriptionKindAppNotificationAction && snapshot.ExternalResourceType == appNotificationExternalResource {
			if _, err := store.MarkAppNotificationActionDeliveredInTx(ctx, tx, snapshot.WorkspaceID, snapshot.EventID, completionTime(params.Now)); err != nil {
				return err
			}
		}
		return nil
	})
	if err != nil {
		return serviceactor.DeliveryAttempt{}, err
	}
	return result, nil
}

func (store *Store) LoadRuntimeCallbackConfig(ctx context.Context, workspaceID string, serviceInstallationID string) (serviceactor.RuntimeCallbackConfig, error) {
	return store.serviceActorStore().LoadRuntimeCallbackConfig(ctx, workspaceID, serviceInstallationID)
}

func (store *Store) LoadDeliveryAttempt(ctx context.Context, workspaceID string, attemptID string) (serviceactor.DeliveryAttempt, error) {
	return store.serviceActorStore().LoadDeliveryAttempt(ctx, workspaceID, attemptID)
}

func (store *Store) ScheduleDeliveryAttemptRetry(ctx context.Context, params serviceactor.DeliveryAttemptRetrySchedule) (serviceactor.DeliveryAttemptRetryResult, error) {
	var result serviceactor.DeliveryAttemptRetryResult
	err := coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		snapshot, err := loadInternalAppNotificationActionDeliverySnapshot(ctx, tx, params.WorkspaceID, params.AttemptID)
		if err != nil {
			return err
		}
		result, err = store.serviceActorStore().InTx(tx).ScheduleDeliveryAttemptRetry(ctx, params)
		if err != nil {
			return err
		}
		if snapshot.SubscriptionKind == serviceactor.SubscriptionKindAppNotificationAction && snapshot.ExternalResourceType == appNotificationExternalResource {
			if _, err := store.MarkAppNotificationActionDeliveredInTx(ctx, tx, snapshot.WorkspaceID, snapshot.EventID, completionTime(params.Now)); err != nil {
				return err
			}
		}
		return nil
	})
	if err != nil {
		return serviceactor.DeliveryAttemptRetryResult{}, err
	}
	return result, nil
}

// CompleteInternalHandlerDelivery atomically completes the service-event
// ledger and its App notification action invocation.
func (store *Store) CompleteInternalHandlerDelivery(ctx context.Context, params serviceactor.InternalHandlerDeliveryCompletion) (serviceactor.InternalHandlerDeliveryCompletionResult, error) {
	if store == nil || store.db == nil {
		return serviceactor.InternalHandlerDeliveryCompletionResult{}, fmt.Errorf("App notification internal dispatcher store is not configured")
	}
	var result serviceactor.InternalHandlerDeliveryCompletionResult
	err := coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		snapshot, err := loadInternalAppNotificationActionDeliverySnapshot(ctx, tx, params.WorkspaceID, params.AttemptID)
		if err != nil {
			return err
		}
		if snapshot.AttemptStatus != serviceactor.DeliveryAttemptStatusAcked || snapshot.DeliveryStatus != string(runeffect.StatusSucceeded) {
			if err := store.validateInternalAppNotificationActionEffectInTx(ctx, tx, snapshot, params.Effect); err != nil {
				return err
			}
		}
		result, err = store.serviceActorStore().InTx(tx).CompleteInternalHandlerDelivery(ctx, params)
		if err != nil {
			return err
		}
		if result.Delivery.ID != snapshot.DeliveryID || result.Delivery.ExternalResourceKey != snapshot.NotificationID {
			return conflict("internal handler delivery changed concurrently")
		}
		outcome, err := decodeInternalHandlerOutcome(params.ResultJSON)
		if err != nil {
			return err
		}
		if err := store.completeAppNotificationActionForDeliveryInTx(ctx, tx, result.Delivery, AppNotificationActionStateAcked, "", outcome, completionTime(params.Now)); err != nil {
			return err
		}
		return store.retireAppNotificationActionSubscriptionAfterDeliveryInTx(ctx, tx, result.Delivery, completionTime(params.Now))
	})
	if err != nil {
		return serviceactor.InternalHandlerDeliveryCompletionResult{}, err
	}
	return result, nil
}

func decodeInternalHandlerOutcome(payload []byte) (map[string]any, error) {
	if len(payload) == 0 {
		return map[string]any{}, nil
	}
	decoder := json.NewDecoder(bytes.NewReader(payload))
	decoder.UseNumber()
	var outcome map[string]any
	if err := decoder.Decode(&outcome); err != nil || outcome == nil {
		return nil, badRequest("internal handler completion result must be a JSON object")
	}
	var trailing any
	if err := decoder.Decode(&trailing); err != io.EOF {
		return nil, badRequest("internal handler completion result must be one JSON object")
	}
	return outcome, nil
}

func completionTime(value time.Time) time.Time {
	if value.IsZero() {
		return time.Now().UTC()
	}
	return value.UTC()
}

type internalAppNotificationActionDeliverySnapshot struct {
	DeliveryID            string
	WorkspaceID           string
	ServiceInstallationID string
	ServiceMemberID       string
	SubscriptionKind      string
	ExternalResourceType  string
	NotificationID        string
	EventID               string
	AttemptStatus         string
	DeliveryStatus        string
}

func loadInternalAppNotificationActionDeliverySnapshot(ctx context.Context, queryer coredb.QueryRower, workspaceID, attemptID string) (internalAppNotificationActionDeliverySnapshot, error) {
	var snapshot internalAppNotificationActionDeliverySnapshot
	err := queryer.QueryRow(ctx, `
SELECT delivery.id, delivery.workspace_id, delivery.service_installation_id,
       delivery.service_member_id, delivery.subscription_kind,
       delivery.external_resource_type, delivery.external_resource_key,
       delivery.event_id,
       attempt.status, delivery.status
FROM service_event_delivery_attempts attempt
JOIN service_event_deliveries delivery
  ON delivery.id = attempt.service_event_delivery_id
 AND delivery.workspace_id = attempt.workspace_id
WHERE attempt.workspace_id = $1 AND attempt.id = $2
LIMIT 1
`, strings.TrimSpace(workspaceID), strings.TrimSpace(attemptID)).Scan(
		&snapshot.DeliveryID, &snapshot.WorkspaceID, &snapshot.ServiceInstallationID,
		&snapshot.ServiceMemberID, &snapshot.SubscriptionKind,
		&snapshot.ExternalResourceType, &snapshot.NotificationID,
		&snapshot.EventID,
		&snapshot.AttemptStatus, &snapshot.DeliveryStatus,
	)
	if err != nil {
		return internalAppNotificationActionDeliverySnapshot{}, fmt.Errorf("load internal App notification action delivery: %w", err)
	}
	return snapshot, nil
}

func (store *Store) validateInternalAppNotificationActionEffectInTx(ctx context.Context, tx coredb.Tx, delivery internalAppNotificationActionDeliverySnapshot, effect runeffect.Effect) error {
	if delivery.SubscriptionKind != serviceactor.SubscriptionKindAppNotificationAction || delivery.ExternalResourceType != appNotificationExternalResource || strings.TrimSpace(delivery.NotificationID) == "" {
		return forbidden("internal handler delivery is not a App notification action")
	}
	notification, err := store.LoadAppNotificationInTx(ctx, tx, delivery.WorkspaceID, delivery.NotificationID, true)
	if err != nil {
		return err
	}
	if notification.ServiceInstallationID != delivery.ServiceInstallationID || notification.ServiceMemberID != delivery.ServiceMemberID || notification.MessageID != strings.TrimSpace(effect.MessageID) || strings.TrimSpace(effect.Ref) != notification.ID {
		return forbidden("internal handler effect does not match the App notification")
	}
	switch effect.Kind {
	case runeffect.EffectKindMessageUpdate:
		if notification.Status != AppNotificationStatusActive && notification.Status != AppNotificationStatusResolved {
			return conflict("internal handler message update does not match notification state")
		}
	case runeffect.EffectKindNotificationResolved:
		if notification.Status != AppNotificationStatusResolved {
			return conflict("internal handler resolve effect requires a resolved notification")
		}
	case runeffect.EffectKindMessageWithdrawal:
		if notification.Status != AppNotificationStatusWithdrawn {
			return conflict("internal handler withdrawal effect requires a withdrawn notification")
		}
	default:
		return badRequest("internal App notification action completion requires a notification mutation effect")
	}
	return nil
}
