package metadata

import (
	"context"
	"fmt"
	"log"
	"strings"

	apperrors "github.com/nexuscrm/kernel/pkg/errors"
	domainrls "github.com/nexuscrm/kernel/pkg/rls"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/events"
	"github.com/nexuscrm/shared/pkg/models"
)

// GetAllObjects returns definitions from the selected metadata authority.
// Callers remain responsible for filtering by object permissions.
func (ms *MetadataService) GetAllObjects(ctx context.Context, user *models.UserSession) ([]models.ObjectMetadata, error) {
	if ms.UsesRequestBoundRuntimeView() {
		view, ok := ms.runtimeView(ctx)
		if !ok || view == nil {
			return nil, fmt.Errorf("request-bound Runtime metadata view is required")
		}
		// Objects materializes detached values from this request's immutable view;
		// never fall back to a repository or another request's/global schema list.
		schemas := view.Objects()
		objects := make([]models.ObjectMetadata, len(schemas))
		for i, schema := range schemas {
			objects[i] = *schema
		}
		return objects, nil
	}
	if ms == nil || ms.repo == nil {
		return nil, fmt.Errorf("metadata repository is required to list objects")
	}
	ms.mu.RLock()
	defer ms.mu.RUnlock()

	schemas, err := ms.repo.GetAllSchemas(ctx)
	if err != nil {
		return nil, apperrors.NewInternalError("failed to query objects", err)
	}
	res := make([]models.ObjectMetadata, len(schemas))
	for i, s := range schemas {
		res[i] = *s
	}
	return res, nil
}

// GetSystemFields returns all system fields for an object using the metadata cache.
// System fields are those with IsSystem=true OR IsNameField=true.
// Uses cached schema snapshot instead of querying the database on every call (#357).
func (ms *MetadataService) GetSystemFields(ctx context.Context, objectAPIName string) []string {
	// Use cached schema (lock-free atomic read) instead of DB query
	schema := ms.GetSchema(ctx, objectAPIName)
	if schema == nil {
		return []string{}
	}

	// Virtual fields (IsVirtual=true) are intentionally excluded even when
	// they're marked IsSystem=true. GetSystemFields feeds the query-service
	// visibleFields seed (analytics/query_service.go), which becomes the
	// SQL SELECT column list in QueryRepository.Find(). Including a virtual
	// field here produces an "Unknown column" error against the physical
	// storage table — the virtual's value is materialized post-fetch by
	// projectPermissionSetGrants / hydrateVirtualFields, never read from a
	// raw column.
	//
	// Why a virtual is marked IsSystem=true in the first place: the
	// SchemaDecorator sets IsSystem to match Salesforce's describe shape
	// for Permissions<X> (Custom=false, part of the standard PermissionSet
	// object). That describe-DTO intent and this SQL-seeding intent diverge
	// — hence the filter here rather than a decorator change.
	var systemFields []string
	for _, field := range schema.Fields {
		if field.IsVirtual {
			continue
		}
		if field.IsSystem || field.IsNameField {
			systemFields = append(systemFields, field.APIName)
		}
	}

	return systemFields
}

// GetSupportedEvents returns a list of all event types supported by the system
func (ms *MetadataService) GetSupportedEvents() []string {
	return []string{
		string(events.RecordBeforeCreate),
		string(events.RecordAfterCreate),
		string(events.RecordBeforeUpdate),
		string(events.RecordAfterUpdate),
		string(events.RecordBeforeDelete),
		string(events.RecordAfterDelete),
	}
}

// HasActiveRecordTriggeredFlow reports whether objectAPIName currently has
// any active record-trigger subscriber in _System_TriggerSubscription. Used by
// OutboxService to decide whether outboxNoSubscriberObjects events for this
// object can be dropped: dropping is only safe when no runtime subscriber is
// present, otherwise the dispatcher would silently never fire.
//
// Returns true (fail-safe = "subscriber present") on any schema or runtime
// lookup failure so the OutboxService never silently drops events when the
// listener state is unknown.
func (ms *MetadataService) HasActiveRecordTriggeredFlow(ctx context.Context, objectAPIName string) bool {
	schema := ms.GetSchema(ctx, objectAPIName)
	if schema == nil {
		log.Printf("\u26a0\ufe0f HasActiveRecordTriggeredFlow: schema lookup failed for %s", objectAPIName)
		return true
	}
	if schema.KeyPrefix == nil || strings.TrimSpace(*schema.KeyPrefix) == "" {
		log.Printf("\u26a0\ufe0f HasActiveRecordTriggeredFlow: key prefix missing for %s", objectAPIName)
		return true
	}

	if ms.UsesRequestBoundRuntimeView() {
		view, err := ms.requireRequestBoundRuntimeMutationView(ctx, "record-trigger subscription metadata")
		return err != nil || view.HasTriggerSubscription(strings.TrimSpace(*schema.KeyPrefix))
	}
	hasSubscriber, err := ms.hasAnyActiveRecordTriggerSubscription(ctx, *schema.KeyPrefix)
	if err != nil {
		log.Printf("\u26a0\ufe0f HasActiveRecordTriggeredFlow: trigger subscription lookup failed for %s: %v", objectAPIName, err)
		return true
	}
	return hasSubscriber
}

// HasActiveRecordTriggeredFlowForEvent reports whether the immutable runtime
// subscription set has a consumer for one exact record-trigger phase. It is
// fail-open: callers use false only to omit work whose sole consumer is that
// phase, so any metadata uncertainty must retain the compatibility behavior.
func (ms *MetadataService) HasActiveRecordTriggeredFlowForEvent(
	ctx context.Context,
	objectAPIName, eventType, recordTriggerType string,
) bool {
	if ms == nil {
		return true
	}
	schema := ms.GetSchema(ctx, objectAPIName)
	if schema == nil || schema.KeyPrefix == nil || strings.TrimSpace(*schema.KeyPrefix) == "" {
		return true
	}
	rows, err := ms.ListActiveTriggerSubscriptions(
		ctx,
		strings.TrimSpace(*schema.KeyPrefix),
		strings.TrimSpace(eventType),
		strings.TrimSpace(recordTriggerType),
	)
	return err != nil || len(rows) != 0
}

func (ms *MetadataService) GetValidationRules(ctx context.Context, objectAPIName string) []*models.ValidationRule {
	// Skip validation rule lookup for system objects - they never have custom rules
	if constants.IsSystemTable(objectAPIName) {
		return []*models.ValidationRule{}
	}
	if ms.UsesRequestBoundRuntimeView() {
		view, ok := ms.runtimeView(ctx)
		if !ok || view == nil {
			return []*models.ValidationRule{}
		}
		return view.ValidationRulesForObject(objectAPIName)
	}

	if err := ms.ensureCacheInitialized(ctx); err != nil {
		log.Printf("\u26a0\ufe0f Failed to initialize cache in GetValidationRules: %v", err)
		return []*models.ValidationRule{}
	}

	snap := ms.loadCache()
	if snap == nil {
		return []*models.ValidationRule{}
	}
	rules := snap.validationRulesMap[strings.ToLower(objectAPIName)]
	if rules == nil {
		return []*models.ValidationRule{}
	}
	return rules
}

func (ms *MetadataService) GetValidationRulesStrict(ctx context.Context, objectAPIName string) ([]*models.ValidationRule, error) {
	if constants.IsSystemTable(objectAPIName) {
		return []*models.ValidationRule{}, nil
	}
	if ms != nil && ms.UsesRequestBoundRuntimeView() {
		view, ok := ms.runtimeView(ctx)
		if !ok || view == nil {
			return nil, fmt.Errorf("request-bound Runtime metadata view is required")
		}
		return view.ValidationRulesForObject(objectAPIName), nil
	}
	if ms == nil || ms.repo == nil {
		return nil, fmt.Errorf("metadata repository is required to load validation rules")
	}
	rules, err := ms.repo.GetValidationRules(ctx, objectAPIName)
	if err != nil {
		return nil, err
	}
	out := make([]*models.ValidationRule, len(rules))
	copy(out, rules)
	return out, nil
}

// GetValidationRulesForMutation is the fail-closed validation-rule read used
// by record writes. A request-bound Runtime may execute mutations only from a
// complete record-mutation projection; treating a missing view or a read-only
// projection as "no validation rules" would silently weaken write semantics.
//
// The legacy in-process composition keeps its existing immutable cache read so
// this ownership hardening does not introduce a repository query per write.
func (ms *MetadataService) GetValidationRulesForMutation(ctx context.Context, objectAPIName string) ([]*models.ValidationRule, error) {
	if ms == nil {
		return nil, fmt.Errorf("metadata service is required to load mutation validation rules")
	}
	if ms.UsesRequestBoundRuntimeView() {
		view, err := ms.requireRequestBoundRuntimeMutationView(ctx, "record mutation validation rules")
		if err != nil {
			return nil, err
		}
		return view.ValidationRulesForObject(objectAPIName), nil
	}
	if constants.IsSystemTable(objectAPIName) {
		return []*models.ValidationRule{}, nil
	}
	if err := ms.ensureCacheInitialized(ctx); err != nil {
		return nil, fmt.Errorf("initialize metadata cache: %w", err)
	}
	snap := ms.loadCache()
	if snap == nil {
		return nil, fmt.Errorf("metadata cache is unavailable")
	}
	rules := snap.validationRulesMap[strings.ToLower(objectAPIName)]
	out := make([]*models.ValidationRule, len(rules))
	copy(out, rules)
	return out, nil
}

func (ms *MetadataService) GetListViews(ctx context.Context, objectAPIName string) []*models.ListView {
	ms.mu.RLock()
	defer ms.mu.RUnlock()
	views, err := ms.repo.GetListViews(ctx, objectAPIName)
	if err != nil {
		log.Printf("Failed to get list views: %v", err)
		return defaultListViews(objectAPIName)
	}
	if len(views) == 0 {
		return defaultListViews(objectAPIName)
	}
	return views
}

type rlsFilteredListViewReader interface {
	GetListViewsWithReadFilter(ctx context.Context, objectAPIName string, readFilterSQL string) ([]*models.ListView, error)
}

func (ms *MetadataService) GetListViewsForUser(ctx context.Context, objectAPIName string, user *models.UserSession) []*models.ListView {
	ms.mu.RLock()
	defer ms.mu.RUnlock()

	views, err := ms.listViewsForUserLocked(ctx, objectAPIName, user)
	if err != nil {
		log.Printf("Failed to get caller-scoped list views: %v", err)
		return defaultListViews(objectAPIName)
	}
	out := defaultListViews(objectAPIName)
	out = append(out, views...)
	return out
}

func (ms *MetadataService) listViewsForUserLocked(ctx context.Context, objectAPIName string, user *models.UserSession) ([]*models.ListView, error) {
	if user != nil && strings.TrimSpace(user.ID) != "" {
		if reader, ok := ms.repo.(rlsFilteredListViewReader); ok && ms.listViewRLSProvider != nil {
			return reader.GetListViewsWithReadFilter(ctx, objectAPIName, ms.listViewReadFilterSQL(ctx, user))
		}
	}
	return ms.repo.GetListViews(ctx, objectAPIName)
}

func (ms *MetadataService) listViewReadFilterSQL(ctx context.Context, user *models.UserSession) string {
	schema := ms.GetSchema(ctx, constants.TableListView)
	if schema == nil {
		return "1 = 0"
	}
	result := ms.listViewRLSProvider.BuildRLSResult(ctx, schema, constants.TableListView, user)
	if result == nil {
		return "1 = 0"
	}
	return domainrls.ToSQLFilter(result, constants.TableListView)
}

// defaultListViews returns the three Salesforce-standard list views (All, My,
// Recently Viewed) that every object provides by default. These are used as a
// fallback when _System_ListView has no rows for the object, preserving
// backward compatibility with environments that have not bootstrapped list views.
func defaultListViews(objectAPIName string) []*models.ListView {
	return []*models.ListView{
		{
			ID:            fmt.Sprintf("%s-all-view", objectAPIName),
			ObjectAPIName: objectAPIName,
			Label:         fmt.Sprintf("All %s", objectAPIName),
		},
		{
			ID:            fmt.Sprintf("%s-my-view", objectAPIName),
			ObjectAPIName: objectAPIName,
			Label:         fmt.Sprintf("My %s", objectAPIName),
			FilterExpr:    "OwnerId = @user_id",
		},
		{
			ID:            fmt.Sprintf("%s-recent-view", objectAPIName),
			ObjectAPIName: objectAPIName,
			Label:         "Recently Viewed",
		},
	}
}

func (ms *MetadataService) GetSharingRules(ctx context.Context, objectAPIName string) []*models.SharingRule {
	if ms.UsesRequestBoundRuntimeView() {
		view, ok := ms.runtimeView(ctx)
		if !ok || view == nil {
			return []*models.SharingRule{}
		}
		return view.SharingRulesForObject(objectAPIName)
	}
	ms.mu.RLock()
	defer ms.mu.RUnlock()
	rules, err := ms.repo.GetSharingRules(ctx, objectAPIName)
	if err != nil {
		log.Printf("Failed to get sharing rules: %v", err)
		return []*models.SharingRule{}
	}
	return rules
}

func (ms *MetadataService) GetActions(ctx context.Context, objectAPIName string) []*models.ActionMetadata {
	ms.mu.RLock()
	defer ms.mu.RUnlock()
	actions, err := ms.repo.GetActions(ctx, objectAPIName)
	if err != nil {
		log.Printf("Failed to get actions: %v", err)
		return []*models.ActionMetadata{}
	}
	return actions
}

// GetAllActions returns all actions from all objects.
// Read failures are logged and collapse to an empty slice — acceptable only
// for callers where "no actions" and "store unavailable" may behave alike.
// Execution-critical callers MUST use GetAllActionsAndError instead so a
// metadata read outage cannot masquerade as "action not found".
func (ms *MetadataService) GetAllActions(ctx context.Context) []*models.ActionMetadata {
	actions, err := ms.GetAllActionsAndError(ctx)
	if err != nil {
		log.Printf("Failed to query actions: %v", err)
		return []*models.ActionMetadata{}
	}
	return actions
}

// GetAllActionsAndError returns all actions from all objects, propagating a
// repository read failure to the caller instead of swallowing it. Flow
// action-call name resolution uses this to distinguish "no match" (NotFound /
// fault connector) from "metadata store unavailable" (internal fault).
func (ms *MetadataService) GetAllActionsAndError(ctx context.Context) ([]*models.ActionMetadata, error) {
	ms.mu.RLock()
	defer ms.mu.RUnlock()

	return ms.repo.GetAllActions(ctx)
}

// NOTE: there is deliberately NO unfiltered action read on the service —
// every service-level consumer (execution dispatch, flow action-call,
// standard-action invoke, listing, resend authority) resolves live rows only
// via GetLiveActionByID/GetActions/GetAllActions. The unfiltered row read
// (repo.GetAction) exists only inside the metadata upsert/revive write path,
// which must see soft-deleted rows to decide create-vs-revive.

// GetLiveActionByID returns the action by ID excluding soft-deleted rows
// (live = COALESCE(IsDeleted, 0) = 0; legacy warm-upgraded NULL rows are
// live). Used by operator-resend authority checks AND normal execution
// dispatch (ActionService.findAction). Returns (nil, nil) if the action is
// missing or soft-deleted; returns (nil, err) on a real DB failure so callers
// can distinguish a transient read error from a genuine not-found.
func (ms *MetadataService) GetLiveActionByID(ctx context.Context, actionID string) (*models.ActionMetadata, error) {
	ms.mu.RLock()
	defer ms.mu.RUnlock()
	return ms.repo.GetLiveAction(ctx, actionID)
}

func (ms *MetadataService) GetRecordTypes(ctx context.Context, objectAPIName string) ([]*models.RecordType, error) {
	if ms == nil {
		return nil, fmt.Errorf("metadata service is required to load record types")
	}
	if ms.UsesRequestBoundRuntimeView() {
		view, err := ms.requireRequestBoundRuntimeMutationView(ctx, "record-type mutation metadata")
		if err != nil {
			return nil, err
		}
		return view.RecordTypesForObject(objectAPIName), nil
	}
	if ms.repo == nil {
		return nil, fmt.Errorf("metadata repository is required to load record types")
	}
	ms.mu.RLock()
	defer ms.mu.RUnlock()
	rts, err := ms.repo.GetRecordTypes(ctx, objectAPIName)
	if err != nil {
		return nil, fmt.Errorf("load record types for %s: %w", objectAPIName, err)
	}
	return rts, nil
}

func (ms *MetadataService) GetRelationships(ctx context.Context, objectAPIName string) []*models.Relationship {
	if ms.UsesRequestBoundRuntimeView() {
		view, ok := ms.runtimeView(ctx)
		if !ok || view == nil {
			return []*models.Relationship{}
		}
		return view.RelationshipsForChild(objectAPIName)
	}
	if err := ms.ensureCoreCacheInitialized(ctx); err != nil {
		log.Printf("⚠️ Failed to initialize core cache in GetRelationships(%s): %v", objectAPIName, err)
		return []*models.Relationship{}
	}

	snap := ms.loadCoreCache()
	if snap == nil {
		return []*models.Relationship{}
	}

	rels := snap.relationshipsMap[strings.ToLower(objectAPIName)]
	if len(rels) == 0 {
		return []*models.Relationship{}
	}
	return rels
}

func (ms *MetadataService) GetFieldDependencies(ctx context.Context, objectAPIName string) []*models.FieldDependency {
	ms.mu.RLock()
	defer ms.mu.RUnlock()
	fds, err := ms.repo.GetFieldDependencies(ctx, objectAPIName)
	if err != nil {
		return []*models.FieldDependency{}
	}
	return fds
}

func (ms *MetadataService) GetChildRelationships(ctx context.Context, parentObjectAPIName string) []*models.ObjectMetadata {
	if ms.UsesRequestBoundRuntimeView() {
		view, ok := ms.runtimeView(ctx)
		if !ok || view == nil {
			return []*models.ObjectMetadata{}
		}
		seen := make(map[string]struct{})
		children := make([]*models.ObjectMetadata, 0)
		for _, relationship := range view.RelationshipsForParent(parentObjectAPIName) {
			key := strings.ToLower(relationship.ChildObjectAPIName)
			if _, duplicate := seen[key]; duplicate {
				continue
			}
			child, exists := view.Object(relationship.ChildObjectAPIName)
			if !exists {
				continue
			}
			seen[key] = struct{}{}
			children = append(children, child)
		}
		return children
	}
	if err := ms.ensureCoreCacheInitialized(ctx); err != nil {
		log.Printf("⚠️ Failed to initialize core cache in GetChildRelationships(%s): %v", parentObjectAPIName, err)
		return []*models.ObjectMetadata{}
	}

	snap := ms.loadCoreCache()
	if snap == nil {
		return []*models.ObjectMetadata{}
	}

	children := snap.childRelationshipsMap[strings.ToLower(parentObjectAPIName)]
	if len(children) == 0 {
		return []*models.ObjectMetadata{}
	}

	return children
}
