package handlers

import (
	"context"
	"errors"
	"strings"
	"time"

	"github.com/gofiber/fiber/v2"

	actionsignoff "wucher/internal/domain/action_signoff"
	complaint "wucher/internal/domain/complaint"
	contact "wucher/internal/domain/contact"
	easarelease "wucher/internal/domain/easa_release"
	fleethistory "wucher/internal/domain/fleet_history"
	"wucher/internal/shared/pkg/apperrorsx"
	"wucher/internal/shared/pkg/uuidv7"
	requestdto "wucher/internal/transport/http/dto/request"
	responsedto "wucher/internal/transport/http/dto/response"
	"wucher/internal/transport/http/jsonapi"
	"wucher/internal/transport/http/response"
	"wucher/internal/transport/http/validators"
)

type EASAReleaseHandler struct {
	svc              easarelease.Service
	complaintChecker complaintActionChecker
	signoffChecker   easaSignoffChecker
	historySvc       fleetHistoryRecorder
	contactSvc       contactNameResolver
	aogRecalc        aogRecalculator
	validate         *validators.Validator
}

// easaSignoffChecker resolves corrective-action sign-offs so the release can be gated
// on them: a release requires the open complaint (via complaint_id) to be signed off,
// or — when there is no open complaint — a standalone (fleet) sign-off.
type easaSignoffChecker interface {
	GetByFlight(ctx context.Context, flightID []byte) (*actionsignoff.ActionSignoff, error)
	GetByComplaintIDs(ctx context.Context, complaintIDs [][]byte) (map[string]*actionsignoff.ActionSignoff, error)
	// Unsign consumes the flight's standalone sign-off after a release, so the next
	// complaint-independent release requires a fresh signature (no stale sign-off).
	Unsign(ctx context.Context, flightID []byte, actor []byte) (*actionsignoff.ActionSignoff, error)
}

// consumeStandaloneSignoff clears the fleet (complaint-independent) sign-off after a
// release. No-op when unwired or when there is no standalone sign-off. Complaint-linked
// sign-offs are not touched — they are spent when the complaint closes/reopens.
func (h *EASAReleaseHandler) consumeStandaloneSignoff(ctx context.Context, flightID, actor []byte) {
	if h.signoffChecker == nil || len(flightID) != 16 {
		return
	}
	_, _ = h.signoffChecker.Unsign(ctx, flightID, actor)
}

// contactNameResolver looks up a contact (the EASA signer) by id to resolve its name.
type contactNameResolver interface {
	GetContactByID(ctx context.Context, userID []byte) (*contact.ContactListItem, error)
}

func (h *EASAReleaseHandler) WithHistoryService(svc fleetHistoryRecorder) *EASAReleaseHandler {
	h.historySvc = svc
	return h
}

func (h *EASAReleaseHandler) WithContactResolver(svc contactNameResolver) *EASAReleaseHandler {
	h.contactSvc = svc
	return h
}

func (h *EASAReleaseHandler) signerNameAndShort(ctx context.Context, signerID *string) (name *string, short *string) {
	if h.contactSvc == nil || signerID == nil || strings.TrimSpace(*signerID) == "" {
		return nil, nil
	}
	idBytes, err := uuidv7.ParseString(strings.TrimSpace(*signerID))
	if err != nil {
		return nil, nil
	}
	ct, err := h.contactSvc.GetContactByID(ctx, idBytes)
	if err != nil || ct == nil {
		return nil, nil
	}
	n := strings.TrimSpace(strings.TrimSpace(ct.FirstName) + " " + strings.TrimSpace(ct.LastName))
	if n == "" {
		n = strings.TrimSpace(ct.ShortName)
	}
	if n != "" {
		name = &n
	}
	if s := strings.TrimSpace(ct.ShortName); s != "" {
		short = &s
	}
	return name, short
}

type complaintActionChecker interface {
	ActiveGroundingByHelicopterIDs(ctx context.Context, helicopterIDs [][]byte) ([]complaint.Complaint, error)
	MarkFixedByHelicopter(ctx context.Context, helicopterID []byte, easaID []byte, actor []byte) error
	MarkFixedByComplaint(ctx context.Context, complaintID []byte, easaID []byte, actor []byte) error
	MarkFixedByFlight(ctx context.Context, flightID []byte, easaID []byte, actor []byte) error
}

// closeComplaintsForRelease closes the defect(s) the signed release resolves, at the
// scope the release was created with (most specific first):
//   - complaint_id set → close only that complaint;
//   - else flight_id set → close every open complaint reported on that flight;
//   - else (standalone / whole-aircraft) → close all of the helicopter's open complaints.
//
// complaint_id and flight_id are mutually exclusive at create time, so at most one of
// the first two branches applies. Then it recomputes the helicopter's AOG state.
func (h *EASAReleaseHandler) closeComplaintsForRelease(ctx context.Context, row *easarelease.EASARelease, actor []byte) {
	if h.complaintChecker == nil || row == nil {
		return
	}
	switch {
	case len(row.ComplaintID) == 16:
		_ = h.complaintChecker.MarkFixedByComplaint(ctx, row.ComplaintID, row.ID, actor)
	case len(row.FlightID) == 16:
		_ = h.complaintChecker.MarkFixedByFlight(ctx, row.FlightID, row.ID, actor)
	default:
		_ = h.complaintChecker.MarkFixedByHelicopter(ctx, row.HelicopterID, row.ID, actor)
	}
	if h.aogRecalc != nil {
		_ = h.aogRecalc.RecalculateAOGByHelicopter(ctx, row.HelicopterID)
	}
}

func NewEASAReleaseHandler(svc easarelease.Service) *EASAReleaseHandler {
	return &EASAReleaseHandler{svc: svc, validate: validators.New()}
}

func (h *EASAReleaseHandler) WithComplaintActionChecker(checker complaintActionChecker) *EASAReleaseHandler {
	h.complaintChecker = checker
	return h
}

func (h *EASAReleaseHandler) WithActionSignoffChecker(checker easaSignoffChecker) *EASAReleaseHandler {
	h.signoffChecker = checker
	return h
}

// requireActionSignoff gates a release on the corrective-action sign-off: when the
// helicopter has open (non-NSR) complaints, each must have a signed sign-off (linked by
// complaint_id); when there are none, a standalone (fleet) sign-off must be signed.
// Returns nil (gate passes) when the checkers are not wired.
func (h *EASAReleaseHandler) requireActionSignoff(ctx context.Context, flightID, helicopterID []byte) *apperrorsx.AppError {
	if h.signoffChecker == nil || h.complaintChecker == nil {
		return nil
	}
	open, err := h.complaintChecker.ActiveGroundingByHelicopterIDs(ctx, [][]byte{helicopterID})
	if err != nil {
		return apperrorsx.New(apperrorsx.ErrEASAReleaseCreateFailed).WithCause(err)
	}
	if len(open) > 0 {
		ids := make([][]byte, 0, len(open))
		for i := range open {
			ids = append(ids, open[i].ID)
		}
		signoffs, serr := h.signoffChecker.GetByComplaintIDs(ctx, ids)
		if serr != nil {
			return apperrorsx.New(apperrorsx.ErrEASAReleaseCreateFailed).WithCause(serr)
		}
		for i := range open {
			if so := signoffs[string(open[i].ID)]; so == nil || !so.IsSigned() {
				return apperrorsx.New(apperrorsx.ErrEASAReleaseSignoffRequired)
			}
		}
		return nil
	}
	standalone, serr := h.signoffChecker.GetByFlight(ctx, flightID)
	if serr != nil {
		return apperrorsx.New(apperrorsx.ErrEASAReleaseCreateFailed).WithCause(serr)
	}
	if standalone == nil || !standalone.IsSigned() {
		return apperrorsx.New(apperrorsx.ErrEASAReleaseSignoffRequired)
	}
	return nil
}

func (h *EASAReleaseHandler) WithAOGRecalculator(svc aogRecalculator) *EASAReleaseHandler {
	h.aogRecalc = svc
	return h
}

func parseEASADate(s *string) (*time.Time, error) {
	if s == nil || strings.TrimSpace(*s) == "" {
		return nil, nil
	}
	t, err := time.Parse("2006-01-02", strings.TrimSpace(*s))
	if err != nil {
		return nil, err
	}
	return &t, nil
}

func writeEASAReleaseServiceError(c *fiber.Ctx, err error, fallback apperrorsx.Def) error {
	switch {
	case errors.Is(err, easarelease.ErrNotFound):
		return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrEASAReleaseNotFound).WithCause(err))
	case errors.Is(err, easarelease.ErrAlreadySigned):
		return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrEASAReleaseAlreadySigned).WithCause(err))
	case errors.Is(err, easarelease.ErrIncompleteForSign):
		return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrEASAReleaseIncompleteForSign).WithCause(err))
	default:
		return apperrorsx.Write(c, apperrorsx.New(fallback).WithCause(err))
	}
}

func (h *EASAReleaseHandler) easaResource(ctx context.Context, row *easarelease.EASARelease) responsedto.EASAReleaseResource {
	res := easaReleaseResource(row)
	if row != nil {
		res.Attributes.EASASignerName, res.Attributes.EASASignerShort = h.signerNameAndShort(ctx, row.EASASignerID)
	}
	return res
}

func easaReleaseResource(row *easarelease.EASARelease) responsedto.EASAReleaseResource {
	if row == nil {
		return responsedto.EASAReleaseResource{}
	}
	id, _ := uuidv7.BytesToString(row.ID)
	out := responsedto.EASAReleaseResource{
		Type: "easa_release",
		ID:   id,
		Attributes: responsedto.EASAReleaseAttributes{
			HelicopterID:               idString(row.HelicopterID),
			ComplaintID:                idString(row.ComplaintID),
			FlightID:                   idString(row.FlightID),
			EASAACHours:                row.EASAACHours,
			EASALocation:               row.EASALocation,
			EASAWONo:                   row.EASAWONo,
			EASAMaintManualRevAirframe: row.EASAMaintManualRevAirframe,
			EASAMaintManualRevEngine:   row.EASAMaintManualRevEngine,
			EASASignerID:               row.EASASignerID,
			EASAPermitNo:               row.EASAPermitNo,
			IsSigned:                   row.IsSigned(),
			SignedBy:                   idString(row.SignedBy),
			MissingSignFields:          row.MissingSignFields(),
		},
	}
	if row.EASADate != nil {
		v := row.EASADate.UTC().Format("2006-01-02")
		out.Attributes.EASADate = &v
	}
	if row.SignedAt != nil {
		v := row.SignedAt.UTC().Format(time.RFC3339)
		out.Attributes.SignedAt = &v
	}
	if !row.CreatedAt.IsZero() {
		out.Attributes.CreatedAt = row.CreatedAt.UTC().Format(time.RFC3339)
	}
	if !row.UpdatedAt.IsZero() {
		out.Attributes.UpdatedAt = row.UpdatedAt.UTC().Format(time.RFC3339)
	}
	return out
}

// Create godoc
// @Summary Create EASA maintenance release (draft) for a helicopter
// @Tags EASA Releases
// @Accept json
// @Produce json
// @Param request body requestdto.EASAReleaseCreateRequest true "JSON:API EASA release create request"
// @Success 201 {object} responsedto.EASAReleaseResponse
// @Router /api/v1/easa-releases/create [post]
func (h *EASAReleaseHandler) Create(c *fiber.Ctx) error {
	var req requestdto.EASAReleaseCreateRequest
	if err := c.BodyParser(&req); err != nil {
		return writeBodyParseError(c, err, apperrorsx.ErrEASAReleaseInvalidJSON)
	}
	if errs := h.validate.ValidateStruct(req); errs != nil {
		return jsonapi.WriteErrors(c, fiber.StatusUnprocessableEntity, errs)
	}
	helicopterID, err := uuidv7.ParseString(strings.TrimSpace(req.Data.Attributes.HelicopterID))
	if err != nil {
		appErr := apperrorsx.New(apperrorsx.ErrEASAReleaseInvalidUUID).WithCause(err)
		appErr.Source = &jsonapi.ErrorSource{Pointer: "/data/attributes/helicopter_id"}
		return apperrorsx.Write(c, appErr)
	}
	var flightID []byte
	if s := strings.TrimSpace(req.Data.Attributes.FlightID); s != "" {
		flightID, err = uuidv7.ParseString(s)
		if err != nil {
			appErr := apperrorsx.New(apperrorsx.ErrEASAReleaseInvalidUUID).WithCause(err)
			appErr.Source = &jsonapi.ErrorSource{Pointer: "/data/attributes/flight_id"}
			return apperrorsx.Write(c, appErr)
		}
	}
	var complaintID []byte
	if s := strings.TrimSpace(req.Data.Attributes.ComplaintID); s != "" {
		complaintID, err = uuidv7.ParseString(s)
		if err != nil {
			appErr := apperrorsx.New(apperrorsx.ErrEASAReleaseInvalidUUID).WithCause(err)
			appErr.Source = &jsonapi.ErrorSource{Pointer: "/data/attributes/complaint_id"}
			return apperrorsx.Write(c, appErr)
		}
	}
	// complaint_id and flight_id are mutually exclusive: a release either closes one
	// specific complaint or all of a flight's complaints, never both.
	if len(complaintID) == 16 && len(flightID) == 16 {
		appErr := apperrorsx.New(apperrorsx.ErrEASAReleaseInvalidPayload)
		appErr.Detail = "complaint_id and flight_id are mutually exclusive; provide at most one"
		appErr.Source = &jsonapi.ErrorSource{Pointer: "/data/attributes/complaint_id"}
		return apperrorsx.Write(c, appErr)
	}
	if aerr := h.requireActionSignoff(c.UserContext(), flightID, helicopterID); aerr != nil {
		return apperrorsx.Write(c, aerr)
	}
	easaDate, err := parseEASADate(req.Data.Attributes.EASADate)
	if err != nil {
		appErr := apperrorsx.New(apperrorsx.ErrEASAReleaseInvalidPayload).WithCause(err)
		appErr.Source = &jsonapi.ErrorSource{Pointer: "/data/attributes/easa_date"}
		return apperrorsx.Write(c, appErr)
	}
	row := &easarelease.EASARelease{
		HelicopterID:               helicopterID,
		ComplaintID:                complaintID,
		FlightID:                   flightID,
		EASADate:                   easaDate,
		EASAACHours:                emptyToNilString(req.Data.Attributes.EASAACHours),
		EASALocation:               emptyToNilString(req.Data.Attributes.EASALocation),
		EASAWONo:                   emptyToNilString(req.Data.Attributes.EASAWONo),
		EASAMaintManualRevAirframe: emptyToNilString(req.Data.Attributes.EASAMaintManualRevAirframe),
		EASAMaintManualRevEngine:   emptyToNilString(req.Data.Attributes.EASAMaintManualRevEngine),
		EASASignerID:               emptyToNilString(req.Data.Attributes.EASASignerID),
		EASAPermitNo:               emptyToNilString(req.Data.Attributes.EASAPermitNo),
		CreatedBy:                  actorUserID(c),
		UpdatedBy:                  actorUserID(c),
	}
	if req.Data.Attributes.Sign {
		if len(row.MissingSignFields()) > 0 {
			appErr := apperrorsx.New(apperrorsx.ErrEASAReleaseIncompleteForSign)
			appErr.Detail = map[string]any{"missing_sign_fields": row.MissingSignFields()}
			return apperrorsx.Write(c, appErr)
		}
		now := time.Now().UTC()
		row.SignedAt = &now
		row.SignedBy = actorUserID(c)
	}
	if err := h.svc.Create(c.UserContext(), row); err != nil {
		return writeEASAReleaseServiceError(c, err, apperrorsx.ErrEASAReleaseCreateFailed)
	}
	created, _ := h.svc.GetByID(c.UserContext(), row.ID)
	if created == nil {
		created = row
	}
	actor := actorUserID(c)
	if created.IsSigned() {
		h.closeComplaintsForRelease(c.UserContext(), created, actor)
		h.consumeStandaloneSignoff(c.UserContext(), created.FlightID, actor)
	}
	if h.historySvc != nil {
		_ = h.historySvc.Record(c.UserContext(), created.HelicopterID, fleethistory.EntityEASARelease, created.ID, fleethistory.ActionEASACreated, "EASA release created (draft)", actor)
		if created.IsSigned() {
			_ = h.historySvc.Record(c.UserContext(), created.HelicopterID, fleethistory.EntityEASARelease, created.ID, fleethistory.ActionEASASigned, "Release to Service (EASA signed)", actor)
		}
	}
	return response.Write(c, fiber.StatusCreated, h.easaResource(c.UserContext(), created))
}

// GetByID godoc
// @Summary Get EASA release by ID
// @Tags EASA Releases
// @Produce json
// @Param id path string true "EASA release ID (UUIDv7)"
// @Success 200 {object} responsedto.EASAReleaseResponse
// @Router /api/v1/easa-releases/get/{id} [get]
func (h *EASAReleaseHandler) GetByID(c *fiber.Ctx) error {
	id, err := uuidv7.ParseString(strings.TrimSpace(c.Params("id")))
	if err != nil {
		appErr := apperrorsx.New(apperrorsx.ErrEASAReleaseInvalidUUID).WithCause(err)
		appErr.Source = &jsonapi.ErrorSource{Pointer: "/path/id"}
		return apperrorsx.Write(c, appErr)
	}
	row, err := h.svc.GetByID(c.UserContext(), id)
	if err != nil {
		return writeEASAReleaseServiceError(c, err, apperrorsx.ErrEASAReleaseNotFound)
	}
	if row == nil {
		return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrEASAReleaseNotFound))
	}
	return response.Write(c, fiber.StatusOK, h.easaResource(c.UserContext(), row))
}

// GetByFlight godoc
// @Summary Get the current EASA release for a flight
// @Tags EASA Releases
// @Produce json
// @Param flight_id path string true "Flight ID (UUIDv7)"
// @Success 200 {object} responsedto.EASAReleaseResponse
// @Router /api/v1/easa-releases/get-by-flight/{flight_id} [get]
func (h *EASAReleaseHandler) GetByFlight(c *fiber.Ctx) error {
	flightID, err := uuidv7.ParseString(strings.TrimSpace(c.Params("flight_id")))
	if err != nil {
		appErr := apperrorsx.New(apperrorsx.ErrEASAReleaseInvalidUUID).WithCause(err)
		appErr.Source = &jsonapi.ErrorSource{Pointer: "/path/flight_id"}
		return apperrorsx.Write(c, appErr)
	}
	row, err := h.svc.GetByFlightID(c.UserContext(), flightID)
	if err != nil {
		return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrEASAReleaseListFailed).WithCause(err))
	}
	if row == nil {
		// No release for this flight yet — return an empty resource (not 404).
		return response.Write(c, fiber.StatusOK, responsedto.EASAReleaseResource{})
	}
	return response.Write(c, fiber.StatusOK, h.easaResource(c.UserContext(), row))
}

// GetByComplaint godoc
// @Summary Get the current EASA release for a complaint
// @Tags EASA Releases
// @Produce json
// @Param complaint_id path string true "Complaint ID (UUIDv7)"
// @Success 200 {object} responsedto.EASAReleaseResponse
// @Router /api/v1/easa-releases/get-by-complaint/{complaint_id} [get]
func (h *EASAReleaseHandler) GetByComplaint(c *fiber.Ctx) error {
	complaintID, err := uuidv7.ParseString(strings.TrimSpace(c.Params("complaint_id")))
	if err != nil {
		appErr := apperrorsx.New(apperrorsx.ErrEASAReleaseInvalidUUID).WithCause(err)
		appErr.Source = &jsonapi.ErrorSource{Pointer: "/path/complaint_id"}
		return apperrorsx.Write(c, appErr)
	}
	row, err := h.svc.GetByComplaintID(c.UserContext(), complaintID)
	if err != nil {
		return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrEASAReleaseListFailed).WithCause(err))
	}
	if row == nil {
		// No release for this complaint yet — return an empty resource (not 404).
		return response.Write(c, fiber.StatusOK, responsedto.EASAReleaseResource{})
	}
	return response.Write(c, fiber.StatusOK, h.easaResource(c.UserContext(), row))
}

// ListByHelicopter godoc
// @Summary List EASA releases for a helicopter
// @Tags EASA Releases
// @Produce json
// @Param helicopter_id path string true "Helicopter ID (UUIDv7)"
// @Success 200 {object} responsedto.EASAReleaseListResponse
// @Router /api/v1/easa-releases/get-by-helicopter/{helicopter_id} [get]
func (h *EASAReleaseHandler) ListByHelicopter(c *fiber.Ctx) error {
	helicopterID, err := uuidv7.ParseString(strings.TrimSpace(c.Params("helicopter_id")))
	if err != nil {
		appErr := apperrorsx.New(apperrorsx.ErrEASAReleaseInvalidUUID).WithCause(err)
		appErr.Source = &jsonapi.ErrorSource{Pointer: "/path/helicopter_id"}
		return apperrorsx.Write(c, appErr)
	}
	rows, err := h.svc.ListByHelicopter(c.UserContext(), helicopterID)
	if err != nil {
		return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrEASAReleaseListFailed).WithCause(err))
	}
	data := make([]responsedto.EASAReleaseResource, 0, len(rows))
	for i := range rows {
		data = append(data, h.easaResource(c.UserContext(), &rows[i]))
	}
	return response.WriteWithMeta(c, fiber.StatusOK, data, map[string]any{
		"total": int64(len(data)),
	})
}

// Update godoc
// @Summary Update EASA release (draft fields)
// @Tags EASA Releases
// @Accept json
// @Produce json
// @Param id path string true "EASA release ID (UUIDv7)"
// @Param request body requestdto.EASAReleaseUpdateRequest true "JSON:API EASA release update request"
// @Success 200 {object} responsedto.EASAReleaseResponse
// @Router /api/v1/easa-releases/update/{id} [patch]
func (h *EASAReleaseHandler) Update(c *fiber.Ctx) error {
	id, err := uuidv7.ParseString(strings.TrimSpace(c.Params("id")))
	if err != nil {
		appErr := apperrorsx.New(apperrorsx.ErrEASAReleaseInvalidUUID).WithCause(err)
		appErr.Source = &jsonapi.ErrorSource{Pointer: "/path/id"}
		return apperrorsx.Write(c, appErr)
	}
	var req requestdto.EASAReleaseUpdateRequest
	if err := c.BodyParser(&req); err != nil {
		return writeBodyParseError(c, err, apperrorsx.ErrEASAReleaseInvalidJSON)
	}
	if errs := h.validate.ValidateStruct(req); errs != nil {
		return jsonapi.WriteErrors(c, fiber.StatusUnprocessableEntity, errs)
	}
	if strings.TrimSpace(req.Data.ID) != "" && strings.TrimSpace(req.Data.ID) != strings.TrimSpace(c.Params("id")) {
		appErr := apperrorsx.New(apperrorsx.ErrEASAReleaseInvalidPayload)
		appErr.Source = &jsonapi.ErrorSource{Pointer: "/data/id"}
		return apperrorsx.Write(c, appErr)
	}
	row, err := h.svc.GetByID(c.UserContext(), id)
	if err != nil {
		return writeEASAReleaseServiceError(c, err, apperrorsx.ErrEASAReleaseNotFound)
	}
	if row == nil {
		return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrEASAReleaseNotFound))
	}
	a := req.Data.Attributes
	if a.EASADate != nil {
		d, perr := parseEASADate(a.EASADate)
		if perr != nil {
			appErr := apperrorsx.New(apperrorsx.ErrEASAReleaseInvalidPayload).WithCause(perr)
			appErr.Source = &jsonapi.ErrorSource{Pointer: "/data/attributes/easa_date"}
			return apperrorsx.Write(c, appErr)
		}
		row.EASADate = d
	}
	if a.EASAACHours != nil {
		row.EASAACHours = emptyToNilString(a.EASAACHours)
	}
	if a.EASALocation != nil {
		row.EASALocation = emptyToNilString(a.EASALocation)
	}
	if a.EASAWONo != nil {
		row.EASAWONo = emptyToNilString(a.EASAWONo)
	}
	if a.EASAMaintManualRevAirframe != nil {
		row.EASAMaintManualRevAirframe = emptyToNilString(a.EASAMaintManualRevAirframe)
	}
	if a.EASAMaintManualRevEngine != nil {
		row.EASAMaintManualRevEngine = emptyToNilString(a.EASAMaintManualRevEngine)
	}
	if a.EASASignerID != nil {
		row.EASASignerID = emptyToNilString(a.EASASignerID)
	}
	if a.EASAPermitNo != nil {
		row.EASAPermitNo = emptyToNilString(a.EASAPermitNo)
	}
	row.UpdatedBy = actorUserID(c)
	if err := h.svc.Update(c.UserContext(), row); err != nil {
		return writeEASAReleaseServiceError(c, err, apperrorsx.ErrEASAReleaseUpdateFailed)
	}
	updated, _ := h.svc.GetByID(c.UserContext(), id)
	if updated == nil {
		updated = row
	}
	return response.Write(c, fiber.StatusOK, h.easaResource(c.UserContext(), updated))
}

// Sign godoc
// @Summary Sign the EASA maintenance release (closes the helicopter's open complaints)
// @Description Signs the release and closes the helicopter's open complaints. The corrective-action sign-off is re-checked at sign time: if the sign-off was invalidated since the draft was created (e.g. its complaint was reopened) it returns 422 `EASA_RELEASE_SIGNOFF_REQUIRED` and does not sign.
// @Tags EASA Releases
// @Produce json
// @Param id path string true "EASA release ID (UUIDv7)"
// @Success 200 {object} responsedto.EASAReleaseResponse
// @Failure 404 {object} jsonapi.ErrorDocument
// @Failure 422 {object} jsonapi.ErrorDocument
// @Router /api/v1/easa-releases/sign/{id} [post]
func (h *EASAReleaseHandler) Sign(c *fiber.Ctx) error {
	id, err := uuidv7.ParseString(strings.TrimSpace(c.Params("id")))
	if err != nil {
		appErr := apperrorsx.New(apperrorsx.ErrEASAReleaseInvalidUUID).WithCause(err)
		appErr.Source = &jsonapi.ErrorSource{Pointer: "/path/id"}
		return apperrorsx.Write(c, appErr)
	}
	// Re-check the sign-off gate at sign time: a draft created earlier may have had its
	// corrective-action sign-off invalidated since (e.g. its complaint was reopened), so
	// signing must not bypass the requirement.
	existing, gerr := h.svc.GetByID(c.UserContext(), id)
	if gerr != nil {
		return writeEASAReleaseServiceError(c, gerr, apperrorsx.ErrEASAReleaseSignFailed)
	}
	if existing == nil {
		return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrEASAReleaseNotFound))
	}
	if aerr := h.requireActionSignoff(c.UserContext(), existing.FlightID, existing.HelicopterID); aerr != nil {
		return apperrorsx.Write(c, aerr)
	}
	if err := h.svc.Sign(c.UserContext(), id, actorUserID(c)); err != nil {
		return writeEASAReleaseServiceError(c, err, apperrorsx.ErrEASAReleaseSignFailed)
	}
	row, _ := h.svc.GetByID(c.UserContext(), id)
	if row == nil {
		return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrEASAReleaseNotFound))
	}
	actor := actorUserID(c)
	h.closeComplaintsForRelease(c.UserContext(), row, actor)
	h.consumeStandaloneSignoff(c.UserContext(), row.FlightID, actor)
	if h.historySvc != nil {
		_ = h.historySvc.Record(c.UserContext(), row.HelicopterID, fleethistory.EntityEASARelease, row.ID, fleethistory.ActionEASASigned, "Release to Service (EASA signed)", actor)
	}
	return response.Write(c, fiber.StatusOK, h.easaResource(c.UserContext(), row))
}

// Delete godoc
// @Summary Delete (void) an EASA release
// @Tags EASA Releases
// @Produce json
// @Param id path string true "EASA release ID (UUIDv7)"
// @Success 200 {object} responsedto.EASAReleaseDeleteResponse
// @Router /api/v1/easa-releases/delete/{id} [delete]
func (h *EASAReleaseHandler) Delete(c *fiber.Ctx) error {
	id, err := uuidv7.ParseString(strings.TrimSpace(c.Params("id")))
	if err != nil {
		appErr := apperrorsx.New(apperrorsx.ErrEASAReleaseInvalidUUID).WithCause(err)
		appErr.Source = &jsonapi.ErrorSource{Pointer: "/path/id"}
		return apperrorsx.Write(c, appErr)
	}
	if err := h.svc.Delete(c.UserContext(), id, actorUserID(c)); err != nil {
		return writeEASAReleaseServiceError(c, err, apperrorsx.ErrEASAReleaseDeleteFailed)
	}
	return response.Write(c, fiber.StatusOK, map[string]any{"data": map[string]any{"type": "easa_release_delete", "attributes": map[string]bool{"deleted": true}}})
}
