package rls

import (
	"fmt"
	"strings"

	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// escapeSQLString escapes backslashes and single quotes for safe
// interpolation inside a SQL string literal. Inlined (formerly
// backend/pkg/utils.EscapeSQLString) to keep this relocated package free of
// backend module dependencies.
func escapeSQLString(s string) string {
	s = strings.ReplaceAll(s, "\\", "\\\\")
	s = strings.ReplaceAll(s, "'", "''")
	return s
}

// Result holds pre-computed record-level security data for a (user, object) pair.
// It is the domain contract between permission resolution and query adapters.
type Result struct {
	Skip          bool
	DenyAll       bool
	ObjectAPIName string
	HasOwnerField bool
	// OwnerIDColumn and RecordIDColumn are query-adapter prepared names for the
	// selected read target. Empty values preserve the logical OwnerId / Id
	// defaults for non-query consumers.
	OwnerIDColumn           string
	RecordIDColumn          string
	UserID                  string
	OwnerUserIDs            []string
	GroupOwnerIDs           []string
	SharingRuleOwnerIDs     []string
	SharingRuleFromGroupIDs []string
	IncludeManualShares     bool
	IncludeTeamMembers      bool
	ParentFilter            *ParentFilter
	// CriteriaSQLFragments holds pre-compiled SQL WHERE fragments from
	// criteria-based sharing rules. Each fragment is a self-contained
	// boolean expression (e.g., "`Industry` = 'Technology'") produced
	// by the expression engine's ToSQL compiler at BuildRLSResult time.
	CriteriaSQLFragments []string
	// CriteriaExpressions preserves the normalized source expressions for the
	// same criteria-based sharing rules. SQL adapters can compile these at the
	// final read target so logical field names map to the selected table/view.
	CriteriaExpressions []string
	AllowedIDs          []string // Pre-computed list of allowed IDs (used by FolderSharing/FolderContentSharing)
	AllowedIDsColumn    string   // Logical/API field to filter on (default "Id"; query adapters map it to the active read target)
	TargetRecordFilters []TargetRecordFilter
	OwnerVisibility     *OwnerVisibilityFilter
}

// ParentFilter enables ControlledByParent sharing (e.g., Contact inherits Account access).
type ParentFilter struct {
	ParentObjectAPIName string
	ForeignKeyField     string
	ParentRLSResult     *Result
	// ParentSchema carries the neutral metadata needed by the query adapter to
	// resolve the parent's canonical read target. ParentReadTable and
	// ParentIDColumn are adapter-prepared values; zero values preserve the
	// logical-object fallback for non-query consumers.
	ParentSchema    *models.ObjectMetadata
	ParentReadTable string
	ParentIDColumn  string
	// ParentDerivedOnly is set when the sharing edge is required /
	// master-detail: the child's row visibility is exclusively
	// parent-derived and no owner/share fallback arms exist by
	// construction. Optional edges leave this false — their fallback arms
	// gate standalone/orphan rows even when they happen to render empty
	// for a given principal.
	ParentDerivedOnly bool
}

// TargetRecordFilter grants rows whose target record is visible through the
// target object's own RLS. The row stores both target object API name and
// target record Id in fields declared by metadata.
type TargetRecordFilter struct {
	TargetObjectAPIName   string
	TargetObjectField     string
	TargetRecordField     string
	TargetRLSResult       *Result
	TargetSchema          *models.ObjectMetadata
	TargetReadTable       string
	TargetIDColumn        string
	TargetIsDeletedColumn string
	TargetIncludeDeleted  bool
	// Alternatives distinguishes the grouped association shape from the
	// legacy flat shape: nil means legacy, while a non-nil slice is grouped.
	// A grouped association with zero alternatives is intentional and must
	// remain fail-closed whenever its record field is present.
	Alternatives []TargetRecordFilterAlternative
}

// TargetRecordFilterAlternative is one polymorphic target object within a
// TargetRecordFilter association group.
type TargetRecordFilterAlternative struct {
	TargetObjectAPIName   string
	TargetRLSResult       *Result
	TargetSchema          *models.ObjectMetadata
	TargetReadTable       string
	TargetIDColumn        string
	TargetIsDeletedColumn string
	TargetIncludeDeleted  bool
}

// TargetRecordAlternatives normalizes legacy single-target filters into the
// grouped representation consumed by all RLS adapters.
func (f TargetRecordFilter) TargetRecordAlternatives() []TargetRecordFilterAlternative {
	if f.Alternatives != nil {
		return f.Alternatives
	}
	if strings.TrimSpace(f.TargetObjectAPIName) == "" && f.TargetRLSResult == nil && f.TargetSchema == nil {
		return nil
	}
	return []TargetRecordFilterAlternative{{
		TargetObjectAPIName:   f.TargetObjectAPIName,
		TargetRLSResult:       f.TargetRLSResult,
		TargetSchema:          f.TargetSchema,
		TargetReadTable:       f.TargetReadTable,
		TargetIDColumn:        f.TargetIDColumn,
		TargetIsDeletedColumn: f.TargetIsDeletedColumn,
		TargetIncludeDeleted:  f.TargetIncludeDeleted,
	}}
}

// OwnerVisibilityFilter grants rows by an explicit owner + visibility policy.
// It is used for system metadata rows where standard object OWD must not expose
// private caller-owned artifacts to other users.
type OwnerVisibilityFilter struct {
	UserID              string
	OwnerField          string
	VisibilityField     string
	OwnerVisibleValues  []string
	PublicVisibleValues []string
}

// ToSQLFilter converts an RLS result into a SQL WHERE fragment.
// tableQualifier is the physical FROM target (view name, alias, or table name)
// used to qualify Id references in manual-share / team-member subqueries.
// Pass "" to default to the logical ObjectAPIName.
// Returns "" when the object is unrestricted (Skip=true).
func ToSQLFilter(result *Result, tableQualifier string) string {
	if result == nil || result.Skip {
		return ""
	}
	if result.DenyAll {
		return "1 = 0"
	}

	// Resolve table qualifier: callers provide the physical FROM target;
	// fall back to the logical object name for unprepared callers.
	tq := tableQualifier
	if tq == "" {
		tq = result.ObjectAPIName
	}

	var parts []string
	ownerIDColumn := strings.TrimSpace(result.OwnerIDColumn)
	if ownerIDColumn == "" {
		ownerIDColumn = constants.FieldOwnerID
	}
	recordIDColumn := strings.TrimSpace(result.RecordIDColumn)
	if recordIDColumn == "" {
		recordIDColumn = constants.FieldID
	}

	// Handle pre-computed allowed IDs (e.g., FolderSharing / FolderContentSharing)
	if len(result.AllowedIDs) > 0 {
		col := "Id"
		if result.AllowedIDsColumn != "" {
			col = result.AllowedIDsColumn
		}
		placeholders := make([]string, len(result.AllowedIDs))
		for i, id := range result.AllowedIDs {
			placeholders[i] = fmt.Sprintf("'%s'", strings.ReplaceAll(id, "'", "''"))
		}
		parts = append(parts, fmt.Sprintf("`%s`.`%s` IN (%s)", tq, col, strings.Join(placeholders, ",")))
	}

	if result.OwnerVisibility != nil {
		return ownerVisibilitySQL(result.OwnerVisibility, tq)
	}

	// Set when the parent set is fully visible (parent result renders to "").
	// The consequence is decided only after ALL arms are assembled: a
	// parent-derived-only result inherits unrestricted visibility, while a
	// result that also carries owner/share fallback arms (optional
	// ControlledByParent edge) must keep those arms gating standalone/orphan
	// rows — so the parent arm is emitted without a WHERE instead of
	// short-circuiting the whole filter.
	parentUnrestricted := false

	if result.ParentFilter != nil {
		pf := result.ParentFilter
		parentReadTable := strings.TrimSpace(pf.ParentReadTable)
		if parentReadTable == "" {
			parentReadTable = pf.ParentObjectAPIName
		}
		parentIDExpression := constants.FieldID
		if parentIDColumn := strings.TrimSpace(pf.ParentIDColumn); parentIDColumn != "" {
			parentIDExpression = escapeIdentifier(parentIDColumn)
		}
		parentExpr := ToSQLFilter(pf.ParentRLSResult, parentReadTable)
		if parentExpr == "" {
			// A non-nil parent result renders to "" only when it is
			// unrestricted: Skip, or a chain that itself collapsed to
			// unrestricted because an ancestor was Skip (e.g. object-level
			// ViewAllRecords on the chain root). A nil parent result stays
			// fail-closed (arm dropped); DenyAll never reaches this branch
			// because its SQL is the non-empty "1 = 0".
			if pf.ParentRLSResult != nil && !pf.ParentRLSResult.DenyAll {
				parentUnrestricted = true
			}
		} else {
			parts = append(parts, fmt.Sprintf(
				"%s IN (SELECT %s FROM %s WHERE %s)",
				escapeIdentifier(pf.ForeignKeyField),
				parentIDExpression,
				escapeIdentifier(parentReadTable),
				parentExpr,
			))
		}
	}

	var targetPresentParts []string
	var targetGuardParts []string
	for _, trf := range normalizeTargetRecordFilterGroups(result.TargetRecordFilters) {
		objectField := strings.TrimSpace(trf.TargetObjectField)
		recordField := strings.TrimSpace(trf.TargetRecordField)
		if objectField == "" || recordField == "" {
			continue
		}
		recordColumn := fmt.Sprintf("%s.%s", escapeIdentifier(tq), escapeIdentifier(recordField))
		targetPresentParts = append(targetPresentParts, fmt.Sprintf(
			"(%s IS NOT NULL AND %s <> '')",
			recordColumn,
			recordColumn,
		))

		var visibleAlternatives []string
		for _, alternative := range trf.TargetRecordAlternatives() {
			if rendered := targetRecordAlternativeSQL(alternative, tq, objectField, recordColumn); rendered != "" {
				visibleAlternatives = append(visibleAlternatives, rendered)
			}
		}
		// Preserve the legacy single-target fail-closed contract: a malformed
		// legacy arm with no renderable target is dropped. Explicit grouped
		// associations remain represented even with zero alternatives so a
		// present value cannot be granted by a different association.
		if len(visibleAlternatives) == 0 && trf.Alternatives == nil {
			targetPresentParts = targetPresentParts[:len(targetPresentParts)-1]
			continue
		}
		visible := "1 = 0"
		if len(visibleAlternatives) == 1 {
			visible = visibleAlternatives[0]
		} else if len(visibleAlternatives) > 1 {
			visible = "(" + strings.Join(visibleAlternatives, " OR ") + ")"
		}
		targetGuardParts = append(targetGuardParts, fmt.Sprintf(
			"(%s IS NULL OR %s = '' OR %s)",
			recordColumn,
			recordColumn,
			visible,
		))
	}
	if len(targetPresentParts) > 0 {
		present := targetPresentParts[0]
		if len(targetPresentParts) > 1 {
			present = "(" + strings.Join(targetPresentParts, " OR ") + ")"
		}
		targetGrantParts := append([]string{present}, targetGuardParts...)
		parts = append(parts, "("+strings.Join(targetGrantParts, " AND ")+")")
	}

	if result.HasOwnerField {
		if len(result.OwnerUserIDs) > 0 {
			parts = append(parts, buildInClause(ownerIDColumn, result.OwnerUserIDs))
		}
		if len(result.SharingRuleOwnerIDs) > 0 {
			parts = append(parts, buildInClause(ownerIDColumn, result.SharingRuleOwnerIDs))
		}
		for _, groupID := range result.SharingRuleFromGroupIDs {
			parts = append(parts, fmt.Sprintf(
				"%s IN (SELECT gmc.MemberId FROM `%s` gmc WHERE gmc.GroupId = '%s')",
				escapeIdentifier(ownerIDColumn),
				constants.TableGroupMembershipClosure,
				escapeSQLString(groupID),
			))
		}
		if len(result.GroupOwnerIDs) > 0 {
			parts = append(parts, buildInClause(ownerIDColumn, result.GroupOwnerIDs))
		}
	}

	if result.IncludeManualShares && result.UserID != "" {
		parts = append(parts, fmt.Sprintf(
			"%s.%s IN (SELECT rs.RecordId FROM _System_RecordShare rs WHERE rs.ObjectApiName = '%s' AND (rs.IsDeleted = 0 OR rs.IsDeleted IS NULL) AND (rs.ShareWithUserId = '%s' OR rs.ShareWithGroupId IN (SELECT gmc2.GroupId FROM `%s` gmc2 WHERE gmc2.MemberType = '%s' AND gmc2.MemberId = '%s')))",
			escapeIdentifier(tq),
			escapeIdentifier(recordIDColumn),
			escapeSQLString(result.ObjectAPIName),
			escapeSQLString(result.UserID),
			constants.TableGroupMembershipClosure,
			escapeSQLString(constants.GroupMembershipClosureMemberTypeUser),
			escapeSQLString(result.UserID),
		))
	}

	if result.IncludeTeamMembers && result.UserID != "" {
		parts = append(parts, fmt.Sprintf(
			"%s.%s IN (SELECT tm.RecordId FROM _System_TeamMember tm WHERE tm.ObjectApiName = '%s' AND (tm.IsDeleted = 0 OR tm.IsDeleted IS NULL) AND tm.UserId = '%s')",
			escapeIdentifier(tq),
			escapeIdentifier(recordIDColumn),
			escapeSQLString(result.ObjectAPIName),
			escapeSQLString(result.UserID),
		))
	}

	// Criteria-based sharing rule fragments (pre-compiled by permission service)
	for _, frag := range result.CriteriaSQLFragments {
		parts = append(parts, frag)
	}

	if parentUnrestricted {
		if result.ParentFilter.ParentDerivedOnly {
			// Required / master-detail sharing edge: the child's visibility
			// is exclusively parent-derived, so it inherits the parent set's
			// unrestricted visibility outright.
			return ""
		}
		// Optional sharing edge: parent-linked rows are all visible, but
		// standalone/orphan rows (NULL or dangling FK) stay gated by the
		// owner/share fallback arms — emit the parent arm without a WHERE
		// instead of short-circuiting the whole filter. This holds even when
		// the fallback arms render empty for this principal (e.g. no OwnerId
		// field, proven-empty share/team probes): arm-count is not a proxy
		// for the edge kind, and record access denies orphans symmetrically.
		parts = append(parts, fmt.Sprintf(
			"%s IN (SELECT %s FROM %s)",
			escapeIdentifier(result.ParentFilter.ForeignKeyField),
			parentFilterIDExpression(result.ParentFilter),
			escapeIdentifier(parentFilterReadTable(result.ParentFilter)),
		))
	}

	if len(parts) == 0 {
		return "1 = 0"
	}
	if len(parts) == 1 {
		return parts[0]
	}
	return "(\n  " + strings.Join(parts, "\n  OR ") + "\n)"
}

func normalizeTargetRecordFilterGroups(filters []TargetRecordFilter) []TargetRecordFilter {
	groups := make([]TargetRecordFilter, 0, len(filters))
	legacyGroupIndexes := make(map[string]int)
	for _, filter := range filters {
		if filter.Alternatives != nil {
			groups = append(groups, filter)
			continue
		}

		alternatives := filter.TargetRecordAlternatives()
		objectField := strings.TrimSpace(filter.TargetObjectField)
		recordField := strings.TrimSpace(filter.TargetRecordField)
		if len(alternatives) == 0 || objectField == "" || recordField == "" {
			groups = append(groups, filter)
			continue
		}

		key := objectField + "\x00" + recordField
		if index, ok := legacyGroupIndexes[key]; ok {
			if groups[index].Alternatives == nil {
				groups[index].Alternatives = groups[index].TargetRecordAlternatives()
			}
			groups[index].Alternatives = append(groups[index].Alternatives, alternatives...)
			continue
		}
		legacyGroupIndexes[key] = len(groups)
		groups = append(groups, filter)
	}
	return groups
}

func targetRecordAlternativeSQL(alternative TargetRecordFilterAlternative, tableQualifier, objectField, recordColumn string) string {
	targetObject := strings.TrimSpace(alternative.TargetObjectAPIName)
	if targetObject == "" {
		return ""
	}
	objectPredicate := fmt.Sprintf("%s.%s = '%s'",
		escapeIdentifier(tableQualifier),
		escapeIdentifier(objectField),
		escapeSQLString(targetObject),
	)

	targetReadTable := strings.TrimSpace(alternative.TargetReadTable)
	if targetReadTable == "" {
		targetReadTable = targetObject
	}
	targetIDColumn := strings.TrimSpace(alternative.TargetIDColumn)
	if targetIDColumn == "" {
		targetIDColumn = constants.FieldID
	}

	targetExpr := ToSQLFilter(alternative.TargetRLSResult, targetReadTable)
	targetConditions := make([]string, 0, 2)
	switch {
	case targetExpr != "":
		targetConditions = append(targetConditions, targetExpr)
	case alternative.TargetRLSResult != nil && !alternative.TargetRLSResult.DenyAll:
		// Unrestricted target visibility still needs deleted-row filtering.
	default:
		return ""
	}

	if deletedColumn := strings.TrimSpace(alternative.TargetIsDeletedColumn); deletedColumn != "" && !alternative.TargetIncludeDeleted {
		targetConditions = append(targetConditions, fmt.Sprintf(
			"%s.%s = 0",
			escapeIdentifier(targetReadTable),
			escapeIdentifier(deletedColumn),
		))
	}

	if len(targetConditions) == 0 {
		return objectPredicate
	}
	if strings.EqualFold(targetReadTable, strings.TrimSpace(tableQualifier)) {
		return fmt.Sprintf(
			"(%s AND %s IN (SELECT %s FROM %s WHERE %s))",
			objectPredicate,
			recordColumn,
			escapeIdentifier(targetIDColumn),
			escapeIdentifier(targetReadTable),
			strings.Join(targetConditions, " AND "),
		)
	}
	return fmt.Sprintf(
		"(%s AND EXISTS (SELECT 1 FROM %s WHERE %s.%s = %s AND %s))",
		objectPredicate,
		escapeIdentifier(targetReadTable),
		escapeIdentifier(targetReadTable),
		escapeIdentifier(targetIDColumn),
		recordColumn,
		strings.Join(targetConditions, " AND "),
	)
}

func parentFilterReadTable(filter *ParentFilter) string {
	if filter != nil && strings.TrimSpace(filter.ParentReadTable) != "" {
		return strings.TrimSpace(filter.ParentReadTable)
	}
	if filter == nil {
		return ""
	}
	return filter.ParentObjectAPIName
}

func parentFilterIDExpression(filter *ParentFilter) string {
	if filter != nil && strings.TrimSpace(filter.ParentIDColumn) != "" {
		return escapeIdentifier(strings.TrimSpace(filter.ParentIDColumn))
	}
	return constants.FieldID
}

func ownerVisibilitySQL(filter *OwnerVisibilityFilter, tableQualifier string) string {
	if filter == nil {
		return "1 = 0"
	}
	ownerField := strings.TrimSpace(filter.OwnerField)
	visibilityField := strings.TrimSpace(filter.VisibilityField)
	if visibilityField == "" {
		return "1 = 0"
	}
	var parts []string
	if ownerField != "" && strings.TrimSpace(filter.UserID) != "" && len(filter.OwnerVisibleValues) > 0 {
		parts = append(parts, fmt.Sprintf(
			"(%s.%s = '%s' AND %s.%s IN (%s))",
			escapeIdentifier(tableQualifier),
			escapeIdentifier(ownerField),
			escapeSQLString(strings.TrimSpace(filter.UserID)),
			escapeIdentifier(tableQualifier),
			escapeIdentifier(visibilityField),
			quotedList(filter.OwnerVisibleValues),
		))
	}
	if len(filter.PublicVisibleValues) > 0 {
		parts = append(parts, fmt.Sprintf(
			"%s.%s IN (%s)",
			escapeIdentifier(tableQualifier),
			escapeIdentifier(visibilityField),
			quotedList(filter.PublicVisibleValues),
		))
	}
	if len(parts) == 0 {
		return "1 = 0"
	}
	if len(parts) == 1 {
		return parts[0]
	}
	return "(" + strings.Join(parts, " OR ") + ")"
}

func quotedList(values []string) string {
	quoted := make([]string, 0, len(values))
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value == "" {
			continue
		}
		quoted = append(quoted, fmt.Sprintf("'%s'", escapeSQLString(value)))
	}
	if len(quoted) == 0 {
		return "''"
	}
	return strings.Join(quoted, ", ")
}

func buildInClause(field string, ids []string) string {
	escaped := escapeIdentifier(field)
	if len(ids) == 1 {
		return fmt.Sprintf("%s = '%s'", escaped, escapeSQLString(ids[0]))
	}
	quoted := make([]string, len(ids))
	for i, id := range ids {
		quoted[i] = fmt.Sprintf("'%s'", escapeSQLString(id))
	}
	return fmt.Sprintf("%s IN (%s)", escaped, strings.Join(quoted, ", "))
}

func escapeIdentifier(s string) string {
	return "`" + strings.ReplaceAll(s, "`", "``") + "`"
}
