package views

import (
	"net/url"
	"slices"
	"strconv"
	"strings"
	"time"

	"github.com/google/uuid"
	tablomodel "xtablo-backend/internal/tablos"
	taskmodel "xtablo-backend/internal/tasks"
	"xtablo-backend/internal/web/ui"
)

type TasksPageViewModel struct {
	State    taskmodel.TaskPageState
	Filters  TasksFiltersView
	Form     TasksFormOptionsView
	Kanban   TasksKanbanView
	List     TasksListView
	Roadmap  TasksRoadmapView
	HasTasks bool
}

type TasksFiltersView struct {
	Tablos    []TasksOptionView
	Assignees []TasksOptionView
	Statuses  []TasksOptionView
}

type TasksFormOptionsView struct {
	Tablos         []TasksOptionView
	Assignees      []TasksOptionView
	EtapesByTablo  map[string][]TasksOptionView
	DefaultTabloID string
}

type TasksOptionView struct {
	Value    string
	Label    string
	Selected bool
}

type TasksKanbanView struct {
	Columns []TasksKanbanColumnView
}

type TasksKanbanColumnView struct {
	ID    string
	Label string
	Tasks []TaskCardView
}

type TasksListView struct {
	Groups []TasksStatusGroupView
}

type TasksStatusGroupView struct {
	ID    string
	Label string
	Tasks []TaskCardView
}

type TasksRoadmapView struct {
	Mode  string
	Lanes []TasksRoadmapLaneView
}

type TasksRoadmapLaneView struct {
	ID      string
	Label   string
	Buckets []TasksRoadmapBucketTasksView
}

type TasksRoadmapBucketTasksView struct {
	ID    string
	Label string
	Tasks []TaskCardView
}

type TaskCardView struct {
	ID           string
	Title        string
	Description  string
	TabloID      string
	TabloName    string
	EtapeName    string
	StatusLabel  string
	StatusValue  string
	DueDate      string
	DueDateValue string
	Assignee     string
	AssigneeID   string
	ParentTaskID string
}

type etapeMeta struct {
	ID      uuid.UUID
	TabloID uuid.UUID
	Title   string
}

func NewTasksPageViewModel(tablos []tablomodel.Record, tasks []taskmodel.Record, assigneeLabels map[uuid.UUID]string, state taskmodel.TaskPageState, now time.Time) TasksPageViewModel {
	tabloByID := make(map[uuid.UUID]tablomodel.Record, len(tablos))
	for _, tablo := range tablos {
		tabloByID[tablo.ID] = tablo
	}

	etapesByID := make(map[uuid.UUID]etapeMeta)
	etapesByTablo := make(map[string][]TasksOptionView)
	for _, record := range tasks {
		if !record.IsEtape {
			continue
		}
		etapesByID[record.ID] = etapeMeta{
			ID:      record.ID,
			TabloID: record.TabloID,
			Title:   record.Title,
		}
		etapesByTablo[record.TabloID.String()] = append(etapesByTablo[record.TabloID.String()], TasksOptionView{
			Value: record.ID.String(),
			Label: record.Title,
		})
	}
	for key := range etapesByTablo {
		slices.SortFunc(etapesByTablo[key], func(a, b TasksOptionView) int {
			return strings.Compare(a.Label, b.Label)
		})
	}

	filteredTasks := filterTasks(tasks, state)
	form := buildTasksFormOptions(tablos, assigneeLabels, etapesByTablo)

	return TasksPageViewModel{
		State:    state,
		Filters:  buildTasksFilters(tablos, assigneeLabels, state),
		Form:     form,
		Kanban:   buildKanbanView(filteredTasks, tabloByID, etapesByID, assigneeLabels),
		List:     buildListView(filteredTasks, tabloByID, etapesByID, assigneeLabels),
		Roadmap:  buildRoadmapView(filteredTasks, tabloByID, etapesByID, assigneeLabels, state.RoadmapMode, now),
		HasTasks: len(filteredTasks) > 0,
	}
}

func buildTasksFilters(tablos []tablomodel.Record, assigneeLabels map[uuid.UUID]string, state taskmodel.TaskPageState) TasksFiltersView {
	sortedTablos := append([]tablomodel.Record(nil), tablos...)
	view := TasksFiltersView{
		Tablos:    make([]TasksOptionView, 0, len(sortedTablos)),
		Assignees: make([]TasksOptionView, 0, len(assigneeLabels)),
		Statuses: []TasksOptionView{
			{Value: string(taskmodel.StatusTodo), Label: "À faire", Selected: containsStatus(state.Statuses, taskmodel.StatusTodo)},
			{Value: string(taskmodel.StatusInProgress), Label: "En cours", Selected: containsStatus(state.Statuses, taskmodel.StatusInProgress)},
			{Value: string(taskmodel.StatusInReview), Label: "Vérification", Selected: containsStatus(state.Statuses, taskmodel.StatusInReview)},
			{Value: string(taskmodel.StatusDone), Label: "Terminé", Selected: containsStatus(state.Statuses, taskmodel.StatusDone)},
		},
	}

	slices.SortFunc(sortedTablos, func(a, b tablomodel.Record) int {
		return strings.Compare(a.Name, b.Name)
	})
	for _, tablo := range sortedTablos {
		view.Tablos = append(view.Tablos, TasksOptionView{
			Value:    tablo.ID.String(),
			Label:    tablo.Name,
			Selected: containsUUID(state.TabloIDs, tablo.ID),
		})
	}

	assigneeIDs := make([]uuid.UUID, 0, len(assigneeLabels))
	for id := range assigneeLabels {
		assigneeIDs = append(assigneeIDs, id)
	}
	slices.SortFunc(assigneeIDs, func(a, b uuid.UUID) int {
		return strings.Compare(assigneeLabels[a], assigneeLabels[b])
	})
	for _, id := range assigneeIDs {
		view.Assignees = append(view.Assignees, TasksOptionView{
			Value:    id.String(),
			Label:    assigneeLabels[id],
			Selected: containsUUID(state.AssigneeIDs, id),
		})
	}

	return view
}

func buildTasksFormOptions(tablos []tablomodel.Record, assigneeLabels map[uuid.UUID]string, etapesByTablo map[string][]TasksOptionView) TasksFormOptionsView {
	sortedTablos := append([]tablomodel.Record(nil), tablos...)
	form := TasksFormOptionsView{
		Tablos:        make([]TasksOptionView, 0, len(sortedTablos)),
		Assignees:     make([]TasksOptionView, 0, len(assigneeLabels)),
		EtapesByTablo: etapesByTablo,
	}
	slices.SortFunc(sortedTablos, func(a, b tablomodel.Record) int {
		return strings.Compare(a.Name, b.Name)
	})
	for index, tablo := range sortedTablos {
		if index == 0 {
			form.DefaultTabloID = tablo.ID.String()
		}
		form.Tablos = append(form.Tablos, TasksOptionView{
			Value: tablo.ID.String(),
			Label: tablo.Name,
		})
	}

	assigneeIDs := make([]uuid.UUID, 0, len(assigneeLabels))
	for id := range assigneeLabels {
		assigneeIDs = append(assigneeIDs, id)
	}
	slices.SortFunc(assigneeIDs, func(a, b uuid.UUID) int {
		return strings.Compare(assigneeLabels[a], assigneeLabels[b])
	})
	for _, id := range assigneeIDs {
		form.Assignees = append(form.Assignees, TasksOptionView{
			Value: id.String(),
			Label: assigneeLabels[id],
		})
	}

	return form
}

func filterTasks(tasks []taskmodel.Record, state taskmodel.TaskPageState) []taskmodel.Record {
	filtered := make([]taskmodel.Record, 0, len(tasks))
	for _, record := range tasks {
		if record.IsEtape {
			continue
		}
		if len(state.TabloIDs) > 0 && !containsUUID(state.TabloIDs, record.TabloID) {
			continue
		}
		if len(state.AssigneeIDs) > 0 {
			if record.AssigneeID == nil || !containsUUID(state.AssigneeIDs, *record.AssigneeID) {
				continue
			}
		}
		if len(state.Statuses) > 0 && !containsStatus(state.Statuses, record.Status) {
			continue
		}
		filtered = append(filtered, record)
	}
	return filtered
}

func buildKanbanView(records []taskmodel.Record, tabloByID map[uuid.UUID]tablomodel.Record, etapesByID map[uuid.UUID]etapeMeta, assigneeLabels map[uuid.UUID]string) TasksKanbanView {
	columns := []TasksKanbanColumnView{
		{ID: string(taskmodel.StatusTodo), Label: "À faire"},
		{ID: string(taskmodel.StatusInProgress), Label: "En cours"},
		{ID: string(taskmodel.StatusInReview), Label: "Vérification"},
		{ID: string(taskmodel.StatusDone), Label: "Terminé"},
	}
	for _, record := range sortTaskRecords(records) {
		card := taskCardFromRecord(record, tabloByID, etapesByID, assigneeLabels)
		for index := range columns {
			if columns[index].ID == string(record.Status) {
				columns[index].Tasks = append(columns[index].Tasks, card)
				break
			}
		}
	}
	return TasksKanbanView{Columns: columns}
}

func buildListView(records []taskmodel.Record, tabloByID map[uuid.UUID]tablomodel.Record, etapesByID map[uuid.UUID]etapeMeta, assigneeLabels map[uuid.UUID]string) TasksListView {
	groups := []TasksStatusGroupView{
		{ID: string(taskmodel.StatusTodo), Label: "À faire"},
		{ID: string(taskmodel.StatusInProgress), Label: "En cours"},
		{ID: string(taskmodel.StatusInReview), Label: "Vérification"},
		{ID: string(taskmodel.StatusDone), Label: "Terminé"},
	}
	for _, record := range sortTaskRecords(records) {
		card := taskCardFromRecord(record, tabloByID, etapesByID, assigneeLabels)
		for index := range groups {
			if groups[index].ID == string(record.Status) {
				groups[index].Tasks = append(groups[index].Tasks, card)
				break
			}
		}
	}
	return TasksListView{Groups: groups}
}

func buildRoadmapView(records []taskmodel.Record, tabloByID map[uuid.UUID]tablomodel.Record, etapesByID map[uuid.UUID]etapeMeta, assigneeLabels map[uuid.UUID]string, mode taskmodel.TaskRoadmapMode, now time.Time) TasksRoadmapView {
	if now.IsZero() {
		now = time.Now().UTC()
	}

	bucketIDs, bucketLabels := roadmapBuckets(mode, now)
	lanesByID := map[string]*TasksRoadmapLaneView{}
	laneOrder := make([]string, 0)

	for _, record := range sortTaskRecords(records) {
		laneID, laneLabel := roadmapLane(record, tabloByID, etapesByID)
		lane, ok := lanesByID[laneID]
		if !ok {
			lane = &TasksRoadmapLaneView{
				ID:      laneID,
				Label:   laneLabel,
				Buckets: make([]TasksRoadmapBucketTasksView, 0, len(bucketIDs)+1),
			}
			for index, bucketID := range bucketIDs {
				lane.Buckets = append(lane.Buckets, TasksRoadmapBucketTasksView{
					ID:    bucketID,
					Label: bucketLabels[index],
				})
			}
			lane.Buckets = append(lane.Buckets, TasksRoadmapBucketTasksView{
				ID:    "sans-date",
				Label: "Sans date",
			})
			lanesByID[laneID] = lane
			laneOrder = append(laneOrder, laneID)
		}

		card := taskCardFromRecord(record, tabloByID, etapesByID, assigneeLabels)
		bucketIndex := roadmapBucketIndex(record.DueDate, mode, now, len(bucketIDs))
		lane.Buckets[bucketIndex].Tasks = append(lane.Buckets[bucketIndex].Tasks, card)
	}

	lanes := make([]TasksRoadmapLaneView, 0, len(laneOrder))
	for _, laneID := range laneOrder {
		lanes = append(lanes, *lanesByID[laneID])
	}

	return TasksRoadmapView{
		Mode: map[taskmodel.TaskRoadmapMode]string{
			taskmodel.TaskRoadmapModeMonth: "Mois",
		}[mode],
		Lanes: lanes,
	}
}

func taskCardFromRecord(record taskmodel.Record, tabloByID map[uuid.UUID]tablomodel.Record, etapesByID map[uuid.UUID]etapeMeta, assigneeLabels map[uuid.UUID]string) TaskCardView {
	tabloName := record.TabloID.String()
	if tablo, ok := tabloByID[record.TabloID]; ok {
		tabloName = tablo.Name
	}

	etapeName := "Sans étape"
	if record.ParentTaskID != nil {
		if etape, ok := etapesByID[*record.ParentTaskID]; ok {
			etapeName = etape.Title
		}
	}

	return TaskCardView{
		ID:           record.ID.String(),
		Title:        record.Title,
		Description:  record.Description,
		TabloID:      record.TabloID.String(),
		TabloName:    tabloName,
		EtapeName:    etapeName,
		StatusLabel:  taskStatusLabel(record.Status),
		StatusValue:  string(record.Status),
		DueDate:      formatOptionalDate(record.DueDate),
		DueDateValue: formatOptionalDateInput(record.DueDate),
		Assignee:     assigneeName(record.AssigneeID, assigneeLabels),
		AssigneeID:   optionalUUIDString(record.AssigneeID),
		ParentTaskID: optionalUUIDString(record.ParentTaskID),
	}
}

func taskViewHref(state taskmodel.TaskPageState, view taskmodel.TaskView) string {
	nextState := state
	nextState.View = view
	if view != taskmodel.TaskViewRoadmap {
		nextState.RoadmapMode = taskmodel.TaskRoadmapModeWeek
	}
	return stateAction("/tasks", nextState)
}

func taskRoadmapModeHref(state taskmodel.TaskPageState, mode taskmodel.TaskRoadmapMode) string {
	nextState := state
	nextState.RoadmapMode = mode
	return stateAction("/tasks", nextState)
}

func taskViewTabClass(state taskmodel.TaskPageState, view taskmodel.TaskView) string {
	base := "flex items-center gap-2 pb-3 pt-1 px-2 text-sm font-semibold transition-colors border-b-2 min-h-[44px] "
	if state.View == view {
		return base + "text-purple-600 border-purple-600 dark:text-purple-400 dark:border-purple-400"
	}
	return base + "text-[#667085] border-transparent hover:text-gray-900 dark:hover:text-gray-100"
}

func taskRoadmapModeClass(state taskmodel.TaskPageState, mode taskmodel.TaskRoadmapMode) string {
	base := "inline-flex items-center rounded-full px-3 py-1.5 text-xs font-semibold "
	if state.RoadmapMode == mode {
		return base + "bg-purple-50 text-purple-700 border border-purple-200 dark:bg-purple-950/40 dark:text-purple-300 dark:border-purple-900"
	}
	return base + "bg-gray-100 text-gray-600 border border-transparent hover:text-gray-900 dark:bg-gray-800 dark:text-gray-300"
}

func taskRoadmapModeSelectProps(state taskmodel.TaskPageState) ui.SelectProps {
	return ui.SelectProps{
		ID:    "tasks-roadmap-mode",
		Name:  "roadmap_mode_nav",
		Value: taskRoadmapModeHref(state, state.RoadmapMode),
		Options: []ui.SelectOption{
			{Value: taskRoadmapModeHref(state, taskmodel.TaskRoadmapModeWeek), Label: "Semaine"},
			{Value: taskRoadmapModeHref(state, taskmodel.TaskRoadmapModeMonth), Label: "Mois"},
		},
		Attrs: map[string]any{
			"onchange": "if (this.value) window.location.href=this.value",
		},
	}
}

func tasksFilterSummaryLabel(filters TasksFiltersView) string {
	count := 0
	for _, option := range filters.Tablos {
		if option.Selected {
			count++
		}
	}
	for _, option := range filters.Statuses {
		if option.Selected {
			count++
		}
	}
	for _, option := range filters.Assignees {
		if option.Selected {
			count++
		}
	}
	if count == 0 {
		return "Filtrer"
	}
	return "Filtrer (" + strconv.Itoa(count) + ")"
}

func tasksFilterGroupAllSelected(options []TasksOptionView) bool {
	for _, option := range options {
		if option.Selected {
			return false
		}
	}
	return true
}

func tasksFilterGroupHasChoices(options []TasksOptionView) bool {
	return len(options) > 0
}

func tasksClearTabloFiltersHref(state taskmodel.TaskPageState) string {
	nextState := state
	nextState.TabloIDs = nil
	return stateAction("/tasks", nextState)
}

func tasksClearStatusFiltersHref(state taskmodel.TaskPageState) string {
	nextState := state
	nextState.Statuses = nil
	return stateAction("/tasks", nextState)
}

func tasksClearAssigneeFiltersHref(state taskmodel.TaskPageState) string {
	nextState := state
	nextState.AssigneeIDs = nil
	return stateAction("/tasks", nextState)
}

func taskCardClass(compact bool) string {
	if compact {
		return "bg-white dark:bg-gray-800 rounded-lg p-3 shadow-sm border border-gray-100 dark:border-gray-700"
	}
	return "bg-white dark:bg-gray-800 rounded-lg p-4 mb-3 shadow-sm hover:shadow-md transition-shadow border border-gray-100 dark:border-gray-700 cursor-pointer"
}

func taskEditHref(task TaskCardView, state taskmodel.TaskPageState) string {
	return stateAction("/tasks/"+task.ID+"/edit", state)
}

func taskDeleteHref(task TaskCardView, state taskmodel.TaskPageState) string {
	return stateAction("/tasks/"+task.ID, state)
}

func taskDeleteAriaLabel(task TaskCardView) string {
	return "Supprimer la tâche " + task.Title
}

func taskHasAssignee(task TaskCardView) bool {
	return strings.TrimSpace(task.Assignee) != ""
}

func statusIconClass(statusID string) string {
	switch statusID {
	case string(taskmodel.StatusInProgress):
		return "text-yellow-500"
	case string(taskmodel.StatusInReview):
		return "text-blue-500"
	case string(taskmodel.StatusDone):
		return "text-green-500"
	default:
		return "text-gray-400"
	}
}

func dueDateToneClass(raw string) string {
	if raw == "" {
		return "text-gray-500 dark:text-gray-400"
	}
	due, err := time.Parse("2006-01-02", raw)
	if err != nil {
		return "text-gray-500 dark:text-gray-400"
	}
	if due.Before(time.Now().UTC().Add(24 * time.Hour)) {
		return "text-red-500"
	}
	return "text-gray-500 dark:text-gray-400"
}

func etapeLabel(task TaskCardView) string {
	if task.EtapeName == "" {
		return "Sans étape"
	}
	return task.EtapeName
}

func etapeBadgeClass(task TaskCardView) string {
	if task.EtapeName == "" || task.EtapeName == "Sans étape" {
		return "bg-blue-500"
	}
	if strings.Contains(strings.ToLower(task.EtapeName), "pré") || strings.Contains(strings.ToLower(task.EtapeName), "commenc") {
		return "bg-blue-500"
	}
	if strings.Contains(strings.ToLower(task.EtapeName), "livr") || strings.Contains(strings.ToLower(task.EtapeName), "review") {
		return "bg-purple-500"
	}
	return "bg-red-500"
}

func etapeIconName(task TaskCardView) string {
	if task.EtapeName == "" || task.EtapeName == "Sans étape" {
		return "zap"
	}
	if strings.Contains(strings.ToLower(task.EtapeName), "livr") || strings.Contains(strings.ToLower(task.EtapeName), "review") {
		return "gem"
	}
	if strings.Contains(strings.ToLower(task.EtapeName), "margot") {
		return "flame"
	}
	return "zap"
}

func assigneeInitials(name string) string {
	parts := strings.Fields(strings.TrimSpace(name))
	if len(parts) == 0 {
		return "?"
	}
	if len(parts) == 1 {
		runes := []rune(parts[0])
		if len(runes) == 1 {
			return strings.ToUpper(string(runes[0]))
		}
		return strings.ToUpper(string(runes[0:2]))
	}
	return strings.ToUpper(string([]rune(parts[0])[0]) + string([]rune(parts[len(parts)-1])[0]))
}

func emptyFallback(value string, fallback string) string {
	if strings.TrimSpace(value) == "" {
		return fallback
	}
	return value
}

func stateAction(path string, state taskmodel.TaskPageState) string {
	values := url.Values{}
	if state.View != "" {
		values.Set("view", string(state.View))
	}
	if state.View == taskmodel.TaskViewRoadmap {
		values.Set("roadmap_mode", string(state.RoadmapMode))
	}
	for _, id := range state.TabloIDs {
		values.Add("tablo", id.String())
	}
	for _, id := range state.AssigneeIDs {
		values.Add("assignee", id.String())
	}
	for _, status := range state.Statuses {
		values.Add("status", string(status))
	}
	encoded := values.Encode()
	if encoded == "" {
		return path
	}
	return path + "?" + encoded
}

func roadmapBuckets(mode taskmodel.TaskRoadmapMode, now time.Time) ([]string, []string) {
	switch mode {
	case taskmodel.TaskRoadmapModeMonth:
		start := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
		ids := make([]string, 0, 6)
		labels := make([]string, 0, 6)
		for i := 0; i < 6; i++ {
			bucket := start.AddDate(0, i, 0)
			ids = append(ids, bucket.Format("2006-01"))
			labels = append(labels, bucket.Format("Jan 2006"))
		}
		return ids, labels
	default:
		start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
		ids := make([]string, 0, 6)
		labels := make([]string, 0, 6)
		for i := 0; i < 6; i++ {
			bucket := start.AddDate(0, 0, i*7)
			ids = append(ids, bucket.Format("2006-01-02"))
			labels = append(labels, bucket.Format("02 Jan"))
		}
		return ids, labels
	}
}

func roadmapBucketIndex(dueDate *time.Time, mode taskmodel.TaskRoadmapMode, now time.Time, bucketCount int) int {
	if dueDate == nil {
		return bucketCount
	}

	if mode == taskmodel.TaskRoadmapModeMonth {
		start := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
		target := time.Date(dueDate.Year(), dueDate.Month(), 1, 0, 0, 0, 0, time.UTC)
		months := int(target.Month()) - int(start.Month()) + (target.Year()-start.Year())*12
		if months <= 0 {
			return 0
		}
		if months >= bucketCount {
			return bucketCount - 1
		}
		return months
	}

	start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
	days := int(dueDate.Sub(start).Hours() / 24)
	if days <= 0 {
		return 0
	}
	weeks := days / 7
	if weeks >= bucketCount {
		return bucketCount - 1
	}
	return weeks
}

func roadmapLane(record taskmodel.Record, tabloByID map[uuid.UUID]tablomodel.Record, etapesByID map[uuid.UUID]etapeMeta) (string, string) {
	tabloName := record.TabloID.String()
	if tablo, ok := tabloByID[record.TabloID]; ok {
		tabloName = tablo.Name
	}
	if record.ParentTaskID == nil {
		return record.TabloID.String() + ":sans-etape", tabloName + " / Sans étape"
	}
	if etape, ok := etapesByID[*record.ParentTaskID]; ok {
		return record.TabloID.String() + ":" + etape.ID.String(), tabloName + " / " + etape.Title
	}
	return record.TabloID.String() + ":sans-etape", tabloName + " / Sans étape"
}

func sortTaskRecords(records []taskmodel.Record) []taskmodel.Record {
	cloned := append([]taskmodel.Record(nil), records...)
	slices.SortFunc(cloned, func(a, b taskmodel.Record) int {
		if diff := strings.Compare(string(a.Status), string(b.Status)); diff != 0 {
			return diff
		}
		return a.CreatedAt.Compare(b.CreatedAt)
	})
	return cloned
}

func containsUUID(ids []uuid.UUID, want uuid.UUID) bool {
	for _, id := range ids {
		if id == want {
			return true
		}
	}
	return false
}

func containsStatus(statuses []taskmodel.Status, want taskmodel.Status) bool {
	for _, status := range statuses {
		if status == want {
			return true
		}
	}
	return false
}

func taskStatusLabel(status taskmodel.Status) string {
	switch status {
	case taskmodel.StatusInProgress:
		return "En cours"
	case taskmodel.StatusInReview:
		return "Vérification"
	case taskmodel.StatusDone:
		return "Terminé"
	default:
		return "À faire"
	}
}

func formatOptionalDate(value *time.Time) string {
	if value == nil {
		return ""
	}
	return value.Format("02 Jan")
}

func formatOptionalDateInput(value *time.Time) string {
	if value == nil {
		return ""
	}
	return value.Format("2006-01-02")
}

func assigneeName(id *uuid.UUID, labels map[uuid.UUID]string) string {
	if id == nil {
		return ""
	}
	if label, ok := labels[*id]; ok {
		return label
	}
	return ""
}

func optionalUUIDString(id *uuid.UUID) string {
	if id == nil {
		return ""
	}
	return id.String()
}

func joinTaskMeta(parts ...string) string {
	filtered := make([]string, 0, len(parts))
	for _, part := range parts {
		part = strings.TrimSpace(part)
		if part == "" {
			continue
		}
		filtered = append(filtered, part)
	}
	return strings.Join(filtered, " · ")
}
