package roles

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"strings"

	"gorm.io/gorm"

	rolesstore "github.com/tidbcloud/AgentTeam/internal/store/resource/roles"
)

// MutationOperation identifies one retryable AgentTeam-to-MeshX Role mutation.
// OperationKey is stable across transport retries; RequestDigest identifies the
// canonical action-specific payload submitted under that key.
type MutationOperation struct {
	OperationKey  string
	RequestDigest string
}

// RoleOperationRecovery is the immutable, receipt-only result of a completed
// operation. A miss authorizes no mutation; callers must continue through the
// normal authority path before executing.
type RoleOperationRecovery struct {
	Found         bool
	RoleID        string
	RoleVersionID string
}

// RoleUpdatePatch is the V1 sparse Role update surface. Nil strings are
// omitted; a non-nil empty Description explicitly clears it.
type RoleUpdatePatch struct {
	Name        *string
	Description *string
	Definition  *string
	Visibility  *string
}

// DeleteRoleResult is the stable replay result for a soft delete.
type DeleteRoleResult struct {
	RoleID        string
	RoleVersionID string
}

// RoleVersionID returns the immutable authority version that was materialized.
// The value is populated from the successful realization or its durable
// operation receipt, never from a transport request projection.
func (result RealizationResult) RoleVersionID() string {
	return result.roleVersionID
}

type roleMutationReceipt struct {
	RoleID        string `json:"roleId"`
	RoleVersionID string `json:"roleVersionId"`
}

type roleApplyReceipt struct {
	RealizationID string `json:"realizationId"`
	RoleID        string `json:"roleId"`
	RoleVersionID string `json:"roleVersionId"`
	AgentID       string `json:"agentId"`
}

var (
	// ErrInvalidRoleMutationOperation reports a missing operation identity or
	// request digest.
	ErrInvalidRoleMutationOperation = errors.New("role mutation operation is invalid")
	// ErrRoleMutationConflict reports operation-key reuse with another digest.
	ErrRoleMutationConflict = errors.New("role mutation operation conflicts with an existing request")
	// ErrRoleVersionConflict reports an expected Role version that is no longer
	// active at the mutation admission point.
	ErrRoleVersionConflict = errors.New("role active version conflict")
)

const (
	roleOperationCreate = "create"
	roleOperationUpdate = "update"
	roleOperationDelete = "delete"
	roleOperationApply  = "apply"
)

var errRoleOperationReceiptReserved = errors.New("role operation receipt is already reserved")

// CreateRoleOperation executes or replays one idempotent Role create.
func (s *Service) CreateRoleOperation(ctx context.Context, workspaceID, subject string, operation MutationOperation, input Mutation) (Role, error) {
	if err := s.validateRoleOperation(workspaceID, subject, operation); err != nil {
		return Role{}, err
	}
	result, operationErr := runRoleOperation(ctx, s, workspaceID, subject, roleOperationCreate, operation, replayRoleMutation, roleReceipt, func(tx *rolesstore.Store, operationService *Service) (Role, error) {
		var manifestItems []rolesstore.RolePackageManifestItem
		var err error
		input, manifestItems, err = admitRoleCreate(input)
		if err != nil {
			return Role{}, err
		}
		name := strings.TrimSpace(input.Name)
		now := s.now()
		id := newRoleID()
		slug, err := operationService.availableSlug(ctx, workspaceID, name, id)
		if err != nil {
			return Role{}, err
		}
		row, err := storeRoleFromMutation(id, strings.TrimSpace(workspaceID), strings.TrimSpace(subject), slug, input, now)
		if err != nil {
			return Role{}, err
		}
		contentOperationKey := rolePackageStableOperationKey("operation", roleOperationCreate, operation.OperationKey, operation.RequestDigest)
		manifestItems, err = operationService.persistRolePackageContent(ctx, row.WorkspaceID, row.ID, subject, contentOperationKey, "", false, manifestItems)
		if err != nil {
			return Role{}, err
		}
		row.IsActive = true
		if input.IsActive != nil {
			row.IsActive = *input.IsActive
		}
		created, err := tx.CreateRole(ctx, row)
		if err != nil {
			return Role{}, mapStoreRoleConflict(err)
		}
		versionRole := created
		versionRole.Definition = input.Definition
		published, err := tx.PublishRoleVersion(ctx, versionRole, "", 0, "", manifestItems, now)
		if err != nil {
			return Role{}, err
		}
		created.ActiveVersionID = published.RoleVersion.ID
		_, err = tx.UpsertRoleGrant(ctx, rolesstore.RoleGrant{
			ID: newRoleGrantID(), WorkspaceID: row.WorkspaceID, RoleID: row.ID,
			SubjectType: "user", SubjectID: row.CreatedBy, Role: roleGrantOwner,
			CreatedBy: row.CreatedBy, UpdatedBy: row.CreatedBy, CreatedAt: now, UpdatedAt: now,
		})
		if err != nil {
			return Role{}, err
		}
		contentByRef, err := operationService.resolveManifestItemContents(ctx, subject, published.ManifestItems)
		if err != nil {
			return Role{}, err
		}
		result := roleFromStore(created, published.ManifestItems, contentByRef, SourceMyRole)
		result.Role = roleGrantOwner
		result.CanManage = true
		result.Editable = true
		return result, nil
	})
	return result, operationErr
}

// UpdateRoleOperation executes or replays one sparse, active-version-fenced
// Role update. Content and visibility are one atomic mutation.
func (s *Service) UpdateRoleOperation(ctx context.Context, workspaceID, subject, roleID string, operation MutationOperation, expectedVersionID string, patch RoleUpdatePatch) (Role, error) {
	if err := s.validateRoleOperation(workspaceID, subject, operation); err != nil {
		return Role{}, err
	}
	if replayed, found, err := readRoleOperationReceipt(ctx, s, workspaceID, subject, roleOperationUpdate, operation, replayRoleMutation); err != nil || found {
		return replayed, err
	}
	prepared, err := s.admitRoleUpdate(ctx, workspaceID, subject, roleID, expectedVersionID, patch)
	if err != nil {
		return Role{}, err
	}
	var preparedManifest []rolesstore.RolePackageManifestItem
	if prepared.contentChanged {
		contentOperationKey := rolePackageStableOperationKey("operation", roleOperationUpdate, operation.OperationKey, operation.RequestDigest)
		preparedManifest, err = s.persistRolePackageContent(ctx, prepared.current.WorkspaceID, prepared.current.ID, subject, contentOperationKey, prepared.current.contentVersionID, false, prepared.contentItems)
		if err != nil {
			return Role{}, err
		}
	}
	result, operationErr := runRoleOperation(ctx, s, workspaceID, subject, roleOperationUpdate, operation, replayRoleMutation, roleReceipt, func(tx *rolesstore.Store, operationService *Service) (Role, error) {
		admitted, err := operationService.admitRoleUpdate(ctx, workspaceID, subject, roleID, expectedVersionID, patch)
		if err != nil {
			return Role{}, err
		}
		current, input := admitted.current, admitted.input

		now := s.now()
		row, err := storeRoleFromMutation(current.ID, current.WorkspaceID, strings.TrimSpace(subject), current.Slug, input, now)
		if err != nil {
			return Role{}, err
		}
		row.SourcePresetID = current.SourcePresetID
		row.SourcePresetVersion = current.SourcePresetVersion
		row.SourceAgentID = current.SourceAgentID
		row.Visibility = admitted.visibility
		row.IsActive = current.IsActive

		var manifestItems []rolesstore.RolePackageManifestItem
		if admitted.contentChanged {
			row.Definition = roleDefinitionSummary(input.Definition)
			manifestItems = append([]rolesstore.RolePackageManifestItem(nil), preparedManifest...)
		} else {
			items, err := tx.ListManifestItemsByRoleVersion(ctx, current.WorkspaceID, current.ID, current.RoleVersionID)
			if err != nil {
				return Role{}, err
			}
			manifestItems = cloneRoleManifestItemsForOperation(items)
		}

		// Recheck the active-version fence in the receipt/product transaction.
		// Content publication, when required, was performed before opening this
		// transaction and is recovered by its exact durable operation receipt.
		updated, err := tx.UpdateRoleIfActiveVersion(ctx, row, expectedVersionID)
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return Role{}, ErrRoleVersionConflict
		}
		if err != nil {
			return Role{}, err
		}
		versionRole := updated
		versionRole.Definition = input.Definition
		published, err := tx.PublishRoleVersion(
			ctx, versionRole, row.SourcePresetID, row.SourcePresetVersion, row.SourceAgentID, manifestItems, now,
		)
		if err != nil {
			return Role{}, err
		}
		updated.ActiveVersionID = published.RoleVersion.ID
		_, sourceItems, err := canonicalizePortableRole(input.Definition, input.RuntimeHints, input.Attachments)
		if err != nil {
			return Role{}, err
		}
		contentByRef := rolePackageContentByPersistedItems(published.ManifestItems, sourceItems)
		result := roleFromStore(updated, published.ManifestItems, contentByRef, SourceMyRole)
		result.Role = current.Role
		result.CanManage = true
		result.Editable = true
		return result, nil
	})
	return result, operationErr
}

func rolePackageContentByPersistedItems(persisted, source []rolesstore.RolePackageManifestItem) map[string]string {
	contentByRef := make(map[string]string, len(persisted))
	for index, item := range persisted {
		if index >= len(source) {
			break
		}
		ref := strings.TrimSpace(item.ContentRef)
		content := strings.TrimSpace(source[index].Content)
		if ref != "" && content != "" {
			contentByRef[ref] = content
		}
	}
	return contentByRef
}

// DeleteRoleOperation executes or replays one active-version-fenced soft delete.
func (s *Service) DeleteRoleOperation(ctx context.Context, workspaceID, subject, roleID string, operation MutationOperation, expectedVersionID string) (DeleteRoleResult, error) {
	if err := s.validateRoleOperation(workspaceID, subject, operation); err != nil {
		return DeleteRoleResult{}, err
	}
	return runRoleOperation(ctx, s, workspaceID, subject, roleOperationDelete, operation, replayDeleteRoleMutation, deleteRoleReceipt, func(tx *rolesstore.Store, operationService *Service) (DeleteRoleResult, error) {
		current, err := operationService.admitRoleDelete(ctx, workspaceID, subject, roleID, expectedVersionID)
		if err != nil {
			return DeleteRoleResult{}, err
		}
		if err := tx.DeleteRoleIfActiveVersion(ctx, current.WorkspaceID, current.ID, expectedVersionID, s.now()); errors.Is(err, gorm.ErrRecordNotFound) {
			return DeleteRoleResult{}, ErrRoleVersionConflict
		} else if err != nil {
			return DeleteRoleResult{}, err
		}
		return DeleteRoleResult{RoleID: current.ID, RoleVersionID: current.RoleVersionID}, nil
	})
}

// ApplyRoleOperation executes or replays one apply of the exact active,
// immutable Role version.
func (s *Service) ApplyRoleOperation(ctx context.Context, workspaceID, subject, roleID string, operation MutationOperation, input RealizationRequest) (RealizationResult, error) {
	if err := s.validateRoleOperation(workspaceID, subject, operation); err != nil {
		return RealizationResult{}, err
	}
	return runRoleOperation(ctx, s, workspaceID, subject, roleOperationApply, operation, replayApplyRoleMutation, applyRoleReceipt, func(_ *rolesstore.Store, operationService *Service) (RealizationResult, error) {
		if _, _, err := operationService.admitRoleApply(ctx, workspaceID, subject, roleID, input); err != nil {
			return RealizationResult{}, err
		}
		return operationService.RealizeRoleForAgent(ctx, workspaceID, subject, roleID, input)
	})
}

func (s *Service) validateRoleOperation(workspaceID, subject string, operation MutationOperation) error {
	if err := s.validate(workspaceID, subject); err != nil {
		return err
	}
	if strings.TrimSpace(operation.OperationKey) == "" || strings.TrimSpace(operation.RequestDigest) == "" {
		return ErrInvalidRoleMutationOperation
	}
	return nil
}

// RecoverRoleOperation reads only the durable operation receipt. It never
// consults mutable Role, grant, ownership, or realization state.
func (s *Service) RecoverRoleOperation(
	ctx context.Context,
	workspaceID, action string,
	operation MutationOperation,
) (RoleOperationRecovery, error) {
	workspaceID = strings.TrimSpace(workspaceID)
	action = strings.TrimSpace(action)
	operation.OperationKey = strings.TrimSpace(operation.OperationKey)
	operation.RequestDigest = strings.TrimSpace(operation.RequestDigest)
	if workspaceID == "" || operation.OperationKey == "" || operation.RequestDigest == "" ||
		(action != roleOperationCreate && action != roleOperationUpdate &&
			action != roleOperationDelete && action != roleOperationApply) {
		return RoleOperationRecovery{}, ErrInvalidRoleMutationOperation
	}
	receipt, err := s.store.GetRoleOperationReceipt(ctx, workspaceID, operation.OperationKey)
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return RoleOperationRecovery{}, nil
	}
	if err != nil {
		return RoleOperationRecovery{}, err
	}
	if receipt.Action != action || receipt.RequestDigest != operation.RequestDigest {
		return RoleOperationRecovery{}, ErrRoleMutationConflict
	}
	var roleID, roleVersionID string
	switch action {
	case roleOperationApply:
		var value roleApplyReceipt
		if err := decodeRoleOperationReceipt(receipt.ResultJSON, &value); err != nil {
			return RoleOperationRecovery{}, err
		}
		if !validRoleOperationReceiptID(value.RealizationID, "rrl") ||
			!validRoleOperationReceiptID(value.AgentID, "agt") {
			return RoleOperationRecovery{}, fmt.Errorf(
				"role operation receipt contains incomplete stable ids",
			)
		}
		roleID, roleVersionID = value.RoleID, value.RoleVersionID
	default:
		var value roleMutationReceipt
		if err := decodeRoleOperationReceipt(receipt.ResultJSON, &value); err != nil {
			return RoleOperationRecovery{}, err
		}
		roleID, roleVersionID = value.RoleID, value.RoleVersionID
	}
	if !validRoleOperationReceiptID(roleID, "role") ||
		!validRoleOperationReceiptID(roleVersionID, "rlv") {
		return RoleOperationRecovery{}, fmt.Errorf("role operation receipt contains incomplete stable ids")
	}
	return RoleOperationRecovery{
		Found: true, RoleID: roleID, RoleVersionID: roleVersionID,
	}, nil
}

func validRoleOperationReceiptID(value, prefix string) bool {
	suffix, found := strings.CutPrefix(value, prefix+"_")
	if !found || len(suffix) == 0 || len(suffix) > 64 {
		return false
	}
	for _, character := range suffix {
		if (character < 'a' || character > 'z') &&
			(character < '0' || character > '9') {
			return false
		}
	}
	return true
}

func (s *Service) withRoleStore(store *rolesstore.Store) *Service {
	cloned := *s
	cloned.store = store
	return &cloned
}

func runRoleOperation[T any](
	ctx context.Context,
	service *Service,
	workspaceID, subject, action string,
	operation MutationOperation,
	replay func(context.Context, *Service, string, string, string) (T, error),
	receiptFor func(T) (any, error),
	execute func(*rolesstore.Store, *Service) (T, error),
) (T, error) {
	replayed, found, err := readRoleOperationReceipt(ctx, service, workspaceID, subject, action, operation, replay)
	if err != nil || found {
		return replayed, err
	}
	var result T
	err = service.store.Transaction(ctx, func(tx *rolesstore.Store) error {
		reserved, err := tx.ReserveRoleOperationReceipt(ctx, rolesstore.RoleOperationReceipt{
			WorkspaceID: strings.TrimSpace(workspaceID), OperationKey: strings.TrimSpace(operation.OperationKey),
			Action: action, RequestDigest: strings.TrimSpace(operation.RequestDigest), ResultJSON: "", CreatedAt: service.now(),
		})
		if err != nil {
			return err
		}
		if !reserved {
			return errRoleOperationReceiptReserved
		}
		result, err = execute(tx, service.withRoleStore(tx))
		if err != nil {
			return err
		}
		receipt, err := receiptFor(result)
		if err != nil {
			return err
		}
		return completeRoleOperationReceipt(ctx, tx, workspaceID, action, operation, receipt)
	})
	if errors.Is(err, errRoleOperationReceiptReserved) {
		replayed, found, readErr := readRoleOperationReceipt(ctx, service, workspaceID, subject, action, operation, replay)
		if readErr != nil {
			return replayed, readErr
		}
		if !found {
			return replayed, fmt.Errorf("role operation receipt disappeared after reservation conflict")
		}
		return replayed, nil
	}
	return result, err
}

func readRoleOperationReceipt[T any](
	ctx context.Context,
	service *Service,
	workspaceID, subject, action string,
	operation MutationOperation,
	replay func(context.Context, *Service, string, string, string) (T, error),
) (T, bool, error) {
	var result T
	receipt, err := service.store.GetRoleOperationReceipt(ctx, workspaceID, operation.OperationKey)
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return result, false, nil
	}
	if err != nil {
		return result, false, err
	}
	if receipt.Action != action || receipt.RequestDigest != strings.TrimSpace(operation.RequestDigest) {
		return result, false, ErrRoleMutationConflict
	}
	result, err = replay(ctx, service, workspaceID, subject, receipt.ResultJSON)
	if err != nil {
		return result, false, err
	}
	return result, true, nil
}

func completeRoleOperationReceipt(ctx context.Context, store *rolesstore.Store, workspaceID, action string, operation MutationOperation, receipt any) error {
	data, err := json.Marshal(receipt)
	if err != nil {
		return fmt.Errorf("encode role operation receipt: %w", err)
	}
	return store.CompleteRoleOperationReceipt(ctx, rolesstore.RoleOperationReceipt{
		WorkspaceID: strings.TrimSpace(workspaceID), OperationKey: strings.TrimSpace(operation.OperationKey),
		Action: action, RequestDigest: strings.TrimSpace(operation.RequestDigest), ResultJSON: string(data),
	})
}

func roleReceipt(role Role) (any, error) {
	return roleMutationReceipt{RoleID: role.ID, RoleVersionID: role.RoleVersionID}, nil
}

func deleteRoleReceipt(result DeleteRoleResult) (any, error) {
	return roleMutationReceipt{RoleID: result.RoleID, RoleVersionID: result.RoleVersionID}, nil
}

func applyRoleReceipt(result RealizationResult) (any, error) {
	if strings.TrimSpace(result.realizationID) == "" {
		return nil, fmt.Errorf("role apply result is missing realization id")
	}
	return roleApplyReceipt{
		RealizationID: result.realizationID,
		RoleID:        result.RoleID,
		RoleVersionID: result.roleVersionID,
		AgentID:       result.AgentID,
	}, nil
}

func replayRoleMutation(_ context.Context, _ *Service, _, _ string, resultJSON string) (Role, error) {
	var receipt roleMutationReceipt
	if err := decodeRoleOperationReceipt(resultJSON, &receipt); err != nil {
		return Role{}, err
	}
	return Role{ID: receipt.RoleID, RoleVersionID: receipt.RoleVersionID}, nil
}

func replayDeleteRoleMutation(_ context.Context, _ *Service, _, _ string, resultJSON string) (DeleteRoleResult, error) {
	var receipt roleMutationReceipt
	if err := decodeRoleOperationReceipt(resultJSON, &receipt); err != nil {
		return DeleteRoleResult{}, err
	}
	return DeleteRoleResult{RoleID: receipt.RoleID, RoleVersionID: receipt.RoleVersionID}, nil
}

func replayApplyRoleMutation(_ context.Context, _ *Service, _, _ string, resultJSON string) (RealizationResult, error) {
	var receipt roleApplyReceipt
	if err := decodeRoleOperationReceipt(resultJSON, &receipt); err != nil {
		return RealizationResult{}, err
	}
	return RealizationResult{
		RoleID: receipt.RoleID, AgentID: receipt.AgentID, Apply: true,
		realizationID: receipt.RealizationID, roleVersionID: receipt.RoleVersionID,
	}, nil
}

func decodeRoleOperationReceipt(resultJSON string, target any) error {
	if err := rejectDuplicateRoleOperationReceiptFields(resultJSON); err != nil {
		return fmt.Errorf("decode role operation receipt: %w", err)
	}
	decoder := json.NewDecoder(strings.NewReader(resultJSON))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(target); err != nil {
		return fmt.Errorf("decode role operation receipt: %w", err)
	}
	var trailing any
	if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
		if err == nil {
			return fmt.Errorf("decode role operation receipt: multiple JSON values")
		}
		return fmt.Errorf("decode role operation receipt: %w", err)
	}
	return nil
}

func rejectDuplicateRoleOperationReceiptFields(resultJSON string) error {
	decoder := json.NewDecoder(strings.NewReader(resultJSON))
	token, err := decoder.Token()
	if err != nil {
		return err
	}
	if token != json.Delim('{') {
		return errors.New("role operation receipt must be an object")
	}
	var keys []string
	for decoder.More() {
		token, err := decoder.Token()
		if err != nil {
			return err
		}
		key, ok := token.(string)
		if !ok {
			return errors.New("role operation receipt field is not a string")
		}
		for _, existing := range keys {
			if strings.EqualFold(existing, key) {
				return fmt.Errorf("duplicate receipt field %q", key)
			}
		}
		keys = append(keys, key)
		var value json.RawMessage
		if err := decoder.Decode(&value); err != nil {
			return err
		}
	}
	if _, err := decoder.Token(); err != nil {
		return err
	}
	var trailing any
	if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
		if err == nil {
			return errors.New("multiple JSON values")
		}
		return err
	}
	return nil
}

func cloneRoleManifestItemsForOperation(items []rolesstore.RolePackageManifestItem) []rolesstore.RolePackageManifestItem {
	versionID := newRoleVersionID()
	cloned := make([]rolesstore.RolePackageManifestItem, len(items))
	for i, item := range items {
		item.ID = newRoleManifestItemID()
		item.RoleVersionID = versionID
		cloned[i] = item
	}
	return cloned
}
