package permissions

import (
	"context"
	"fmt"
	"strings"

	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/rs/zerolog/log"
)

// PermRule defines per-object permission flags used during seeding.
type PermRule struct {
	Read, Create, Edit, Delete, ViewAll, ModifyAll bool
}

// profilePermRules is the default permission rule matrix for the built-in
// profiles the platform owns defaults for. Unknown profiles (SF-imported /
// admin-created) get NO default rule — see resolvePermRule — because their
// access is import-/admin-owned, not platform-seeded. This map's key set is the
// source of truth bound to constants.PlatformDefaultProfileIDs (guard test in
// permission_seeder_source_limit_test.go).
var profilePermRules = map[string]map[string]PermRule{
	constants.ProfileSystemAdmin: {
		"default": {true, true, true, true, true, true},
	},
	constants.ProfileStandardUser: {
		"system":   {true, false, false, false, false, false},
		"business": {true, true, true, false, false, false},
	},
}

// resolvePermRule determines the permission rule for a given profile and object schema.
//
// Source-limited: the platform only seeds default permissions for the built-in
// profiles it declares defaults for (profilePermRules). An unknown profile —
// SF-imported or admin-created — is import-/admin-owned, not platform-owned;
// it receives NO default rule here. Falling back to StandardUser defaults was
// the root of the 1/0/0/0/0/0 → 1/1/1/0/0/0 over-grant: an imported profile
// whose source access is read-only was treated as "unknown" and silently
// granted Create/Edit. Such profiles own their access through import or
// explicit admin authoring; absence of a default is intentional, not a gap.
func resolvePermRule(profileID string, schema *models.ObjectMetadata) PermRule {
	rules, ok := profilePermRules[profileID]
	if !ok {
		return PermRule{}
	}
	if r, ok := rules["default"]; ok {
		return r
	}
	category := "business"
	if constants.IsSystemTable(schema.APIName) {
		category = "system"
	}
	if r, ok := rules[category]; ok {
		return r
	}
	return PermRule{}
}

// ShouldMaterializeFieldPermissionRow returns true for every logical schema
// field that participates in the admin-manageable FieldPermissions contract.
// Read-only business fields and platform virtual compound fields still
// materialize rows; runtime fallback is not a replacement authority for the
// persisted metadata/UI contract.
func ShouldMaterializeFieldPermissionRow(field *models.FieldMetadata) bool {
	return field != nil
}

func schemaFieldPermissionNames(schema *models.ObjectMetadata) map[string]models.FieldMetadata {
	if schema == nil {
		return nil
	}
	out := make(map[string]models.FieldMetadata, len(schema.Fields))
	for _, field := range schema.Fields {
		out[strings.ToLower(field.APIName)] = field
	}
	return out
}

// BuildPermissionsForProfileAndSchema builds ObjectPermissions and FieldPermissions
// for a single profile+schema pair. Returns nil slices when no permissions should be granted.
//
// `isParentControlled` suppresses the ObjectPermissions row only — FLS rows
// continue to materialize because field-level security applies independently
// per child field (AC3). Derived AccessPolicy schemas still produce nil for
// both (existing behavior).
func BuildPermissionsForProfileAndSchema(profileID, permSetID string, schema *models.ObjectMetadata, isParentControlled bool) ([]models.ObjectPermissions, []models.FieldPermissions) {
	if schema != nil && schema.UsesDerivedAccessPolicy() {
		return nil, nil
	}
	rule := resolvePermRule(profileID, schema)

	var objPerms []models.ObjectPermissions
	var fieldPerms []models.FieldPermissions

	if !isParentControlled && (rule.Read || rule.Create || rule.Edit || rule.Delete || rule.ViewAll || rule.ModifyAll) {
		objPerms = append(objPerms, models.ObjectPermissions{
			ParentID:                    permSetID,
			SobjectType:                 schema.APIName,
			PermissionsRead:             rule.Read,
			PermissionsCreate:           rule.Create,
			PermissionsEdit:             rule.Edit,
			PermissionsDelete:           rule.Delete,
			PermissionsViewAllRecords:   rule.ViewAll,
			PermissionsModifyAllRecords: rule.ModifyAll,
		})
	}

	if rule.Read || rule.Edit {
		for _, field := range schema.Fields {
			fieldCopy := field
			if !ShouldMaterializeFieldPermissionRow(&fieldCopy) {
				continue
			}
			fieldPerms = append(fieldPerms, models.FieldPermissions{
				ParentID:        permSetID,
				SobjectType:     schema.APIName,
				Field:           field.APIName,
				PermissionsRead: rule.Read,
				PermissionsEdit: rule.Edit,
			})
		}
	}

	return objPerms, fieldPerms
}

func (ps *PermissionService) seedPermissionRows(
	ctx context.Context,
	objectPerms []models.ObjectPermissions,
	fieldPerms []models.FieldPermissions,
) error {
	// Additive-only: seeding paints blank canvases. An existing row (imported
	// or admin-edited) owns its values and must never be overwritten or widened
	// by a default-seed pass — see InsertMissingObjectPermissions.
	if len(objectPerms) > 0 {
		if err := ps.InsertMissingObjectPermissions(ctx, objectPerms); err != nil {
			return err
		}
	}
	if len(fieldPerms) > 0 {
		if err := ps.InsertMissingFieldPermissions(ctx, fieldPerms); err != nil {
			return err
		}
	}
	return nil
}

func buildProfilePermissionSetMap(profiles []*models.Profile) []struct{ ID, Name string } {
	profileInfos := make([]struct{ ID, Name string }, 0, len(profiles))
	for _, profile := range profiles {
		if profile == nil {
			continue
		}
		profileInfos = append(profileInfos, struct{ ID, Name string }{
			ID:   profile.ID,
			Name: profile.Name,
		})
	}
	return profileInfos
}

// SeedPermissionsForProfile seeds ObjectPermissions and FieldPermissions for a single
// profile across all provided schemas.
// Caller is responsible for invalidating the permission cache after commit.
func (ps *PermissionService) SeedPermissionsForProfile(ctx context.Context, profileID, permSetID string, schemas []*models.ObjectMetadata) error {
	var allObjPerms []models.ObjectPermissions
	var allFieldPerms []models.FieldPermissions

	for _, schema := range schemas {
		isPC := false
		if ps.metadata != nil {
			isPC = ps.metadata.GetParentControlledObject(ctx, schema) != ""
		}
		objPerms, fieldPerms := BuildPermissionsForProfileAndSchema(profileID, permSetID, schema, isPC)
		allObjPerms = append(allObjPerms, objPerms...)
		allFieldPerms = append(allFieldPerms, fieldPerms...)
	}

	if err := ps.seedPermissionRows(ctx, allObjPerms, allFieldPerms); err != nil {
		return fmt.Errorf("seed permissions for profile %s: %w", profileID, err)
	}

	log.Info().Str("profileID", profileID).
		Int("objectPerms", len(allObjPerms)).Int("fieldPerms", len(allFieldPerms)).
		Msg("Seeded permissions for profile")
	return nil
}

// SeedPermissionsForObject seeds ObjectPermissions and FieldPermissions for a single
// object across all profiles.
// Caller is responsible for invalidating the permission cache after commit.
func (ps *PermissionService) SeedPermissionsForObject(ctx context.Context, objectAPIName string) error {
	schema := ps.metadata.GetSchema(ctx, objectAPIName)
	if schema == nil {
		return fmt.Errorf("schema not found for object %s", objectAPIName)
	}

	profiles, err := ps.GetAllProfiles(ctx)
	if err != nil {
		return fmt.Errorf("get all profiles: %w", err)
	}
	permSetIDs, err := ps.BulkEnsureProfilePermissionSets(ctx, buildProfilePermissionSetMap(profiles))
	if err != nil {
		return fmt.Errorf("ensure permission sets for object %s: %w", objectAPIName, err)
	}

	var allObjPerms []models.ObjectPermissions
	var allFieldPerms []models.FieldPermissions

	isPC := ps.metadata != nil && ps.metadata.GetParentControlledObject(ctx, schema) != ""

	for _, profile := range profiles {
		permSetID, ok := permSetIDs[profile.ID]
		if !ok {
			return fmt.Errorf("missing permission set for profile %s while seeding object %s", profile.ID, objectAPIName)
		}

		objPerms, fieldPerms := BuildPermissionsForProfileAndSchema(profile.ID, permSetID, schema, isPC)
		allObjPerms = append(allObjPerms, objPerms...)
		allFieldPerms = append(allFieldPerms, fieldPerms...)
	}

	if err := ps.seedPermissionRows(ctx, allObjPerms, allFieldPerms); err != nil {
		return fmt.Errorf("seed permissions for object %s: %w", objectAPIName, err)
	}

	log.Info().Str("object", objectAPIName).
		Int("profiles", len(profiles)).Int("objectPerms", len(allObjPerms)).Int("fieldPerms", len(allFieldPerms)).
		Msg("Seeded permissions for object")
	return nil
}

// SeedFieldPermissionsForField seeds FieldPermissions for a single field across all
// profiles.
// Caller is responsible for invalidating the permission cache after commit.
func (ps *PermissionService) SeedFieldPermissionsForField(ctx context.Context, objectAPIName, fieldAPIName string) error {
	return ps.SeedFieldPermissionsForFields(ctx, objectAPIName, []string{fieldAPIName})
}

// SeedFieldPermissionsForFields seeds FieldPermissions for specific fields across all
// profiles.
// Caller is responsible for invalidating the permission cache after commit.
func (ps *PermissionService) SeedFieldPermissionsForFields(ctx context.Context, objectAPIName string, fieldAPINames []string) error {
	if len(fieldAPINames) == 0 {
		return nil
	}

	profiles, err := ps.GetAllProfiles(ctx)
	if err != nil {
		return fmt.Errorf("get all profiles: %w", err)
	}

	schema := ps.metadata.GetSchema(ctx, objectAPIName)
	if schema == nil {
		return fmt.Errorf("schema not found for object %s", objectAPIName)
	}
	if schema.UsesDerivedAccessPolicy() {
		return nil
	}
	permSetIDs, err := ps.BulkEnsureProfilePermissionSets(ctx, buildProfilePermissionSetMap(profiles))
	if err != nil {
		return fmt.Errorf("ensure permission sets for fields on %s: %w", objectAPIName, err)
	}

	fieldByKey := schemaFieldPermissionNames(schema)
	normalizedFieldNames := make([]string, 0, len(fieldAPINames))
	seenFieldNames := make(map[string]bool, len(fieldAPINames))
	for _, fieldAPIName := range fieldAPINames {
		key := strings.ToLower(fieldAPIName)
		actualField, ok := fieldByKey[key]
		if !ok || seenFieldNames[key] {
			continue
		}
		fieldCopy := actualField
		if !ShouldMaterializeFieldPermissionRow(&fieldCopy) {
			continue
		}
		seenFieldNames[key] = true
		normalizedFieldNames = append(normalizedFieldNames, actualField.APIName)
	}
	if len(normalizedFieldNames) == 0 {
		return nil
	}

	var allFieldPerms []models.FieldPermissions

	for _, profile := range profiles {
		permSetID, ok := permSetIDs[profile.ID]
		if !ok {
			return fmt.Errorf("missing permission set for profile %s while seeding fields on %s", profile.ID, objectAPIName)
		}

		rule := resolvePermRule(profile.ID, schema)
		if rule.Read || rule.Edit {
			for _, fieldAPIName := range normalizedFieldNames {
				allFieldPerms = append(allFieldPerms, models.FieldPermissions{
					ParentID:        permSetID,
					SobjectType:     objectAPIName,
					Field:           fieldAPIName,
					PermissionsRead: rule.Read,
					PermissionsEdit: rule.Edit,
				})
			}
		}
	}

	if err := ps.seedPermissionRows(ctx, nil, allFieldPerms); err != nil {
		return fmt.Errorf("seed field permissions for %s: %w", objectAPIName, err)
	}

	log.Info().Str("object", objectAPIName).
		Int("fieldPerms", len(allFieldPerms)).
		Msg("Seeded field permissions for new fields")
	return nil
}
