package handlers

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

	"github.com/gofiber/fiber/v2"

	afterflightinspection "wucher/internal/domain/after_flight_inspection"
	fleetstatus "wucher/internal/domain/fleet_status"
	filemanager "wucher/internal/domain/file_manager"
	flightinspection "wucher/internal/domain/flight_inspection"
	fmreport "wucher/internal/domain/fm_report"
	helicopterdomain "wucher/internal/domain/helicopter"
	helicopterusage "wucher/internal/domain/helicopter_usage"
	takeoverdomain "wucher/internal/domain/takeover"
	"wucher/internal/shared/pkg/metricparse"
	"wucher/internal/shared/pkg/userctx"
	"wucher/internal/shared/pkg/uuidv7"
	rootdto "wucher/internal/transport/http/dto"
	requestdto "wucher/internal/transport/http/dto/request"
	responsedto "wucher/internal/transport/http/dto/response"
	shareddto "wucher/internal/transport/http/dto/shared"
	"wucher/internal/transport/http/jsonapi"
	"wucher/internal/transport/http/response"
	"wucher/internal/transport/http/validators"
)

type helicopterLookupService interface {
	GetByID(context.Context, []byte) (*helicopterdomain.Helicopter, error)
}

type FMReportHandler struct {
	svc                   fmreport.Service
	flight                *FlightHandler
	mission               *MissionHandler
	flightInspection      flightinspection.FlightInspectionService
	afterFlightInspection afterflightinspection.Service
	fleetStatus           *FleetStatusHandler
	helicopterUsage       helicopterusage.Service
	helicopter            helicopterLookupService
	takeover              takeoverLookupService
	storage               fileManagerDownloadURLStorage
	fileManager           filemanager.Service
	validate              *validators.Validator
}

type takeoverLookupService interface {
	GetByID(context.Context, []byte) (*takeoverdomain.TakeoverAc, error)
}

type fmReportCompleter interface {
	Complete(context.Context, []byte) (*fmreport.Report, error)
}

func NewFMReportHandler(svc fmreport.Service) *FMReportHandler {
	return &FMReportHandler{svc: svc, validate: validators.New()}
}

func (h *FMReportHandler) WithFlightHandler(flightHandler *FlightHandler) *FMReportHandler {
	h.flight = flightHandler
	return h
}

func (h *FMReportHandler) WithMissionHandler(missionHandler *MissionHandler) *FMReportHandler {
	h.mission = missionHandler
	return h
}

func (h *FMReportHandler) WithFlightInspectionService(svc flightinspection.FlightInspectionService) *FMReportHandler {
	h.flightInspection = svc
	return h
}

func (h *FMReportHandler) WithAfterFlightInspectionService(svc afterflightinspection.Service) *FMReportHandler {
	h.afterFlightInspection = svc
	return h
}

func (h *FMReportHandler) WithHelicopterUsageService(svc helicopterusage.Service) *FMReportHandler {
	h.helicopterUsage = svc
	return h
}

func (h *FMReportHandler) WithFleetStatusHandler(fleetStatusHandler *FleetStatusHandler) *FMReportHandler {
	h.fleetStatus = fleetStatusHandler
	return h
}

func (h *FMReportHandler) WithTakeoverService(takeoverSvc takeoverLookupService) *FMReportHandler {
	h.takeover = takeoverSvc
	return h
}

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

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

func (h *FMReportHandler) WithHelicopterService(svc helicopterLookupService) *FMReportHandler {
	h.helicopter = svc
	return h
}

// GetFMReport godoc
// @Summary Get FM report by flight ID
// @Description Returns a complete FM report for a flight. The persisted table keeps only the report keys; the response preloads flight, missions, flight inspection, and after-flight inspection details. It also embeds `fleet_status` (next inspection / maintenance schedules + base hours, component summary, EASA maintenance release) — a snapshot frozen when the report was created at after-flight inspection, so later complaints / maintenance changes do not alter a historical report. `fleet_status` is omitted when the helicopter has no fleet status.
// @Tags Flight Report
// @Produce json
// @Param flight_id path string true "Flight ID (UUIDv7)"
// @Success 200 {object} rootdto.FMReportResponse
// @Failure 404 {object} jsonapi.ErrorDocument
// @Failure 422 {object} jsonapi.ErrorDocument
// @Failure 400 {object} jsonapi.ErrorDocument
// @Router /api/v1/fm-reports/get/{flight_id} [get]
func (h *FMReportHandler) GetByFlightID(c *fiber.Ctx) error {
	flightRaw := strings.TrimSpace(c.Params("flight_id"))
	flightID, err := uuidv7.ParseString(flightRaw)
	if err != nil {
		return writeFMReportErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: "flight_id is invalid UUID",
			Source: &jsonapi.ErrorSource{Pointer: "/path/flight_id"},
		}})
	}
	if h == nil || h.svc == nil || h.flight == nil || h.flight.svc == nil || h.mission == nil || h.mission.svc == nil {
		return writeFMReportErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
			Status: "500",
			Title:  "Get failed",
			Detail: safeInternalDetail(),
		}})
	}

	report, err := h.svc.GetByFlightID(c.UserContext(), flightID)
	if err != nil {
		return writeFMReportErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Get failed",
			Detail: safeInternalDetail(),
		}})
	}
	if report == nil {
		return writeFMReportErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
			Status: "404",
			Title:  "Not found",
			Detail: "fm report not found for flight",
			Source: &jsonapi.ErrorSource{Pointer: "/path/flight_id"},
		}})
	}
	payload, err := h.buildFMReportResponse(c, report)
	if err != nil {
		return err
	}
	return response.Write(c, fiber.StatusOK, payload.Data)
}

// ListFMReports godoc
// @Summary List FM reports
// @Description Returns a paginated list of FM reports. Supports `search`, `filter[search]`, `filter[flight_id]`, `filter[takeover_id]`, `filter[flight_inspection_id]`, `filter[flight_date]`, `filter[has_engine1_gpc_n1]`, `filter[has_engine1_ptc_n2]`, `filter[has_engine2_gpc_n1]`, `filter[has_engine2_ptc_n2]`, `sort`, `page[number]`, `page[size]`, `page`, `size`, and `limit`.
// @Tags Flight Report
// @Produce json
// @Param search query string false "Search text"
// @Param filter[search] query string false "Search text (JSON:API style)"
// @Param filter[flight_id] query string false "Filter by flight ID"
// @Param filter[takeover_id] query string false "Filter by takeover ID"
// @Param filter[flight_inspection_id] query string false "Filter by flight inspection ID"
// @Param filter[flight_date] query string false "Filter by flight date (YYYY-MM-DD)"
// @Param filter[has_engine1_gpc_n1] query bool false "Filter reports with or without engine1_gpc_n1"
// @Param filter[has_engine1_ptc_n2] query bool false "Filter reports with or without engine1_ptc_n2"
// @Param filter[has_engine2_gpc_n1] query bool false "Filter reports with or without engine2_gpc_n1"
// @Param filter[has_engine2_ptc_n2] query bool false "Filter reports with or without engine2_ptc_n2"
// @Param sort query string false "Sort order: flight_date, -flight_date, created_at, -created_at, updated_at, -updated_at, engine1_gpc_n1, -engine1_gpc_n1, engine1_ptc_n2, -engine1_ptc_n2, engine2_gpc_n1, -engine2_gpc_n1, engine2_ptc_n2, -engine2_ptc_n2"
// @Param page[number] query int false "Page number"
// @Param page[size] query int false "Page size"
// @Param page query int false "Page number (fallback)"
// @Param size query int false "Page size (fallback)"
// @Param limit query int false "Page size override"
// @Success 200 {object} rootdto.FMReportListResponse
// @Failure 400 {object} jsonapi.ErrorDocument
// @Failure 422 {object} jsonapi.ErrorDocument
// @Router /api/v1/fm-reports/get-all [get]
func (h *FMReportHandler) List(c *fiber.Ctx) error {
	if h == nil || h.svc == nil {
		return writeFMReportErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
			Status: "500",
			Title:  "List failed",
			Detail: safeInternalDetail(),
		}})
	}

	pageNumber := parseIntDefault(c.Query("page[number]"), 0)
	if pageNumber <= 0 {
		pageNumber = parseIntDefault(c.Query("page"), 1)
	}
	pageSize := parseIntDefault(c.Query("page[size]"), 0)
	if pageSize <= 0 {
		pageSize = parseIntDefault(c.Query("limit"), 0)
	}
	if pageSize <= 0 {
		pageSize = parseIntDefault(c.Query("size"), 20)
	}
	if pageSize > 100 {
		pageSize = 100
	}
	if pageSize < 1 {
		pageSize = 20
	}
	if pageNumber < 1 {
		pageNumber = 1
	}
	offset := (pageNumber - 1) * pageSize

	search := strings.TrimSpace(c.Query("filter[search]"))
	if search == "" {
		search = strings.TrimSpace(c.Query("search"))
	}
	flightDate := strings.TrimSpace(c.Query("filter[flight_date]"))
	if flightDate == "" {
		flightDate = strings.TrimSpace(c.Query("flight_date"))
	}
	if flightDate != "" {
		if _, err := time.Parse("2006-01-02", flightDate); err != nil {
			return writeFMReportErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
				Status: "422",
				Title:  "Validation error",
				Detail: "flight_date must be YYYY-MM-DD",
				Source: &jsonapi.ErrorSource{Pointer: "/query/filter[flight_date]"},
			}})
		}
	}
	fromDate := strings.TrimSpace(c.Query("filter[from_date]"))
	if fromDate != "" {
		if _, err := time.Parse("2006-01-02", fromDate); err != nil {
			return writeFMReportErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
				Status: "422",
				Title:  "Validation error",
				Detail: "from_date must be YYYY-MM-DD",
				Source: &jsonapi.ErrorSource{Pointer: "/query/filter[from_date]"},
			}})
		}
	}
	toDate := strings.TrimSpace(c.Query("filter[to_date]"))
	if toDate != "" {
		if _, err := time.Parse("2006-01-02", toDate); err != nil {
			return writeFMReportErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
				Status: "422",
				Title:  "Validation error",
				Detail: "to_date must be YYYY-MM-DD",
				Source: &jsonapi.ErrorSource{Pointer: "/query/filter[to_date]"},
			}})
		}
	}
	helicopterIdentifier := strings.TrimSpace(c.Query("filter[helicopter_identifier]"))
	pilotName := strings.TrimSpace(c.Query("filter[pilot_name]"))

	flightIDRaw := strings.TrimSpace(c.Query("filter[flight_id]"))
	takeoverIDRaw := strings.TrimSpace(c.Query("filter[takeover_id]"))
	inspectionIDRaw := strings.TrimSpace(c.Query("filter[flight_inspection_id]"))
	flightID, errObj := parseOptionalUUID(&flightIDRaw, "/query/filter[flight_id]")
	if errObj != nil {
		return writeFMReportErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
	}
	takeoverID, errObj := parseOptionalUUID(&takeoverIDRaw, "/query/filter[takeover_id]")
	if errObj != nil {
		return writeFMReportErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
	}
	inspectionID, errObj := parseOptionalUUID(&inspectionIDRaw, "/query/filter[flight_inspection_id]")
	if errObj != nil {
		return writeFMReportErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
	}

	var hasEngine1GpcN1 *bool
	if raw := strings.TrimSpace(c.Query("filter[has_engine1_gpc_n1]")); raw != "" {
		v, err := strconv.ParseBool(raw)
		if err != nil {
			return writeFMReportErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
				Status: "422",
				Title:  "Validation error",
				Detail: "filter[has_engine1_gpc_n1] must be true or false",
				Source: &jsonapi.ErrorSource{Pointer: "/query/filter[has_engine1_gpc_n1]"},
			}})
		}
		hasEngine1GpcN1 = &v
	}

	var hasEngine1PtcN2 *bool
	if raw := strings.TrimSpace(c.Query("filter[has_engine1_ptc_n2]")); raw != "" {
		v, err := strconv.ParseBool(raw)
		if err != nil {
			return writeFMReportErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
				Status: "422",
				Title:  "Validation error",
				Detail: "filter[has_engine1_ptc_n2] must be true or false",
				Source: &jsonapi.ErrorSource{Pointer: "/query/filter[has_engine1_ptc_n2]"},
			}})
		}
		hasEngine1PtcN2 = &v
	}

	var hasEngine2GpcN1 *bool
	if raw := strings.TrimSpace(c.Query("filter[has_engine2_gpc_n1]")); raw != "" {
		v, err := strconv.ParseBool(raw)
		if err != nil {
			return writeFMReportErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
				Status: "422",
				Title:  "Validation error",
				Detail: "filter[has_engine2_gpc_n1] must be true or false",
				Source: &jsonapi.ErrorSource{Pointer: "/query/filter[has_engine2_gpc_n1]"},
			}})
		}
		hasEngine2GpcN1 = &v
	}

	var hasEngine2PtcN2 *bool
	if raw := strings.TrimSpace(c.Query("filter[has_engine2_ptc_n2]")); raw != "" {
		v, err := strconv.ParseBool(raw)
		if err != nil {
			return writeFMReportErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
				Status: "422",
				Title:  "Validation error",
				Detail: "filter[has_engine2_ptc_n2] must be true or false",
				Source: &jsonapi.ErrorSource{Pointer: "/query/filter[has_engine2_ptc_n2]"},
			}})
		}
		hasEngine2PtcN2 = &v
	}

	rows, total, err := h.svc.List(c.UserContext(), fmreport.ListFilter{
		Search:               search,
		FlightID:             flightID,
		TakeoverID:           takeoverID,
		FlightInspectionID:   inspectionID,
		FlightDate:           flightDate,
		HasEngine1GpcN1:      hasEngine1GpcN1,
		HasEngine1PtcN2:      hasEngine1PtcN2,
		HasEngine2GpcN1:      hasEngine2GpcN1,
		HasEngine2PtcN2:      hasEngine2PtcN2,
		Sort:                 strings.TrimSpace(c.Query("sort")),
		FromDate:             fromDate,
		ToDate:               toDate,
		HelicopterIdentifier: helicopterIdentifier,
		PilotName:            pilotName,
	}, pageSize, offset)
	if err != nil {
		return writeFMReportErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "List failed",
			Detail: safeInternalDetail(),
		}})
	}

	data := make([]rootdto.FMReportResource, 0, len(rows))
	for i := range rows {
		payload, buildErr := h.buildFMReportResponse(c, &rows[i])
		if buildErr != nil {
			return buildErr
		}
		data = append(data, payload.Data)
	}

	return response.WriteWithMeta(c, fiber.StatusOK, data, map[string]any{
		"page_number": pageNumber,
		"page_size":   pageSize,
		"total":       total,
	})
}

// UpdateFMReport godoc
// @Summary Update FM report metrics
// @Description Update only the engine metric fields for an existing FM report. All other FM report fields stay read-only on this endpoint. The response mirrors the GET FM report and embeds the frozen `fleet_status` snapshot (next inspection / maintenance schedules + base hours, component summary, EASA maintenance release).
// @Tags Flight Report
// @Accept json
// @Produce json
// @Param flight_id path string true "Flight ID (UUIDv7)"
// @Param request body requestdto.FMReportUpdateRequest true "JSON:API FM report update request"
// @Success 200 {object} rootdto.FMReportResponse
// @Failure 400 {object} jsonapi.ErrorDocument
// @Failure 404 {object} jsonapi.ErrorDocument
// @Failure 422 {object} jsonapi.ErrorDocument
// @Router /api/v1/fm-reports/update/{flight_id} [patch]
func (h *FMReportHandler) UpdateByFlightID(c *fiber.Ctx) error {
	if h == nil || h.svc == nil {
		return writeFMReportErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
			Status: "500",
			Title:  "Update failed",
			Detail: safeInternalDetail(),
		}})
	}

	flightRaw := strings.TrimSpace(c.Params("flight_id"))
	flightID, err := uuidv7.ParseString(flightRaw)
	if err != nil {
		return writeFMReportErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: "flight_id is invalid UUID",
			Source: &jsonapi.ErrorSource{Pointer: "/path/flight_id"},
		}})
	}

	var req requestdto.FMReportUpdateRequest
	if err := c.BodyParser(&req); err != nil {
		return writeFMReportErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Invalid JSON",
			Detail: safeInternalDetail(),
		}})
	}
	if errs := h.validate.ValidateStruct(req); errs != nil {
		return writeFMReportErrors(c, fiber.StatusUnprocessableEntity, errs)
	}

	report, err := h.svc.UpdateForFlight(
		c.UserContext(),
		flightID,
		req.Data.Attributes.Engine1GpcN1,
		req.Data.Attributes.Engine1PtcN2,
		req.Data.Attributes.Engine2GpcN1,
		req.Data.Attributes.Engine2PtcN2,
	)
	if err != nil {
		if errors.Is(err, fmreport.ErrCompleted) {
			return writeFMReportErrors(c, fiber.StatusLocked, []jsonapi.ErrorObject{{
				Status: "423",
				Title:  "Locked",
				Detail: "fm report is completed and can no longer be edited",
				Source: &jsonapi.ErrorSource{Pointer: "/path/flight_id"},
			}})
		}
		if strings.Contains(strings.ToLower(err.Error()), "not found") {
			return writeFMReportErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
				Status: "404",
				Title:  "Not found",
				Detail: "fm report not found for flight",
				Source: &jsonapi.ErrorSource{Pointer: "/path/flight_id"},
			}})
		}
		return writeFMReportErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Update failed",
			Detail: safeInternalDetail(),
		}})
	}

	payload, err := h.buildFMReportResponse(c, report)
	if err != nil {
		return err
	}
	return response.Write(c, fiber.StatusOK, payload.Data)
}

// CompleteFMReport godoc
// @Summary Complete FM report
// @Description Mark an FM report as completed for a flight. Once completed, the report becomes read-only and flight data edits are locked. Requires a verified security PIN: send the one-time token from POST /auth/pin/verify (action 1a75135b-430a-4592-83e1-6d27814185af) in the X-PIN-Verification-Token header.
// @Tags Flight Report
// @Produce json
// @Param flight_id path string true "Flight ID (UUIDv7)"
// @Param X-PIN-Verification-Token header string true "One-time security-PIN action token"
// @Success 200 {object} rootdto.FMReportResponse
// @Failure 202 {object} jsonapi.ErrorDocument "PIN verification required"
// @Failure 404 {object} jsonapi.ErrorDocument
// @Failure 422 {object} jsonapi.ErrorDocument
// @Failure 400 {object} jsonapi.ErrorDocument
// @Router /api/v1/fm-reports/complete/{flight_id} [post]
func (h *FMReportHandler) Complete(c *fiber.Ctx) error {
	if h == nil || h.svc == nil {
		return writeFMReportErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
			Status: "500",
			Title:  "Complete failed",
			Detail: safeInternalDetail(),
		}})
	}
	completer, ok := h.svc.(fmReportCompleter)
	if !ok {
		return writeFMReportErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
			Status: "500",
			Title:  "Complete failed",
			Detail: safeInternalDetail(),
		}})
	}

	flightRaw := strings.TrimSpace(c.Params("flight_id"))
	flightID, err := uuidv7.ParseString(flightRaw)
	if err != nil {
		return writeFMReportErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: "flight_id is invalid UUID",
			Source: &jsonapi.ErrorSource{Pointer: "/path/flight_id"},
		}})
	}

	report, err := completer.Complete(c.UserContext(), flightID)
	if err != nil {
		if errors.Is(err, fmreport.ErrNotFound) {
			return writeFMReportErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
				Status: "404",
				Title:  "Not found",
				Detail: "fm report not found for flight",
				Source: &jsonapi.ErrorSource{Pointer: "/path/flight_id"},
			}})
		}
		return writeFMReportErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Complete failed",
			Detail: safeInternalDetail(),
		}})
	}

	payload, err := h.buildFMReportResponse(c, report)
	if err != nil {
		return err
	}
	return response.Write(c, fiber.StatusOK, payload.Data)
}

func (h *FMReportHandler) buildFMReportResponse(c *fiber.Ctx, report *fmreport.Report) (*rootdto.FMReportResponse, error) {
	if h == nil || h.flight == nil || h.flight.svc == nil || h.mission == nil || h.mission.svc == nil {
		return nil, writeFMReportErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
			Status: "500",
			Title:  "Get failed",
			Detail: safeInternalDetail(),
		}})
	}
	if report == nil {
		return nil, writeFMReportErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
			Status: "404",
			Title:  "Not found",
			Detail: "fm report not found for flight",
			Source: &jsonapi.ErrorSource{Pointer: "/path/flight_id"},
		}})
	}

	flightRow, err := h.flight.svc.GetByID(c.UserContext(), report.FlightID)
	if err != nil || flightRow == nil {
		return nil, writeFMReportErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
			Status: "404",
			Title:  "Not found",
			Detail: "flight not found",
			Source: &jsonapi.ErrorSource{Pointer: "/path/flight_id"},
		}})
	}

	flightResource := h.flight.flightResource(c.UserContext(), c, flightRow)
	missionRows, err := h.mission.svc.ListByFlightID(c.UserContext(), report.FlightID)
	if err != nil {
		return nil, writeFMReportErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Get failed",
			Detail: safeInternalDetail(),
		}})
	}

	missions := make([]responsedto.MissionResource, 0, len(missionRows))
	for i := range missionRows {
		resource, buildErr := h.mission.missionResource(c.UserContext(), &missionRows[i])
		if buildErr != nil {
			return nil, writeFMReportErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
				Status: "400",
				Title:  "Get failed",
				Detail: buildErr.Error(),
			}})
		}
		missions = append(missions, resource)
	}

	var flightInspectionResource *rootdto.FlightInspectionResource
	if h.flightInspection != nil && len(report.FlightInspectionID) == 16 {
		if inspectionRow, inspectionErr := h.flightInspection.GetByID(c.UserContext(), report.FlightInspectionID); inspectionErr == nil && inspectionRow != nil {
			flightInspectionResource = &rootdto.FlightInspectionResource{
				Type: "flight_inspection",
				ID:   idString(inspectionRow.ID),
				Attributes: rootdto.FlightInspectionAttributes{
					InspectionDate: inspectionRow.InspectionDate.UTC().Format("2006-01-02"),
					Status:         inspectionRow.Status,
					CreatedAt:      inspectionRow.CreatedAt.UTC().Format(time.RFC3339),
					CreatedBy:      uuidStringOrEmpty(inspectionRow.CreatedBy),
					UpdatedAt:      inspectionRow.UpdatedAt.UTC().Format(time.RFC3339),
					UpdatedBy:      uuidStringOrEmpty(inspectionRow.UpdatedBy),
				},
			}
		}
	}

	var afterFlightInspectionResource *rootdto.AfterFlightInspectionResource
	if h.afterFlightInspection != nil && len(report.FlightInspectionID) == 16 {
		if afterRow, afterErr := h.afterFlightInspection.GetByFlightInspectionID(c.UserContext(), report.FlightInspectionID); afterErr == nil && afterRow != nil {
			afterFlightInspectionResource = buildAfterFlightInspectionResource(afterRow, report.FlightInspectionID, flightInspectionResource)
		}
	}

	// Prefer the fleet status snapshot frozen when the report was created; fall
	// back to a live build only for legacy reports created before snapshots existed.
	var fleetStatusResource *responsedto.FleetStatusResource
	var helicopterUsageSnapshot *rootdto.HelicopterUsageResource
	var helicopterID []byte
	if h.flight != nil {
		helicopterID = h.flight.ResolveHelicopterIDByTakeover(c.UserContext(), report.TakeoverID)
	}
	// While the report is still OPEN, show LIVE fleet status so fleet updates (maintenance
	// schedules, summary, …) reflect immediately. Once the report is COMPLETED, show the
	// snapshot frozen at completion.
	if report.CompletedAt != nil {
		if history, histErr := h.svc.GetFleetHistory(c.UserContext(), report.ID); histErr == nil && history != nil {
			fleetStatusResource = fmReportFleetHistoryResource(history)
		}
	}
	if fleetStatusResource == nil && h.fleetStatus != nil {
		if len(helicopterID) == 16 {
			fleetStatusResource = h.fleetStatus.FMReportFleetStatus(c.UserContext(), helicopterID)
		}
	}
	var usageBreakdown *rootdto.FMReportUsageBreakdown
	if len(helicopterID) == 16 {
		resource := helicopterUsageZeroResource(helicopterID)
		helicopterUsageSnapshot = &resource
		if h.helicopterUsage != nil {
			var usageRow *helicopterusage.HelicopterUsage
			if r, usageErr := h.helicopterUsage.GetByHelicopterID(c.UserContext(), helicopterID); usageErr == nil && r != nil {
				usageRow = r
				resource = helicopterUsageResource(usageRow)
				helicopterUsageSnapshot = &resource
			}
			today, _ := h.helicopterUsage.FlightStatsByFlightID(c.UserContext(), report.FlightID)
			// Absent engines (single-engine aircraft) are left empty, not mirrored from airframe.
			hasE1, hasE2 := true, true
			if h.fleetStatus != nil && h.fleetStatus.helicopterSvc != nil {
				if heli, herr := h.fleetStatus.helicopterSvc.GetByID(c.UserContext(), helicopterID); herr == nil && heli != nil {
					hasE1, hasE2 = heli.NR1, heli.NR2
				}
			}
			breakdown := buildFMReportUsageBreakdown(usageRow, today, report, hasE1, hasE2)
			usageBreakdown = &breakdown
		}
	}

	var takeoverFiles []responsedto.TakeoverFileResource
	var pilotName *string
	var helicopterIdentifier *string
	if h.takeover != nil && len(report.TakeoverID) == 16 {
		if takeoverRow, takeoverErr := h.takeover.GetByID(c.UserContext(), report.TakeoverID); takeoverErr == nil && takeoverRow != nil {
			takeoverFiles = takeoverFileResources(c.UserContext(), h.storage, takeoverRow.Files)
			var pilotNames []string
			for _, crew := range takeoverRow.RosterCrews {
				if strings.EqualFold(crew.RoleCode, "PILOT") {
					name := strings.TrimSpace(crew.NameLabel)
					if name == "" && crew.User != nil {
						name = strings.TrimSpace(crew.User.FirstName + " " + crew.User.LastName)
					}
					if name != "" {
						pilotNames = append(pilotNames, name)
					}
				}
			}
			if len(pilotNames) > 0 {
				joined := strings.Join(pilotNames, ", ")
				pilotName = &joined
			}
			if takeoverRow.ReserveAc != nil && takeoverRow.ReserveAc.Aircraft != nil {
				ident := takeoverRow.ReserveAc.Aircraft.Identifier
				helicopterIdentifier = &ident
			}
		}
	}

	var fmReportAttachments *rootdto.FMReportAttachments
	if h.fileManager != nil && len(report.ID) == 16 {
		refID := idString(report.ID)
		atts, _, attErr := h.fileManager.ListAttachmentsByReference(c.UserContext(), filemanager.ListAttachmentsParams{
			RefType: "fm_report",
			RefID:   refID,
			Limit:   1000,
		})
		if attErr == nil && len(atts) > 0 {
			fmReportAttachments = &rootdto.FMReportAttachments{}
			for i := range atts {
				att := &atts[i]
				item := rootdto.FMReportAttachmentItem{
					ID:     idString(att.ID),
					FileID: idString(att.FileID),
				}
				if att.File != nil {
					url := resolveFileManagerDownloadURL(c.UserContext(), h.storage, att.File)
					if url != "" {
						item.DownloadURL = &url
					}
				}
				cat := ""
				if att.Category != nil {
					cat = *att.Category
				}
				switch cat {
				case "additional_fm_report":
					fmReportAttachments.AdditionalFMReport = append(fmReportAttachments.AdditionalFMReport, item)
				case "briefing":
					fmReportAttachments.Briefing = append(fmReportAttachments.Briefing, item)
				case "weight_balance":
					fmReportAttachments.WeightBalance = append(fmReportAttachments.WeightBalance, item)
				case "ofp":
					fmReportAttachments.OFP = append(fmReportAttachments.OFP, item)
				}
			}
		}

		// Auto-fetch OFP attachments from takeover (uploaded in Step 1 with category="ofp").
		if len(report.TakeoverID) == 16 {
			takeoverRefID := idString(report.TakeoverID)
			ofpCategory := "ofp"
			ofpAtts, _, ofpErr := h.fileManager.ListAttachmentsByReference(c.UserContext(), filemanager.ListAttachmentsParams{
				RefType:  "takeover",
				RefID:    takeoverRefID,
				Category: &ofpCategory,
				Limit:    1000,
			})
			if ofpErr == nil && len(ofpAtts) > 0 {
				if fmReportAttachments == nil {
					fmReportAttachments = &rootdto.FMReportAttachments{}
				}
				existingOFPFileIDs := make(map[string]struct{})
				for _, existing := range fmReportAttachments.OFP {
					existingOFPFileIDs[existing.FileID] = struct{}{}
				}
				for i := range ofpAtts {
					att := &ofpAtts[i]
					fileIDStr := idString(att.FileID)
					if _, dup := existingOFPFileIDs[fileIDStr]; dup {
						continue
					}
					item := rootdto.FMReportAttachmentItem{
						ID:     idString(att.ID),
						FileID: fileIDStr,
					}
					if att.File != nil {
						url := resolveFileManagerDownloadURL(c.UserContext(), h.storage, att.File)
						if url != "" {
							item.DownloadURL = &url
						}
					}
					fmReportAttachments.OFP = append(fmReportAttachments.OFP, item)
				}
			}
		}

		// Auto-fetch edited W&B template files from takeover (set by pencil-icon edits in Step 1).
		// These live in file_files with takeover_id+template_uuid set but have no fm_report attachment record.
		if len(report.TakeoverID) == 16 {
			wbFiles, wbErr := h.fileManager.ListEditedWBFilesByTakeoverID(c.UserContext(), report.TakeoverID)
			if wbErr == nil && len(wbFiles) > 0 {
				// Build a set of file IDs already present in WeightBalance to avoid duplicates.
				existingWBFileIDs := make(map[string]struct{})
				if fmReportAttachments != nil {
					for _, existing := range fmReportAttachments.WeightBalance {
						existingWBFileIDs[existing.FileID] = struct{}{}
					}
				}
				if fmReportAttachments == nil {
					fmReportAttachments = &rootdto.FMReportAttachments{}
				}
				for i := range wbFiles {
					f := &wbFiles[i]
					fileIDStr := idString(f.ID)
					if _, dup := existingWBFileIDs[fileIDStr]; dup {
						continue
					}
					item := rootdto.FMReportAttachmentItem{
						ID:     fileIDStr,
						FileID: fileIDStr,
					}
					url := resolveFileManagerDownloadURL(c.UserContext(), h.storage, f)
					if url != "" {
						item.DownloadURL = &url
					}
					fmReportAttachments.WeightBalance = append(fmReportAttachments.WeightBalance, item)
				}
			}
		}
	}

	payload := &rootdto.FMReportResponse{
		Data: rootdto.FMReportResource{
			Type: "fm_report",
			ID:   idString(report.ID),
			Attributes: rootdto.FMReportAttributes{
				ID:                    idString(report.ID),
				ReportCode:            report.ReportCode,
				TakeoverID:            idString(report.TakeoverID),
				FlightID:              idString(report.FlightID),
				FlightInspectionID:    idString(report.FlightInspectionID),
				Engine1GpcN1:          report.Engine1GpcN1,
				Engine1PtcN2:          report.Engine1PtcN2,
				Engine2GpcN1:          report.Engine2GpcN1,
				Engine2PtcN2:          report.Engine2PtcN2,
				CompletedAt:           timeStringOrEmpty(report.CompletedAt),
				CompletedBy:           uuidStringOrEmpty(report.CompletedBy),
				PilotName:             pilotName,
				HelicopterIdentifier:  helicopterIdentifier,
				Flight:                &flightResource,
				Missions:              missions,
				FlightInspection:      flightInspectionResource,
				AfterFlightInspection: afterFlightInspectionResource,
				FleetStatus:           fleetStatusResource,
				HelicopterUsage:       helicopterUsageSnapshot,
				UsageBreakdown:        usageBreakdown,
				TakeoverFiles:         takeoverFiles,
				FMReportAttachments:   fmReportAttachments,
				CreatedAt:             report.CreatedAt.UTC().Format(time.RFC3339),
				UpdatedAt:             report.UpdatedAt.UTC().Format(time.RFC3339),
			},
		},
	}
	return payload, nil
}

const fmReportFileFolderName = "fm-reports"

var validFMReportAttachmentCategories = map[string]bool{
	"additional_fm_report": true,
	"briefing":             true,
	"weight_balance":       true,
	"ofp":                  true,
}

// ensureFMReportFileFolderPath ensures the folder structure:
// __system_root_files__ / fm-reports / {helicopterIdentifier}
// Returns the innermost folder ID.
func ensureFMReportFileFolderPath(ctx context.Context, svc filemanager.Service, helicopterIdentifier string, actorID []byte) ([]byte, error) {
	rootFolder, err := svc.GetOrCreateSystemRootFolder(ctx)
	if err != nil {
		return nil, err
	}
	if rootFolder == nil || len(rootFolder.ID) != 16 {
		return nil, filemanager.ErrFolderNotFound
	}
	fmReportsFolderID, err := ensureFileManagerFolder(ctx, svc, rootFolder.ID, fmReportFileFolderName, actorID)
	if err != nil {
		return nil, err
	}
	return ensureFileManagerFolder(ctx, svc, fmReportsFolderID, helicopterIdentifier, actorID)
}

// RequestAttachmentUploadIntent godoc
// @Summary Request upload intent for FM report attachment
// @Description Returns a presigned PUT URL and upload_intent_uuid. After uploading the file to the presigned URL, call POST /fm-reports/{id}/attachments with the upload_intent_uuid to create the file node and attach it to the report.
// @Tags Flight Report
// @Accept json
// @Produce json
// @Param id path string true "FM Report ID (UUIDv7)"
// @Param request body object true "body with name, content_type, size_bytes"
// @Success 201 {object} object
// @Failure 400 {object} jsonapi.ErrorDocument
// @Failure 422 {object} jsonapi.ErrorDocument
// @Router /api/v1/fm-reports/{id}/attachments/upload-intent [post]
func (h *FMReportHandler) RequestAttachmentUploadIntent(c *fiber.Ctx) error {
	if h.fileManager == nil {
		return writeFMReportErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
			Status: "500",
			Title:  "Upload failed",
			Detail: safeInternalDetail(),
		}})
	}

	idRaw := strings.TrimSpace(c.Params("id"))
	if _, err := uuidv7.ParseString(idRaw); err != nil {
		return writeFMReportErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: "id is invalid UUID",
			Source: &jsonapi.ErrorSource{Pointer: "/path/id"},
		}})
	}

	var body struct {
		Name        string `json:"name"`
		ContentType string `json:"content_type"`
		SizeBytes   int64  `json:"size_bytes"`
	}
	if err := c.BodyParser(&body); err != nil {
		return writeFMReportErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Invalid JSON",
			Detail: safeInternalDetail(),
		}})
	}

	name := strings.TrimSpace(body.Name)
	contentType := strings.TrimSpace(body.ContentType)
	if name == "" {
		return writeFMReportErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: "name is required",
			Source: &jsonapi.ErrorSource{Pointer: "/body/name"},
		}})
	}
	if contentType == "" {
		return writeFMReportErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: "content_type is required",
			Source: &jsonapi.ErrorSource{Pointer: "/body/content_type"},
		}})
	}

	grant, err := h.fileManager.RequestFileUploadIntent(c.UserContext(), filemanager.RequestFileUploadIntentParams{
		Name:        name,
		ContentType: contentType,
		SizeBytes:   body.SizeBytes,
		ActorID:     actorUserID(c),
	})
	if err != nil {
		return writeFileManagerActionError(c, "Upload intent failed", err)
	}

	return response.Write(c, fiber.StatusCreated, map[string]interface{}{
		"upload_intent_uuid": idString(grant.UploadIntentID),
		"upload_url":         grant.UploadURL,
		"method":             grant.Method,
		"required_headers":   grant.Headers,
		"expires_at":         grant.ExpiresAt,
		"name":               grant.Name,
		"mime_type":          grant.MimeType,
		"size_bytes":         grant.SizeBytes,
	})
}

// UploadAttachment godoc
// @Summary Create FM report attachment from upload intent
// @Description After uploading the file to the presigned URL (from /attachments/upload-intent), call this endpoint with the upload_intent_uuid, name, and category to create the file node and attach it to the FM report. Valid categories: additional_fm_report, briefing, weight_balance, ofp.
// @Tags Flight Report
// @Accept json
// @Produce json
// @Param id path string true "FM Report ID (UUIDv7)"
// @Param request body object true "body with upload_intent_uuid, name, and category"
// @Success 201 {object} object
// @Failure 400 {object} jsonapi.ErrorDocument
// @Failure 422 {object} jsonapi.ErrorDocument
// @Router /api/v1/fm-reports/{id}/attachments [post]
func (h *FMReportHandler) UploadAttachment(c *fiber.Ctx) error {
	if h.fileManager == nil {
		return writeFMReportErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
			Status: "500",
			Title:  "Upload failed",
			Detail: safeInternalDetail(),
		}})
	}

	idRaw := strings.TrimSpace(c.Params("id"))
	reportID, err := uuidv7.ParseString(idRaw)
	if err != nil {
		return writeFMReportErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: "id is invalid UUID",
			Source: &jsonapi.ErrorSource{Pointer: "/path/id"},
		}})
	}

	var body struct {
		UploadIntentUUID string `json:"upload_intent_uuid"`
		Name             string `json:"name"`
		Category         string `json:"category"`
	}
	if err := c.BodyParser(&body); err != nil {
		return writeFMReportErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Invalid JSON",
			Detail: safeInternalDetail(),
		}})
	}

	uploadIntentID, err := uuidv7.ParseString(strings.TrimSpace(body.UploadIntentUUID))
	if err != nil {
		return writeFMReportErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: "upload_intent_uuid is invalid UUID",
			Source: &jsonapi.ErrorSource{Pointer: "/body/upload_intent_uuid"},
		}})
	}

	name := strings.TrimSpace(body.Name)
	if name == "" {
		return writeFMReportErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: "name is required",
			Source: &jsonapi.ErrorSource{Pointer: "/body/name"},
		}})
	}

	category := strings.TrimSpace(body.Category)
	if !validFMReportAttachmentCategories[category] {
		return writeFMReportErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: "category must be one of: additional_fm_report, briefing, weight_balance, ofp",
			Source: &jsonapi.ErrorSource{Pointer: "/body/category"},
		}})
	}

	// Fetch FM report to get HelicopterID for folder naming.
	report, err := h.svc.GetByID(c.UserContext(), reportID)
	if err != nil || report == nil {
		return writeFMReportErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
			Status: "404",
			Title:  "Not found",
			Detail: "FM report not found",
		}})
	}

	// Resolve helicopter identifier for folder name.
	// Priority: heli.Identifier → idString(HelicopterID) → idString(reportID)
	// Sanitize slashes/backslashes since they are invalid in folder names.
	helicopterIdentifier := ""
	if h.helicopter != nil && len(report.HelicopterID) == 16 {
		if heli, heliErr := h.helicopter.GetByID(c.UserContext(), report.HelicopterID); heliErr == nil && heli != nil {
			helicopterIdentifier = strings.TrimSpace(heli.Identifier)
		}
	}
	if helicopterIdentifier == "" {
		helicopterIdentifier = idString(report.HelicopterID)
	}
	if helicopterIdentifier == "" {
		helicopterIdentifier = idString(reportID)
	}
	helicopterIdentifier = strings.NewReplacer("/", "-", "\\", "-").Replace(helicopterIdentifier)

	// Ensure folder: __system_root_files__ / fm-reports / {helicopterIdentifier}
	folderID, folderErr := ensureFMReportFileFolderPath(c.UserContext(), h.fileManager, helicopterIdentifier, actorUserID(c))
	if folderErr != nil {
		logHandlerError(c, "Ensure FM report folder", fiber.StatusInternalServerError, "INTERNAL_SERVER_ERROR", "5000005", folderErr)
		return writeFMReportErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
			Status: "500",
			Title:  "Upload failed",
			Detail: safeInternalDetail(),
		}})
	}

	// Create file node from upload intent inside the folder.
	created, createErr := h.fileManager.CreateFileNodeFromUploadIntent(c.UserContext(), filemanager.CreateFileFromUploadIntentParams{
		UploadIntentID: uploadIntentID,
		FolderID:       folderID,
		FolderProvided: true,
		Name:           name,
		ActorID:        actorUserID(c),
	})
	if createErr != nil {
		if errors.Is(createErr, filemanager.ErrDuplicateName) {
			existing, exErr := h.fileManager.GetFileByFolderAndName(c.UserContext(), folderID, strings.ToLower(name))
			if exErr != nil || existing == nil {
				return writeFMReportErrors(c, fiber.StatusConflict, []jsonapi.ErrorObject{{
					Status: "409",
					Title:  "Conflict",
					Detail: "a file with this name already exists in the folder",
				}})
			}
			created = existing
		} else {
			return writeFileManagerActionError(c, "Create file failed", createErr)
		}
	}
	if created == nil {
		return writeFMReportErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
			Status: "500",
			Title:  "Upload failed",
			Detail: safeInternalDetail(),
		}})
	}

	// Create attachment linking the file to this FM report.
	// The file finishes processing asynchronously — retry briefly for readiness (bounded ~10s).
	refID := idString(reportID)
	attachParams := filemanager.CreateAttachmentParams{
		FileID:  created.ID,
		RefType: "fm_report",
		RefID:   refID,
		Category: &category,
		ActorID: actorUserID(c),
	}
	var att *filemanager.Attachment
	var attachErr error
	for attempt := 0; attempt < 40; attempt++ {
		att, attachErr = h.fileManager.CreateAttachment(c.UserContext(), attachParams)
		if attachErr == nil || !errors.Is(attachErr, filemanager.ErrFileNotReadyForAttachment) {
			break
		}
		if c.UserContext().Err() != nil {
			break
		}
		time.Sleep(250 * time.Millisecond)
	}
	if attachErr != nil {
		if errors.Is(attachErr, filemanager.ErrAttachmentAlreadyExists) {
			return writeFMReportErrors(c, fiber.StatusConflict, []jsonapi.ErrorObject{{
				Status: "409",
				Title:  "Conflict",
				Detail: "attachment already exists for this file and reference",
			}})
		}
		return writeFMReportErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Upload failed",
			Detail: safeInternalDetail(),
		}})
	}

	item := rootdto.FMReportAttachmentItem{
		ID:     idString(att.ID),
		FileID: idString(att.FileID),
	}
	return response.Write(c, fiber.StatusCreated, item)
}

// DeleteAttachment godoc
// @Summary Delete attachment from FM report
// @Description Remove a specific attachment from an FM report. Verifies ownership before deletion.
// @Tags Flight Report
// @Produce json
// @Param id path string true "FM Report ID (UUIDv7)"
// @Param att_id path string true "Attachment ID (UUIDv7)"
// @Success 204
// @Failure 404 {object} jsonapi.ErrorDocument
// @Failure 422 {object} jsonapi.ErrorDocument
// @Router /api/v1/fm-reports/{id}/attachments/{att_id} [delete]
func (h *FMReportHandler) DeleteAttachment(c *fiber.Ctx) error {
	if h.fileManager == nil {
		return writeFMReportErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
			Status: "500",
			Title:  "Delete failed",
			Detail: safeInternalDetail(),
		}})
	}

	idRaw := strings.TrimSpace(c.Params("id"))
	reportID, err := uuidv7.ParseString(idRaw)
	if err != nil {
		return writeFMReportErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: "id is invalid UUID",
			Source: &jsonapi.ErrorSource{Pointer: "/path/id"},
		}})
	}

	attIDRaw := strings.TrimSpace(c.Params("att_id"))
	attID, err := uuidv7.ParseString(attIDRaw)
	if err != nil {
		return writeFMReportErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: "att_id is invalid UUID",
			Source: &jsonapi.ErrorSource{Pointer: "/path/att_id"},
		}})
	}

	att, err := h.fileManager.GetAttachmentByID(c.UserContext(), attID)
	if err != nil || att == nil {
		return writeFMReportErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
			Status: "404",
			Title:  "Not found",
			Detail: "attachment not found",
		}})
	}

	expectedRefID := idString(reportID)
	if att.RefType != "fm_report" || att.RefID != expectedRefID {
		return writeFMReportErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
			Status: "404",
			Title:  "Not found",
			Detail: "attachment not found for this fm report",
		}})
	}

	if err := h.fileManager.DeleteAttachment(c.UserContext(), attID); err != nil {
		return writeFMReportErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Delete failed",
			Detail: safeInternalDetail(),
		}})
	}

	return c.SendStatus(fiber.StatusNoContent)
}

// DeleteFMReport godoc
// @Summary Delete FM report
// @Description Soft-delete an FM report by ID. Requires a verified security PIN.
// @Tags Flight Report
// @Produce json
// @Param id path string true "FM Report ID (UUIDv7)"
// @Param X-PIN-Verification-Token header string true "One-time security-PIN action token"
// @Success 204
// @Failure 404 {object} jsonapi.ErrorDocument
// @Failure 422 {object} jsonapi.ErrorDocument
// @Router /api/v1/fm-reports/{id} [delete]
func (h *FMReportHandler) Delete(c *fiber.Ctx) error {
	if h == nil || h.svc == nil {
		return writeFMReportErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
			Status: "500",
			Title:  "Delete failed",
			Detail: safeInternalDetail(),
		}})
	}

	idRaw := strings.TrimSpace(c.Params("id"))
	reportID, err := uuidv7.ParseString(idRaw)
	if err != nil {
		return writeFMReportErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: "id is invalid UUID",
			Source: &jsonapi.ErrorSource{Pointer: "/path/id"},
		}})
	}

	if err := h.svc.Delete(c.UserContext(), reportID); err != nil {
		if errors.Is(err, fmreport.ErrNotFound) {
			return writeFMReportErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
				Status: "404",
				Title:  "Not found",
				Detail: "fm report not found",
				Source: &jsonapi.ErrorSource{Pointer: "/path/id"},
			}})
		}
		return writeFMReportErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Delete failed",
			Detail: safeInternalDetail(),
		}})
	}

	return c.SendStatus(fiber.StatusNoContent)
}

func buildAfterFlightInspectionResource(row *afterflightinspection.AfterFlightInspection, inspectionID []byte, inspection *rootdto.FlightInspectionResource) *rootdto.AfterFlightInspectionResource {
	if row == nil {
		return nil
	}
	out := &rootdto.AfterFlightInspectionResource{
		Type: "after_flight_inspection",
		ID:   idString(row.ID),
		Attributes: rootdto.AfterFlightInspectionAttributes{
			ID:                        idString(row.ID),
			FlightInspectionID:        idString(inspectionID),
			FlightInspection:          inspection,
			MissionType:               []string{},
			Route:                     []string{},
			Total:                     0,
			Completed:                 0,
			DoneFileChecklist:         []rootdto.AfterFlightInspectionFileChecklistItem{},
			FileChecklist:             []rootdto.AfterFlightInspectionFileChecklistItem{},
			OilEngineNR1Checked:       row.OilEngineNR1Checked,
			OilEngineNR2Checked:       row.OilEngineNR2Checked,
			HydraulicLHChecked:        row.HydraulicLHChecked,
			HydraulicRHChecked:        row.HydraulicRHChecked,
			OilTransmissionMGBChecked: row.OilTransmissionMGBChecked,
			OilTransmissionIGBChecked: row.OilTransmissionIGBChecked,
			OilTransmissionTGBChecked: row.OilTransmissionTGBChecked,
			Note:                      row.Note,
			CreatedAt:                 row.CreatedAt.UTC().Format(time.RFC3339),
			UpdatedAt:                 row.UpdatedAt.UTC().Format(time.RFC3339),
		},
	}
	return out
}

// subtractFMReportSnapshot computes prev = total - today, field by field. Values can go
// negative when the total was manually adjusted below the derived value (matches the page).
func subtractFMReportSnapshot(total, today rootdto.FMReportUsageSnapshot) rootdto.FMReportUsageSnapshot {
	sub := func(a, b rootdto.FMReportEngineValues) rootdto.FMReportEngineValues {
		return rootdto.FMReportEngineValues{
			Hours:   a.Hours - b.Hours,
			GpcNgN1: a.GpcNgN1 - b.GpcNgN1,
			PtcNfN2: a.PtcNfN2 - b.PtcNfN2,
			Ccc:     a.Ccc - b.Ccc,
		}
	}
	return rootdto.FMReportUsageSnapshot{
		Airframe: rootdto.FMReportAirframeValues{
			Hours:  total.Airframe.Hours - today.Airframe.Hours,
			Cycles: total.Airframe.Cycles - today.Airframe.Cycles,
		},
		Engine1: sub(total.Engine1, today.Engine1),
		Engine2: sub(total.Engine2, today.Engine2),
		Counters: rootdto.FMReportCountersValues{
			Landings:         total.Counters.Landings - today.Counters.Landings,
			FlightReports:    total.Counters.FlightReports - today.Counters.FlightReports,
			HookReleases:     total.Counters.HookReleases - today.Counters.HookReleases,
			RotorBrakeCycles: total.Counters.RotorBrakeCycles - today.Counters.RotorBrakeCycles,
		},
	}
}

// buildFMReportUsageBreakdown groups the table by row: total (cumulative), today (this
// flight's contribution), and prev = total - today. Engine hours mirror airframe hours;
// engine GPC/PTC "today" are parsed from THIS report with the shared parser; CCC has no
// flight contribution. hasE1/hasE2 reflect installed engines — an absent engine (single-
// engine aircraft) stays zero-valued across all three rows.
func buildFMReportUsageBreakdown(usage *helicopterusage.HelicopterUsage, today helicopterusage.FlightDayStats, report *fmreport.Report, hasE1, hasE2 bool) rootdto.FMReportUsageBreakdown {
	var u helicopterusage.HelicopterUsage
	if usage != nil {
		u = *usage
	}

	total := rootdto.FMReportUsageSnapshot{
		Airframe: rootdto.FMReportAirframeValues{Hours: u.TotalAirframeHours, Cycles: u.TotalAirframeCycles},
		Counters: rootdto.FMReportCountersValues{
			Landings:         u.TotalLanding,
			FlightReports:    u.TotalFlightReport,
			HookReleases:     u.TotalHookRelease,
			RotorBrakeCycles: u.TotalRotorBrakeCycle,
		},
	}
	todaySnap := rootdto.FMReportUsageSnapshot{
		Airframe: rootdto.FMReportAirframeValues{Hours: today.AirframeHours, Cycles: today.AirframeCycles},
		Counters: rootdto.FMReportCountersValues{
			Landings:         today.Landing,
			FlightReports:    1, // this report contributes one flight report to the total
			HookReleases:     today.HookRelease,
			RotorBrakeCycles: today.RotorBrakeCycle,
		},
	}
	if hasE1 {
		total.Engine1 = rootdto.FMReportEngineValues{
			Hours:   u.TotalEngine1Hours,
			GpcNgN1: u.TotalEngine1GpcNgN1,
			PtcNfN2: u.TotalEngine1PtcNfN2,
			Ccc:     u.TotalEngine1Ccc,
		}
		todaySnap.Engine1 = rootdto.FMReportEngineValues{
			Hours:   today.AirframeHours,
			GpcNgN1: metricparse.Parse(report.Engine1GpcN1),
			PtcNfN2: metricparse.Parse(report.Engine1PtcN2),
		}
	}
	if hasE2 {
		total.Engine2 = rootdto.FMReportEngineValues{
			Hours:   u.TotalEngine2Hours,
			GpcNgN1: u.TotalEngine2GpcNgN1,
			PtcNfN2: u.TotalEngine2PtcNfN2,
			Ccc:     u.TotalEngine2Ccc,
		}
		todaySnap.Engine2 = rootdto.FMReportEngineValues{
			Hours:   today.AirframeHours,
			GpcNgN1: metricparse.Parse(report.Engine2GpcN1),
			PtcNfN2: metricparse.Parse(report.Engine2PtcN2),
		}
	}

	return rootdto.FMReportUsageBreakdown{
		Prev:  subtractFMReportSnapshot(total, todaySnap),
		Today: todaySnap,
		Total: total,
	}
}

func helicopterUsageZeroResource(helicopterID []byte) rootdto.HelicopterUsageResource {
	return rootdto.HelicopterUsageResource{
		Type: "helicopter_usage",
		ID:   "",
		Attributes: rootdto.HelicopterUsageAttributes{
			HelicopterID:         idString(helicopterID),
			TotalLanding:         0,
			TotalAirframeHours:   0,
			TotalAirframeCycles:  0,
			TotalEngine1Hours:    0,
			TotalEngine1GpcNgN1:  0,
			TotalEngine1PtcNfN2:  0,
			TotalEngine1Ccc:      0,
			TotalEngine2Hours:    0,
			TotalEngine2GpcNgN1:  0,
			TotalEngine2PtcNfN2:  0,
			TotalEngine2Ccc:      0,
			TotalFlightReport:    0,
			TotalHookRelease:     0,
			TotalRotorBrakeCycle: 0,
		},
	}
}

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

type FMReportFleetHistoryBuilder struct {
	fleetStatus *FleetStatusHandler
	flight      *FlightHandler
}

func NewFMReportFleetHistoryBuilder(fleetStatus *FleetStatusHandler, flight *FlightHandler) *FMReportFleetHistoryBuilder {
	return &FMReportFleetHistoryBuilder{fleetStatus: fleetStatus, flight: flight}
}

func (b *FMReportFleetHistoryBuilder) BuildFleetHistory(ctx context.Context, takeoverID []byte) *fmreport.FleetHistory {
	if b == nil || b.fleetStatus == nil || b.flight == nil {
		return nil
	}
	helicopterID := b.flight.ResolveHelicopterIDByTakeover(ctx, takeoverID)
	if len(helicopterID) != 16 {
		return nil
	}
	fsh := b.fleetStatus
	if fsh.svc == nil {
		return nil
	}
	latest, err := fsh.svc.LatestByHelicopterIDs(ctx, [][]byte{helicopterID})
	if err != nil {
		return nil
	}
	row := latest[string(helicopterID)]
	if row == nil {
		return nil
	}

	history := &fmreport.FleetHistory{
		HelicopterStatus: fsh.deriveHelicopterStatus(ctx, row.Helicopter, row.HelicopterID),
	}
	if summary, sErr := fsh.svc.GetSummaryByHelicopterID(ctx, row.HelicopterID); sErr == nil && summary != nil {
		history.AirframeHours = derefFloat64(summary.Airframe.Hours)
		history.Engine1Hours = derefFloat64(summary.Engine1.Hours)
		history.Engine2Hours = derefFloat64(summary.Engine2.Hours)
	}
	for i := range row.MaintenanceSchedules {
		ms := row.MaintenanceSchedules[i]
		switch ms.InspectionType {
		case fleetstatus.InspectionTypeAF:
			history.AFType, history.AFDue, history.AFExt = ms.Type, ms.Due, ms.Ext
		case fleetstatus.InspectionTypeENG1:
			history.Eng1Type, history.Eng1Due, history.Eng1Ext = ms.Type, ms.Due, ms.Ext
		case fleetstatus.InspectionTypeENG2:
			history.Eng2Type, history.Eng2Due, history.Eng2Ext = ms.Type, ms.Due, ms.Ext
		case fleetstatus.InspectionTypeMAIN1:
			history.Main1Type, history.Main1Due, history.Main1Ext = ms.Type, ms.Due, ms.Ext
		case fleetstatus.InspectionTypeMAIN2:
			history.Main2Type, history.Main2Due, history.Main2Ext = ms.Type, ms.Due, ms.Ext
		}
	}
	if fsh.easaSvc != nil {
		if rel, relErr := fsh.easaSvc.GetLatestByHelicopter(ctx, helicopterID); relErr == nil && rel != nil {
			history.EASAReleaseID = rel.ID
			history.EASAIsSigned = rel.IsSigned()
			history.EASADate = rel.EASADate
			history.EASAACHours = rel.EASAACHours
			history.EASALocation = rel.EASALocation
			history.EASAWONo = rel.EASAWONo
			history.EASAMaintManualRevAirframe = rel.EASAMaintManualRevAirframe
			history.EASAMaintManualRevEngine = rel.EASAMaintManualRevEngine
			history.EASASignerID = rel.EASASignerID
			history.EASAPermitNo = rel.EASAPermitNo
			history.EASASignedAt = rel.SignedAt
			history.EASASignedBy = rel.SignedBy
		}
	}
	// Capture the helicopter's latest MCF so the report keeps "MCF signed" even after the
	// live fleet page resets its display.
	if fsh.mcfSvc != nil {
		if m, mErr := fsh.mcfSvc.LatestByHelicopterIDs(ctx, [][]byte{helicopterID}); mErr == nil {
			if row := m[string(helicopterID)]; row != nil {
				history.MCFID = row.ID
				history.MCFIsSigned = row.IsComplete()
				history.MCFDate = row.Date
				if af := strings.TrimSpace(row.AFHours); af != "" {
					history.MCFAFHours = &af
				}
				lc := row.LandingCount
				history.MCFLandingCount = &lc
				history.MCFSignedBy = row.CompletedBy
				history.MCFSignedByName = userctx.GetDisplayName(ctx, row.CompletedBy)
			}
		}
	}
	return history
}

func fmReportFleetHistoryResource(h *fmreport.FleetHistory) *responsedto.FleetStatusResource {
	if h == nil {
		return nil
	}
	res := &responsedto.FleetStatusResource{
		Type: "fleet_status",
		Attributes: responsedto.FleetStatusAttributes{
			Helicopter: &shareddto.FleetStatusHelicopter{Status: h.HelicopterStatus},
			Summary: shareddto.FleetStatusSummary{
				Airframe: shareddto.FleetStatusSummaryAirframe{Hours: float64Ptr(h.AirframeHours)},
				Engine1:  shareddto.FleetStatusSummaryEngine{Hours: float64Ptr(h.Engine1Hours)},
				Engine2:  shareddto.FleetStatusSummaryEngine{Hours: float64Ptr(h.Engine2Hours)},
			},
			MaintenanceSchedules: fmReportFleetHistorySchedules(h),
		},
	}
	if h.HasEASA() {
		res.Attributes.EASARelease = &shareddto.FleetStatusEASARelease{
			ID:                         idString(h.EASAReleaseID),
			IsSigned:                   h.EASAIsSigned,
			EASADate:                   timeDatePtrOrNil(h.EASADate),
			EASAACHours:                h.EASAACHours,
			EASALocation:               h.EASALocation,
			EASAWONo:                   h.EASAWONo,
			EASAMaintManualRevAirframe: h.EASAMaintManualRevAirframe,
			EASAMaintManualRevEngine:   h.EASAMaintManualRevEngine,
			EASASignerID:               h.EASASignerID,
			EASAPermitNo:               h.EASAPermitNo,
			SignedAt:                   timePtrOrNil(h.EASASignedAt),
			SignedBy:                   idString(h.EASASignedBy),
		}
	}
	if h.HasMCF() {
		m := &shareddto.FleetStatusMCF{
			ID:           idString(h.MCFID),
			Sign:         h.MCFIsSigned,
			Date:         timeDatePtrOrNil(h.MCFDate),
			AFHours:      h.MCFAFHours,
			LandingCount: h.MCFLandingCount,
			SignedBy:     idString(h.MCFSignedBy),
		}
		if h.MCFSignedByName != "" {
			name := h.MCFSignedByName
			m.SignedByName = &name
		}
		res.Attributes.MCF = m
	}
	return res
}

func fmReportFleetHistorySchedules(h *fmreport.FleetHistory) []shareddto.MaintenanceScheduleAttributes {
	out := make([]shareddto.MaintenanceScheduleAttributes, 0, 5)
	add := func(inspectionType, t, due, ext string) {
		if t == "" && due == "" && ext == "" {
			return
		}
		out = append(out, shareddto.MaintenanceScheduleAttributes{
			InspectionType: inspectionType,
			Type:           t,
			Due:            due,
			Ext:            ext,
		})
	}
	add(fleetstatus.InspectionTypeAF, h.AFType, h.AFDue, h.AFExt)
	add(fleetstatus.InspectionTypeENG1, h.Eng1Type, h.Eng1Due, h.Eng1Ext)
	add(fleetstatus.InspectionTypeENG2, h.Eng2Type, h.Eng2Due, h.Eng2Ext)
	add(fleetstatus.InspectionTypeMAIN1, h.Main1Type, h.Main1Due, h.Main1Ext)
	add(fleetstatus.InspectionTypeMAIN2, h.Main2Type, h.Main2Due, h.Main2Ext)
	return out
}

func float64Ptr(v float64) *float64 { return &v }

func derefFloat64(v *float64) float64 {
	if v == nil {
		return 0
	}
	return *v
}
