package dialectsfcrm

import (
	"context"
	"fmt"
	"strings"

	cp "github.com/pingcap-inc/nexuscrm-contract/pkg/controlplane"
)

type approvalProcessManagedRetirement struct {
	id          string
	businessKey string
}

func reconcileApprovalProcessManagedSet(ctx context.Context, store cp.ComponentMutationStore, request cp.ComponentManagedSetRequest, declaredKeys []string) error {
	if store == nil {
		return fmt.Errorf("approval process reconcile mutation store is required")
	}
	if request.PackName == "" || strings.TrimSpace(request.PackName) != request.PackName {
		return fmt.Errorf("pack name %q is not canonical", request.PackName)
	}
	if request.SourceOwner != "" {
		return fmt.Errorf("pack-owned approval process set must not declare source owner %q", request.SourceOwner)
	}
	if request.Kind != cp.ComponentKindApprovalProcess {
		return fmt.Errorf("component kind = %q, want %q", request.Kind, cp.ComponentKindApprovalProcess)
	}

	declared := make(map[string]struct{}, len(declaredKeys))
	developerObjects := make(map[string]string, len(declaredKeys))
	for _, businessKey := range declaredKeys {
		objectAPIName, developerName, err := splitApprovalProcessComponentBusinessKey(businessKey)
		if err != nil {
			return fmt.Errorf("approval process reconcile declared set: %w", err)
		}
		folded, err := addApprovalProcessManagedSetIdentity("declared", businessKey, objectAPIName, developerName, declared, developerObjects)
		if err != nil {
			return err
		}
		declared[folded] = struct{}{}
	}

	rows, err := store.LockRows(ctx, cp.ComponentRowQuery{
		Table: approvalProcessDefinitionTable,
		Columns: []string{
			"Id", "DeveloperName", "TableEnumOrId", "OwnerPack", "State", "IsActive",
		},
		Equal: cp.ComponentRow{"OwnerPack": request.PackName},
	})
	if err != nil {
		return fmt.Errorf("lock owner-scoped ProcessDefinition rows for %s: %w", request.PackName, err)
	}

	live := make(map[string]struct{}, len(rows))
	liveIDs := make(map[string]struct{}, len(rows))
	retirements := make([]approvalProcessManagedRetirement, 0, len(rows))
	activeState := approvalProcessState(true)
	inactiveState := approvalProcessState(false)
	for _, source := range rows {
		row := cloneApprovalProcessManagedRow(source)
		id, err := approvalProcessManagedProjectionString("ProcessDefinition", row, "Id")
		if err != nil {
			return err
		}
		if _, duplicate := liveIDs[id]; duplicate {
			return fmt.Errorf("owner %s has duplicate ProcessDefinition Id %q", request.PackName, id)
		}
		liveIDs[id] = struct{}{}
		developerName, err := approvalProcessManagedProjectionString("ProcessDefinition", row, "DeveloperName")
		if err != nil {
			return err
		}
		objectAPIName, err := approvalProcessManagedProjectionString("ProcessDefinition", row, "TableEnumOrId")
		if err != nil {
			return err
		}
		ownerPack, err := approvalProcessManagedProjectionString("ProcessDefinition", row, "OwnerPack")
		if err != nil {
			return err
		}
		if ownerPack != request.PackName {
			return fmt.Errorf("ProcessDefinition %s owner %q escaped owner-scoped lock for %q", id, ownerPack, request.PackName)
		}
		if !metadataDeploySafeIdentifier.MatchString(objectAPIName) || !metadataDeploySafeIdentifier.MatchString(developerName) {
			return fmt.Errorf("ProcessDefinition %s has non-canonical business key %q/%q", id, objectAPIName, developerName)
		}
		businessKey := objectAPIName + "/" + developerName
		folded, err := addApprovalProcessManagedSetIdentity("live", businessKey, objectAPIName, developerName, live, developerObjects)
		if err != nil {
			return err
		}
		live[folded] = struct{}{}
		state, err := approvalProcessManagedProjectionString("ProcessDefinition", row, "State")
		if err != nil {
			return err
		}
		if state != activeState && state != inactiveState {
			return fmt.Errorf("ProcessDefinition %s has invalid State value %q", id, state)
		}
		active, ok := row["IsActive"].(bool)
		if !ok {
			return fmt.Errorf("ProcessDefinition %s has invalid IsActive value %T", id, row["IsActive"])
		}
		if _, retained := declared[folded]; retained || state == inactiveState && !active {
			continue
		}
		retirements = append(retirements, approvalProcessManagedRetirement{id: id, businessKey: businessKey})
	}

	for _, retirement := range retirements {
		affected, err := store.UpdateRows(ctx, cp.ComponentUpdate{
			Table:                   approvalProcessDefinitionTable,
			Match:                   cp.ComponentRow{"Id": retirement.id, "OwnerPack": request.PackName},
			Values:                  cp.ComponentRow{"State": inactiveState, "IsActive": false},
			CurrentTimestampColumns: []string{"LastModifiedDate"},
		})
		if err != nil {
			return fmt.Errorf("retire ProcessDefinition %s: %w", retirement.businessKey, err)
		}
		if affected != 1 {
			return fmt.Errorf("retire ProcessDefinition %s affected %d rows; expected exactly one", retirement.businessKey, affected)
		}
	}
	return nil
}

func addApprovalProcessManagedSetIdentity(label, businessKey, objectAPIName, developerName string, seen map[string]struct{}, developerObjects map[string]string) (string, error) {
	foldedObject := strings.ToLower(objectAPIName)
	foldedDeveloper := strings.ToLower(developerName)
	folded := foldedObject + "\x00" + foldedDeveloper
	if _, duplicate := seen[folded]; duplicate {
		return "", fmt.Errorf("approval process %s business key %q is duplicated case-insensitively", label, businessKey)
	}
	if previousObject, exists := developerObjects[foldedDeveloper]; exists && previousObject != foldedObject {
		return "", fmt.Errorf("approval process developer name %q collides across objects in the declared/live set", developerName)
	}
	developerObjects[foldedDeveloper] = foldedObject
	return folded, nil
}
