package work

import (
	"context"
	"errors"
	"fmt"
	"tercul/internal/app/analytics"
	"tercul/internal/app/authz"
	"tercul/internal/domain"
	"tercul/internal/domain/search"
	platform_auth "tercul/internal/platform/auth"
	"tercul/internal/platform/log"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/trace"
	"gorm.io/gorm"
)

// WorkCommands contains the command handlers for the work aggregate.
type WorkCommands struct {
	repo         domain.WorkRepository
	authorRepo   domain.AuthorRepository
	userRepo     domain.UserRepository
	searchClient search.SearchClient
	authzSvc     *authz.Service
	analyticsSvc analytics.Service
	tracer       trace.Tracer
}

// NewWorkCommands creates a new WorkCommands handler.
func NewWorkCommands(repo domain.WorkRepository, authorRepo domain.AuthorRepository, userRepo domain.UserRepository, searchClient search.SearchClient, authzSvc *authz.Service, analyticsSvc analytics.Service) *WorkCommands {
	return &WorkCommands{
		repo:         repo,
		authorRepo:   authorRepo,
		userRepo:     userRepo,
		searchClient: searchClient,
		authzSvc:     authzSvc,
		analyticsSvc: analyticsSvc,
		tracer:       otel.Tracer("work.commands"),
	}
}

// CreateWork creates a new work.
func (c *WorkCommands) CreateWork(ctx context.Context, work *domain.Work) (*domain.Work, error) {
	ctx, span := c.tracer.Start(ctx, "CreateWork")
	defer span.End()
	if work == nil {
		return nil, errors.New("work cannot be nil")
	}
	if work.Title == "" {
		return nil, errors.New("work title cannot be empty")
	}
	if work.Language == "" {
		return nil, errors.New("work language cannot be empty")
	}

	userID, ok := platform_auth.GetUserIDFromContext(ctx)
	if !ok {
		return nil, domain.ErrUnauthorized
	}

	user, err := c.userRepo.GetByID(ctx, userID)
	if err != nil {
		return nil, fmt.Errorf("failed to get user for author creation: %w", err)
	}

	// Find or create an author for the user
	author, err := c.authorRepo.FindByName(ctx, user.Username)
	if err != nil {
		if errors.Is(err, domain.ErrEntityNotFound) {
			// Author doesn't exist, create one
			newAuthor := &domain.Author{
				Name: user.Username,
			}
			if err := c.authorRepo.Create(ctx, newAuthor); err != nil {
				return nil, fmt.Errorf("failed to create author for user: %w", err)
			}
			author = newAuthor
		} else {
			// Another error occurred
			return nil, fmt.Errorf("failed to find author: %w", err)
		}
	}

	// Associate the author with the work
	work.Authors = []*domain.Author{author}

	err = c.repo.Create(ctx, work)
	if err != nil {
		return nil, err
	}
	// Index the work in the search client
	var content string
	if len(work.Translations) > 0 {
		content = work.Translations[0].Content
	}
	if err := c.searchClient.IndexWork(ctx, work, content); err != nil {
		// Log the error but don't fail the operation
		log.FromContext(ctx).Warn(fmt.Sprintf("Failed to index work after creation: %v", err))
	}
	return work, nil
}

// UpdateWork updates an existing work after performing an authorization check.
func (c *WorkCommands) UpdateWork(ctx context.Context, work *domain.Work) error {
	ctx, span := c.tracer.Start(ctx, "UpdateWork")
	defer span.End()
	if work == nil {
		return fmt.Errorf("%w: work cannot be nil", domain.ErrValidation)
	}
	if work.ID == 0 {
		return fmt.Errorf("%w: work ID cannot be zero", domain.ErrValidation)
	}

	userID, ok := platform_auth.GetUserIDFromContext(ctx)
	if !ok {
		return domain.ErrUnauthorized
	}

	existingWork, err := c.repo.GetByID(ctx, work.ID)
	if err != nil {
		if errors.Is(err, domain.ErrEntityNotFound) {
			return fmt.Errorf("%w: work with id %d not found", domain.ErrEntityNotFound, work.ID)
		}
		return fmt.Errorf("failed to get work for authorization: %w", err)
	}

	can, err := c.authzSvc.CanEditWork(ctx, userID, existingWork)
	if err != nil {
		return err
	}
	if !can {
		return domain.ErrForbidden
	}

	if work.Title == "" {
		return fmt.Errorf("%w: work title cannot be empty", domain.ErrValidation)
	}
	if work.Language == "" {
		return fmt.Errorf("%w: work language cannot be empty", domain.ErrValidation)
	}

	err = c.repo.Update(ctx, work)
	if err != nil {
		return err
	}
	// Index the work in the search client
	return c.searchClient.IndexWork(ctx, work, work.Description)
}

// DeleteWork deletes a work by ID after performing an authorization check.
func (c *WorkCommands) DeleteWork(ctx context.Context, id uint) error {
	ctx, span := c.tracer.Start(ctx, "DeleteWork")
	defer span.End()
	if id == 0 {
		return fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
	}

	userID, ok := platform_auth.GetUserIDFromContext(ctx)
	if !ok {
		return domain.ErrUnauthorized
	}

	existingWork, err := c.repo.GetByID(ctx, id)
	if err != nil {
		if errors.Is(err, domain.ErrEntityNotFound) {
			return fmt.Errorf("%w: work with id %d not found", domain.ErrEntityNotFound, id)
		}
		return fmt.Errorf("failed to get work for authorization: %w", err)
	}

	can, err := c.authzSvc.CanDeleteWork(ctx, userID, existingWork)
	if err != nil {
		return err
	}
	if !can {
		return domain.ErrForbidden
	}

	return c.repo.Delete(ctx, id)
}

// AnalyzeWork performs linguistic analysis on a work and its translations.
func (c *WorkCommands) AnalyzeWork(ctx context.Context, workID uint) error {
	ctx, span := c.tracer.Start(ctx, "AnalyzeWork")
	defer span.End()
	logger := log.FromContext(ctx).With("workID", workID)

	work, err := c.repo.GetWithTranslations(ctx, workID)
	if err != nil {
		return fmt.Errorf("failed to get work for analysis: %w", err)
	}

	logger.Info("Starting analysis for work")

	// Analyze the parent work's metadata.
	if err := c.analyticsSvc.UpdateWorkReadingTime(ctx, workID); err != nil {
		logger.Error(err, "failed to update work reading time")
	}
	if err := c.analyticsSvc.UpdateWorkComplexity(ctx, workID); err != nil {
		logger.Error(err, "failed to update work complexity")
	}
	if err := c.analyticsSvc.UpdateWorkSentiment(ctx, workID); err != nil {
		logger.Error(err, "failed to update work sentiment")
	}

	// Analyze each translation.
	for _, translation := range work.Translations {
		logger.Info(fmt.Sprintf("Analyzing translation %d", translation.ID))
		if err := c.analyticsSvc.UpdateTranslationReadingTime(ctx, translation.ID); err != nil {
			logger.Error(err, fmt.Sprintf("failed to update translation reading time for translation %d", translation.ID))
		}
		if err := c.analyticsSvc.UpdateTranslationSentiment(ctx, translation.ID); err != nil {
			logger.Error(err, fmt.Sprintf("failed to update translation sentiment for translation %d", translation.ID))
		}
	}

	logger.Info("Finished analysis for work")
	return nil
}

// MergeWork merges two works, moving all associations from the source to the target and deleting the source.
func (c *WorkCommands) MergeWork(ctx context.Context, sourceID, targetID uint) error {
	ctx, span := c.tracer.Start(ctx, "MergeWork")
	defer span.End()
	if sourceID == targetID {
		return fmt.Errorf("%w: source and target work IDs cannot be the same", domain.ErrValidation)
	}

	userID, ok := platform_auth.GetUserIDFromContext(ctx)
	if !ok {
		return domain.ErrUnauthorized
	}

	// The repo is a domain.WorkRepository, which embeds domain.BaseRepository.
	// We can use the WithTx method from the base repository to run the merge in a transaction.
	err := c.repo.WithTx(ctx, func(tx *gorm.DB) error {
		// We need to use the transaction `tx` for all operations inside this function.
		// For repository methods that are not on the base repository, we need to
		// create a new repository instance that uses the transaction.
		// However, since we added `GetWithAssociationsInTx`, we can pass the tx directly.

		// Authorization: Ensure user can edit both works
		sourceWork, err := c.repo.GetWithAssociationsInTx(ctx, tx, sourceID)
		if err != nil {
			return fmt.Errorf("failed to get source work: %w", err)
		}
		targetWork, err := c.repo.GetWithAssociationsInTx(ctx, tx, targetID)
		if err != nil {
			return fmt.Errorf("failed to get target work: %w", err)
		}

		canEditSource, err := c.authzSvc.CanEditWork(ctx, userID, sourceWork)
		if err != nil {
			return err
		}
		canEditTarget, err := c.authzSvc.CanEditWork(ctx, userID, targetWork)
		if err != nil {
			return err
		}
		if !canEditSource || !canEditTarget {
			return domain.ErrForbidden
		}

		// Merge WorkStats
		if err = mergeWorkStats(tx, sourceID, targetID); err != nil {
			return err
		}

		// Merge translations intelligently to avoid unique constraint violations.
		targetLanguages := make(map[string]bool)
		for _, t := range targetWork.Translations {
			targetLanguages[t.Language] = true
		}

		for _, sTranslation := range sourceWork.Translations {
			if _, exists := targetLanguages[sTranslation.Language]; !exists {
				// No conflict, re-associate this translation with the target work.
				if err := tx.Model(&sTranslation).Update("translatable_id", targetID).Error; err != nil {
					return fmt.Errorf("failed to merge translation for language %s: %w", sTranslation.Language, err)
				}
			}
			// If a translation for the language already exists on the target, we do nothing.
			// The source translation will be implicitly deleted with the source work.
		}

		// Append many-to-many associations
		if err = tx.Model(targetWork).Association("Authors").Append(sourceWork.Authors); err != nil {
			return fmt.Errorf("failed to merge authors: %w", err)
		}
		if err = tx.Model(targetWork).Association("Tags").Append(sourceWork.Tags); err != nil {
			return fmt.Errorf("failed to merge tags: %w", err)
		}
		if err = tx.Model(targetWork).Association("Categories").Append(sourceWork.Categories); err != nil {
			return fmt.Errorf("failed to merge categories: %w", err)
		}
		if err = tx.Model(targetWork).Association("Copyrights").Append(sourceWork.Copyrights); err != nil {
			return fmt.Errorf("failed to merge copyrights: %w", err)
		}
		if err = tx.Model(targetWork).Association("Monetizations").Append(sourceWork.Monetizations); err != nil {
			return fmt.Errorf("failed to merge monetizations: %w", err)
		}

		// Finally, delete the source work.
		if err = tx.Select("Authors", "Tags", "Categories", "Copyrights", "Monetizations").Delete(sourceWork).Error; err != nil {
			return fmt.Errorf("failed to delete source work associations: %w", err)
		}
		if err = tx.Delete(&domain.Work{}, sourceID).Error; err != nil {
			return fmt.Errorf("failed to delete source work: %w", err)
		}

		// Re-index the target work in the search client *after* the transaction commits.
		// We can't do it here, so we'll do it after the WithTx call.
		return nil
	})

	if err != nil {
		return err
	}

	// Re-index the target work in the search client now that the transaction is committed.
	targetWork, err := c.repo.GetByID(ctx, targetID)
	if err == nil && targetWork != nil {
		if searchErr := c.searchClient.IndexWork(ctx, targetWork, ""); searchErr != nil {
			// Log the error but don't fail the main operation
			log.FromContext(ctx).Warn(fmt.Sprintf("Failed to re-index target work after merge: %v", searchErr))
		}
	}

	return nil
}

func mergeWorkStats(tx *gorm.DB, sourceWorkID, targetWorkID uint) error {
	var sourceStats domain.WorkStats
	err := tx.Where("work_id = ?", sourceWorkID).First(&sourceStats).Error
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return fmt.Errorf("failed to get source work stats: %w", err)
	}

	// If source has no stats, there's nothing to do.
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil
	}

	// Store the original ID to delete later, as the sourceStats.ID might be overwritten.
	originalSourceStatsID := sourceStats.ID

	var targetStats domain.WorkStats
	err = tx.Where("work_id = ?", targetWorkID).First(&targetStats).Error

	if errors.Is(err, gorm.ErrRecordNotFound) {
		// If target has no stats, create a new stats record for it.
		newStats := sourceStats
		newStats.ID = 0
		newStats.WorkID = targetWorkID
		if err = tx.Create(&newStats).Error; err != nil {
			return fmt.Errorf("failed to create new target stats: %w", err)
		}
	} else if err != nil {
		return fmt.Errorf("failed to get target work stats: %w", err)
	} else {
		// Both have stats, so add source to target.
		targetStats.Add(&sourceStats)
		if err = tx.Save(&targetStats).Error; err != nil {
			return fmt.Errorf("failed to save merged target stats: %w", err)
		}
	}

	// Delete the old source stats using the stored original ID.
	if err = tx.Delete(&domain.WorkStats{}, originalSourceStatsID).Error; err != nil {
		return fmt.Errorf("failed to delete source work stats: %w", err)
	}

	return nil
}