package metadata

import (
	"context"
	"strings"
	"time"

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

// --- App Methods ---

func (ms *MetadataService) GetApps(ctx context.Context) ([]*models.AppConfig, error) {
	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 nil, err
	}
	return apps, nil
}

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")
	}
	app.FrontendAppID = strings.TrimSpace(app.FrontendAppID)
	if app.FrontendAppID == "" {
		return apperrors.NewValidationError("frontendAppId", "frontend app owner 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()
	app.AssignedProfiles = nil
	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 {
	return ms.updateAppWithPatchPresence(ctx, appID, updates, true, nil)
}

// UpdateAppWithPatchPresence updates an existing app configuration while
// letting sparse PATCH callers preserve omitted bool fields.
func (ms *MetadataService) UpdateAppWithPatchPresence(ctx context.Context, appID string, updates *models.AppConfig, isDefaultProvided bool) error {
	return ms.updateAppWithPatchPresence(ctx, appID, updates, isDefaultProvided, nil)
}

// UpdateAppWithPatchPresenceAndNavigationIdentities preserves existing rich
// navigation items for sparse PATCH rows that still resolve to the same neutral
// navigation identity.
func (ms *MetadataService) UpdateAppWithPatchPresenceAndNavigationIdentities(ctx context.Context, appID string, updates *models.AppConfig, isDefaultProvided bool, identities []contract.ApplicationNavigationItemIdentity) error {
	return ms.updateAppWithPatchPresence(ctx, appID, updates, isDefaultProvided, identities)
}

func (ms *MetadataService) updateAppWithPatchPresence(ctx context.Context, appID string, updates *models.AppConfig, isDefaultProvided bool, identities []contract.ApplicationNavigationItemIdentity) 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
	}
	if updates.FrontendAppID != "" {
		existing.FrontendAppID = updates.FrontendAppID
	}
	if isDefaultProvided {
		existing.IsDefault = updates.IsDefault
	}

	if updates.NavigationItems != nil {
		if identities != nil {
			updates.NavigationItems = mergeNavigationPatchItems(existing.NavigationItems, updates.NavigationItems, identities)
		}
		existing.NavigationItems = updates.NavigationItems
	} else {
		// Preserve PATCH omission through the repository boundary. A shell edit
		// must not write a navigation snapshot read before a concurrent save.
		existing.NavigationItems = nil
	}
	if updates.UtilityItems != nil {
		existing.UtilityItems = updates.UtilityItems
	}

	existing.LastModifiedDate = time.Now()

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

	return nil
}

func mergeNavigationPatchItems(existing []models.NavigationItem, requested []models.NavigationItem, identities []contract.ApplicationNavigationItemIdentity) []models.NavigationItem {
	if requested == nil || identities == nil {
		return requested
	}

	existingByTab := make(map[string]models.NavigationItem, len(existing))
	for _, item := range existing {
		if key := navigationItemIdentityKey(navigationItemIdentity(item)); key != "" {
			existingByTab[key] = item
		}
	}

	merged := make([]models.NavigationItem, 0, len(identities))
	for i, identity := range identities {
		if existingItem, ok := existingByTab[navigationItemIdentityKey(identity)]; ok {
			merged = append(merged, existingItem)
			continue
		}
		if i < len(requested) {
			merged = append(merged, requested[i])
		}
	}
	return merged
}

func navigationItemIdentity(item models.NavigationItem) contract.ApplicationNavigationItemIdentity {
	switch item.Type {
	case constants.NavigationTypeObject:
		return contract.ApplicationNavigationItemIdentity{
			Kind: contract.ApplicationNavigationItemIdentityObject,
			Key:  item.ObjectAPIName,
		}
	case constants.NavigationTypePage:
		if tabName, ok := item.CanonicalTabName(); ok {
			return contract.ApplicationNavigationItemIdentity{
				Kind: contract.ApplicationNavigationItemIdentityPage,
				Key:  tabName,
			}
		}
	}
	if item.ID != "" {
		return contract.ApplicationNavigationItemIdentity{
			Kind: contract.ApplicationNavigationItemIdentityID,
			Key:  item.ID,
		}
	}
	return contract.ApplicationNavigationItemIdentity{}
}

func navigationItemIdentityKey(identity contract.ApplicationNavigationItemIdentity) string {
	key := strings.ToLower(strings.TrimSpace(identity.Key))
	if key == "" || identity.Kind == "" {
		return ""
	}
	return string(identity.Kind) + ":" + key
}

// 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)
}
