package team

import (
	"context"
	"database/sql"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/presets"
)

const (
	TemplateSourceBuiltin = "builtin"
	TemplateSourceCustom  = "custom"
)

const (
	TemplateScopePrivate   = "private"
	TemplateScopeWorkspace = "workspace"
)

// CreateTeamTemplateParams creates one custom Team template.
type CreateTeamTemplateParams struct {
	ID                string
	WorkspaceID       string
	Name              string
	Description       string
	Visibility        string
	Source            string
	Scope             string
	CreatedByMemberID string
	CreatedFromTeamID string
	Members           []CreateTeamTemplateMemberParams
	Now               time.Time
}

// UpdateTeamTemplateParams updates custom Team template metadata.
type UpdateTeamTemplateParams struct {
	Name        string
	Description string
	Scope       string
	Now         time.Time
}

// CreateTeamTemplateMemberParams creates one template member slot.
type CreateTeamTemplateMemberParams struct {
	ID                string
	WorkspaceID       string
	TemplateID        string
	ActorKind         string
	ActorID           string
	Role              string
	RoleLabel         string
	RoleOverlayPrompt string
	WakeOnTeamMention *bool
	SortOrder         int64
}

// ListTeamTemplateFilter describes Team template directory filtering.
type ListTeamTemplateFilter struct {
	Query  string
	Limit  int
	Locale string
}

// TeamTemplateMemberView is one actor slot in a Team template.
type TeamTemplateMemberView = model.WorkspaceTeamTemplateMember

// TeamTemplateView is one Team template plus member slots.
type TeamTemplateView struct {
	Template  model.WorkspaceTeamTemplate
	Members   []TeamTemplateMemberView
	CanManage bool
}

// TeamTemplateMemberBinding binds a template member slot to a concrete actor.
type TeamTemplateMemberBinding struct {
	TemplateMemberID     string
	ActorID              string
	Role                 string
	RoleSet              bool
	RoleLabel            string
	RoleLabelSet         bool
	RoleOverlayPrompt    string
	RoleOverlayPromptSet bool
	WakeOnTeamMention    *bool
}

// InstantiateTeamTemplateParams creates one real Team from a template.
type InstantiateTeamTemplateParams struct {
	TemplateID      string
	Handle          string
	DisplayName     string
	Description     *string
	Visibility      *string
	MemberBindings  []TeamTemplateMemberBinding
	InitialMembers  []AddTeamMemberParams
	CreatedByMember string
	Now             time.Time
	Locale          string
}

type InstantiateTeamTemplateResult struct {
	Team TeamView
}

const (
	DefaultTeamTemplateLimit = 50
	MaxTeamTemplateLimit     = 100
)

var builtinTeamTemplates = loadBuiltinTeamTemplates()

const builtinTeamTemplateDefaultLocale = presets.LocaleEnglish

func loadBuiltinTeamTemplates() map[string][]TeamTemplateView {
	cache := make(map[string][]TeamTemplateView)
	for _, locale := range []string{presets.LocaleEnglish, presets.LocaleSimplifiedChinese, presets.LocaleJapanese} {
		loaded, err := presets.LoadTeamTemplates(locale)
		if err != nil {
			panic(fmt.Sprintf("load built-in team templates for locale %q: %v", locale, err))
		}
		views := make([]TeamTemplateView, 0, len(loaded))
		for _, template := range loaded {
			views = append(views, teamTemplateViewFromPreset(template))
		}
		cache[locale] = views
	}
	return cache
}

func teamTemplateViewFromPreset(template presets.TeamTemplate) TeamTemplateView {
	members := make([]TeamTemplateMemberView, 0, len(template.Members))
	for _, member := range template.Members {
		role := strings.TrimSpace(member.Role)
		if role == "" {
			role = MemberRoleMember
		}
		members = append(members, model.WorkspaceTeamTemplateMember{
			ID:                member.ID,
			ActorKind:         member.ActorKind,
			Role:              role,
			RoleLabel:         member.RoleLabel,
			RoleOverlayPrompt: member.RoleOverlayPrompt,
			WakeOnTeamMention: member.WakeOnTeamMention,
			SortOrder:         member.SortOrder,
		})
	}
	return TeamTemplateView{
		Template: model.WorkspaceTeamTemplate{
			ID:          template.ID,
			Name:        template.Name,
			Description: template.Description,
			Visibility:  template.Visibility,
			Source:      TemplateSourceBuiltin,
			Scope:       template.Scope,
		},
		Members: members,
	}
}

// ListTeamTemplatesForUser lists built-in and custom Team templates.
func (store *Store) ListTeamTemplatesForUser(ctx context.Context, userID string, workspaceID string, filter ListTeamTemplateFilter) ([]TeamTemplateView, error) {
	queryer, err := queryerFromStore(store)
	if err != nil {
		return nil, err
	}
	viewer, err := loadTeamViewer(ctx, queryer, userID, workspaceID)
	if err != nil {
		return nil, err
	}
	limit := boundedLimit(filter.Limit, DefaultTeamTemplateLimit, MaxTeamTemplateLimit)
	query := strings.ToLower(trim(filter.Query))
	views := builtinTeamTemplateViews(workspaceID, query, filter.Locale)
	custom, err := listCustomTeamTemplateViews(ctx, queryer, workspaceID, viewer, query, limit)
	if err != nil {
		return nil, err
	}
	views = append(views, custom...)
	if len(views) > limit {
		views = views[:limit]
	}
	return views, nil
}

// CreateTeamTemplateForUser creates one custom template from an explicit payload.
func (store *Store) CreateTeamTemplateForUser(ctx context.Context, userID string, workspaceID string, params CreateTeamTemplateParams) (TeamTemplateView, error) {
	queryer, err := queryerFromStore(store)
	if err != nil {
		return TeamTemplateView{}, err
	}
	viewer, err := loadTeamViewer(ctx, queryer, userID, workspaceID)
	if err != nil {
		return TeamTemplateView{}, err
	}
	if err := validateUserTeamTemplateMemberParams(ctx, queryer, workspaceID, userID, params.Members); err != nil {
		return TeamTemplateView{}, err
	}
	params.WorkspaceID = trim(workspaceID)
	params.CreatedByMemberID = viewer.memberID
	params.Source = TemplateSourceCustom
	template, err := store.CreateTeamTemplate(ctx, params)
	if err != nil {
		return TeamTemplateView{}, err
	}
	return loadCustomTeamTemplateView(ctx, queryer, workspaceID, template.ID, viewer)
}

// SaveTeamTemplateFromTeamForUser creates a custom template by copying one Team.
func (store *Store) SaveTeamTemplateFromTeamForUser(ctx context.Context, userID string, workspaceID string, teamID string, name string, description string, scope string) (TeamTemplateView, error) {
	queryer, err := queryerFromStore(store)
	if err != nil {
		return TeamTemplateView{}, err
	}
	viewer, err := requireTeamManager(ctx, queryer, userID, workspaceID, teamID)
	if err != nil {
		return TeamTemplateView{}, err
	}
	team, err := loadTeam(ctx, queryer, workspaceID, teamID)
	if err != nil {
		return TeamTemplateView{}, err
	}
	members, err := listActiveTeamMembers(ctx, queryer, workspaceID, teamID)
	if err != nil {
		return TeamTemplateView{}, err
	}
	memberParams := make([]CreateTeamTemplateMemberParams, 0, len(members))
	for _, member := range members {
		if member.ActorKind != ActorKindAgent {
			continue
		}
		wakeOnTeamMention := member.WakeOnTeamMention
		memberParams = append(memberParams, CreateTeamTemplateMemberParams{
			ActorKind:         member.ActorKind,
			ActorID:           member.ActorID,
			Role:              member.Role,
			RoleLabel:         member.RoleLabel,
			RoleOverlayPrompt: member.RoleOverlayPrompt,
			WakeOnTeamMention: &wakeOnTeamMention,
			SortOrder:         member.SortOrder,
		})
	}
	templateName := trim(name)
	if templateName == "" {
		templateName = team.Handle
	}
	templateDescription := description
	if trim(templateDescription) == "" {
		templateDescription = team.Description
	}
	created, err := store.CreateTeamTemplate(ctx, CreateTeamTemplateParams{
		WorkspaceID:       workspaceID,
		Name:              templateName,
		Description:       templateDescription,
		Visibility:        team.Visibility,
		Source:            TemplateSourceCustom,
		Scope:             scope,
		CreatedByMemberID: viewer.memberID,
		CreatedFromTeamID: team.ID,
		Members:           memberParams,
	})
	if err != nil {
		return TeamTemplateView{}, err
	}
	return loadCustomTeamTemplateView(ctx, queryer, workspaceID, created.ID, viewer)
}

// UpdateTeamTemplateForUser updates one custom template metadata row.
func (store *Store) UpdateTeamTemplateForUser(ctx context.Context, userID string, workspaceID string, templateID string, params UpdateTeamTemplateParams) (TeamTemplateView, error) {
	queryer, err := queryerFromStore(store)
	if err != nil {
		return TeamTemplateView{}, err
	}
	viewer, err := loadTeamViewer(ctx, queryer, userID, workspaceID)
	if err != nil {
		return TeamTemplateView{}, err
	}
	if _, ok := builtinTeamTemplateByID(templateID, workspaceID, ""); ok {
		return TeamTemplateView{}, badRequest("built-in team template cannot be updated")
	}
	view, err := loadCustomTeamTemplateView(ctx, queryer, workspaceID, templateID, viewer)
	if err != nil {
		return TeamTemplateView{}, err
	}
	if !view.CanManage {
		return TeamTemplateView{}, forbidden("team template creator or workspace admin is required")
	}
	name, err := normalizeHandle(params.Name)
	if err != nil {
		return TeamTemplateView{}, err
	}
	description, err := normalizeOptionalTeamString(params.Description, MaxTeamDescriptionChars, "team template description")
	if err != nil {
		return TeamTemplateView{}, err
	}
	scopeInput := params.Scope
	if trim(scopeInput) == "" {
		scopeInput = view.Template.Scope
	}
	scope, err := normalizeTemplateScope(scopeInput)
	if err != nil {
		return TeamTemplateView{}, err
	}
	if scope == TemplateScopePrivate && view.Template.CreatedByMemberID != viewer.memberID {
		return TeamTemplateView{}, forbidden("team template creator is required to make a template private")
	}
	now := params.Now.UTC()
	if now.IsZero() {
		now = time.Now().UTC()
	}
	execer, err := execerFromStore(store)
	if err != nil {
		return TeamTemplateView{}, err
	}
	if _, err := execer.Exec(ctx, `
UPDATE workspace_team_templates
SET name = $1, description = $2, template_scope = $3, updated_at = $4
WHERE workspace_id = $5 AND id = $6
`, name, description, scope, now, trim(workspaceID), trim(templateID)); err != nil {
		return TeamTemplateView{}, fmt.Errorf("update team template: %w", mapTeamWriteError(err))
	}
	return loadCustomTeamTemplateView(ctx, queryer, workspaceID, templateID, viewer)
}

// DeleteTeamTemplateForUser deletes one custom template.
func (store *Store) DeleteTeamTemplateForUser(ctx context.Context, userID string, workspaceID string, templateID string) error {
	queryer, err := queryerFromStore(store)
	if err != nil {
		return err
	}
	viewer, err := loadTeamViewer(ctx, queryer, userID, workspaceID)
	if err != nil {
		return err
	}
	if _, ok := builtinTeamTemplateByID(templateID, workspaceID, ""); ok {
		return badRequest("built-in team template cannot be deleted")
	}
	view, err := loadCustomTeamTemplateView(ctx, queryer, workspaceID, templateID, viewer)
	if err != nil {
		return err
	}
	if !view.CanManage {
		return forbidden("team template creator or workspace admin is required")
	}
	execer, err := execerFromStore(store)
	if err != nil {
		return err
	}
	_, err = execer.Exec(ctx, `DELETE FROM workspace_team_templates WHERE workspace_id = $1 AND id = $2`, trim(workspaceID), trim(templateID))
	if err != nil {
		return fmt.Errorf("delete team template: %w", err)
	}
	return nil
}

// GetTeamTemplateForUser returns one Team template with members for the caller.
func (store *Store) GetTeamTemplateForUser(ctx context.Context, userID string, workspaceID string, templateID string, locale string) (TeamTemplateView, error) {
	queryer, err := queryerFromStore(store)
	if err != nil {
		return TeamTemplateView{}, err
	}
	viewer, err := loadTeamViewer(ctx, queryer, userID, workspaceID)
	if err != nil {
		return TeamTemplateView{}, err
	}
	template, members, err := loadTemplateForInstantiate(ctx, queryer, workspaceID, viewer, templateID, locale)
	if err != nil {
		return TeamTemplateView{}, err
	}
	return TeamTemplateView{
		Template:  template,
		Members:   members,
		CanManage: viewer.workspaceAdmin || viewer.workspaceOwner,
	}, nil
}

// InstantiateTeamTemplate creates a real Team from one template for an
// active workspace member.
func (store *Store) InstantiateTeamTemplate(ctx context.Context, userID string, workspaceID string, params InstantiateTeamTemplateParams) (InstantiateTeamTemplateResult, error) {
	queryer, err := queryerFromStore(store)
	if err != nil {
		return InstantiateTeamTemplateResult{}, err
	}
	viewer, err := loadTeamViewer(ctx, queryer, userID, workspaceID)
	if err != nil {
		return InstantiateTeamTemplateResult{}, err
	}
	template, members, err := loadTemplateForInstantiate(ctx, queryer, workspaceID, viewer, params.TemplateID, params.Locale)
	if err != nil {
		return InstantiateTeamTemplateResult{}, err
	}
	if err := validateUserAddedTeamMemberParams(ctx, queryer, workspaceID, userID, params.InitialMembers); err != nil {
		return InstantiateTeamTemplateResult{}, err
	}
	bindings := bindingMap(params.MemberBindings)
	params.CreatedByMember = viewer.memberID
	createdTeamID := ""
	txBeginner, err := txBeginnerFromStore(store)
	if err != nil {
		return InstantiateTeamTemplateResult{}, err
	}
	err = coredb.WithTx(ctx, txBeginner, func(tx coredb.Tx) error {
		created, err := createTeam(ctx, tx, CreateTeamParams{
			WorkspaceID:       workspaceID,
			Handle:            firstNonEmptyString(params.Handle, params.DisplayName, template.Name),
			DisplayName:       firstNonEmptyString(params.DisplayName, params.Handle, template.Name),
			Description:       valueOrDefault(params.Description, template.Description),
			Visibility:        VisibilityPrivate,
			CreatedByMemberID: viewer.memberID,
			Now:               params.Now,
		})
		if err != nil {
			return err
		}
		createdTeamID = created.ID
		seenMembers := make(map[string]struct{})
		for _, templateMember := range members {
			if templateMember.ActorKind != ActorKindAgent {
				continue
			}
			binding, hasBinding := bindings[templateMember.ID]
			actorID := trim(binding.ActorID)
			if actorID == "" {
				if hasBinding {
					continue
				}
				actorID = trim(templateMember.ActorID)
			}
			if actorID == "" {
				continue
			}
			memberKey := activeMemberKey(created.ID, templateMember.ActorKind, actorID, false).(string)
			if _, ok := seenMembers[memberKey]; ok {
				continue
			}
			seenMembers[memberKey] = struct{}{}
			role := templateMember.Role
			if hasBinding && binding.RoleSet {
				role = binding.Role
			}
			roleLabel := templateMember.RoleLabel
			roleOverlayPrompt := templateMember.RoleOverlayPrompt
			if hasBinding && binding.RoleLabelSet {
				roleLabel = binding.RoleLabel
			}
			if hasBinding && binding.RoleOverlayPromptSet {
				roleOverlayPrompt = binding.RoleOverlayPrompt
			}
			wakeOnTeamMention := templateMember.WakeOnTeamMention
			if hasBinding && binding.WakeOnTeamMention != nil {
				wakeOnTeamMention = *binding.WakeOnTeamMention
			}
			_, err := addTeamMember(ctx, tx, AddTeamMemberParams{
				WorkspaceID:       workspaceID,
				TeamID:            created.ID,
				ActorKind:         templateMember.ActorKind,
				ActorID:           actorID,
				Role:              role,
				RoleLabel:         roleLabel,
				RoleOverlayPrompt: roleOverlayPrompt,
				WakeOnTeamMention: &wakeOnTeamMention,
				SortOrder:         templateMember.SortOrder,
				AddedByMemberID:   viewer.memberID,
				Now:               params.Now,
			})
			if err != nil {
				return err
			}
		}
		if err := addInitialTeamMembers(ctx, tx, workspaceID, created.ID, viewer.memberID, params.Now, seenMembers, params.InitialMembers, int64((len(members)+2)*10)); err != nil {
			return err
		}
		if err := requireTeamMemberInvariants(ctx, tx, workspaceID, created.ID); err != nil {
			return err
		}
		if _, err := ensureTeamChannelForTeam(ctx, tx, created, params.Now); err != nil {
			return err
		}
		return nil
	})
	if err != nil {
		return InstantiateTeamTemplateResult{}, err
	}
	loadedTeam, err := loadTeamViewForViewer(ctx, queryer, viewer, workspaceID, createdTeamID)
	if err != nil {
		return InstantiateTeamTemplateResult{}, err
	}
	return InstantiateTeamTemplateResult{
		Team: loadedTeam,
	}, nil
}

// InstantiateTeamTemplateForUser preserves the existing API-facing contract.
func (store *Store) InstantiateTeamTemplateForUser(ctx context.Context, userID string, workspaceID string, params InstantiateTeamTemplateParams) (TeamView, error) {
	result, err := store.InstantiateTeamTemplate(ctx, userID, workspaceID, params)
	if err != nil {
		return TeamView{}, err
	}
	return result.Team, nil
}

// CreateTeamTemplate creates a custom Team template.
func (store *Store) CreateTeamTemplate(ctx context.Context, params CreateTeamTemplateParams) (model.WorkspaceTeamTemplate, error) {
	txBeginner, err := txBeginnerFromStore(store)
	if err != nil {
		return model.WorkspaceTeamTemplate{}, err
	}
	var created model.WorkspaceTeamTemplate
	err = coredb.WithTx(ctx, txBeginner, func(tx coredb.Tx) error {
		template, err := createTeamTemplate(ctx, tx, params)
		if err != nil {
			return err
		}
		for index, member := range params.Members {
			member.WorkspaceID = template.WorkspaceID
			member.TemplateID = template.ID
			if member.SortOrder == 0 {
				member.SortOrder = int64((index + 1) * 10)
			}
			if _, err := createTeamTemplateMember(ctx, tx, member); err != nil {
				return err
			}
		}
		created = template
		return nil
	})
	if err != nil {
		return model.WorkspaceTeamTemplate{}, err
	}
	return created, nil
}

func createTeamTemplate(ctx context.Context, execer coredb.QueryExecer, params CreateTeamTemplateParams) (model.WorkspaceTeamTemplate, error) {
	workspaceID := trim(params.WorkspaceID)
	name, err := normalizeHandle(params.Name)
	if err != nil {
		return model.WorkspaceTeamTemplate{}, err
	}
	description, err := normalizeOptionalTeamString(params.Description, MaxTeamDescriptionChars, "team template description")
	if err != nil {
		return model.WorkspaceTeamTemplate{}, err
	}
	visibility, err := normalizeVisibility(params.Visibility)
	if err != nil {
		return model.WorkspaceTeamTemplate{}, err
	}
	source := trim(params.Source)
	if source == "" {
		source = TemplateSourceCustom
	}
	if source != TemplateSourceCustom {
		return model.WorkspaceTeamTemplate{}, badRequest("team template source must be custom")
	}
	scope, err := normalizeTemplateScope(params.Scope)
	if err != nil {
		return model.WorkspaceTeamTemplate{}, err
	}
	createdBy := trim(params.CreatedByMemberID)
	if workspaceID == "" || createdBy == "" {
		return model.WorkspaceTeamTemplate{}, badRequest("team template workspaceId, name, and createdByMemberId are required")
	}
	if err := requireActiveWorkspaceMember(ctx, execer, workspaceID, createdBy, ActorKindHuman); err != nil {
		return model.WorkspaceTeamTemplate{}, err
	}
	templateID := trim(params.ID)
	if templateID == "" {
		generatedID, err := identifiers.NewEntityID("ttpl")
		if err != nil {
			return model.WorkspaceTeamTemplate{}, fmt.Errorf("generate team template id: %w", err)
		}
		templateID = generatedID
	}
	now := params.Now.UTC()
	if now.IsZero() {
		now = time.Now().UTC()
	}
	createdFromTeamID := sql.NullString{String: trim(params.CreatedFromTeamID), Valid: trim(params.CreatedFromTeamID) != ""}
	if _, err := execer.Exec(ctx, `
INSERT INTO workspace_team_templates (
    id, workspace_id, name, description, visibility, source, template_scope,
    created_by_member_id, created_from_team_id, created_at, updated_at
) VALUES (
    $1, $2, $3, $4, $5, $6, $7,
    $8, $9, $10, $10
)
`, templateID, workspaceID, name, description, visibility, source, scope, createdBy, createdFromTeamID, now); err != nil {
		return model.WorkspaceTeamTemplate{}, fmt.Errorf("create team template: %w", mapTeamWriteError(err))
	}
	return loadTeamTemplate(ctx, execer, workspaceID, templateID)
}

func createTeamTemplateMember(ctx context.Context, execer coredb.QueryExecer, params CreateTeamTemplateMemberParams) (model.WorkspaceTeamTemplateMember, error) {
	workspaceID := trim(params.WorkspaceID)
	templateID := trim(params.TemplateID)
	actorKind, err := normalizeActorKind(params.ActorKind)
	if err != nil {
		return model.WorkspaceTeamTemplateMember{}, err
	}
	actorID := trim(params.ActorID)
	if workspaceID == "" || templateID == "" {
		return model.WorkspaceTeamTemplateMember{}, badRequest("team template member workspaceId and templateId are required")
	}
	if actorID != "" {
		if err := validateTeamTemplateActor(ctx, execer, workspaceID, actorKind, actorID); err != nil {
			return model.WorkspaceTeamTemplateMember{}, err
		}
	}
	role, err := normalizeMemberRole(params.Role)
	if err != nil {
		return model.WorkspaceTeamTemplateMember{}, err
	}
	if role == "" {
		role = MemberRoleMember
	}
	roleLabel, err := normalizeOptionalTeamString(params.RoleLabel, MaxTeamRoleLabelChars, "team template member roleLabel")
	if err != nil {
		return model.WorkspaceTeamTemplateMember{}, err
	}
	roleOverlayPrompt, err := normalizeRoleOverlayPromptForActorKind(actorKind, params.RoleOverlayPrompt)
	if err != nil {
		return model.WorkspaceTeamTemplateMember{}, err
	}
	memberID := trim(params.ID)
	if memberID == "" {
		generatedID, err := identifiers.NewEntityID("ttm")
		if err != nil {
			return model.WorkspaceTeamTemplateMember{}, fmt.Errorf("generate team template member id: %w", err)
		}
		memberID = generatedID
	}
	wakeOnTeamMention := role == MemberRoleLeader
	if params.WakeOnTeamMention != nil {
		wakeOnTeamMention = *params.WakeOnTeamMention
	}
	if _, err := execer.Exec(ctx, `
INSERT INTO workspace_team_template_members (
    id, workspace_id, template_id, actor_kind, actor_id,
    role, role_label, role_overlay_prompt,
    wake_on_team_mention, sort_order
) VALUES (
    $1, $2, $3, $4, $5,
    $6, $7, $8,
    $9, $10
)
`, memberID, workspaceID, templateID, actorKind, actorID, role, roleLabel, roleOverlayPrompt, wakeOnTeamMention, params.SortOrder); err != nil {
		return model.WorkspaceTeamTemplateMember{}, fmt.Errorf("create team template member: %w", mapTeamWriteError(err))
	}
	return loadTeamTemplateMember(ctx, execer, workspaceID, templateID, memberID)
}

func listCustomTeamTemplateViews(ctx context.Context, queryer coredb.Queryer, workspaceID string, viewer teamViewer, query string, limit int) ([]TeamTemplateView, error) {
	pattern := "%" + query + "%"
	rows, err := queryer.Query(ctx, teamTemplateSelectSQL()+`
WHERE workspace_id = $1
  AND source = $2
  AND (template_scope = $3 OR created_by_member_id = $4)
  AND ($5 = '' OR LOWER(name) LIKE $6 OR LOWER(description) LIKE $6)
ORDER BY name, id
LIMIT $7
`, trim(workspaceID), TemplateSourceCustom, TemplateScopeWorkspace, viewer.memberID, query, pattern, limit)
	if err != nil {
		return nil, fmt.Errorf("list team templates: %w", err)
	}
	defer rows.Close()
	templates := make([]model.WorkspaceTeamTemplate, 0)
	for rows.Next() {
		template, err := scanTeamTemplate(rows)
		if err != nil {
			return nil, fmt.Errorf("scan team template: %w", err)
		}
		templates = append(templates, template)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate team templates: %w", err)
	}
	rows.Close()
	views := make([]TeamTemplateView, 0, len(templates))
	for _, template := range templates {
		members, err := listTeamTemplateMembers(ctx, queryer, workspaceID, template.ID)
		if err != nil {
			return nil, err
		}
		views = append(views, TeamTemplateView{
			Template:  template,
			Members:   members,
			CanManage: canManageTeamTemplate(viewer, template),
		})
	}
	return views, nil
}

func loadCustomTeamTemplateView(ctx context.Context, queryer coredb.Queryer, workspaceID string, templateID string, viewer teamViewer) (TeamTemplateView, error) {
	template, err := loadTeamTemplate(ctx, queryer, workspaceID, templateID)
	if err != nil {
		return TeamTemplateView{}, err
	}
	if !canViewTeamTemplate(viewer, template) {
		return TeamTemplateView{}, notFound("team template not found")
	}
	members, err := listTeamTemplateMembers(ctx, queryer, workspaceID, template.ID)
	if err != nil {
		return TeamTemplateView{}, err
	}
	return TeamTemplateView{
		Template:  template,
		Members:   members,
		CanManage: canManageTeamTemplate(viewer, template),
	}, nil
}

func loadTemplateForInstantiate(ctx context.Context, queryer coredb.Queryer, workspaceID string, viewer teamViewer, templateID string, locale string) (model.WorkspaceTeamTemplate, []model.WorkspaceTeamTemplateMember, error) {
	if builtin, ok := builtinTeamTemplateByID(templateID, workspaceID, locale); ok {
		return builtin.Template, builtin.Members, nil
	}
	view, err := loadCustomTeamTemplateView(ctx, queryer, workspaceID, templateID, viewer)
	if err != nil {
		return model.WorkspaceTeamTemplate{}, nil, err
	}
	return view.Template, view.Members, nil
}

func loadTeamTemplate(ctx context.Context, queryer coredb.QueryRower, workspaceID string, templateID string) (model.WorkspaceTeamTemplate, error) {
	template, err := scanTeamTemplate(queryer.QueryRow(ctx, teamTemplateSelectSQL()+` WHERE workspace_id = $1 AND id = $2`, trim(workspaceID), trim(templateID)))
	if err != nil {
		if coredb.IsNotFound(err) {
			return model.WorkspaceTeamTemplate{}, notFound("team template not found")
		}
		return model.WorkspaceTeamTemplate{}, fmt.Errorf("load team template: %w", err)
	}
	return template, nil
}

func loadTeamTemplateMember(ctx context.Context, queryer coredb.QueryRower, workspaceID string, templateID string, memberID string) (model.WorkspaceTeamTemplateMember, error) {
	member, err := scanTeamTemplateMember(queryer.QueryRow(ctx, teamTemplateMemberSelectSQL()+` WHERE workspace_id = $1 AND template_id = $2 AND id = $3`, trim(workspaceID), trim(templateID), trim(memberID)))
	if err != nil {
		if coredb.IsNotFound(err) {
			return model.WorkspaceTeamTemplateMember{}, notFound("team template member not found")
		}
		return model.WorkspaceTeamTemplateMember{}, fmt.Errorf("load team template member: %w", err)
	}
	return member, nil
}

func listTeamTemplateMembers(ctx context.Context, queryer coredb.Queryer, workspaceID string, templateID string) ([]model.WorkspaceTeamTemplateMember, error) {
	rows, err := queryer.Query(ctx, teamTemplateMemberSelectSQL()+` WHERE workspace_id = $1 AND template_id = $2 ORDER BY sort_order, id`, trim(workspaceID), trim(templateID))
	if err != nil {
		return nil, fmt.Errorf("list team template members: %w", err)
	}
	defer rows.Close()
	members := make([]model.WorkspaceTeamTemplateMember, 0)
	for rows.Next() {
		member, err := scanTeamTemplateMember(rows)
		if err != nil {
			return nil, fmt.Errorf("scan team template member: %w", err)
		}
		members = append(members, member)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate team template members: %w", err)
	}
	return members, nil
}

func teamTemplateSelectSQL() string {
	return `
SELECT id, workspace_id, name, description, visibility, source, template_scope,
       created_by_member_id, COALESCE(created_from_team_id, ''), created_at, updated_at
FROM workspace_team_templates`
}

func scanTeamTemplate(row coredb.Row) (model.WorkspaceTeamTemplate, error) {
	var template model.WorkspaceTeamTemplate
	if err := row.Scan(
		&template.ID,
		&template.WorkspaceID,
		&template.Name,
		&template.Description,
		&template.Visibility,
		&template.Source,
		&template.Scope,
		&template.CreatedByMemberID,
		&template.CreatedFromTeamID,
		&template.CreatedAt,
		&template.UpdatedAt,
	); err != nil {
		return model.WorkspaceTeamTemplate{}, err
	}
	template.ID = trim(template.ID)
	template.WorkspaceID = trim(template.WorkspaceID)
	template.Name = trim(template.Name)
	template.Visibility = trim(template.Visibility)
	template.Source = trim(template.Source)
	template.Scope = trim(template.Scope)
	if template.Scope == "" {
		template.Scope = TemplateScopeWorkspace
	}
	template.CreatedByMemberID = trim(template.CreatedByMemberID)
	template.CreatedFromTeamID = trim(template.CreatedFromTeamID)
	return template, nil
}

func normalizeTemplateScope(scope string) (string, error) {
	value := trim(scope)
	if value == "" {
		return TemplateScopePrivate, nil
	}
	switch value {
	case TemplateScopePrivate, TemplateScopeWorkspace:
		return value, nil
	default:
		return "", badRequest("team template scope must be private or workspace")
	}
}

func canViewTeamTemplate(viewer teamViewer, template model.WorkspaceTeamTemplate) bool {
	if template.Source == TemplateSourceBuiltin {
		return true
	}
	if template.Scope == TemplateScopeWorkspace {
		return true
	}
	return template.CreatedByMemberID == viewer.memberID
}

func canManageTeamTemplate(viewer teamViewer, template model.WorkspaceTeamTemplate) bool {
	if template.Source == TemplateSourceBuiltin {
		return false
	}
	if template.CreatedByMemberID == viewer.memberID {
		return true
	}
	return template.Scope == TemplateScopeWorkspace && viewer.workspaceAdmin
}

func teamTemplateMemberSelectSQL() string {
	return `
SELECT id, workspace_id, template_id, actor_kind, actor_id,
       role,
       role_label, role_overlay_prompt,
       wake_on_team_mention, sort_order
FROM workspace_team_template_members`
}

func scanTeamTemplateMember(row coredb.Row) (model.WorkspaceTeamTemplateMember, error) {
	var member model.WorkspaceTeamTemplateMember
	if err := row.Scan(
		&member.ID,
		&member.WorkspaceID,
		&member.TemplateID,
		&member.ActorKind,
		&member.ActorID,
		&member.Role,
		&member.RoleLabel,
		&member.RoleOverlayPrompt,
		&member.WakeOnTeamMention,
		&member.SortOrder,
	); err != nil {
		return model.WorkspaceTeamTemplateMember{}, err
	}
	member.ID = trim(member.ID)
	member.WorkspaceID = trim(member.WorkspaceID)
	member.TemplateID = trim(member.TemplateID)
	member.ActorKind = trim(member.ActorKind)
	member.ActorID = trim(member.ActorID)
	member.Role = trim(member.Role)
	if member.Role == "" {
		member.Role = MemberRoleMember
	}
	return member, nil
}

func builtinTeamTemplatesFor(locale string) []TeamTemplateView {
	if views, ok := builtinTeamTemplates[presets.NormalizeLocale(locale)]; ok {
		return views
	}
	if views, ok := builtinTeamTemplates[builtinTeamTemplateDefaultLocale]; ok {
		return views
	}
	return nil
}

func builtinTeamTemplateViews(workspaceID string, query string, locale string) []TeamTemplateView {
	templates := builtinTeamTemplatesFor(locale)
	views := make([]TeamTemplateView, 0, len(templates))
	for _, builtin := range templates {
		view := copyBuiltinTeamTemplateView(builtin, workspaceID)
		if query != "" && !strings.Contains(strings.ToLower(view.Template.Name+" "+view.Template.Description), query) {
			continue
		}
		views = append(views, view)
	}
	return views
}

func builtinTeamTemplateByID(templateID string, workspaceID string, locale string) (TeamTemplateView, bool) {
	for _, builtin := range builtinTeamTemplatesFor(locale) {
		if builtin.Template.ID == trim(templateID) {
			return copyBuiltinTeamTemplateView(builtin, workspaceID), true
		}
	}
	return TeamTemplateView{}, false
}

func copyBuiltinTeamTemplateView(view TeamTemplateView, workspaceID string) TeamTemplateView {
	copied := TeamTemplateView{
		Template:  view.Template,
		Members:   make([]model.WorkspaceTeamTemplateMember, len(view.Members)),
		CanManage: false,
	}
	copied.Template.WorkspaceID = trim(workspaceID)
	copied.Template.Source = TemplateSourceBuiltin
	copied.Template.Scope = TemplateScopeWorkspace
	copied.Template.CreatedByMemberID = ""
	copy(copied.Members, view.Members)
	for index := range copied.Members {
		copied.Members[index].WorkspaceID = trim(workspaceID)
	}
	return copied
}

func bindingMap(bindings []TeamTemplateMemberBinding) map[string]TeamTemplateMemberBinding {
	result := make(map[string]TeamTemplateMemberBinding, len(bindings))
	for _, binding := range bindings {
		memberID := trim(binding.TemplateMemberID)
		if memberID == "" {
			continue
		}
		binding.TemplateMemberID = memberID
		binding.ActorID = trim(binding.ActorID)
		binding.Role = trim(binding.Role)
		binding.RoleLabel = trim(binding.RoleLabel)
		binding.RoleOverlayPrompt = trim(binding.RoleOverlayPrompt)
		result[memberID] = binding
	}
	return result
}

func firstNonEmptyString(values ...string) string {
	for _, value := range values {
		if trim(value) != "" {
			return value
		}
	}
	return ""
}

func valueOrDefault(value *string, fallback string) string {
	if value == nil {
		return fallback
	}
	return *value
}

func validateTeamTemplateActor(ctx context.Context, queryer coredb.Queryer, workspaceID string, actorKind string, actorID string) error {
	switch actorKind {
	case ActorKindAgent:
		return requireActiveWorkspaceMember(ctx, queryer, workspaceID, actorID, actorKind)
	default:
		return badRequest("team template members can only be agents")
	}
}
