package uiapi

import (
	"context"
	"time"

	"github.com/nexuscrm/backend/internal/features/metadata"
	"github.com/nexuscrm/backend/internal/features/permissions"
	"github.com/nexuscrm/backend/internal/features/records"
	"github.com/nexuscrm/backend/pkg/formula"
	"github.com/nexuscrm/backend/pkg/logging"
	"github.com/nexuscrm/backend/pkg/utils"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

type Service struct {
	recordSvc   *records.PersistenceService
	metadataSvc *metadata.MetadataService
	permSvc     *permissions.PermissionService
	validator   *metadata.ValidationService
	logger      *logging.Logger
	objCache    *cache
	layoutCache *cache
}

// NewService creates a new UI API service
func NewService(recordSvc *records.PersistenceService, metadataSvc *metadata.MetadataService, permSvc *permissions.PermissionService, logger *logging.Logger) *Service {
	return &Service{
		recordSvc:   recordSvc,
		metadataSvc: metadataSvc,
		permSvc:     permSvc,
		validator:   metadata.NewValidationService(formula.NewEngine()),
		logger:      logger,
		objCache:    newCache(5 * time.Minute),
		layoutCache: newCache(5 * time.Minute),
	}
}

// Close stops all background goroutines (cache cleanup workers)
// Must be called during graceful shutdown to prevent goroutine leaks
func (s *Service) Close() {
	if s.objCache != nil {
		s.objCache.Close()
	}
	if s.layoutCache != nil {
		s.layoutCache.Close()
	}
}

// InvalidateCaches clears both objCache and layoutCache.
// Must be called after any operation that changes object metadata or layouts
// (e.g. UpdateSchema, field changes) to prevent stale API responses.
func (s *Service) InvalidateCaches() {
	if s.objCache != nil {
		s.objCache.InvalidateAll()
	}
	if s.layoutCache != nil {
		s.layoutCache.InvalidateAll()
	}
}

// GetLayoutInternal returns the raw internal layout model (not Salesforce UI API format).
// Used by the save handler to look up existing layout IDs.
func (s *Service) GetLayoutInternal(ctx context.Context, objectApiName string) *models.PageLayout {
	return s.metadataSvc.GetLayout(ctx, objectApiName, nil)
}

// ListLayouts returns all layouts for an object (summary metadata, no UI API transform)
func (s *Service) ListLayouts(ctx context.Context, objectApiName string) ([]*models.PageLayout, error) {
	return s.metadataSvc.GetAllLayouts(ctx, objectApiName)
}

// GetLayoutMetadata returns the raw internal layout model by ID (no UI API transformation).
func (s *Service) GetLayoutMetadata(ctx context.Context, layoutID string) (*models.PageLayout, error) {
	return s.metadataSvc.GetLayoutByID(ctx, layoutID)
}

// GetLayoutByID loads a specific layout by ID and transforms it to UI API format.
// mode should be "View", "Edit", or "Create".
func (s *Service) GetLayoutByID(ctx context.Context, layoutID string, objectType string, mode string) (*Layout, error) {
	internalLayout, err := s.metadataSvc.GetLayoutByID(ctx, layoutID)
	if err != nil {
		return nil, err
	}
	if internalLayout == nil {
		return nil, nil
	}
	return s.transformLayout(ctx, internalLayout, objectType, mode)
}

// SaveLayout persists a layout and invalidates the layout cache for that object
func (s *Service) SaveLayout(ctx context.Context, layout *models.PageLayout) error {
	if err := s.metadataSvc.SaveLayout(ctx, layout); err != nil {
		return err
	}
	s.layoutCache.InvalidateAll()
	return nil
}

// CloneLayout deep-copies a layout with a new name and ID. The clone starts unassigned
// (no LayoutAssignment rows copied) so it won't compete with the source layout's audience.
func (s *Service) CloneLayout(ctx context.Context, sourceLayoutID string, newName string) (*models.PageLayout, error) {
	source, err := s.metadataSvc.GetLayoutByID(ctx, sourceLayoutID)
	if err != nil {
		return nil, err
	}
	if source == nil {
		return nil, nil
	}

	// Deep copy via struct copy — all sub-structs are value types (no shared pointers)
	cloned := *source
	cloned.ID = utils.GenerateID()
	cloned.LayoutName = newName
	cloned.IsDefault = false
	cloned.IsCustomized = false
	cloned.Source = "cloned"
	cloned.GenerationVersion = 0
	cloned.CreatedDate = time.Now()
	cloned.LastModifiedDate = time.Now()

	// Deep copy slices to avoid shared backing arrays
	cloned.Sections = make([]models.PageSection, len(source.Sections))
	copy(cloned.Sections, source.Sections)
	for i, sec := range source.Sections {
		cloned.Sections[i].Fields = make([]string, len(sec.Fields))
		copy(cloned.Sections[i].Fields, sec.Fields)
	}
	cloned.CompactLayout = make([]string, len(source.CompactLayout))
	copy(cloned.CompactLayout, source.CompactLayout)
	cloned.RelatedLists = make([]models.RelatedListConfig, len(source.RelatedLists))
	copy(cloned.RelatedLists, source.RelatedLists)
	cloned.HeaderActions = make([]models.ActionConfig, len(source.HeaderActions))
	copy(cloned.HeaderActions, source.HeaderActions)
	cloned.QuickActions = make([]models.ActionConfig, len(source.QuickActions))
	copy(cloned.QuickActions, source.QuickActions)

	if err := s.metadataSvc.SaveLayout(ctx, &cloned); err != nil {
		return nil, err
	}
	s.layoutCache.InvalidateAll()
	return &cloned, nil
}

// MarkLayoutCustomized marks a layout as admin-modified so bootstrap won't overwrite it
func (s *Service) MarkLayoutCustomized(ctx context.Context, layoutID string) {
	s.metadataSvc.MarkLayoutCustomized(ctx, layoutID)
}

// SetDefaultLayout marks a layout as the default for its object and invalidates the cache.
// actorID is the UI user who authored the wildcard routing choice.
func (s *Service) SetDefaultLayout(ctx context.Context, objectAPIName string, layoutID string, actorID *string) error {
	if err := s.metadataSvc.SetDefaultLayout(ctx, objectAPIName, layoutID, actorID); err != nil {
		return err
	}
	s.layoutCache.InvalidateAll()
	return nil
}

// DeleteLayout removes a layout by ID and invalidates the cache
func (s *Service) DeleteLayout(ctx context.Context, layoutID string) error {
	if err := s.metadataSvc.DeleteLayout(ctx, layoutID); err != nil {
		return err
	}
	s.layoutCache.InvalidateAll()
	return nil
}

// WriteLayoutHistory delegates to MetadataService to write a version history entry.
func (s *Service) WriteLayoutHistory(ctx context.Context, layoutID string, reason string, changedByID *string) {
	s.metadataSvc.WriteLayoutHistory(ctx, layoutID, reason, changedByID)
}

// GetAssignmentsForObject returns all layout assignments for an object.
func (s *Service) GetAssignmentsForObject(ctx context.Context, objectAPIName string) ([]*models.LayoutAssignment, error) {
	return s.metadataSvc.GetAssignmentsForObject(ctx, objectAPIName)
}

// CreateAssignment creates a new layout assignment.
func (s *Service) CreateAssignment(ctx context.Context, assignment *models.LayoutAssignment) error {
	if assignment != nil {
		routeSource := models.LayoutAssignmentRouteSourceAdminAuthored
		assignment.RouteSource = &routeSource
	}
	if err := s.metadataSvc.CreateAssignment(ctx, assignment); err != nil {
		return err
	}
	s.layoutCache.InvalidateAll()
	return nil
}

// DeleteAssignment removes a layout assignment.
func (s *Service) DeleteAssignment(ctx context.Context, id string) error {
	if err := s.metadataSvc.DeleteAssignment(ctx, id); err != nil {
		return err
	}
	s.layoutCache.InvalidateAll()
	return nil
}

// RestoreLayout restores a layout to a previous version from history.
func (s *Service) RestoreLayout(ctx context.Context, layoutID string, versionNumber int) error {
	if err := s.metadataSvc.RestoreLayout(ctx, layoutID, versionNumber); err != nil {
		return err
	}
	s.layoutCache.InvalidateAll()
	return nil
}

// GetApprovalLayouts returns all approval layouts for an object
func (s *Service) GetApprovalLayouts(ctx context.Context, objectApiName string) ([]*models.ApprovalPageLayout, error) {
	return s.metadataSvc.GetAllApprovalLayouts(ctx, objectApiName)
}

// GetApprovalLayoutMetadata returns the raw internal approval layout model by ID
func (s *Service) GetApprovalLayoutMetadata(ctx context.Context, layoutID string) (*models.ApprovalPageLayout, error) {
	return s.metadataSvc.GetApprovalLayoutByID(ctx, layoutID)
}

// SaveApprovalLayout persists an approval layout and invalidates the layout cache for that object
func (s *Service) SaveApprovalLayout(ctx context.Context, layout *models.ApprovalPageLayout) error {
	if err := s.metadataSvc.SaveApprovalLayout(ctx, layout); err != nil {
		return err
	}
	s.layoutCache.InvalidateAll()
	return nil
}

// SetDefaultApprovalLayout marks an approval layout as the default for its object and invalidates the cache
func (s *Service) SetDefaultApprovalLayout(ctx context.Context, objectAPIName string, layoutID string) error {
	if err := s.metadataSvc.SetDefaultApprovalLayout(ctx, objectAPIName, layoutID); err != nil {
		return err
	}
	s.layoutCache.InvalidateAll()
	return nil
}

// DeleteApprovalLayout removes an approval layout by ID and invalidates the cache
func (s *Service) DeleteApprovalLayout(ctx context.Context, layoutID string) error {
	if err := s.metadataSvc.DeleteApprovalLayout(ctx, layoutID); err != nil {
		return err
	}
	s.layoutCache.InvalidateAll()
	return nil
}

// CheckReadPermission verifies the user has object-level Read permission.
func (s *Service) CheckReadPermission(ctx context.Context, objectName string, user *models.UserSession) error {
	return s.permSvc.CheckPermissionOrErrorWithUser(ctx, objectName, constants.PermRead, user)
}

// GetBatchRecordAccess retrieves records by IDs and computes record-level access for each.
func (s *Service) GetBatchRecordAccess(ctx context.Context, objectName string, recordIDs []string, user *models.UserSession) (map[string]*RecordAccess, error) {
	schema := s.metadataSvc.GetSchema(ctx, objectName)
	if schema == nil {
		return map[string]*RecordAccess{}, nil
	}

	// Retrieve all records in batch
	recordMap, err := s.recordSvc.RetrieveMany(ctx, objectName, recordIDs, user)
	if err != nil {
		return nil, err
	}

	// Validation-rule edit locks must use the same raw prior image as the
	// write path; RetrieveMany returns FLS-filtered records for UI payloads.
	// Only fetch raw rows for records already admitted by RetrieveMany so the
	// bypassing raw read cannot widen the access boundary.
	rawRecordMap := map[string]models.SObject(nil)
	if rawRecordIDs := rawRecordIDsForVisibleRecords(recordMap); len(rawRecordIDs) > 0 {
		if rawRecords, rawErr := s.recordSvc.GetManyRaw(ctx, objectName, rawRecordIDs); rawErr == nil {
			rawRecordMap = rawRecords
		}
	}

	// Convert to slice for batch check
	var recs []models.SObject
	for _, rec := range recordMap {
		recs = append(recs, rec)
	}

	// Compute access for all records
	accessResults := s.permSvc.BatchCheckRecordAccess(ctx, schema, recs, user)

	// Convert to API response format
	result := make(map[string]*RecordAccess, len(accessResults))
	for id, access := range accessResults {
		hasEditAccess := access.HasEditAccess
		if hasEditAccess {
			recordForRules := recordMap[id]
			if rawRecordMap != nil {
				if rawRecord := rawRecordMap[id]; rawRecord != nil {
					recordForRules = rawRecord
				}
			}
			hasEditAccess = !s.recordEditBlockedByValidationRules(ctx, schema, recordForRules)
		}
		result[id] = &RecordAccess{
			HasReadAccess:   access.HasReadAccess,
			HasEditAccess:   hasEditAccess,
			HasDeleteAccess: access.HasDeleteAccess,
		}
	}

	return result, nil
}

func rawRecordIDsForVisibleRecords(recordMap map[string]models.SObject) []string {
	if len(recordMap) == 0 {
		return nil
	}
	ids := make([]string, 0, len(recordMap))
	for id := range recordMap {
		if id == "" {
			continue
		}
		ids = append(ids, id)
	}
	return ids
}

// ResolveObjectType deduces ObjectType from ID using metadata lookup
// In Salesforce, ID prefix determines type. In Nexus (UUIDs), we use metadata.
func (s *Service) ResolveObjectType(ctx context.Context, id string, hintType string, user *models.UserSession) (string, error) {
	if hintType != "" {
		return hintType, nil
	}
	if user == nil {
		return "", nil
	}

	schemas := s.metadataSvc.GetSchemas(ctx)
	for _, schema := range schemas {
		if schema.IsSystem {
			continue
		}
		if !s.permSvc.CheckObjectPermissionWithUser(ctx, schema.APIName, constants.PermRead, user) {
			continue
		}

		rec, err := s.recordSvc.Retrieve(ctx, schema.APIName, id, user)
		if err == nil && rec != nil {
			return schema.APIName, nil
		}
	}

	return "", nil
}
