package metadata

import (
	"context"
	"time"

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

// --- App Methods ---

func (ms *MetadataService) GetApps(ctx context.Context) []*models.AppConfig {
	ms.mu.RLock()
	defer ms.mu.RUnlock()
	apps, err := ms.repo.GetAllApps(ctx)
	if err != nil {
		log.Error().Err(err).Msg("Failed to get apps")
		return []*models.AppConfig{}
	}
	return apps
}

func (ms *MetadataService) GetApp(ctx context.Context, id string) *models.AppConfig {
	ms.mu.RLock()
	defer ms.mu.RUnlock()
	app, err := ms.repo.GetApp(ctx, id)
	if err != nil {
		log.Warn().Err(err).Str("appId", id).Msg("Failed to query app")
		return nil
	}
	return app
}

func (ms *MetadataService) GetAppWithTx(ctx context.Context, tx interface{}, id string) (*models.AppConfig, error) {
	// No invalidation lock needed for read in Tx usually, but logic depends on consistency needs.
	// We delegate to repo.
	return ms.repo.GetAppWithTx(ctx, tx, id)
}

// CreateApp creates a new app configuration
func (ms *MetadataService) CreateApp(ctx context.Context, app *models.AppConfig) error {
	ms.mu.Lock()
	defer ms.mu.Unlock()

	// Validate
	if app.ID == "" {
		return apperrors.NewValidationError("id", "app ID is required")
	}
	if app.Label == "" {
		return apperrors.NewValidationError("label", "app label is required")
	}

	// Check if exists
	existing, err := ms.repo.GetApp(ctx, app.ID)
	if err != nil {
		return apperrors.NewInternalError("failed to check app existence", err)
	}
	if existing != nil {
		return apperrors.NewConflictError("AppConfig", constants.FieldID, app.ID)
	}

	// Insert into DB via Repo
	app.CreatedDate = time.Now()
	app.LastModifiedDate = time.Now()
	if err := ms.repo.CreateApp(ctx, app); err != nil {
		return apperrors.NewInternalError("failed to insert app", err)
	}

	// Sync to AppMenuItem for App Launcher visibility (Salesforce parity)
	if err := ms.repo.CreateAppMenuItem(ctx, app); err != nil {
		log.Warn().Err(err).Str("appId", app.ID).Msg("Failed to create AppMenuItem")
		// Non-fatal: App is created, just won't appear in launcher until fixed
	}

	return nil
}

// UpdateApp updates an existing app configuration
func (ms *MetadataService) UpdateApp(ctx context.Context, appID string, updates *models.AppConfig) error {
	ms.mu.Lock()
	defer ms.mu.Unlock()

	// Check if exists
	existing, err := ms.repo.GetApp(ctx, appID)
	if err != nil {
		return apperrors.NewInternalError("failed to check app existence", err)
	}
	if existing == nil {
		return apperrors.NewNotFoundError("AppConfig", appID)
	}

	// Ensure ID doesn't change
	updates.ID = appID

	// Merge updates with existing values - only update fields that are provided
	// We update `existing` object with `updates` values, then save `existing`.
	if updates.Label != "" {
		existing.Label = updates.Label
	}
	if updates.Description != "" {
		existing.Description = updates.Description
	}
	if updates.Icon != "" {
		existing.Icon = updates.Icon
	}
	if updates.Color != "" {
		existing.Color = updates.Color
	}
	// Note: IsDefault is a boolean and lacks "not set" state in the struct.
	// If updates.IsDefault is false (default), it will overwrite existing true.
	// This assumes 'updates' contains the authoritative state for IsDefault.
	existing.IsDefault = updates.IsDefault

	if updates.NavigationItems != nil {
		existing.NavigationItems = updates.NavigationItems
	}
	if updates.UtilityItems != nil {
		existing.UtilityItems = updates.UtilityItems
	}
	// AssignedProfiles: nil means "don't change", empty slice means "visible to all"
	if updates.AssignedProfiles != nil {
		existing.AssignedProfiles = updates.AssignedProfiles
	}

	existing.LastModifiedDate = time.Now()

	// Update DB via Repo
	if err := ms.repo.UpdateApp(ctx, appID, existing); err != nil {
		return apperrors.NewInternalError("failed to update app", err)
	}

	return nil
}

// UpdateAppTx updates an existing app configuration within a transaction
func (ms *MetadataService) UpdateAppTx(ctx context.Context, tx interface{}, appID string, updates *models.AppConfig) error {
	// Note: We don't lock mutex here because caller handles transaction and concurrency usually means specialized flow.
	// But strictly, we should be careful. Since it's a DB transaction, DB locks apply.
	// We should update the DB via Repo.

	// Ensure ID doesn't change
	updates.ID = appID
	updates.LastModifiedDate = time.Now()

	// Update DB via Repo with Tx (which accepts interface{})
	if err := ms.repo.UpdateAppWithTx(ctx, tx, appID, updates); err != nil {
		return apperrors.NewInternalError("failed to update app in tx", err)
	}

	return nil
}

// DeleteApp deletes an app configuration
func (ms *MetadataService) DeleteApp(ctx context.Context, appID string) error {
	ms.mu.Lock()
	defer ms.mu.Unlock()

	return ms.repo.DeleteApp(ctx, appID)
}
