package permissions

import (
	"encoding/json"
	"fmt"
	"sort"
	"strings"
	"time"

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

const (
	assignmentPolicyInstallPlanPhase      cp.InstallPlanPhase = "security"
	assignmentPolicyTargetCollisionDomain                     = "permission_set_assignment_policy/target/utf8mb4_unicode_ci-v1"
)

type assignmentPolicyDeclarationProjection struct {
	Present       bool     `json:"present"`
	PolicyKey     string   `json:"policyKey"`
	OwnerPack     string   `json:"ownerPack"`
	PermissionSet string   `json:"permissionSet"`
	ProfilesAnyOf []string `json:"profiles_any_of"`
}

// NewPermissionSetAssignmentPolicyManagedLifecycleRegistrationWithEligibility
// binds the product-owned opaque selector projection to the L0 lifecycle. A
// nil provider leaves the dark registration fail-closed at Plan time.
func NewPermissionSetAssignmentPolicyManagedLifecycleRegistrationWithEligibility(
	eligibility AssignmentPolicyEligibilityProjectionProvider,
) (
	cp.ComponentKindRegistration,
	cp.ComponentManagedLifecycleRegistration,
	error,
) {
	base, err := newPermissionSetAssignmentPolicyComponentRegistration()
	if err != nil {
		return cp.ComponentKindRegistration{}, cp.ComponentManagedLifecycleRegistration{}, err
	}
	readSet, err := assignmentPolicyReadSet(eligibility)
	if err != nil {
		return cp.ComponentKindRegistration{}, cp.ComponentManagedLifecycleRegistration{}, err
	}
	lifecycle, err := cp.NewComponentManagedLifecycleRegistration(cp.ComponentManagedLifecycleRegistrationSpec{
		Kind:         PermissionSetAssignmentPolicyComponentKind,
		ReadSet:      readSet,
		Capabilities: assignmentPolicyCapabilities(),
		EffectClasses: []cp.ComponentManagedEffectClass{
			PermissionSetDefinitionPermissionStateEffectClass,
		},
		TransactionMode: cp.ComponentManagedTransactionPessimisticReadCommitted,
		Budget:          assignmentPolicyManagedBudget(),
		Lifecycle:       permissionSetAssignmentPolicyLifecycleProvider(eligibility),
	})
	if err != nil {
		return cp.ComponentKindRegistration{}, cp.ComponentManagedLifecycleRegistration{},
			fmt.Errorf("assignment policy lifecycle registration: %w", err)
	}
	return base, lifecycle, nil
}

func assignmentPolicyManagedBudget() cp.ComponentManagedBudget {
	return cp.ComponentManagedBudget{
		MaxTargets:            2 + 3*assignmentPolicyMaximumRows,
		MaxCurrentItems:       50000,
		MaxDesiredItems:       50000,
		MaxDeltaRows:          50000,
		MaxMutationBytes:      32 << 20,
		MaxMutationStatements: 50000,
		MaxBatchRows:          assignmentPolicyMaximumRows,
		MaxEvidenceWrites:     1,
		MaxEffectCalls:        1,
		MaxLockWait:           5 * time.Second,
		MaxElapsed:            30 * time.Second,
		MaxAttempts:           3,
	}
}

func assignmentPolicyReadSet(
	eligibility AssignmentPolicyEligibilityProjectionProvider,
) ([]cp.ComponentReadSurface, error) {
	storeInventory, err := schemacatalog.ManagedAssignmentPolicyStoreInventory()
	if err != nil {
		return nil, fmt.Errorf("assignment policy lifecycle inventory: %w", err)
	}
	definitionInventory, err := schemacatalog.ManagedPermissionSetDefinitionInventory()
	if err != nil {
		return nil, fmt.Errorf("assignment policy Definition inventory: %w", err)
	}
	tables := map[string]string{
		"PermissionSet":           "managed Definition target",
		"PermissionSetAssignment": "exact active direct assignment population",
	}
	if !assignmentPolicyEligibilityProjectionProviderIsNil(eligibility) {
		for _, surface := range eligibility.AssignmentPolicyEligibilityReadSurfaces() {
			if previous, duplicate := tables[surface.Table]; duplicate && previous != surface.Fact {
				return nil, fmt.Errorf(
					"assignment policy eligibility read surface %s conflicts with L0 inventory",
					surface.Table,
				)
			}
			tables[surface.Table] = surface.Fact
		}
	}
	for _, store := range storeInventory.Stores() {
		tables[store.TableName] = "durable assignment policy state"
	}
	for _, relation := range definitionInventory.Relations() {
		if relation.Role == schemacatalog.PermissionSetDefinitionRoleNegativeEdge &&
			relation.TableName != schemacatalog.AssignmentPolicyClaimTable {
			tables[relation.TableName] = "unsupported Definition authorization edge"
		}
	}
	out := make([]cp.ComponentReadSurface, 0, len(tables))
	for table, fact := range tables {
		out = append(out, cp.ComponentReadSurface{Table: table, Fact: fact})
	}
	sort.Slice(out, func(i, j int) bool { return out[i].Table < out[j].Table })
	return out, nil
}

// newPermissionSetAssignmentPolicyComponentRegistration returns the source
// declaration contract. It stays unexported until the complete managed
// lifecycle is available, so an intermediate commit cannot activate a partial
// policy implementation in product composition.
func newPermissionSetAssignmentPolicyComponentRegistration() (cp.ComponentKindRegistration, error) {
	if _, err := schemacatalog.ManagedAssignmentPolicyStoreInventory(); err != nil {
		return cp.ComponentKindRegistration{}, fmt.Errorf("assignment policy lifecycle inventory: %w", err)
	}
	manageabilities := map[cp.Manageability]struct{}{cp.ManageabilityLocked: {}}
	return cp.ComponentKindRegistration{
		Contract: cp.ComponentKindContract{
			Kind:  PermissionSetAssignmentPolicyComponentKind,
			Phase: assignmentPolicyInstallPlanPhase,
			WriteSet: []cp.ComponentWriteSurface{
				{Table: schemacatalog.AssignmentPolicyClaimTable, Fact: "assignment policy authority claim"},
				{Table: schemacatalog.AssignmentPolicyReleaseReceiptTable, Fact: "clear-before-release receipt"},
				{Table: schemacatalog.AssignmentPolicySelectorTable, Fact: "normalized Profile selector"},
				{Table: schemacatalog.AssignmentPolicyTargetStateTable, Fact: "durable target generation and retry state"},
				{Table: schemacatalog.AssignmentPolicyTombstoneReceiptTable, Fact: "exact policy soft-delete provenance"},
				{Table: "PermissionSetAssignment", Fact: "exact active direct assignment population"},
			},
			BusinessKey:        "Policy key",
			OwnerDiscriminator: cp.ComponentOwnerStrategyOwnerPack,
			Manageabilities:    manageabilities,
			VerifyProjection:   PermissionSetAssignmentPolicySchema,
			CollisionKey:       assignmentPolicyTargetCollisionDomain,
		},
		Capability: cp.SourceBackedComponentCapability{
			Kind:            PermissionSetAssignmentPolicyComponentKind,
			Installable:     true,
			Manageabilities: manageabilities,
		},
		AllowedCategories: []cp.PackCategory{cp.AppPack},
		BuildHandler:      buildPermissionSetAssignmentPolicy,
	}, nil
}

func buildPermissionSetAssignmentPolicy(ctx cp.ComponentBuildContext, request cp.ComponentBuildRequest) error {
	if request.Component.Kind != PermissionSetAssignmentPolicyComponentKind {
		return fmt.Errorf("assignment policy payload: component kind %q is unsupported", request.Component.Kind)
	}
	if request.Component.Manageability != cp.ManageabilityLocked {
		return fmt.Errorf("assignment policy payload: manageability must be %q", cp.ManageabilityLocked)
	}
	if ctx == nil || request.PackName != ctx.PackName() {
		return fmt.Errorf("assignment policy payload: pack name does not match build context")
	}
	source, err := NewPermissionSetAssignmentPolicySource(request.PackName, request.Component.BusinessKey)
	if err != nil {
		return err
	}
	policy, err := DecodePermissionSetAssignmentPolicy(request.Payload)
	if err != nil {
		return err
	}
	projection, err := json.Marshal(assignmentPolicyDeclarationProjection{
		Present:       true,
		PolicyKey:     source.PolicyKey(),
		OwnerPack:     source.OwnerPack(),
		PermissionSet: policy.PermissionSet,
		ProfilesAnyOf: append([]string(nil), policy.ProfilesAnyOf...),
	})
	if err != nil {
		return fmt.Errorf("assignment policy payload: encode canonical projection: %w", err)
	}
	targets, err := source.ManagedTargets(policy)
	if err != nil {
		return err
	}
	definitionTarget := targets[1]
	return ctx.AddManagedComponent(cp.ComponentBuildManagedComponent{
		ExpectedProjection: projection,
		CollisionClaims: []cp.ComponentCollisionClaim{{
			Namespace: assignmentPolicyTargetCollisionDomain,
			Key:       string(definitionTarget.PhysicalKeyBytes),
		}},
	})
}

func assignmentPolicyExpectedRequest(request cp.ComponentManagedRequest) (
	PermissionSetAssignmentPolicy,
	PermissionSetAssignmentPolicySource,
	assignmentPolicyDeclarationProjection,
	error,
) {
	if request.SourceOwner != "" || strings.TrimSpace(request.PackName) != request.PackName {
		return PermissionSetAssignmentPolicy{}, PermissionSetAssignmentPolicySource{}, assignmentPolicyDeclarationProjection{},
			fmt.Errorf("assignment policy request owner is not canonical")
	}
	if request.Component.Kind != PermissionSetAssignmentPolicyComponentKind || request.Component.Manageability != cp.ManageabilityLocked {
		return PermissionSetAssignmentPolicy{}, PermissionSetAssignmentPolicySource{}, assignmentPolicyDeclarationProjection{},
			fmt.Errorf("assignment policy request kind or manageability is invalid")
	}
	source, err := NewPermissionSetAssignmentPolicySource(request.PackName, request.Component.BusinessKey)
	if err != nil {
		return PermissionSetAssignmentPolicy{}, PermissionSetAssignmentPolicySource{}, assignmentPolicyDeclarationProjection{}, err
	}
	policy, err := DecodePermissionSetAssignmentPolicy(request.Payload)
	if err != nil {
		return PermissionSetAssignmentPolicy{}, PermissionSetAssignmentPolicySource{}, assignmentPolicyDeclarationProjection{}, err
	}
	expected := assignmentPolicyDeclarationProjection{
		Present:       true,
		PolicyKey:     source.PolicyKey(),
		OwnerPack:     source.OwnerPack(),
		PermissionSet: policy.PermissionSet,
		ProfilesAnyOf: append([]string(nil), policy.ProfilesAnyOf...),
	}
	canonical, err := json.Marshal(expected)
	if err != nil {
		return PermissionSetAssignmentPolicy{}, PermissionSetAssignmentPolicySource{}, assignmentPolicyDeclarationProjection{}, err
	}
	if !json.Valid(request.ExpectedProjection) || string(canonical) != string(request.ExpectedProjection) {
		return PermissionSetAssignmentPolicy{}, PermissionSetAssignmentPolicySource{}, assignmentPolicyDeclarationProjection{},
			fmt.Errorf("assignment policy expected projection is not canonical")
	}
	return policy, source, expected, nil
}
