package handlers

import (
	"context"
	"encoding/hex"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/gofiber/fiber/v2"
	"gorm.io/gorm"

	"wucher/internal/domain/auth"
	"wucher/internal/domain/contact"
	filemanager "wucher/internal/domain/file_manager"
	"wucher/internal/shared/pkg/apperrorsx"
	"wucher/internal/shared/pkg/attachmentresolver"
	"wucher/internal/shared/pkg/uuidv7"
	"wucher/internal/transport/http/dto"
	"wucher/internal/transport/http/jsonapi"
	"wucher/internal/transport/http/response"
	"wucher/internal/transport/http/validators"
)

const (
	contactProfileAttachmentRefType  = "user_profile"
	contactProfileAttachmentCategory = "profile_picture"
	contactListMaxLimit              = 1000
)

type ContactHandler struct {
	svc         contact.Service
	fileManager filemanager.Service
	storage     fileManagerDownloadURLStorage
	inviter     contactInviter
	validate    *validators.Validator
}

type contactInviter interface {
	SendInviteSetPasswordForUser(ctx context.Context, userID []byte) error
}

func NewContactHandler(svc contact.Service, inviter contactInviter) *ContactHandler {
	return &ContactHandler{svc: svc, inviter: inviter, validate: validators.New()}
}

func (h *ContactHandler) WithFileManagerService(fileManager filemanager.Service) *ContactHandler {
	h.fileManager = fileManager
	return h
}

func (h *ContactHandler) WithFileStorage(storage fileManagerDownloadURLStorage) *ContactHandler {
	h.storage = storage
	return h
}

type contactProfilePayload struct {
	roleCode string
	field    string
	present  bool
}

// CreateContact 	godoc
// @Summary Create contact
// @Description Create new contact user and role-specific profile.
// @Description `data.type` must be `contact_create`.
// @Description `role_ids` determines roles (use `role_id` only for backward compatibility). Every selected role must include its matching role-specific profile object in `attributes`.
// @Tags Contacts
// @Accept json
// @Produce json
// @Param request body dto.ContactCreateRequest true "JSON:API Contact create request"
// @Success 201 {object} dto.ContactListResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 422 {object} dto.ErrorResponse
// @Router /api/v1/contacts/create [post]
func (h *ContactHandler) Create(c *fiber.Ctx) error {
	var req dto.ContactCreateRequest
	if err := c.BodyParser(&req); err != nil {
		return writeContactErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Invalid JSON",
			Detail: "an internal error occurred",
		}})
	}
	if errs := h.validate.ValidateStruct(req); errs != nil {
		return writeContactErrors(c, fiber.StatusUnprocessableEntity, errs)
	}
	if errs := validatePilotDULBaseCreateInput(req.Data.Attributes); len(errs) > 0 {
		return writeContactErrors(c, fiber.StatusUnprocessableEntity, errs)
	}
	primaryRoleID, roleIDs, errObjs := parseContactCreateRoleIDs(req.Data.Attributes)
	if len(errObjs) > 0 {
		return writeContactErrors(c, fiber.StatusUnprocessableEntity, errObjs)
	}
	roleCodes := make([]string, 0, len(roleIDs))
	for i := range roleIDs {
		roleCode, err := h.svc.ResolveRoleCodeByID(c.UserContext(), roleIDs[i])
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return writeContactErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{
					contactValidationError("/data/attributes/role_ids", "one or more role_ids do not exist"),
				})
			}
			return writeContactErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{Status: "400", Title: "Create failed", Detail: "an internal error occurred"}})
		}
		roleCodes = append(roleCodes, roleCode)
	}
	if len(roleCodes) == 0 {
		roleCodes = inferContactCreateRoleCodes(req.Data.Attributes)
	}
	if errs := validateContactCreatePayload(req.Data.Type, roleCodes, req.Data.Attributes); len(errs) > 0 {
		return writeContactErrors(c, fiber.StatusUnprocessableEntity, errs)
	}

	in := contact.CreateInput{
		RoleID:  primaryRoleID,
		RoleIDs: roleIDs,
		User: contact.UserPatch{
			Email:       strPtr(strings.TrimSpace(req.Data.Attributes.Email)),
			SSOEmail:    emptyToNil(strings.TrimSpace(req.Data.Attributes.SSOEmail)),
			IsAdmin:     req.Data.Attributes.IsAdmin,
			Username:    strPtr(strings.TrimSpace(req.Data.Attributes.Username)),
			FirstName:   strPtr(strings.TrimSpace(req.Data.Attributes.FirstName)),
			LastName:    strPtr(strings.TrimSpace(req.Data.Attributes.LastName)),
			MobilePhone: strPtr(strings.TrimSpace(req.Data.Attributes.MobilePhone)),
			Timezone:    strPtr(strings.TrimSpace(req.Data.Attributes.Timezone)),
			SortKey:     cloneIntPointer(req.Data.Attributes.SortKey),
			IsActive:    req.Data.Attributes.IsActive,
		},
	}
	for i := range roleCodes {
		in.Profiles = append(in.Profiles, contact.CreateRoleProfile{
			RoleCode: roleCodes[i],
			Patch:    profilePatchFromCreate(roleCodes[i], req.Data.Attributes),
		})
	}
	if len(in.Profiles) > 0 {
		in.Profile = in.Profiles[0].Patch
	}

	userID, err := h.svc.CreateContact(c.UserContext(), in)
	if err != nil {
		if errors.Is(err, contact.ErrUsernameAlreadyExists) {
			return writeContactErrors(c, fiber.StatusConflict, []jsonapi.ErrorObject{{
				Status: "409",
				Title:  "Conflict",
				Detail: "username already exists",
			}})
		}
		return writeContactErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Create failed",
			Detail: "an internal error occurred",
		}})
	}

	if rawFileUUID := strings.TrimSpace(req.Data.Attributes.FileUUID); rawFileUUID != "" {
		attachmentID, handled := h.resolveContactProfileAttachmentID(c, userID, &rawFileUUID)
		if handled {
			return nil
		}
		if err := h.svc.UpdateContact(c.UserContext(), contact.UpdateInput{
			UserID: userID,
			User: contact.UserPatch{
				ProfileAttachmentID: &attachmentID,
			},
		}); err != nil {
			return writeContactErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
				Status: "400",
				Title:  "Create failed",
				Detail: "an internal error occurred",
			}})
		}
	}

	inviteSent := false
	inviteError := ""
	if h.inviter != nil {
		if err := h.inviter.SendInviteSetPasswordForUser(c.UserContext(), userID); err != nil {
			inviteError = "an internal error occurred"
		} else {
			inviteSent = true
		}
	}

	item, err := h.svc.GetContactByID(c.UserContext(), userID)
	if err != nil {
		return writeContactErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Get failed",
			Detail: "an internal error occurred",
		}})
	}
	if item == nil {
		return response.Write(c, fiber.StatusCreated, jsonapi.Resource{
			Type: "contact",
			ID:   contactUUIDString(userID),
			Attributes: map[string]any{
				"created":      true,
				"invite_sent":  inviteSent,
				"invite_error": inviteError,
			},
		})
	}

	isAdmin, err := h.svc.IsActorAdmin(c.UserContext())
	if err != nil {
		return writeContactErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{Status: "400", Title: "Create failed", Detail: "an internal error occurred"}})
	}
	attrs := contactAttributes(item, isAdmin)
	h.applyContactFoto(c.UserContext(), item, attrs)
	attrs["invite_sent"] = inviteSent
	attrs["invite_error"] = inviteError
	return response.Write(c, fiber.StatusCreated, jsonapi.Resource{
		Type:       "contact",
		ID:         contactUUIDString(item.UserID),
		Attributes: attrs,
	})
}

// ListContacts godoc
// @Summary List contacts
// @Description Filter contacts by role and pilot_category. Sort values: name, -name, first_name, -first_name, last_name, -last_name, email, -email, sortkey, -sortkey, note, -note, is_active, -is_active, created_at, -created_at, updated_at, -updated_at.
// @Tags Contacts
// @Produce json
// @Param role query string false "Role: pilot|doctor|air_rescuer|technician|flight_assistant|staff"
// @Param pilot_category query string false "Pilot category: regular|freelance|dry_lease"
// @Param chief_doctor query bool false "Filter doctor chief flag"
// @Param chief_technician query bool false "Filter technician chief flag"
// @Param responsible_complaint query bool false "Filter responsible complaint flag"
// @Param chief_pilot query bool false "Filter pilot chief flag"
// @Param ifr_qualified query bool false "Filter IFR qualified flag"
// @Param search query string false "Search by username/email/name"
// @Param sort query string false "Sort order"
// @Param limit query int false "Limit (default 20)"
// @Param offset query int false "Offset (default 0)"
// @Success 200 {object} dto.ContactListResponse
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/contacts/get-all [get]
func (h *ContactHandler) List(c *fiber.Ctx) error {
	isAdmin, err := h.svc.IsActorAdmin(c.UserContext())
	if err != nil {
		return writeContactErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "List failed",
			Detail: "an internal error occurred",
		}})
	}
	limit := 0
	offset := 0

	filter := contact.ListFilter{
		Role:                 normalizeContactRoleFilter(c.Query("role")),
		PilotCategory:        strings.TrimSpace(c.Query("pilot_category")),
		ChiefDoctor:          parseOptionalBoolQuery(c.Query("chief_doctor")),
		ChiefTechnician:      parseOptionalBoolQuery(c.Query("chief_technician")),
		ResponsibleComplaint: parseOptionalBoolQuery(c.Query("responsible_complaint")),
		ChiefPilot:           parseOptionalBoolQuery(c.Query("chief_pilot")),
		IFRQualified:         parseOptionalBoolQuery(c.Query("ifr_qualified")),
		Search:               strings.TrimSpace(c.Query("search")),
		Sort:                 strings.TrimSpace(c.Query("sort")),
		Limit:                limit,
		Offset:               offset,
	}
	items, total, err := h.svc.ListContacts(c.UserContext(), filter)
	if err != nil {
		return writeContactErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "List failed",
			Detail: "an internal error occurred",
		}})
	}

	out := make([]jsonapi.Resource, 0, len(items))
	for i := range items {
		attrs := contactAttributes(&items[i], isAdmin)
		h.applyContactFoto(c.UserContext(), &items[i], attrs)
		out = append(out, jsonapi.Resource{
			Type:       "contact",
			ID:         contactUUIDString(items[i].UserID),
			Attributes: attrs,
		})
	}
	meta := map[string]any{"total": total, "limit": limit, "offset": offset}
	return response.WriteWithMeta(c, fiber.StatusOK, out, meta)
}

// ListContactsDatatable godoc
// @Summary List contacts (datatable)
// @Description Datatable response with pagination params.
// @Tags Contacts
// @Produce json
// @Param page query int false "Page number (default 1)"
// @Param size query int false "Page size (default 20, max 1000)"
// @Param limit query int false "Page size override (same as size)"
// @Param draw query int false "Draw counter (optional)"
// @Param search query string false "Search term"
// @Param role query string false "Role filter"
// @Param pilot_category query string false "Pilot category filter"
// @Param chief_doctor query bool false "Filter doctor chief flag"
// @Param chief_technician query bool false "Filter technician chief flag"
// @Param responsible_complaint query bool false "Filter responsible complaint flag"
// @Param chief_pilot query bool false "Filter pilot chief flag"
// @Param ifr_qualified query bool false "Filter IFR qualified flag"
// @Success 200 {object} dto.ContactDataTableResponse
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/contacts/get-all/dt [get]
func (h *ContactHandler) ListDatatable(c *fiber.Ctx) error {
	isAdmin, err := h.svc.IsActorAdmin(c.UserContext())
	if err != nil {
		return writeContactErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "List failed",
			Detail: "an internal error occurred",
		}})
	}
	pageNumber := parseIntDefault(c.Query("page"), 1)
	if pageNumber < 1 {
		pageNumber = 1
	}
	length := parseIntDefault(c.Query("limit"), 0)
	if length <= 0 {
		length = parseIntDefault(c.Query("size"), 20)
	}
	if length > contactListMaxLimit {
		length = contactListMaxLimit
	}
	if length < 1 {
		length = 20
	}
	start := (pageNumber - 1) * length
	draw := parseIntDefault(c.Query("draw"), pageNumber)

	filter := contact.ListFilter{
		Role:                 normalizeContactRoleFilter(c.Query("role")),
		PilotCategory:        strings.TrimSpace(c.Query("pilot_category")),
		ChiefDoctor:          parseOptionalBoolQuery(c.Query("chief_doctor")),
		ChiefTechnician:      parseOptionalBoolQuery(c.Query("chief_technician")),
		ResponsibleComplaint: parseOptionalBoolQuery(c.Query("responsible_complaint")),
		ChiefPilot:           parseOptionalBoolQuery(c.Query("chief_pilot")),
		IFRQualified:         parseOptionalBoolQuery(c.Query("ifr_qualified")),
		Search:               strings.TrimSpace(c.Query("search")),
		Sort:                 strings.TrimSpace(c.Query("sort")),
		Limit:                length,
		Offset:               start,
	}
	items, total, err := h.svc.ListContacts(c.UserContext(), filter)
	if err != nil {
		return writeContactErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "List failed",
			Detail: "an internal error occurred",
		}})
	}

	out := make([]jsonapi.Resource, 0, len(items))
	for i := range items {
		attrs := contactAttributes(&items[i], isAdmin)
		h.applyContactFoto(c.UserContext(), &items[i], attrs)
		out = append(out, jsonapi.Resource{
			Type:       "contact",
			ID:         contactUUIDString(items[i].UserID),
			Attributes: attrs,
		})
	}
	meta := map[string]any{
		"draw":             draw,
		"records_total":    total,
		"records_filtered": total,
	}
	return response.WriteWithMeta(c, fiber.StatusOK, out, meta)
}

// GetContact godoc
// @Summary Get contact
// @Tags Contacts
// @Produce json
// @Param user_id path string true "User UUID"
// @Success 200 {object} dto.ContactListResponse
// @Failure 404 {object} dto.ErrorResponse
// @Router /api/v1/contacts/{user_id} [get]
func (h *ContactHandler) Get(c *fiber.Ctx) error {
	userID, err := uuidv7.ParseString(strings.TrimSpace(c.Params("user_id")))
	if err != nil {
		return writeContactErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422", Title: "Validation error", Detail: "user_id is invalid UUID", Source: &jsonapi.ErrorSource{Pointer: "/path/user_id"},
		}})
	}

	return h.writeContactByID(c, userID)
}

func (h *ContactHandler) writeContactByID(c *fiber.Ctx, userID []byte) error {
	item, err := h.svc.GetContactByID(c.UserContext(), userID)
	if err != nil {
		return writeContactErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{Status: "400", Title: "Get failed", Detail: "an internal error occurred"}})
	}
	if item == nil {
		return writeContactErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{Status: "404", Title: "Not found", Detail: "contact not found"}})
	}
	isAdmin, err := h.svc.IsActorAdmin(c.UserContext())
	if err != nil {
		return writeContactErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{Status: "400", Title: "Get failed", Detail: "an internal error occurred"}})
	}
	attrs := contactAttributes(item, isAdmin)
	h.applyContactFoto(c.UserContext(), item, attrs)
	return response.Write(c, fiber.StatusOK, jsonapi.Resource{Type: "contact", ID: contactUUIDString(item.UserID), Attributes: attrs})
}

// UpdateContact godoc
// @Summary Update contact
// @Description `data.type` must be `contact_update`.
// @Description Supports updating `role_id` and `role_ids` (multiple roles) in the same request. Sending `role_ids: []` clears all assigned roles.
// @Description Only one role-specific profile object is allowed in `attributes`. If provided, it must match one of the contact's assigned roles.
// @Tags Contacts
// @Accept json
// @Produce json
// @Param user_id path string true "User UUID"
// @Param request body dto.ContactUpdateRequest true "JSON:API Contact update request"
// @Success 200 {object} dto.ContactListResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 422 {object} dto.ErrorResponse
// @Router /api/v1/contacts/{user_id} [patch]
func (h *ContactHandler) Update(c *fiber.Ctx) error {
	userID, err := uuidv7.ParseString(strings.TrimSpace(c.Params("user_id")))
	if err != nil {
		return writeContactErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422", Title: "Validation error", Detail: "user_id is invalid UUID", Source: &jsonapi.ErrorSource{Pointer: "/path/user_id"},
		}})
	}

	var req dto.ContactUpdateRequest
	if err := c.BodyParser(&req); err != nil {
		return writeContactErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{Status: "400", Title: "Invalid JSON", Detail: "an internal error occurred"}})
	}
	ssoEmailCleared := normalizeContactUpdateSSOEmailForValidation(&req)
	if errs := h.validate.ValidateStruct(req); errs != nil {
		return writeContactErrors(c, fiber.StatusUnprocessableEntity, errs)
	}
	if errs := validatePilotDULBaseUpdateInput(req.Data.Attributes); len(errs) > 0 {
		return writeContactErrors(c, fiber.StatusUnprocessableEntity, errs)
	}
	if ssoEmailCleared {
		empty := ""
		req.Data.Attributes.SSOEmail = &empty
	}
	if strings.TrimSpace(req.Data.Type) != contactUpdateType() {
		return writeContactErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{
			contactValidationError("/data/type", fmt.Sprintf("type must be %s", contactUpdateType())),
		})
	}
	current, err := h.svc.GetContactByID(c.UserContext(), userID)
	if err != nil {
		return writeContactErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{Status: "400", Title: "Get failed", Detail: "an internal error occurred"}})
	}
	if current == nil {
		return writeContactErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{Status: "404", Title: "Not found", Detail: "contact not found"}})
	}
	currentRoleCode := normalizeContactRoleCode(current.RoleCode)
	nextPrimaryRoleID, nextRoleIDs, nextRoleCodes, updateRoles, roleErrs := h.parseContactUpdateRoleIDs(c, current, req.Data.Attributes)
	if len(roleErrs) > 0 {
		return writeContactErrors(c, fiber.StatusUnprocessableEntity, roleErrs)
	}
	allowedRoleCodes := assignedContactRoleCodes(current)
	if updateRoles {
		allowedRoleCodes = nextRoleCodes
		if errs := validateRoleSpecificCreatePayload(nextRoleCodes, req.Data.Type, profilePayloadsFromUpdate(req.Data.Attributes)); len(errs) > 0 {
			return writeContactErrors(c, fiber.StatusUnprocessableEntity, errs)
		}
	}
	requestedProfileRoleCodes, errs := validateRoleSpecificPayload(allowedRoleCodes, req.Data.Type, profilePayloadsFromUpdate(req.Data.Attributes))
	if len(errs) > 0 {
		return writeContactErrors(c, fiber.StatusUnprocessableEntity, errs)
	}
	roleCodeForAdminValidation := currentRoleCode
	if len(requestedProfileRoleCodes) > 0 {
		roleCodeForAdminValidation = requestedProfileRoleCodes[0]
	}
	_ = updateRoles

	profiles := make([]contact.CreateRoleProfile, 0, len(requestedProfileRoleCodes))
	for i := range requestedProfileRoleCodes {
		profiles = append(profiles, contact.CreateRoleProfile{
			RoleCode: requestedProfileRoleCodes[i],
			Patch:    profilePatchFromUpdate(requestedProfileRoleCodes[i], req.Data.Attributes),
		})
	}

	in := contact.UpdateInput{
		UserID: userID,
		User: contact.UserPatch{
			Email:       req.Data.Attributes.Email,
			SSOEmail:    req.Data.Attributes.SSOEmail,
			IsAdmin:     req.Data.Attributes.IsAdmin,
			Username:    req.Data.Attributes.Username,
			FirstName:   req.Data.Attributes.FirstName,
			LastName:    req.Data.Attributes.LastName,
			MobilePhone: req.Data.Attributes.MobilePhone,
			Timezone:    req.Data.Attributes.Timezone,
			SortKeySet:  req.Data.Attributes.SortKey.Set,
			IsActive:    req.Data.Attributes.IsActive,
		},
		RoleID:            nextPrimaryRoleID,
		RoleIDs:           nextRoleIDs,
		UpdateRoles:       updateRoles,
		Profile:           profilePatchFromUpdate(roleCodeForAdminValidation, req.Data.Attributes),
		Profiles:          profiles,
		TargetProfileRole: roleCodeForAdminValidation,
	}
	if req.Data.Attributes.SortKey.Set && req.Data.Attributes.SortKey.Valid {
		sortKey := req.Data.Attributes.SortKey.Value
		in.User.SortKey = &sortKey
	}

	if req.Data.Attributes.FileUUID != nil {
		rawFileUUID := strings.TrimSpace(*req.Data.Attributes.FileUUID)
		if rawFileUUID == "" {
			empty := []byte(nil)
			in.User.ProfileAttachmentID = &empty
		} else {
			attachmentID, handled := h.resolveContactProfileAttachmentID(c, userID, &rawFileUUID)
			if handled {
				return nil
			}
			in.User.ProfileAttachmentID = &attachmentID
		}
	}

	if err := h.svc.UpdateContact(c.UserContext(), in); err != nil {
		status := fiber.StatusBadRequest
		if errors.Is(err, gorm.ErrRecordNotFound) {
			status = fiber.StatusNotFound
		} else if errors.Is(err, contact.ErrUsernameAlreadyExists) {
			status = fiber.StatusConflict
		}
		title := "Update failed"
		if status == fiber.StatusConflict {
			title = "Conflict"
		}
		return writeContactErrors(c, status, []jsonapi.ErrorObject{{Status: intStatus(status), Title: title, Detail: "an internal error occurred"}})
	}
	return h.writeContactByID(c, userID)
}

// DeleteContact godoc
// @Summary Delete contact
// @Tags Contacts
// @Produce json
// @Param user_id path string true "User UUID"
// @Success 200 {object} dto.ContactListResponse
// @Failure 404 {object} dto.ErrorResponse
// @Router /api/v1/contacts/{user_id} [delete]
func (h *ContactHandler) Delete(c *fiber.Ctx) error {
	userID, err := uuidv7.ParseString(strings.TrimSpace(c.Params("user_id")))
	if err != nil {
		return writeContactErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{Status: "422", Title: "Validation error", Detail: "user_id is invalid UUID"}})
	}
	if err := h.svc.DeleteContact(c.UserContext(), userID); err != nil {
		status := fiber.StatusBadRequest
		if errors.Is(err, gorm.ErrRecordNotFound) {
			status = fiber.StatusNotFound
		}
		return writeContactErrors(c, status, []jsonapi.ErrorObject{{Status: intStatus(status), Title: "Delete failed", Detail: "an internal error occurred"}})
	}
	return response.Write(c, fiber.StatusOK, jsonapi.Resource{Type: "contact_delete", Attributes: map[string]any{"deleted": true}})
}

// UpdateContactStatus godoc
// @Summary Update contact active status
// @Tags Contacts
// @Accept json
// @Produce json
// @Param user_id path string true "User UUID"
// @Param request body dto.ContactStatusUpdateRequest true "JSON:API status update request"
// @Success 200 {object} dto.ContactSimpleSuccessResponse
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/contacts/{user_id}/status [patch]
func (h *ContactHandler) UpdateContactStatus(c *fiber.Ctx) error {
	userID, err := uuidv7.ParseString(strings.TrimSpace(c.Params("user_id")))
	if err != nil {
		return writeContactErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{Status: "422", Title: "Validation error", Detail: "user_id is invalid UUID"}})
	}
	var req dto.ContactStatusUpdateRequest
	if err := c.BodyParser(&req); err != nil {
		return writeContactErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{Status: "400", Title: "Invalid JSON", Detail: "an internal error occurred"}})
	}
	if errs := h.validate.ValidateStruct(req); errs != nil {
		return writeContactErrors(c, fiber.StatusUnprocessableEntity, errs)
	}
	if err := h.svc.UpdateContactStatus(c.UserContext(), userID, req.Data.Attributes.IsActive); err != nil {
		return writeContactErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{Status: "400", Title: "Update status failed", Detail: "an internal error occurred"}})
	}
	return h.writeContactByID(c, userID)
}

// UpdateContactRole godoc
// @Summary Update contact role
// @Tags Contacts
// @Accept json
// @Produce json
// @Param user_id path string true "User UUID"
// @Param request body dto.ContactRoleUpdateRequest true "JSON:API role update request"
// @Success 200 {object} dto.ContactListResponse
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/contacts/{user_id}/role [patch]
func (h *ContactHandler) UpdateContactRole(c *fiber.Ctx) error {
	userID, err := uuidv7.ParseString(strings.TrimSpace(c.Params("user_id")))
	if err != nil {
		return writeContactErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{Status: "422", Title: "Validation error", Detail: "user_id is invalid UUID"}})
	}
	var req dto.ContactRoleUpdateRequest
	if err := c.BodyParser(&req); err != nil {
		return writeContactErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{Status: "400", Title: "Invalid JSON", Detail: "an internal error occurred"}})
	}
	if errs := h.validate.ValidateStruct(req); errs != nil {
		return writeContactErrors(c, fiber.StatusUnprocessableEntity, errs)
	}
	if err := h.svc.UpdateContactRole(c.UserContext(), userID, req.Data.Attributes.RoleCode); err != nil {
		return writeContactErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{Status: "400", Title: "Update role failed", Detail: "an internal error occurred"}})
	}
	return h.writeContactByID(c, userID)
}

// UpdatePilotCategory godoc
// @Summary Update pilot category
// @Tags Contacts
// @Accept json
// @Produce json
// @Param user_id path string true "User UUID"
// @Param request body dto.ContactPilotCategoryUpdateRequest true "JSON:API pilot category request"
// @Success 200 {object} dto.ContactListResponse
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/contacts/{user_id}/pilot-category [patch]
func (h *ContactHandler) UpdatePilotCategory(c *fiber.Ctx) error {
	userID, err := uuidv7.ParseString(strings.TrimSpace(c.Params("user_id")))
	if err != nil {
		return writeContactErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{Status: "422", Title: "Validation error", Detail: "user_id is invalid UUID"}})
	}
	var req dto.ContactPilotCategoryUpdateRequest
	if err := c.BodyParser(&req); err != nil {
		return writeContactErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{Status: "400", Title: "Invalid JSON", Detail: "an internal error occurred"}})
	}
	if errs := h.validate.ValidateStruct(req); errs != nil {
		return writeContactErrors(c, fiber.StatusUnprocessableEntity, errs)
	}
	if err := h.svc.UpdatePilotCategory(c.UserContext(), userID, req.Data.Attributes.PilotCategory); err != nil {
		return writeContactErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{Status: "400", Title: "Update pilot category failed", Detail: "an internal error occurred"}})
	}
	return h.writeContactByID(c, userID)
}

// UpdateLicense godoc
// @Summary Update contact license fields
// @Tags Contacts
// @Accept json
// @Produce json
// @Param user_id path string true "User UUID"
// @Param request body dto.ContactLicenseUpdateRequest true "JSON:API license update request"
// @Success 200 {object} dto.ContactListResponse
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/contacts/{user_id}/license [patch]
func (h *ContactHandler) UpdateLicense(c *fiber.Ctx) error {
	userID, err := uuidv7.ParseString(strings.TrimSpace(c.Params("user_id")))
	if err != nil {
		return writeContactErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{Status: "422", Title: "Validation error", Detail: "user_id is invalid UUID"}})
	}
	var req dto.ContactLicenseUpdateRequest
	if err := c.BodyParser(&req); err != nil {
		return writeContactErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{Status: "400", Title: "Invalid JSON", Detail: "an internal error occurred"}})
	}
	if errs := h.validate.ValidateStruct(req); errs != nil {
		return writeContactErrors(c, fiber.StatusUnprocessableEntity, errs)
	}
	if err := h.svc.UpdateLicense(c.UserContext(), userID, req.Data.Attributes.LicenseNo); err != nil {
		return writeContactErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{Status: "400", Title: "Update license failed", Detail: "an internal error occurred"}})
	}
	return h.writeContactByID(c, userID)
}

// UpdateChiefFlags godoc
// @Summary Update chief flags
// @Tags Contacts
// @Accept json
// @Produce json
// @Param user_id path string true "User UUID"
// @Param request body dto.ContactChiefFlagsUpdateRequest true "JSON:API chief flags request"
// @Success 200 {object} dto.ContactListResponse
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/contacts/{user_id}/chief-flags [patch]
func (h *ContactHandler) UpdateChiefFlags(c *fiber.Ctx) error {
	userID, err := uuidv7.ParseString(strings.TrimSpace(c.Params("user_id")))
	if err != nil {
		return writeContactErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{Status: "422", Title: "Validation error", Detail: "user_id is invalid UUID"}})
	}
	var req dto.ContactChiefFlagsUpdateRequest
	if err := c.BodyParser(&req); err != nil {
		return writeContactErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{Status: "400", Title: "Invalid JSON", Detail: "an internal error occurred"}})
	}
	if errs := h.validate.ValidateStruct(req); errs != nil {
		return writeContactErrors(c, fiber.StatusUnprocessableEntity, errs)
	}
	patch := contact.ProfilePatch{
		IsChiefPilot:             req.Data.Attributes.IsChiefPilot,
		IsChiefPhysician:         req.Data.Attributes.IsChiefPhysician,
		IsChiefTechnician:        req.Data.Attributes.IsChiefTechnician,
	}
	if err := h.svc.UpdateChiefFlags(c.UserContext(), userID, patch); err != nil {
		return writeContactErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{Status: "400", Title: "Update chief flags failed", Detail: "an internal error occurred"}})
	}
	return h.writeContactByID(c, userID)
}

// BulkUpdateContacts godoc
// @Summary Bulk update contacts status
// @Tags Contacts
// @Accept json
// @Produce json
// @Param request body dto.ContactBulkUpdateRequest true "JSON:API bulk update request"
// @Success 200 {object} dto.ContactSimpleSuccessResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 422 {object} dto.ErrorResponse
// @Router /api/v1/contacts/bulk-update [post]
func (h *ContactHandler) BulkUpdate(c *fiber.Ctx) error {
	var req dto.ContactBulkUpdateRequest
	if err := c.BodyParser(&req); err != nil {
		return writeContactErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{Status: "400", Title: "Invalid JSON", Detail: "an internal error occurred"}})
	}
	if errs := h.validate.ValidateStruct(req); errs != nil {
		return writeContactErrors(c, fiber.StatusUnprocessableEntity, errs)
	}

	ids := make([][]byte, 0, len(req.Data.Attributes.UserIDs))
	for i := range req.Data.Attributes.UserIDs {
		id, err := uuidv7.ParseString(strings.TrimSpace(req.Data.Attributes.UserIDs[i]))
		if err != nil {
			return writeContactErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
				Status: "422", Title: "Validation error", Detail: "user_ids contains invalid UUID", Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/user_ids"},
			}})
		}
		ids = append(ids, id)
	}

	count, err := h.svc.BulkUpdateStatus(c.UserContext(), ids, req.Data.Attributes.IsActive)
	if err != nil {
		return writeContactErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{Status: "400", Title: "Bulk update failed", Detail: "an internal error occurred"}})
	}
	return response.Write(c, fiber.StatusOK, jsonapi.Resource{Type: "contact_bulk_update", Attributes: map[string]any{"updated": true, "count": count}})
}

func profilePatchFromCreate(roleCode string, attrs dto.ContactCreateAttributes) contact.ProfilePatch {
	p := contact.ProfilePatch{}
	switch normalizeContactRoleCode(roleCode) {
	case auth.RoleCodePilot:
		fillPatchFromPilot(&p, attrs.Pilot)
	case auth.RoleCodeDoctor:
		fillPatchFromDoctor(&p, attrs.Doctor)
	case auth.RoleCodeAirRescuer:
		fillPatchFromAirRescuer(&p, attrs.AirRescuer)
	case auth.RoleCodeTechnician:
		fillPatchFromTechnician(&p, attrs.Technician)
	case auth.RoleCodeFlightAssistant:
		fillPatchFromFlightAssistant(&p, attrs.FlightAssistant)
	case auth.RoleCodeStaff:
		fillPatchFromStaff(&p, attrs.Staff)
	}
	return p
}

func profilePatchFromUpdate(roleCode string, attrs dto.ContactUpdateAttributes) contact.ProfilePatch {
	p := contact.ProfilePatch{}
	switch normalizeContactRoleCode(roleCode) {
	case auth.RoleCodePilot:
		fillPatchFromPilotUpdate(&p, attrs.Pilot)
	case auth.RoleCodeDoctor:
		fillPatchFromDoctorUpdate(&p, attrs.Doctor)
	case auth.RoleCodeAirRescuer:
		fillPatchFromAirRescuerUpdate(&p, attrs.AirRescuer)
	case auth.RoleCodeTechnician:
		fillPatchFromTechnicianUpdate(&p, attrs.Technician)
	case auth.RoleCodeFlightAssistant:
		fillPatchFromFlightAssistantUpdate(&p, attrs.FlightAssistant)
	case auth.RoleCodeStaff:
		fillPatchFromStaffUpdate(&p, attrs.Staff)
	}
	return p
}

func writeContactErrors(c *fiber.Ctx, status int, errs []jsonapi.ErrorObject) error {
	return response.WriteErrors(c, status, apperrorsx.EnrichErrorObjects(status, errs))
}

func inferContactErrorCode(status int, title, detail string) (string, string) {
	lowerTitle := strings.ToLower(strings.TrimSpace(title))
	lowerDetail := strings.ToLower(strings.TrimSpace(detail))
	switch status {
	case fiber.StatusNotFound:
		return apperrorsx.CodeContactNotFound, apperrorsx.ErrContactNotFound.ErrorCode
	case fiber.StatusUnprocessableEntity:
		switch {
		case strings.Contains(lowerDetail, "user_id is invalid uuid"):
			return apperrorsx.CodeContactInvalidUserID, apperrorsx.ErrContactInvalidUserID.ErrorCode
		default:
			return apperrorsx.CodeContactValidationFailed, apperrorsx.ErrContactValidationFailed.ErrorCode
		}
	case fiber.StatusBadRequest:
		switch {
		case strings.Contains(lowerTitle, "invalid json"):
			return apperrorsx.CodeContactInvalidJSON, apperrorsx.ErrContactInvalidJSON.ErrorCode
		case strings.Contains(lowerTitle, "create failed"):
			return apperrorsx.CodeContactCreateFailed, apperrorsx.ErrContactCreateFailed.ErrorCode
		case strings.Contains(lowerTitle, "get failed"):
			return apperrorsx.CodeContactGetFailed, apperrorsx.ErrContactGetFailed.ErrorCode
		case strings.Contains(lowerTitle, "list failed"):
			return apperrorsx.CodeContactListFailed, apperrorsx.ErrContactListFailed.ErrorCode
		case strings.Contains(lowerTitle, "delete failed"):
			return apperrorsx.CodeContactDeleteFailed, apperrorsx.ErrContactDeleteFailed.ErrorCode
		case strings.Contains(lowerTitle, "update status failed"):
			return apperrorsx.CodeContactUpdateStatusFailed, apperrorsx.ErrContactUpdateStatusFailed.ErrorCode
		case strings.Contains(lowerTitle, "update role failed"):
			return apperrorsx.CodeContactUpdateRoleFailed, apperrorsx.ErrContactUpdateRoleFailed.ErrorCode
		case strings.Contains(lowerTitle, "update pilot category failed"):
			return apperrorsx.CodeContactUpdatePilotCategoryFailed, apperrorsx.ErrContactUpdatePilotCategoryFailed.ErrorCode
		case strings.Contains(lowerTitle, "update license failed"):
			return apperrorsx.CodeContactUpdateLicenseFailed, apperrorsx.ErrContactUpdateLicenseFailed.ErrorCode
		case strings.Contains(lowerTitle, "update chief flags failed"):
			return apperrorsx.CodeContactUpdateChiefFlagsFailed, apperrorsx.ErrContactUpdateChiefFlagsFailed.ErrorCode
		case strings.Contains(lowerTitle, "update responsible complaints failed"):
			return apperrorsx.CodeContactUpdateResponsibleComplaintsFailed, apperrorsx.ErrContactUpdateResponsibleComplaintsFailed.ErrorCode
		case strings.Contains(lowerTitle, "bulk update failed"):
			return apperrorsx.CodeContactBulkUpdateFailed, apperrorsx.ErrContactBulkUpdateFailed.ErrorCode
		case strings.Contains(lowerTitle, "update failed"):
			return apperrorsx.CodeContactUpdateFailed, apperrorsx.ErrContactUpdateFailed.ErrorCode
		default:
			if strings.Contains(lowerDetail, "duplicate") || strings.Contains(lowerDetail, "already") {
				return apperrorsx.CodeContactInvalidPayload, apperrorsx.ErrContactInvalidPayload.ErrorCode
			}
			return apperrorsx.CodeContactInvalidPayload, apperrorsx.ErrContactInvalidPayload.ErrorCode
		}
	case fiber.StatusConflict:
		switch {
		case strings.Contains(lowerDetail, "username already exists"):
			return apperrorsx.CodeContactUsernameConflict, apperrorsx.ErrContactUsernameConflict.ErrorCode
		default:
			return apperrorsx.CodeContactUsernameConflict, apperrorsx.ErrContactUsernameConflict.ErrorCode
		}
	default:
		return apperrorsx.CodeInternalServerError, apperrorsx.ErrInternal.ErrorCode
	}
}

func validateContactCreatePayload(requestType string, resolvedRoleCodes []string, attrs dto.ContactCreateAttributes) []jsonapi.ErrorObject {
	if strings.TrimSpace(requestType) != contactCreateType() {
		return []jsonapi.ErrorObject{
			contactValidationError("/data/type", fmt.Sprintf("type must be %s", contactCreateType())),
		}
	}
	roleCodes := make([]string, 0, len(resolvedRoleCodes))
	for i := range resolvedRoleCodes {
		code := normalizeContactRoleCode(resolvedRoleCodes[i])
		if !isSupportedContactRole(code) {
			return []jsonapi.ErrorObject{
				contactValidationError("/data/attributes/role_ids", "one or more role_ids are not supported contact roles"),
			}
		}
		roleCodes = append(roleCodes, code)
	}
	errs := validateRoleSpecificCreatePayload(roleCodes, requestType, profilePayloadsFromCreate(attrs))
	if len(errs) > 0 {
		return errs
	}
	return errs
}

func validateRoleSpecificCreatePayload(expectedRoleCodes []string, requestType string, payloads []contactProfilePayload) []jsonapi.ErrorObject {
	expected := make(map[string]struct{}, len(expectedRoleCodes))
	for i := range expectedRoleCodes {
		expected[normalizeContactRoleCode(expectedRoleCodes[i])] = struct{}{}
	}
	errs := make([]jsonapi.ErrorObject, 0)
	for i := range payloads {
		if !payloads[i].present {
			continue
		}
		roleCode := normalizeContactRoleCode(payloads[i].roleCode)
		if _, ok := expected[roleCode]; !ok {
			errs = append(errs, contactValidationError("/data/attributes/"+payloads[i].field, fmt.Sprintf("%s payload is not allowed for type %s", payloads[i].field, strings.TrimSpace(requestType))))
		}
	}
	for i := range payloads {
		if _, ok := expected[normalizeContactRoleCode(payloads[i].roleCode)]; !ok {
			continue
		}
		if !payloads[i].present {
			errs = append(errs, contactValidationError("/data/attributes/"+payloads[i].field, fmt.Sprintf("%s payload is required when role is selected", payloads[i].field)))
		}
	}
	return errs
}

func parseContactCreateRoleIDs(attrs dto.ContactCreateAttributes) ([]byte, [][]byte, []jsonapi.ErrorObject) {
	raw := make([]string, 0, len(attrs.RoleIDs)+1)
	for i := range attrs.RoleIDs {
		roleID := strings.TrimSpace(attrs.RoleIDs[i])
		if roleID != "" {
			raw = append(raw, roleID)
		}
	}
	if strings.TrimSpace(attrs.RoleID) != "" {
		raw = append(raw, strings.TrimSpace(attrs.RoleID))
	}
	if len(raw) == 0 {
		return nil, nil, nil
	}
	uniq := make([][]byte, 0, len(raw))
	seen := make(map[string]struct{}, len(raw))
	errs := make([]jsonapi.ErrorObject, 0)
	for i := range raw {
		roleID, err := uuidv7.ParseString(raw[i])
		if err != nil {
			errs = append(errs, contactValidationError("/data/attributes/role_ids", "role_ids contains invalid UUID"))
			continue
		}
		key := string(roleID)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		uniq = append(uniq, roleID)
	}
	if len(errs) > 0 {
		return nil, nil, errs
	}
	return uniq[0], uniq, nil
}

func (h *ContactHandler) parseContactUpdateRoleIDs(
	c *fiber.Ctx,
	current *contact.ContactListItem,
	attrs dto.ContactUpdateAttributes,
) ([]byte, [][]byte, []string, bool, []jsonapi.ErrorObject) {
	roleIDProvided := attrs.RoleID != nil
	roleIDsProvided := attrs.RoleIDs != nil
	roleIDTrimmed := ""
	roleIDExplicitClear := false
	if roleIDProvided {
		roleIDTrimmed = strings.TrimSpace(*attrs.RoleID)
		if roleIDTrimmed == "" || strings.EqualFold(roleIDTrimmed, "null") {
			roleIDExplicitClear = true
			roleIDProvided = false
		}
	}
	if !roleIDProvided && !roleIDsProvided {
		if roleIDExplicitClear {
			return nil, [][]byte{}, []string{}, true, nil
		}
		return nil, nil, nil, false, nil
	}
	if current == nil {
		return nil, nil, nil, false, []jsonapi.ErrorObject{
			contactValidationError("/data/attributes/role_ids", "contact not found"),
		}
	}

	currentPrimaryRoleID, _ := roleIDBytesFromRoles(current)
	currentRoleIDs := roleIDBytesListFromRoles(current)

	primaryRoleID := currentPrimaryRoleID
	if roleIDProvided {
		parsedRoleID, err := uuidv7.ParseString(roleIDTrimmed)
		if err != nil {
			return nil, nil, nil, false, []jsonapi.ErrorObject{
				contactValidationError("/data/attributes/role_id", "role_id is invalid UUID"),
			}
		}
		primaryRoleID = parsedRoleID
	}

	roleIDs := currentRoleIDs
	if roleIDsProvided {
		if len(attrs.RoleIDs) == 0 {
			if roleIDProvided {
				return nil, nil, nil, false, []jsonapi.ErrorObject{
					contactValidationError("/data/attributes/role_ids", "role_ids cannot be empty when role_id is provided"),
				}
			}
			return nil, [][]byte{}, []string{}, true, nil
		}
		parsedRoleIDs := make([][]byte, 0, len(attrs.RoleIDs))
		for i := range attrs.RoleIDs {
			id, err := uuidv7.ParseString(strings.TrimSpace(attrs.RoleIDs[i]))
			if err != nil {
				return nil, nil, nil, false, []jsonapi.ErrorObject{
					contactValidationError("/data/attributes/role_ids", "role_ids contains invalid UUID"),
				}
			}
			parsedRoleIDs = append(parsedRoleIDs, id)
		}
		if !roleIDProvided && len(parsedRoleIDs) > 0 {
			// When only role_ids is provided, treat the first role as the new primary.
			primaryRoleID = parsedRoleIDs[0]
		}
		roleIDs = normalizeContactAssignedRoleIDs(primaryRoleID, parsedRoleIDs)
	} else if roleIDProvided {
		roleIDs = normalizeContactAssignedRoleIDs(primaryRoleID, currentRoleIDs)
	}

	if len(primaryRoleID) != 16 {
		if len(roleIDs) > 0 {
			primaryRoleID = roleIDs[0]
		} else {
			return nil, [][]byte{}, []string{}, true, nil
		}
	}
	if !containsContactRoleID(roleIDs, primaryRoleID) {
		roleIDs = normalizeContactAssignedRoleIDs(primaryRoleID, roleIDs)
	}

	roleCodes := make([]string, 0, len(roleIDs))
	for i := range roleIDs {
		code, err := h.svc.ResolveRoleCodeByID(c.UserContext(), roleIDs[i])
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return nil, nil, nil, false, []jsonapi.ErrorObject{
					contactValidationError("/data/attributes/role_ids", "one or more role_ids do not exist"),
				}
			}
			return nil, nil, nil, false, []jsonapi.ErrorObject{
				{Status: "400", Title: "Update failed", Detail: "an internal error occurred"},
			}
		}
		norm := normalizeContactRoleCode(code)
		if !isSupportedContactRole(norm) {
			return nil, nil, nil, false, []jsonapi.ErrorObject{
				contactValidationError("/data/attributes/role_ids", "one or more role_ids are not supported contact roles"),
			}
		}
		roleCodes = append(roleCodes, norm)
	}
	return primaryRoleID, roleIDs, roleCodes, true, nil
}

func normalizeContactAssignedRoleIDs(primaryRoleID []byte, roleIDs [][]byte) [][]byte {
	out := make([][]byte, 0, len(roleIDs)+1)
	seen := make(map[string]struct{}, len(roleIDs)+1)
	appendRole := func(roleID []byte) {
		if len(roleID) != 16 {
			return
		}
		key := string(roleID)
		if _, exists := seen[key]; exists {
			return
		}
		seen[key] = struct{}{}
		out = append(out, append([]byte(nil), roleID...))
	}
	appendRole(primaryRoleID)
	for i := range roleIDs {
		appendRole(roleIDs[i])
	}
	return out
}

func containsContactRoleID(roleIDs [][]byte, roleID []byte) bool {
	if len(roleID) != 16 {
		return false
	}
	target := string(roleID)
	for i := range roleIDs {
		if string(roleIDs[i]) == target {
			return true
		}
	}
	return false
}

func roleIDBytesFromRoles(item *contact.ContactListItem) ([]byte, bool) {
	if item == nil || item.RolesRaw == nil {
		return nil, false
	}
	roles := contactRoles(item.RolesRaw)
	if len(roles) == 0 {
		return nil, false
	}
	id, _ := roles[0]["id"].(string)
	if strings.TrimSpace(id) == "" {
		return nil, false
	}
	b, err := uuidv7.ParseString(strings.TrimSpace(id))
	if err != nil {
		return nil, false
	}
	return b, true
}

func roleIDBytesListFromRoles(item *contact.ContactListItem) [][]byte {
	if item == nil || item.RolesRaw == nil {
		return nil
	}
	roles := contactRoles(item.RolesRaw)
	out := make([][]byte, 0, len(roles))
	for i := range roles {
		id, _ := roles[i]["id"].(string)
		if strings.TrimSpace(id) == "" {
			continue
		}
		b, err := uuidv7.ParseString(strings.TrimSpace(id))
		if err != nil {
			continue
		}
		out = append(out, b)
	}
	return out
}

func inferContactCreateRoleCodes(attrs dto.ContactCreateAttributes) []string {
	payloads := profilePayloadsFromCreate(attrs)
	roleCodes := make([]string, 0, len(payloads))
	for i := range payloads {
		if !payloads[i].present {
			continue
		}
		roleCodes = append(roleCodes, normalizeContactRoleCode(payloads[i].roleCode))
	}
	return roleCodes
}

func validateRoleSpecificPayload(expectedRoleCodes []string, requestType string, payloads []contactProfilePayload) ([]string, []jsonapi.ErrorObject) {
	expected := make(map[string]struct{}, len(expectedRoleCodes))
	for i := range expectedRoleCodes {
		code := normalizeContactRoleCode(expectedRoleCodes[i])
		if code == "" {
			continue
		}
		expected[code] = struct{}{}
	}
	requestedRoleCodes := make([]string, 0, len(payloads))
	seenRequested := map[string]struct{}{}
	errs := make([]jsonapi.ErrorObject, 0)
	for i := range payloads {
		if !payloads[i].present {
			continue
		}
		code := normalizeContactRoleCode(payloads[i].roleCode)
		if _, ok := seenRequested[code]; !ok {
			seenRequested[code] = struct{}{}
			requestedRoleCodes = append(requestedRoleCodes, code)
		}
	}
	for i := range payloads {
		if !payloads[i].present {
			continue
		}
		roleCode := normalizeContactRoleCode(payloads[i].roleCode)
		if _, ok := expected[roleCode]; !ok {
			errs = append(errs, contactValidationError("/data/attributes/"+payloads[i].field, fmt.Sprintf("%s payload is not allowed for type %s", payloads[i].field, strings.TrimSpace(requestType))))
		}
	}
	if len(errs) > 0 {
		return nil, errs
	}
	return requestedRoleCodes, nil
}

func assignedContactRoleCodes(item *contact.ContactListItem) []string {
	if item == nil {
		return nil
	}
	seen := map[string]struct{}{}
	out := make([]string, 0, 4)
	add := func(code string) {
		code = normalizeContactRoleCode(code)
		if code == "" {
			return
		}
		if _, ok := seen[code]; ok {
			return
		}
		seen[code] = struct{}{}
		out = append(out, code)
	}
	add(item.RoleCode)
	for _, role := range contactRoles(item.RolesRaw) {
		code, _ := role["role_code"].(string)
		add(code)
	}
	return out
}

func profilePayloadsFromCreate(attrs dto.ContactCreateAttributes) []contactProfilePayload {
	return []contactProfilePayload{
		{roleCode: auth.RoleCodePilot, field: "pilot", present: attrs.Pilot != nil},
		{roleCode: auth.RoleCodeDoctor, field: "doctor", present: attrs.Doctor != nil},
		{roleCode: auth.RoleCodeAirRescuer, field: "air_rescuer", present: attrs.AirRescuer != nil},
		{roleCode: auth.RoleCodeTechnician, field: "technician", present: attrs.Technician != nil},
		{roleCode: auth.RoleCodeFlightAssistant, field: "flight_assistant", present: attrs.FlightAssistant != nil},
		{roleCode: auth.RoleCodeStaff, field: "staff", present: attrs.Staff != nil},
	}
}

func profilePayloadsFromUpdate(attrs dto.ContactUpdateAttributes) []contactProfilePayload {
	return []contactProfilePayload{
		{roleCode: auth.RoleCodePilot, field: "pilot", present: attrs.Pilot != nil},
		{roleCode: auth.RoleCodeDoctor, field: "doctor", present: attrs.Doctor != nil},
		{roleCode: auth.RoleCodeAirRescuer, field: "air_rescuer", present: attrs.AirRescuer != nil},
		{roleCode: auth.RoleCodeTechnician, field: "technician", present: attrs.Technician != nil},
		{roleCode: auth.RoleCodeFlightAssistant, field: "flight_assistant", present: attrs.FlightAssistant != nil},
		{roleCode: auth.RoleCodeStaff, field: "staff", present: attrs.Staff != nil},
	}
}

func normalizeContactRoleCode(roleCode string) string {
	return strings.ToLower(strings.TrimSpace(roleCode))
}

func normalizeContactRoleFilter(role string) string {
	v := normalizeContactRoleCode(role)
	switch v {
	case "technicians":
		return auth.RoleCodeTechnician
	default:
		return v
	}
}

func parseOptionalBoolQuery(value string) *bool {
	v := strings.ToLower(strings.TrimSpace(value))
	switch v {
	case "", "null":
		return nil
	case "1", "true", "yes", "y":
		out := true
		return &out
	case "0", "false", "no", "n":
		out := false
		return &out
	default:
		return nil
	}
}

func contactCreateType() string {
	return "contact_create"
}

func contactUpdateType() string {
	return "contact_update"
}

func isSupportedContactRole(roleCode string) bool {
	switch normalizeContactRoleCode(roleCode) {
	case auth.RoleCodePilot, auth.RoleCodeDoctor, auth.RoleCodeAirRescuer, auth.RoleCodeTechnician, auth.RoleCodeFlightAssistant, auth.RoleCodeStaff:
		return true
	default:
		return false
	}
}

func contactValidationError(pointer string, detail string) jsonapi.ErrorObject {
	return jsonapi.ErrorObject{
		Status: "422",
		Title:  "Validation error",
		Detail: detail,
		Source: &jsonapi.ErrorSource{Pointer: pointer},
	}
}

func fillPatchFromPilot(out *contact.ProfilePatch, in *dto.PilotProfileAttributes) {
	if out == nil || in == nil {
		return
	}
	out.PilotCategory = optionalStr(in.PilotCategory)
	out.ShortName = optionalStr(in.ShortName)
	out.LicenseNo = optionalStr(in.LicenseNo)
	out.LicenseIssuedAt = in.LicenseIssuedAt
	out.LicenseExpiredAt = in.LicenseExpiredAt
	out.TechnicianLicenseNo = optionalStr(in.TechnicianLicenseNo)
	out.HasIFRQualification = in.HasIFRQualification
	out.IsChiefPilot = in.IsChiefPilot
	if in.DULBaseIDs != nil {
		out.DULBaseIDsSet = true
		out.DULBaseIDs = make([][]byte, 0, len(in.DULBaseIDs))
		for i := range in.DULBaseIDs {
			id, err := uuidv7.ParseString(strings.TrimSpace(in.DULBaseIDs[i]))
			if err != nil {
				continue
			}
			out.DULBaseIDs = append(out.DULBaseIDs, id)
		}
	} else if in.DULBase != nil {
		// Support FE payload shape using dul_base object array.
		out.DULBaseIDsSet = true
		out.DULBaseIDs = make([][]byte, 0, len(in.DULBase))
		for i := range in.DULBase {
			id, err := uuidv7.ParseString(strings.TrimSpace(in.DULBase[i].ID))
			if err != nil {
				continue
			}
			out.DULBaseIDs = append(out.DULBaseIDs, id)
		}
	}
	out.TotalFlightMinutes = in.TotalFlightMinutes
	out.ResponsiblePilotMinutes = in.ResponsiblePilotMinutes
	out.SecondPilotMinutes = in.SecondPilotMinutes
	out.DoubleCommandMinutes = in.DoubleCommandMinutes
	out.FlightInstructorMinutes = in.FlightInstructorMinutes
	out.NightFlightMinutes = in.NightFlightMinutes
	out.IFRFlightMinutes = in.IFRFlightMinutes
	out.Location = optionalStr(in.Location)
	out.Postcode = optionalStr(in.Postcode)
	out.StreetLine = optionalStr(in.StreetLine)
	out.Note = optionalStr(in.Note)
	out.WeightKG = in.WeightKG
}

func fillPatchFromPilotUpdate(out *contact.ProfilePatch, in *dto.PilotProfileUpdateAttributes) {
	if out == nil || in == nil {
		return
	}
	out.PilotCategory = trimStringPointer(in.PilotCategory)
	out.ShortName = trimStringPointer(in.ShortName)
	out.LicenseNo = trimStringPointer(in.LicenseNo)
	out.LicenseIssuedAt = in.LicenseIssuedAt
	out.LicenseExpiredAt = in.LicenseExpiredAt
	out.TechnicianLicenseNo = trimStringPointer(in.TechnicianLicenseNo)
	out.HasIFRQualification = in.HasIFRQualification
	out.IsChiefPilot = in.IsChiefPilot
	if in.DULBaseIDs != nil {
		out.DULBaseIDsSet = true
		out.DULBaseIDs = make([][]byte, 0, len(in.DULBaseIDs))
		for i := range in.DULBaseIDs {
			id, err := uuidv7.ParseString(strings.TrimSpace(in.DULBaseIDs[i]))
			if err != nil {
				continue
			}
			out.DULBaseIDs = append(out.DULBaseIDs, id)
		}
	} else if in.DULBase != nil {
		out.DULBaseIDsSet = true
		out.DULBaseIDs = make([][]byte, 0, len(in.DULBase))
		for i := range in.DULBase {
			id, err := uuidv7.ParseString(strings.TrimSpace(in.DULBase[i].ID))
			if err != nil {
				continue
			}
			out.DULBaseIDs = append(out.DULBaseIDs, id)
		}
	}
	out.TotalFlightMinutes = in.TotalFlightMinutes
	out.ResponsiblePilotMinutes = in.ResponsiblePilotMinutes
	out.SecondPilotMinutes = in.SecondPilotMinutes
	out.DoubleCommandMinutes = in.DoubleCommandMinutes
	out.FlightInstructorMinutes = in.FlightInstructorMinutes
	out.NightFlightMinutes = in.NightFlightMinutes
	out.IFRFlightMinutes = in.IFRFlightMinutes
	out.Location = trimStringPointer(in.Location)
	out.Postcode = trimStringPointer(in.Postcode)
	out.StreetLine = trimStringPointer(in.StreetLine)
	out.Note = trimStringPointer(in.Note)
	out.WeightKG = in.WeightKG
}

func validatePilotDULBaseCreateInput(attrs dto.ContactCreateAttributes) []jsonapi.ErrorObject {
	if attrs.Pilot == nil {
		return nil
	}
	return validatePilotDULBaseValues(attrs.Pilot.DULBaseIDs, attrs.Pilot.DULBase)
}

func validatePilotDULBaseUpdateInput(attrs dto.ContactUpdateAttributes) []jsonapi.ErrorObject {
	if attrs.Pilot == nil {
		return nil
	}
	return validatePilotDULBaseValues(attrs.Pilot.DULBaseIDs, attrs.Pilot.DULBase)
}

func validatePilotDULBasePayload(pilot *dto.PilotProfileAttributes) []jsonapi.ErrorObject {
	if pilot == nil {
		return nil
	}
	return validatePilotDULBaseValues(pilot.DULBaseIDs, pilot.DULBase)
}

func validatePilotDULBaseValues(baseIDs []string, bases []dto.DULBase) []jsonapi.ErrorObject {
	if baseIDs != nil && bases != nil {
		return []jsonapi.ErrorObject{
			contactValidationError("/data/attributes/pilot", "provide only one of dul_base_ids or dul_base"),
		}
	}
	if baseIDs != nil {
		for i := range baseIDs {
			if _, err := uuidv7.ParseString(strings.TrimSpace(baseIDs[i])); err != nil {
				return []jsonapi.ErrorObject{
					contactValidationError("/data/attributes/pilot/dul_base_ids", "dul_base_ids contains invalid UUID"),
				}
			}
		}
	}
	if bases != nil {
		for i := range bases {
			if _, err := uuidv7.ParseString(strings.TrimSpace(bases[i].ID)); err != nil {
				return []jsonapi.ErrorObject{
					contactValidationError("/data/attributes/pilot/dul_base", "dul_base contains invalid UUID"),
				}
			}
		}
	}
	return nil
}

func validatePilotDULBasePayloadUpdate(pilot *dto.PilotProfileUpdateAttributes) []jsonapi.ErrorObject {
	if pilot == nil {
		return nil
	}
	return validatePilotDULBaseValues(pilot.DULBaseIDs, pilot.DULBase)
}

func fillPatchFromDoctor(out *contact.ProfilePatch, in *dto.DoctorProfileAttributes) {
	if out == nil || in == nil {
		return
	}
	out.ShortName = optionalStr(in.ShortName)
	out.Specialization = optionalStr(in.Specialization)
	out.SubSpecialization = optionalStr(in.SubSpecialization)
	out.IsChiefPhysician = in.IsChiefPhysician
	out.Location = optionalStr(in.Location)
	out.Postcode = optionalStr(in.Postcode)
	out.StreetLine = optionalStr(in.StreetLine)
	out.Note = optionalStr(in.Note)
}

func fillPatchFromDoctorUpdate(out *contact.ProfilePatch, in *dto.DoctorProfileUpdateAttributes) {
	if out == nil || in == nil {
		return
	}
	out.ShortName = trimStringPointer(in.ShortName)
	out.Specialization = trimStringPointer(in.Specialization)
	out.SubSpecialization = trimStringPointer(in.SubSpecialization)
	out.IsChiefPhysician = in.IsChiefPhysician
	out.Location = trimStringPointer(in.Location)
	out.Postcode = trimStringPointer(in.Postcode)
	out.StreetLine = trimStringPointer(in.StreetLine)
	out.Note = trimStringPointer(in.Note)
}

func fillPatchFromAirRescuer(out *contact.ProfilePatch, in *dto.AirRescuerProfileAttributes) {
	if out == nil || in == nil {
		return
	}
	out.ShortName = optionalStr(in.ShortName)
	out.ResponsibleFlightRescuer = in.ResponsibleFlightRescuer
	out.Location = optionalStr(in.Location)
	out.Postcode = optionalStr(in.Postcode)
	out.StreetLine = optionalStr(in.StreetLine)
	out.Note = optionalStr(in.Note)
}

func fillPatchFromAirRescuerUpdate(out *contact.ProfilePatch, in *dto.AirRescuerProfileUpdateAttributes) {
	if out == nil || in == nil {
		return
	}
	out.ShortName = trimStringPointer(in.ShortName)
	out.ResponsibleFlightRescuer = in.ResponsibleFlightRescuer
	out.Location = trimStringPointer(in.Location)
	out.Postcode = trimStringPointer(in.Postcode)
	out.StreetLine = trimStringPointer(in.StreetLine)
	out.Note = trimStringPointer(in.Note)
}

func fillPatchFromTechnician(out *contact.ProfilePatch, in *dto.TechnicianProfileAttributes) {
	if out == nil || in == nil {
		return
	}
	out.ShortName = optionalStr(in.ShortName)
	out.LicenseNo = optionalStr(in.LicenseNo)
	out.IsChiefTechnician = in.IsChiefTechnician
	out.IsResponsibleComplaints = in.IsResponsibleComplaints
	out.Location = optionalStr(in.Location)
	out.Postcode = optionalStr(in.Postcode)
	out.StreetLine = optionalStr(in.StreetLine)
	out.Note = optionalStr(in.Note)
}

func fillPatchFromTechnicianUpdate(out *contact.ProfilePatch, in *dto.TechnicianProfileUpdateAttributes) {
	if out == nil || in == nil {
		return
	}
	out.ShortName = trimStringPointer(in.ShortName)
	out.LicenseNo = trimStringPointer(in.LicenseNo)
	out.IsChiefTechnician = in.IsChiefTechnician
	out.IsResponsibleComplaints = in.IsResponsibleComplaints
	out.Location = trimStringPointer(in.Location)
	out.Postcode = trimStringPointer(in.Postcode)
	out.StreetLine = trimStringPointer(in.StreetLine)
	out.Note = trimStringPointer(in.Note)
}

func fillPatchFromFlightAssistant(out *contact.ProfilePatch, in *dto.FlightAssistantProfileAttributes) {
	if out == nil || in == nil {
		return
	}
	out.ShortName = optionalStr(in.ShortName)
	out.Location = optionalStr(in.Location)
	out.Postcode = optionalStr(in.Postcode)
	out.StreetLine = optionalStr(in.StreetLine)
	out.Note = optionalStr(in.Note)
}

func fillPatchFromFlightAssistantUpdate(out *contact.ProfilePatch, in *dto.FlightAssistantProfileUpdateAttributes) {
	if out == nil || in == nil {
		return
	}
	out.ShortName = trimStringPointer(in.ShortName)
	out.Location = trimStringPointer(in.Location)
	out.Postcode = trimStringPointer(in.Postcode)
	out.StreetLine = trimStringPointer(in.StreetLine)
	out.Note = trimStringPointer(in.Note)
}

func fillPatchFromStaff(out *contact.ProfilePatch, in *dto.StaffProfileAttributes) {
	if out == nil || in == nil {
		return
	}
	out.ShortName = optionalStr(in.ShortName)
	out.RoleLabel = optionalStr(in.RoleLabel)
	out.Location = optionalStr(in.Location)
	out.Postcode = optionalStr(in.Postcode)
	out.StreetLine = optionalStr(in.StreetLine)
	out.Note = optionalStr(in.Note)
}

func fillPatchFromStaffUpdate(out *contact.ProfilePatch, in *dto.StaffProfileUpdateAttributes) {
	if out == nil || in == nil {
		return
	}
	out.ShortName = trimStringPointer(in.ShortName)
	out.RoleLabel = trimStringPointer(in.RoleLabel)
	out.Location = trimStringPointer(in.Location)
	out.Postcode = trimStringPointer(in.Postcode)
	out.StreetLine = trimStringPointer(in.StreetLine)
	out.Note = trimStringPointer(in.Note)
}

func optionalStr(v string) *string {
	t := strings.TrimSpace(v)
	if t == "" {
		return nil
	}
	return &t
}

func trimStringPointer(v *string) *string {
	if v == nil {
		return nil
	}
	trimmed := strings.TrimSpace(*v)
	return &trimmed
}

func strPtr(v string) *string {
	return &v
}

func emptyToNil(v string) *string {
	if strings.TrimSpace(v) == "" {
		return nil
	}
	return &v
}

func normalizeContactUpdateSSOEmailForValidation(req *dto.ContactUpdateRequest) bool {
	if req == nil {
		return false
	}
	if req.Data.Attributes.SSOEmail == nil {
		return false
	}
	trimmed := strings.TrimSpace(*req.Data.Attributes.SSOEmail)
	if trimmed == "" {
		// For validation, empty SSO email should be treated as omitted.
		// After validation succeeds, caller can restore explicit empty to clear DB value.
		req.Data.Attributes.SSOEmail = nil
		return true
	}
	req.Data.Attributes.SSOEmail = &trimmed
	return false
}

func contactUUIDString(id []byte) string {
	s, _ := uuidv7.BytesToString(id)
	return s
}

func contactAttributes(item *contact.ContactListItem, includeSSOEmail bool) map[string]any {
	if item == nil {
		return map[string]any{}
	}
	roles := contactRoles(item.RolesRaw)
	if roles == nil {
		roles = []map[string]any{}
	}
	attrs := map[string]any{
		"pilot_category":  item.PilotCategory,
		"username":        item.Username,
		"email":           item.Email,
		"is_admin":        item.IsAdmin,
		"first_name":      item.FirstName,
		"last_name":       item.LastName,
		"short_name":      item.ShortName,
		"license_no":      item.LicenseNo,
		"location":        item.Location,
		"mobile_phone":    item.MobilePhone,
		"set_password":    !item.HasPassword,
		"sortkey":         cloneIntPointer(item.SortKey),
		"note":            item.Note,
		"is_active":       item.IsActive,
		"created_by":      uuidStringOrEmpty(item.CreatedBy),
		"updated_by":      uuidStringOrEmpty(item.UpdatedBy),
		"created_at":      optionalTimeRFC3339(item.CreatedAt),
		"updated_at":      optionalTimeRFC3339(item.UpdatedAt),
		"base_roles":      contactBaseRoles(item.BaseRolesRaw),
	}
	attrs["roles"] = roles
	if includeSSOEmail {
		attrs["sso_email"] = item.SSOEmail
	}
	roleCodes := map[string]struct{}{normalizeContactRoleCode(item.RoleCode): {}}
	for _, role := range roles {
		code, _ := role["role_code"].(string)
		if strings.TrimSpace(code) == "" {
			continue
		}
		roleCodes[normalizeContactRoleCode(code)] = struct{}{}
	}
	if _, ok := roleCodes[auth.RoleCodePilot]; ok {
		dulBases := contactDULBases(item.BaseRolesRaw)
		if strings.TrimSpace(item.PilotCategory) != "" ||
			item.PilotShortName != nil || item.PilotLicenseNo != nil || item.PilotLicenseIssuedAt != nil || item.PilotLicenseExpiredAt != nil ||
			item.PilotTechnicianLicenseNo != nil || item.PilotHasIFRQualification != nil || item.PilotIsChiefPilot != nil || item.PilotIsDUL != nil ||
			item.PilotTotalFlightMinutes != nil || item.PilotResponsiblePilotMinutes != nil ||
			item.PilotSecondPilotMinutes != nil || item.PilotDoubleCommandMinutes != nil || item.PilotFlightInstructorMinutes != nil ||
			item.PilotNightFlightMinutes != nil || item.PilotIFRFlightMinutes != nil || item.PilotLocation != nil || item.PilotPostcode != nil ||
			item.PilotStreetLine != nil || item.PilotNote != nil || item.PilotWeightKG != nil || len(dulBases) > 0 {
			attrs["pilot"] = map[string]any{
				"pilot_category":            item.PilotCategory,
				"short_name":                item.PilotShortName,
				"license_no":                item.PilotLicenseNo,
				"license_issued_at":         item.PilotLicenseIssuedAt,
				"license_expired_at":        item.PilotLicenseExpiredAt,
				"technician_license_no":     item.PilotTechnicianLicenseNo,
				"has_ifr_qualification":     item.PilotHasIFRQualification,
				"is_chief_pilot":            item.PilotIsChiefPilot,
				"is_dul":                    item.PilotIsDUL,
				"dul_base":                  dulBases,
				"total_flight_minutes":      item.PilotTotalFlightMinutes,
				"responsible_pilot_minutes": item.PilotResponsiblePilotMinutes,
				"second_pilot_minutes":      item.PilotSecondPilotMinutes,
				"double_command_minutes":    item.PilotDoubleCommandMinutes,
				"flight_instructor_minutes": item.PilotFlightInstructorMinutes,
				"night_flight_minutes":      item.PilotNightFlightMinutes,
				"ifr_flight_minutes":        item.PilotIFRFlightMinutes,
				"location":                  item.PilotLocation,
				"postcode":                  item.PilotPostcode,
				"street_line":               item.PilotStreetLine,
				"note":                      item.PilotNote,
				"weight_kg":                 item.PilotWeightKG,
			}
			removeContactAttributeKeys(attrs, "pilot_category", "short_name", "license_no", "location", "note")
		}
	}
	if _, ok := roleCodes[auth.RoleCodeDoctor]; ok {
		if item.DoctorShortName != nil || item.DoctorSpecialization != nil || item.DoctorSubSpecialization != nil ||
			item.DoctorIsChiefPhysician != nil || item.DoctorLocation != nil || item.DoctorPostcode != nil ||
			item.DoctorStreetLine != nil || item.DoctorNote != nil {
			attrs["doctor"] = map[string]any{
				"short_name":         item.DoctorShortName,
				"specialization":     item.DoctorSpecialization,
				"sub_specialization": item.DoctorSubSpecialization,
				"is_chief_physician": item.DoctorIsChiefPhysician,
				"location":           item.DoctorLocation,
				"postcode":           item.DoctorPostcode,
				"street_line":        item.DoctorStreetLine,
				"note":               item.DoctorNote,
			}
			removeContactAttributeKeys(attrs, "short_name", "location", "note", "license_no", "pilot_category")
		}
	}
	if _, ok := roleCodes[auth.RoleCodeAirRescuer]; ok {
		if item.AirRescuerShortName != nil || item.AirRescuerResponsibleFlightRescuer != nil ||
			item.AirRescuerLocation != nil || item.AirRescuerPostcode != nil || item.AirRescuerStreetLine != nil || item.AirRescuerNote != nil {
			attrs["air_rescuer"] = map[string]any{
				"short_name":                 item.AirRescuerShortName,
				"responsible_flight_rescuer": item.AirRescuerResponsibleFlightRescuer,
				"location":                   item.AirRescuerLocation,
				"postcode":                   item.AirRescuerPostcode,
				"street_line":                item.AirRescuerStreetLine,
				"note":                       item.AirRescuerNote,
			}
			removeContactAttributeKeys(attrs, "short_name", "location", "note", "license_no", "pilot_category")
		}
	}
	if _, ok := roleCodes[auth.RoleCodeTechnician]; ok {
		if item.TechnicianShortName != nil || item.TechnicianLicenseNo != nil || item.TechnicianIsChiefTechnician != nil ||
			item.TechnicianIsResponsibleComplaints != nil || item.TechnicianLocation != nil || item.TechnicianPostcode != nil ||
			item.TechnicianStreetLine != nil || item.TechnicianNote != nil {
			attrs["technician"] = map[string]any{
				"short_name":                item.TechnicianShortName,
				"license_no":                item.TechnicianLicenseNo,
				"is_chief_technician":       item.TechnicianIsChiefTechnician,
				"is_responsible_complaints": item.TechnicianIsResponsibleComplaints,
				"location":                  item.TechnicianLocation,
				"postcode":                  item.TechnicianPostcode,
				"street_line":               item.TechnicianStreetLine,
				"note":                      item.TechnicianNote,
			}
			removeContactAttributeKeys(attrs, "short_name", "license_no", "location", "note", "pilot_category")
		}
	}
	if _, ok := roleCodes[auth.RoleCodeFlightAssistant]; ok {
		if item.FlightAssistantShortName != nil || item.FlightAssistantLocation != nil || item.FlightAssistantPostcode != nil ||
			item.FlightAssistantStreetLine != nil || item.FlightAssistantNote != nil {
			attrs["flight_assistant"] = map[string]any{
				"short_name":  item.FlightAssistantShortName,
				"location":    item.FlightAssistantLocation,
				"postcode":    item.FlightAssistantPostcode,
				"street_line": item.FlightAssistantStreetLine,
				"note":        item.FlightAssistantNote,
			}
			removeContactAttributeKeys(attrs, "short_name", "location", "note", "license_no", "pilot_category")
		}
	}
	if _, ok := roleCodes[auth.RoleCodeStaff]; ok {
		if item.StaffShortName != nil || item.StaffRoleLabel != nil || item.StaffLocation != nil ||
			item.StaffPostcode != nil || item.StaffStreetLine != nil || item.StaffNote != nil {
			attrs["staff"] = map[string]any{
				"short_name":  item.StaffShortName,
				"role_label":  item.StaffRoleLabel,
				"location":    item.StaffLocation,
				"postcode":    item.StaffPostcode,
				"street_line": item.StaffStreetLine,
				"note":        item.StaffNote,
			}
			removeContactAttributeKeys(attrs, "short_name", "location", "note", "license_no", "pilot_category")
		}
	}
	return attrs
}

func removeContactAttributeKeys(attrs map[string]any, keys ...string) {
	for _, key := range keys {
		delete(attrs, key)
	}
}

func (h *ContactHandler) resolveContactProfileAttachmentID(c *fiber.Ctx, userID []byte, rawFileUUID *string) ([]byte, bool) {
	if rawFileUUID == nil {
		return nil, false
	}
	if strings.TrimSpace(*rawFileUUID) == "" {
		return nil, false
	}
	return resolveAttachmentIDFromInput(c, attachmentresolver.Params{
		FileManager: h.fileManager,

		RawAttachmentID: nil,
		RawFileID:       rawFileUUID,

		AttachmentField: "profile_attachment_id",
		FileField:       "file_uuid",

		RequireValue: false,

		RefType:  contactProfileAttachmentRefType,
		RefID:    idString(userID),
		Category: ptrString(contactProfileAttachmentCategory),

		IsPrimary: true,
		ActorID:   actorUserID(c),

		ValidateAttachmentExists: false,
	}, "Update failed")
}

func (h *ContactHandler) applyContactFoto(ctx context.Context, item *contact.ContactListItem, attrs map[string]any) {
	if item == nil || attrs == nil || len(item.ProfileAttachmentID) != 16 {
		return
	}
	fotoUUID := idString(item.ProfileAttachmentID)
	fotoDownloadURL := ""
	fotoThumbnailURL := ""
	var fotoOriginalSizeBytes *int64
	if h.fileManager != nil {
		attachment, err := h.fileManager.GetAttachmentByID(ctx, item.ProfileAttachmentID)
		if err == nil && attachment != nil {
			fotoUUID = idString(attachment.ID)
			if attachment.File != nil {
				asset := resolveFileManagerDownloadURLs(ctx, h.storage, attachment.File)
				fotoDownloadURL = strings.TrimSpace(asset.OriginalURL)
				fotoThumbnailURL = strings.TrimSpace(asset.ThumbnailURL)
				fotoOriginalSizeBytes = int64Ptr(asset.OriginalSizeBytes)
			}
		}
	}
	attrs["foto"] = map[string]any{
		"uuid":                   fotoUUID,
		"download_url":           fotoDownloadURL,
		"thumbnail_download_url": fotoThumbnailURL,
		"original_size_bytes":    fotoOriginalSizeBytes,
	}
}

func (h *ContactHandler) ensureContactProfileAttachmentSet(ctx context.Context, userID []byte) error {
	item, err := h.svc.GetContactByID(ctx, userID)
	if err != nil {
		return err
	}
	if item == nil || len(item.ProfileAttachmentID) != 16 {
		return errors.New("failed to set profile photo")
	}
	if h.fileManager != nil {
		attachment, err := h.fileManager.GetAttachmentByID(ctx, item.ProfileAttachmentID)
		if err != nil || attachment == nil {
			return errors.New("failed to set profile photo")
		}
	}
	return nil
}

func contactBaseRoles(raw *string) []map[string]any {
	if raw == nil || strings.TrimSpace(*raw) == "" {
		return nil
	}
	parts := strings.Split(*raw, ",")
	out := make([]map[string]any, 0, len(parts))
	for i := range parts {
		fields := strings.Split(parts[i], "|")
		if len(fields) < 3 {
			continue
		}
		hexID := strings.TrimSpace(fields[0])
		baseNameHex := strings.TrimSpace(fields[1])
		roleCode := strings.TrimSpace(fields[2])
		baseCategoryHex := ""
		if len(fields) >= 4 {
			baseCategoryHex = strings.TrimSpace(fields[3])
		}
		if hexID == "" || roleCode == "" {
			continue
		}
		idBytes, err := hex.DecodeString(hexID)
		if err != nil {
			continue
		}
		baseID, err := uuidv7.BytesToString(idBytes)
		if err != nil {
			continue
		}
		baseNameBytes, err := hex.DecodeString(baseNameHex)
		if err != nil {
			continue
		}
		item := map[string]any{
			"base_id":   baseID,
			"base_name": string(baseNameBytes),
			"role_code": roleCode,
		}
		if baseCategoryHex != "" {
			if baseCategoryBytes, err := hex.DecodeString(baseCategoryHex); err == nil {
				item["base_category"] = string(baseCategoryBytes)
			}
		}
		out = append(out, item)
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func contactDULBases(raw *string) []map[string]any {
	roles := contactBaseRoles(raw)
	if len(roles) == 0 {
		return nil
	}
	out := make([]map[string]any, 0)
	for i := range roles {
		roleCode, _ := roles[i]["role_code"].(string)
		if strings.TrimSpace(strings.ToLower(roleCode)) != "dul" {
			continue
		}
		baseID, _ := roles[i]["base_id"].(string)
		baseName, _ := roles[i]["base_name"].(string)
		baseCategory, _ := roles[i]["base_category"].(string)
		if strings.TrimSpace(baseID) == "" {
			continue
		}
		out = append(out, map[string]any{
			"id":       baseID,
			"name":     baseName,
			"category": baseCategory,
		})
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func contactRoles(raw *string) []map[string]any {
	if raw == nil || strings.TrimSpace(*raw) == "" {
		return []map[string]any{}
	}
	parts := strings.Split(*raw, ",")
	out := make([]map[string]any, 0, len(parts))
	for i := range parts {
		fields := strings.Split(parts[i], "|")
		if len(fields) != 3 {
			continue
		}
		roleIDHex := strings.TrimSpace(fields[0])
		roleNameHex := strings.TrimSpace(fields[1])
		roleCode := strings.TrimSpace(fields[2])
		if roleIDHex == "" || roleCode == "" {
			continue
		}
		roleIDBytes, err := hex.DecodeString(roleIDHex)
		if err != nil {
			continue
		}
		roleID, err := uuidv7.BytesToString(roleIDBytes)
		if err != nil {
			continue
		}
		roleNameBytes, err := hex.DecodeString(roleNameHex)
		if err != nil {
			continue
		}
		out = append(out, map[string]any{
			"id":        roleID,
			"role_code": roleCode,
			"role_name": string(roleNameBytes),
		})
	}
	if len(out) == 0 {
		return []map[string]any{}
	}
	return out
}

func intStatus(status int) string {
	if status == fiber.StatusNotFound {
		return "404"
	}
	return "400"
}

func optionalTimeRFC3339(v time.Time) string {
	if v.IsZero() {
		return ""
	}
	return v.UTC().Format(time.RFC3339)
}
