package permissions

import (
	"context"
	"errors"
	"fmt"
	"sort"
	"strings"

	appErrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/permissionauthoringv1"
)

// DefaultPermissionAuthoringMutationPlanner validates and snapshots all five
// permission families before producing any write. Object/Field lifecycle is
// delegated to Records; the planner does not own a transaction, audit,
// Permission State advance, convergence, or terminal result.
type DefaultPermissionAuthoringMutationPlanner struct {
	permissions *PermissionService
	records     ports.PermissionAuthoringRecordMutationPlanner
}

func NewDefaultPermissionAuthoringMutationPlanner(
	permissions *PermissionService,
	records ports.PermissionAuthoringRecordMutationPlanner,
) (*DefaultPermissionAuthoringMutationPlanner, error) {
	if permissions == nil || permissions.repo == nil || records == nil {
		return nil, errors.New("permission authoring planner requires permission and Records authorities")
	}
	return &DefaultPermissionAuthoringMutationPlanner{permissions: permissions, records: records}, nil
}

type permissionAuthoringOperation func(context.Context) error

type defaultPermissionAuthoringMutationPlan struct {
	operations []permissionAuthoringOperation
	changes    permissionauthoringv1.Changes
}

func (p *defaultPermissionAuthoringMutationPlan) Changed() bool {
	return p != nil && permissionAuthoringChangeCount(p.changes) != 0
}

func (p *defaultPermissionAuthoringMutationPlan) FinalChanges() permissionauthoringv1.Changes {
	if p == nil {
		return permissionauthoringv1.Changes{}
	}
	return clonePermissionAuthoringChanges(p.changes)
}

func (p *defaultPermissionAuthoringMutationPlan) Apply(ctx context.Context) error {
	if p == nil {
		return errors.New("permission authoring mutation plan is unavailable")
	}
	for _, operation := range p.operations {
		if err := operation(ctx); err != nil {
			return err
		}
	}
	return nil
}

func (p *DefaultPermissionAuthoringMutationPlanner) PlanPermissionAuthoringMutation(
	ctx context.Context,
	aggregate ports.LockedPermissionAuthoringAggregate,
	changes permissionauthoringv1.Changes,
	actor *models.UserSession,
) (PermissionAuthoringMutationPlan, error) {
	if p == nil || p.permissions == nil || p.permissions.repo == nil || p.records == nil {
		return nil, errors.New("permission authoring planner is unavailable")
	}
	permissionSetID := strings.TrimSpace(aggregate.CanonicalPermissionSetID)
	if permissionSetID == "" || aggregate.Revision == 0 ||
		(aggregate.Role != ports.PermissionAuthoringProfileBaseline && aggregate.Role != ports.PermissionAuthoringAdditivePermissionSet) {
		return nil, errors.New("permission authoring planner requires a locked canonical aggregate")
	}
	plan := &defaultPermissionAuthoringMutationPlan{}
	if err := p.planSystem(ctx, permissionSetID, changes.System, plan); err != nil {
		return nil, err
	}
	recordPlan, err := p.records.PlanPermissionAuthoringRecordMutation(
		ctx, permissionSetID, changes.Objects, changes.Fields, actor,
	)
	if err != nil {
		if validation, ok := appErrors.AsClientValidationError(err); ok {
			return nil, &PermissionAuthoringRejection{
				Code:  permissionauthoringv1.RejectionInvalidChange,
				Cause: validation,
			}
		}
		return nil, err
	}
	if recordPlan.Changed() {
		plan.operations = append(plan.operations, recordPlan.Apply)
		recordChanges := recordPlan.FinalChanges()
		plan.changes.Objects = append(plan.changes.Objects, recordChanges.Objects...)
		plan.changes.Fields = append(plan.changes.Fields, recordChanges.Fields...)
	}
	if err := p.planRecordTypes(ctx, aggregate, changes.RecordTypes, plan); err != nil {
		return nil, err
	}
	if err := p.planCustom(ctx, permissionSetID, changes.Custom, plan); err != nil {
		return nil, err
	}
	return plan, nil
}

func (p *DefaultPermissionAuthoringMutationPlanner) planSystem(
	ctx context.Context,
	permissionSetID string,
	changes []permissionauthoringv1.CapabilityGrantChange,
	plan *defaultPermissionAuthoringMutationPlan,
) error {
	if len(changes) == 0 {
		return nil
	}
	current, err := p.permissions.repo.GetPermissionSetGrants(ctx, permissionSetID)
	if err != nil {
		return err
	}
	catalog, err := p.permissions.repo.ListSystemPermissions(ctx)
	if err != nil {
		return err
	}
	active := make(map[string]models.SystemPermission, len(catalog))
	for _, permission := range catalog {
		if permission.IsActive {
			active[permission.APIName] = permission
		}
	}
	final := clonePermissionAuthoringBools(current)
	for _, change := range changes {
		if _, ok := active[change.CapabilityID]; !ok {
			return invalidPermissionAuthoringChange("unknown or inactive system permission " + change.CapabilityID)
		}
		final[change.CapabilityID] = change.Enabled
	}
	for _, permission := range active {
		if permission.RequiresPermission == nil || *permission.RequiresPermission == "" || !final[permission.APIName] {
			continue
		}
		if !final[*permission.RequiresPermission] {
			return invalidPermissionAuthoringChange(fmt.Sprintf(
				"%s requires %s", permission.APIName, *permission.RequiresPermission,
			))
		}
	}
	for _, change := range changes {
		if current[change.CapabilityID] == change.Enabled {
			continue
		}
		change := change
		plan.operations = append(plan.operations, func(txCtx context.Context) error {
			return p.permissions.repo.SetSystemPermission(txCtx, permissionSetID, change.CapabilityID, change.Enabled)
		})
		plan.changes.System = append(plan.changes.System, change)
	}
	return nil
}

func (p *DefaultPermissionAuthoringMutationPlanner) planRecordTypes(
	ctx context.Context,
	aggregate ports.LockedPermissionAuthoringAggregate,
	changes []permissionauthoringv1.RecordTypeGrantChange,
	plan *defaultPermissionAuthoringMutationPlan,
) error {
	if len(changes) == 0 {
		return nil
	}
	catalog, err := p.permissions.repo.ListRecordTypeCatalog(ctx)
	if err != nil {
		return err
	}
	semanticCatalog := make([]permissionauthoringv1.RecordType, 0, len(catalog))
	for _, recordType := range catalog {
		semanticCatalog = append(semanticCatalog, permissionauthoringv1.RecordType{
			RecordTypeID: recordType.RecordTypeID,
			ResourceType: recordType.ObjectAPIName,
			StableName:   recordType.Name,
			Label:        recordType.Label,
			Active:       recordType.IsActive,
		})
	}
	sort.Slice(semanticCatalog, func(i, j int) bool {
		return semanticCatalog[i].RecordTypeID < semanticCatalog[j].RecordTypeID
	})
	targetKind := permissionauthoringv1.TargetPermissionSet
	if aggregate.Role == ports.PermissionAuthoringProfileBaseline {
		targetKind = permissionauthoringv1.TargetProfile
	}
	switch permissionauthoringv1.ClassifyRecordTypeObjectGroups(
		permissionauthoringv1.Target{Kind: targetKind, ID: aggregate.CanonicalPermissionSetID},
		semanticCatalog,
		changes,
	) {
	case permissionauthoringv1.RecordTypeObjectGroupComplete:
	case permissionauthoringv1.RecordTypeObjectGroupIncomplete,
		permissionauthoringv1.RecordTypeObjectGroupMembershipChanged:
		return &PermissionAuthoringRejection{
			Code:  permissionauthoringv1.RejectionConflict,
			Cause: errors.New("record type Catalog membership changed or the touched Profile object is incomplete"),
		}
	case permissionauthoringv1.RecordTypeObjectGroupUnknownOrInactive:
		return invalidPermissionAuthoringChange("unknown or inactive record type")
	default:
		return errors.New("permission authoring RecordType Catalog is invalid")
	}
	currentRows, err := p.permissions.repo.GetPermissionSetRecordTypeGrants(ctx, aggregate.CanonicalPermissionSetID)
	if err != nil {
		return err
	}
	current := make(map[string]bool, len(currentRows))
	for _, row := range currentRows {
		current[row.RecordTypeID] = row.IsVisible
	}
	for _, change := range changes {
		currentVisible, present := current[change.RecordTypeID]
		unchanged := false
		switch aggregate.Role {
		case ports.PermissionAuthoringProfileBaseline:
			// Presence is semantic for Profile baselines. A complete touched
			// object lazily canonicalizes every absent sibling, including false.
			unchanged = present && currentVisible == change.Visible
		case ports.PermissionAuthoringAdditivePermissionSet:
			// Ordinary PermissionSets represent false by row absence. A retained
			// false row is non-canonical and is deleted when touched.
			unchanged = present && currentVisible && change.Visible || !present && !change.Visible
		}
		if unchanged {
			continue
		}
		change := change
		plan.operations = append(plan.operations, func(txCtx context.Context) error {
			return p.permissions.repo.SetPermissionSetRecordTypeGrant(
				txCtx, aggregate.CanonicalPermissionSetID, change.RecordTypeID, change.Visible,
			)
		})
		plan.changes.RecordTypes = append(plan.changes.RecordTypes, change)
	}
	return nil
}

func (p *DefaultPermissionAuthoringMutationPlanner) planCustom(
	ctx context.Context,
	permissionSetID string,
	changes []permissionauthoringv1.CapabilityGrantChange,
	plan *defaultPermissionAuthoringMutationPlan,
) error {
	if len(changes) == 0 {
		return nil
	}
	catalog, err := p.permissions.repo.ListCustomPermissions(ctx)
	if err != nil {
		return err
	}
	active := make(map[string]struct{}, len(catalog))
	for _, permission := range catalog {
		if permission.IsActive && !permission.IsDeleted {
			active[permission.ID] = struct{}{}
		}
	}
	currentRows, err := p.permissions.repo.GetPermissionSetCustomPermGrants(ctx, permissionSetID)
	if err != nil {
		return err
	}
	current := make(map[string]bool, len(currentRows))
	for _, row := range currentRows {
		current[row.CustomPermissionID] = true
	}
	for _, change := range changes {
		if _, ok := active[change.CapabilityID]; !ok {
			return invalidPermissionAuthoringChange("unknown or inactive custom permission " + change.CapabilityID)
		}
		if current[change.CapabilityID] == change.Enabled {
			continue
		}
		change := change
		plan.operations = append(plan.operations, func(txCtx context.Context) error {
			if change.Enabled {
				return p.permissions.repo.SetPermissionSetCustomPermGrant(txCtx, permissionSetID, change.CapabilityID)
			}
			return p.permissions.repo.DeletePermissionSetCustomPermGrant(txCtx, permissionSetID, change.CapabilityID)
		})
		plan.changes.Custom = append(plan.changes.Custom, change)
	}
	return nil
}

func invalidPermissionAuthoringChange(message string) error {
	return &PermissionAuthoringRejection{
		Code:  permissionauthoringv1.RejectionInvalidChange,
		Cause: errors.New(message),
	}
}

func clonePermissionAuthoringBools(source map[string]bool) map[string]bool {
	cloned := make(map[string]bool, len(source))
	for key, value := range source {
		cloned[key] = value
	}
	return cloned
}

func permissionAuthoringChangeCount(changes permissionauthoringv1.Changes) int {
	return len(changes.System) + len(changes.Objects) + len(changes.Fields) + len(changes.RecordTypes) + len(changes.Custom)
}

func clonePermissionAuthoringChanges(changes permissionauthoringv1.Changes) permissionauthoringv1.Changes {
	return permissionauthoringv1.Changes{
		System:      append([]permissionauthoringv1.CapabilityGrantChange(nil), changes.System...),
		Objects:     append([]permissionauthoringv1.ObjectGrantChange(nil), changes.Objects...),
		Fields:      append([]permissionauthoringv1.FieldGrantChange(nil), changes.Fields...),
		RecordTypes: append([]permissionauthoringv1.RecordTypeGrantChange(nil), changes.RecordTypes...),
		Custom:      append([]permissionauthoringv1.CapabilityGrantChange(nil), changes.Custom...),
	}
}
