package service

import (
	"context"
	"fmt"
	"strings"

	"github.com/pingcap/agent-git-service/internal/db"
	"gorm.io/gorm"
)

const maxOrganizationProfilePinsPerAudience = 6

// OrganizationProfilePinInput is one ordered profile repository. RepositoryID
// is resolved by the transport layer after verifying it belongs to the target
// organization, so callers cannot pin repositories they do not administer.
type OrganizationProfilePinInput struct {
	RepositoryID uint
	Audience     string
}

func normalizeOrganizationProfileAudience(value string) (string, bool) {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "", db.OrganizationProfileAudiencePublic:
		return db.OrganizationProfileAudiencePublic, true
	case db.OrganizationProfileAudienceMember:
		return db.OrganizationProfileAudienceMember, true
	default:
		return "", false
	}
}

// ListOrganizationProfilePins returns public pins for everyone and member pins
// only to active organization members. The access check belongs here instead of
// the web client so a direct API caller cannot discover internal priorities.
func (s *Service) ListOrganizationProfilePins(ctx context.Context, orgID uint) ([]db.OrganizationProfilePin, error) {
	if orgID == 0 {
		return []db.OrganizationProfilePin{}, nil
	}
	audiences := []string{db.OrganizationProfileAudiencePublic}
	if viewer, ok := UserFromContext(ctx); ok && viewer.ID != 0 {
		member, err := s.IsOrgMember(ctx, orgID, viewer.ID)
		if err != nil {
			return nil, err
		}
		if member {
			audiences = append(audiences, db.OrganizationProfileAudienceMember)
		}
	}
	var pins []db.OrganizationProfilePin
	err := s.DBForCtx(ctx).
		Preload("Repository.Owner").
		Where("organization_id = ? AND audience IN ?", orgID, audiences).
		Order("CASE audience WHEN 'public' THEN 0 ELSE 1 END, position ASC").
		Find(&pins).Error
	return pins, err
}

// ReplaceOrganizationProfilePins atomically replaces both audience lists. It
// is the only write path so reordering cannot leave a partial pin layout.
func (s *Service) ReplaceOrganizationProfilePins(ctx context.Context, orgID uint, inputs []OrganizationProfilePinInput) ([]db.OrganizationProfilePin, error) {
	if orgID == 0 {
		return nil, ErrValidation
	}
	counts := map[string]int{}
	seen := map[string]struct{}{}
	normalized := make([]OrganizationProfilePinInput, 0, len(inputs))
	for _, input := range inputs {
		if input.RepositoryID == 0 {
			return nil, ErrValidation
		}
		audience, ok := normalizeOrganizationProfileAudience(input.Audience)
		if !ok {
			return nil, fmt.Errorf("%w: audience must be public or member", ErrValidation)
		}
		counts[audience]++
		if counts[audience] > maxOrganizationProfilePinsPerAudience {
			return nil, fmt.Errorf("%w: no more than %d %s profile pins", ErrValidation, maxOrganizationProfilePinsPerAudience, audience)
		}
		key := fmt.Sprintf("%s:%d", audience, input.RepositoryID)
		if _, exists := seen[key]; exists {
			return nil, fmt.Errorf("%w: duplicate profile repository", ErrValidation)
		}
		seen[key] = struct{}{}
		normalized = append(normalized, OrganizationProfilePinInput{RepositoryID: input.RepositoryID, Audience: audience})
	}

	if err := s.DBForCtx(ctx).Transaction(func(tx *gorm.DB) error {
		var owned int64
		ids := make([]uint, 0, len(normalized))
		for _, input := range normalized {
			ids = append(ids, input.RepositoryID)
		}
		if len(ids) > 0 {
			if err := tx.Model(&db.Repository{}).Where("owner_id = ? AND id IN ?", orgID, ids).Count(&owned).Error; err != nil {
				return err
			}
			if owned != int64(len(ids)) {
				return fmt.Errorf("%w: each pinned repository must belong to this organization", ErrValidation)
			}
		}
		if err := tx.Where("organization_id = ?", orgID).Delete(&db.OrganizationProfilePin{}).Error; err != nil {
			return err
		}
		positions := map[string]uint{}
		pins := make([]db.OrganizationProfilePin, 0, len(normalized))
		for _, input := range normalized {
			positions[input.Audience]++
			pins = append(pins, db.OrganizationProfilePin{
				OrganizationID: orgID,
				RepositoryID:   input.RepositoryID,
				Audience:       input.Audience,
				Position:       positions[input.Audience],
			})
		}
		if len(pins) == 0 {
			return nil
		}
		return tx.Create(&pins).Error
	}); err != nil {
		return nil, err
	}
	return s.ListOrganizationProfilePins(ctx, orgID)
}
