package projects

import (
	"errors"
	"strings"

	"gorm.io/gorm"
	"senlinai-agent/backend/internal/models"
)

type Service struct{}

func NewService() *Service {
	return &Service{}
}

func (s *Service) CreateProject(ownerID uint, name string, description string) (*models.SenlinAgentProject, error) {
	return s.CreateProjectWithInput(ownerID, CreateProjectRequest{Name: name, Description: description})
}

func (s *Service) CreateProjectWithInput(ownerID uint, input CreateProjectRequest) (*models.SenlinAgentProject, error) {
	name := strings.TrimSpace(input.Name)
	if name == "" {
		return nil, ErrProjectNameRequired
	}
	identifier := strings.TrimSpace(input.Identifier)
	if identifier == "" {
		identifier = projectInitials(name)
	}
	project := &models.SenlinAgentProject{
		OwnerID: ownerID, Name: name, Identifier: identifier,
		Icon: strings.TrimSpace(input.Icon), Background: strings.TrimSpace(input.Background),
		Description: strings.TrimSpace(input.Description),
	}
	if err := projectWriteError(models.DBService.Create(project).Error); err != nil {
		return nil, err
	}
	return project, nil
}

// ListProjects 只返回当前所有者的公开 DTO，不让 handler 接触或序列化数据库模型。
func (s *Service) ListProjects(ownerID uint) ([]ProjectDTO, error) {
	var projects []models.SenlinAgentProject
	if err := models.DBService.Where("owner_id = ?", ownerID).Order("updated_at desc").Find(&projects).Error; err != nil {
		return nil, err
	}
	result := make([]ProjectDTO, 0, len(projects))
	for _, project := range projects {
		result = append(result, projectDTO(project))
	}
	return result, nil
}

// GetProject 使用公开 identity 和所有权边界读取单个项目。
func (s *Service) GetProject(ownerID uint, identity string) (ProjectDTO, error) {
	project, err := FindOwnedProject(ownerID, identity)
	if err != nil {
		return ProjectDTO{}, err
	}
	return projectDTO(*project), nil
}

var (
	ErrProjectNameRequired       = errors.New("project name is required")
	ErrProjectIdentifierRequired = errors.New("project identifier is required")
	ErrProjectIdentifierConflict = errors.New("project identifier already exists")
)

// projectWriteError 将 Postgres/SQLite 经 Gorm 翻译后的唯一约束错误收敛为稳定业务冲突。
func projectWriteError(err error) error {
	if errors.Is(err, gorm.ErrDuplicatedKey) {
		return ErrProjectIdentifierConflict
	}
	return err
}

// UpdateProject 仅更新项目设置白名单字段，查询和冲突检查均限定在当前所有者内。
func (s *Service) UpdateProject(ownerID uint, identity string, input UpdateProjectRequest) error {
	project, err := FindOwnedProject(ownerID, identity)
	if err != nil {
		return err
	}

	updates := make(map[string]any, 5)
	if input.Name != nil {
		name := strings.TrimSpace(*input.Name)
		if name == "" {
			return ErrProjectNameRequired
		}
		updates["name"] = name
	}
	if input.Identifier != nil {
		identifier := strings.TrimSpace(*input.Identifier)
		if identifier == "" {
			return ErrProjectIdentifierRequired
		}
		updates["identifier"] = identifier
	}
	if input.Icon != nil {
		updates["icon"] = strings.TrimSpace(*input.Icon)
	}
	if input.Background != nil {
		updates["background"] = strings.TrimSpace(*input.Background)
	}
	if input.Description != nil {
		updates["description"] = strings.TrimSpace(*input.Description)
	}
	if len(updates) == 0 {
		return nil
	}
	return projectWriteError(models.DBService.Model(project).Updates(updates).Error)
}

func projectInitials(name string) string {
	words := strings.Fields(name)
	if len(words) == 0 {
		return "P"
	}
	for i := len(words) - 1; i >= 0; i-- {
		word := strings.Trim(words[i], "#_-")
		if isShortCode(word) {
			return strings.ToUpper(word)
		}
	}
	if len(words) == 1 {
		runes := []rune(words[0])
		if len(runes) == 1 {
			return strings.ToUpper(string(runes[0]))
		}
		return strings.ToUpper(string(runes[:2]))
	}
	return strings.ToUpper(string([]rune(words[0])[0]) + string([]rune(words[len(words)-1])[0]))
}

func isShortCode(value string) bool {
	runes := []rune(value)
	if len(runes) == 0 || len(runes) > 3 {
		return false
	}
	hasDigit := false
	for _, r := range runes {
		if r >= '0' && r <= '9' {
			hasDigit = true
			continue
		}
		if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') {
			continue
		}
		return false
	}
	return hasDigit
}
