package schemacatalog

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

	"github.com/nexuscrm/kernel/pkg/schema"
)

const (
	PermissionSetDefinitionRoleRoot               = "root"
	PermissionSetDefinitionRoleModeledGrant       = "modeled_grant"
	PermissionSetDefinitionRoleSeparateAssignment = "separate_assignment"
	PermissionSetDefinitionRoleNegativeEdge       = "negative_edge"
	PermissionSetRootDeleteCascade                = "cascade"

	managedPermissionSetDefinitionCapability = "platform.security.write.managed-permission-set-definition"
)

// PermissionSetDefinitionRelation classifies one managed-catalog table's
// lifecycle relationship to a Permission Set definition.
type PermissionSetDefinitionRelation struct {
	TableName   string
	Role        string
	ParentField string
	// OnRootDelete is an explicit Runtime action, not physical FK ON DELETE.
	// Empty means restrict: independently owned references must be detached by
	// their owner before a definition can be deleted.
	OnRootDelete string
}

// PermissionSetDefinitionInventory is the validated projection of
// permissionSetDefinitionRelation declarations in the embedded catalog.
type PermissionSetDefinitionInventory struct {
	relations []PermissionSetDefinitionRelation
}

type permissionSetDefinitionCatalogEntry struct {
	TableName                       string                                  `json:"tableName"`
	PermissionSetDefinitionRelation *permissionSetDefinitionCatalogRelation `json:"permissionSetDefinitionRelation,omitempty"`
}

type permissionSetDefinitionCatalogRelation struct {
	Role         string `json:"role"`
	ParentField  string `json:"parentField,omitempty"`
	OnRootDelete string `json:"onRootDelete,omitempty"`
}

// ManagedPermissionSetDefinitionInventory loads only the platform-owned
// embedded catalog and fails closed when its reference closure, lifecycle role,
// or validator phases are inconsistent.
func ManagedPermissionSetDefinitionInventory() (PermissionSetDefinitionInventory, error) {
	definitions, err := ManagedTableDefinitions()
	if err != nil {
		return PermissionSetDefinitionInventory{}, err
	}
	return permissionSetDefinitionInventoryFromCatalog(systemTablesJSON, definitions)
}

// Relations returns a canonical table-name-ordered copy.
func (inventory PermissionSetDefinitionInventory) Relations() []PermissionSetDefinitionRelation {
	return append([]PermissionSetDefinitionRelation(nil), inventory.relations...)
}

// RelationForTable returns the declared lifecycle relation for tableName.
func (inventory PermissionSetDefinitionInventory) RelationForTable(tableName string) (PermissionSetDefinitionRelation, bool) {
	for _, relation := range inventory.relations {
		if strings.EqualFold(relation.TableName, strings.TrimSpace(tableName)) {
			return relation, true
		}
	}
	return PermissionSetDefinitionRelation{}, false
}

func permissionSetDefinitionInventoryFromCatalog(
	raw []byte,
	definitions []schema.TableDefinition,
) (PermissionSetDefinitionInventory, error) {
	var entries []permissionSetDefinitionCatalogEntry
	if err := json.Unmarshal(raw, &entries); err != nil {
		return PermissionSetDefinitionInventory{}, fmt.Errorf("permission set definition inventory: parse managed catalog: %w", err)
	}

	declarations := make(map[string]permissionSetDefinitionCatalogRelation)
	for _, entry := range entries {
		if entry.PermissionSetDefinitionRelation == nil {
			continue
		}
		key := strings.ToLower(strings.TrimSpace(entry.TableName))
		if key == "" || entry.TableName != strings.TrimSpace(entry.TableName) {
			return PermissionSetDefinitionInventory{}, fmt.Errorf("permission set definition inventory: invalid table name %q", entry.TableName)
		}
		if _, duplicate := declarations[key]; duplicate {
			return PermissionSetDefinitionInventory{}, fmt.Errorf("permission set definition inventory: duplicate declaration for %s", entry.TableName)
		}
		declarations[key] = *entry.PermissionSetDefinitionRelation
	}

	relations := make([]PermissionSetDefinitionRelation, 0, len(declarations))
	for _, definition := range definitions {
		key := strings.ToLower(definition.TableName)
		declaration, declared := declarations[key]
		referencesRoot := definitionReferencesPermissionSet(definition)
		isRoot := strings.EqualFold(definition.TableName, "PermissionSet")
		if (referencesRoot || isRoot) && !declared {
			return PermissionSetDefinitionInventory{}, fmt.Errorf(
				"permission set definition inventory: table %s is in the reference closure but has no declaration",
				definition.TableName,
			)
		}
		if !declared {
			continue
		}
		delete(declarations, key)
		relation, err := validatePermissionSetDefinitionRelation(definition, declaration, referencesRoot, isRoot)
		if err != nil {
			return PermissionSetDefinitionInventory{}, err
		}
		relations = append(relations, relation)
	}
	if len(declarations) > 0 {
		for tableName := range declarations {
			return PermissionSetDefinitionInventory{}, fmt.Errorf(
				"permission set definition inventory: declaration table %s is not managed",
				tableName,
			)
		}
	}
	sort.Slice(relations, func(i, j int) bool {
		return relations[i].TableName < relations[j].TableName
	})
	return PermissionSetDefinitionInventory{relations: relations}, nil
}

func validatePermissionSetDefinitionRelation(
	definition schema.TableDefinition,
	declaration permissionSetDefinitionCatalogRelation,
	referencesRoot bool,
	isRoot bool,
) (PermissionSetDefinitionRelation, error) {
	role := strings.TrimSpace(declaration.Role)
	parentField := strings.TrimSpace(declaration.ParentField)
	if declaration.OnRootDelete != "" && (declaration.OnRootDelete != PermissionSetRootDeleteCascade || role == PermissionSetDefinitionRoleRoot) {
		return PermissionSetDefinitionRelation{}, fmt.Errorf("permission set definition inventory: table %s has invalid onRootDelete %q", definition.TableName, declaration.OnRootDelete)
	}
	switch role {
	case PermissionSetDefinitionRoleRoot:
		if !isRoot || parentField != "" {
			return PermissionSetDefinitionRelation{}, fmt.Errorf(
				"permission set definition inventory: table %s has invalid root declaration",
				definition.TableName,
			)
		}
	case PermissionSetDefinitionRoleModeledGrant,
		PermissionSetDefinitionRoleSeparateAssignment,
		PermissionSetDefinitionRoleNegativeEdge:
		if isRoot || !referencesRoot || parentField == "" || !definitionFieldReferencesPermissionSet(definition, parentField) {
			return PermissionSetDefinitionRelation{}, fmt.Errorf(
				"permission set definition inventory: table %s has invalid parent field %q",
				definition.TableName,
				declaration.ParentField,
			)
		}
	default:
		return PermissionSetDefinitionRelation{}, fmt.Errorf(
			"permission set definition inventory: table %s has invalid role %q",
			definition.TableName,
			declaration.Role,
		)
	}

	wantPhases := []string(nil)
	switch role {
	case PermissionSetDefinitionRoleRoot, PermissionSetDefinitionRoleModeledGrant:
		wantPhases = []string{"before_create", "before_delete", "before_purge", "before_restore", "before_update"}
	case PermissionSetDefinitionRoleNegativeEdge:
		wantPhases = []string{"before_create", "before_restore", "before_update"}
	}
	gotPhases := managedPermissionSetDefinitionValidatorPhases(definition)
	if !equalPermissionSetDefinitionStrings(gotPhases, wantPhases) {
		return PermissionSetDefinitionRelation{}, fmt.Errorf(
			"permission set definition inventory: table %s validator phases %v do not match role %s phases %v",
			definition.TableName,
			gotPhases,
			role,
			wantPhases,
		)
	}
	return PermissionSetDefinitionRelation{
		TableName:    definition.TableName,
		Role:         role,
		ParentField:  parentField,
		OnRootDelete: declaration.OnRootDelete,
	}, nil
}

func managedPermissionSetDefinitionValidatorPhases(definition schema.TableDefinition) []string {
	var phases []string
	if definition.AccessPolicy != nil && definition.AccessPolicy.WriteValidation != nil {
		for _, validator := range definition.AccessPolicy.WriteValidation.Validators {
			if validator.Capability == managedPermissionSetDefinitionCapability {
				phases = append(phases, validator.Phase)
			}
		}
	}
	sort.Strings(phases)
	return phases
}

func definitionReferencesPermissionSet(definition schema.TableDefinition) bool {
	for _, foreignKey := range definition.ForeignKeys {
		if strings.EqualFold(strings.TrimSpace(foreignKey.References), "PermissionSet(Id)") {
			return true
		}
	}
	for _, column := range definition.Columns {
		for _, target := range column.ReferenceTo {
			if strings.EqualFold(strings.TrimSpace(target), "PermissionSet") {
				return true
			}
		}
	}
	if definition.AccessPolicy == nil || definition.AccessPolicy.WriteValidation == nil ||
		definition.AccessPolicy.WriteValidation.OwnerResolution == nil {
		return false
	}
	for _, path := range definition.AccessPolicy.WriteValidation.OwnerResolution.Paths {
		for _, hop := range path.Hops {
			if strings.EqualFold(strings.TrimSpace(hop.ToObject), "PermissionSet") {
				return true
			}
		}
	}
	return false
}

func definitionFieldReferencesPermissionSet(definition schema.TableDefinition, fieldName string) bool {
	for _, foreignKey := range definition.ForeignKeys {
		if strings.EqualFold(foreignKey.Column, fieldName) &&
			strings.EqualFold(strings.TrimSpace(foreignKey.References), "PermissionSet(Id)") {
			return true
		}
	}
	for _, column := range definition.Columns {
		if !strings.EqualFold(column.Name, fieldName) {
			continue
		}
		for _, target := range column.ReferenceTo {
			if strings.EqualFold(strings.TrimSpace(target), "PermissionSet") {
				return true
			}
		}
	}
	if definition.AccessPolicy == nil || definition.AccessPolicy.WriteValidation == nil ||
		definition.AccessPolicy.WriteValidation.OwnerResolution == nil {
		return false
	}
	for _, path := range definition.AccessPolicy.WriteValidation.OwnerResolution.Paths {
		for _, hop := range path.Hops {
			if strings.EqualFold(hop.ViaField, fieldName) &&
				strings.EqualFold(strings.TrimSpace(hop.ToObject), "PermissionSet") {
				return true
			}
		}
	}
	return false
}

func equalPermissionSetDefinitionStrings(left, right []string) bool {
	if len(left) != len(right) {
		return false
	}
	for index := range left {
		if left[index] != right[index] {
			return false
		}
	}
	return true
}
