package handlers

import (
	"context"
	"fmt"
	"log/slog"
	"strings"
	"time"

	actionsignoff "wucher/internal/domain/action_signoff"
	complaint "wucher/internal/domain/complaint"
	easarelease "wucher/internal/domain/easa_release"
	fleetstatus "wucher/internal/domain/fleet_status"
	"wucher/internal/domain/helicopter"
	"wucher/internal/domain/mcf"
	helicopterusage "wucher/internal/domain/helicopter_usage"
	"wucher/internal/shared/pkg/userctx"
	"wucher/internal/shared/pkg/uuidv7"
	requestdto "wucher/internal/transport/http/dto/request"
	responsedto "wucher/internal/transport/http/dto/response"
	shareddto "wucher/internal/transport/http/dto/shared"
)

func fleetSummaryManualInput(s *requestdto.FleetStatusSummaryUpdate) helicopterusage.ManualSummaryInput {
	in := helicopterusage.ManualSummaryInput{
		Landing:         s.Landings,
		FlightReport:    s.FlightReports,
		HookRelease:     s.HookReleases,
		RotorBrakeCycle: s.RotorBrakeCycles,
	}
	if s.Airframe != nil {
		in.AirframeHours = s.Airframe.Hours
		in.AirframeCycles = s.Airframe.Cycles
	}
	if s.Engine1 != nil {
		in.Engine1Hours = s.Engine1.Hours
		in.Engine1GpcNgN1 = s.Engine1.GpcNgN1
		in.Engine1PtcNfN2 = s.Engine1.PtcNfN2
		in.Engine1Ccc = s.Engine1.Ccc
	}
	if s.Engine2 != nil {
		in.Engine2Hours = s.Engine2.Hours
		in.Engine2GpcNgN1 = s.Engine2.GpcNgN1
		in.Engine2PtcNfN2 = s.Engine2.PtcNfN2
		in.Engine2Ccc = s.Engine2.Ccc
	}
	return in
}

func mapFleetStatusPayload(attrs requestdto.FleetStatusCreateAttributes, actor []byte) (*fleetstatus.FleetStatus, error) {
	helicopterID, err := uuidv7.ParseString(strings.TrimSpace(attrs.HelicopterID))
	if err != nil {
		return nil, err
	}
	row := &fleetstatus.FleetStatus{HelicopterID: helicopterID, CreatedBy: actor, UpdatedBy: actor}
	for i := range attrs.MaintenanceSchedules {
		item := attrs.MaintenanceSchedules[i]
		row.MaintenanceSchedules = append(row.MaintenanceSchedules, fleetstatus.MaintenanceSchedule{
			FleetStatusID:  row.ID,
			InspectionType: strings.TrimSpace(item.InspectionType),
			Type:           strings.TrimSpace(item.Type),
			Due:            strings.TrimSpace(item.Due),
			Ext:            strings.TrimSpace(item.Ext),
		})
	}
	for i := range attrs.Files {
		fileID, parseErr := uuidv7.ParseString(strings.TrimSpace(attrs.Files[i].FileID))
		if parseErr != nil {
			return nil, parseErr
		}
		row.Files = append(row.Files, fleetstatus.FleetStatusFile{
			FleetStatusID: row.ID,
			FileID:        fileID,
			Category:      strings.TrimSpace(attrs.Files[i].Category),
			IsMandatory:   attrs.Files[i].IsMandatory != nil && *attrs.Files[i].IsMandatory,
		})
	}
	return row, nil
}

func mergeFleetStatusPatch(existing *fleetstatus.FleetStatus, attrs requestdto.FleetStatusUpdateAttributes, actor []byte) (*fleetstatus.FleetStatus, error) {
	row := &fleetstatus.FleetStatus{
		ID:           existing.ID,
		HelicopterID: existing.HelicopterID,
		Status:       existing.Status,
		ServicedAt:   existing.ServicedAt,
		CreatedAt:    existing.CreatedAt,
		CreatedBy:    existing.CreatedBy,
		UpdatedBy:    actor,
	}

	if attrs.HelicopterID != nil && strings.TrimSpace(*attrs.HelicopterID) != "" {
		helicopterID, err := uuidv7.ParseString(strings.TrimSpace(*attrs.HelicopterID))
		if err != nil {
			return nil, err
		}
		row.HelicopterID = helicopterID
	}
	if attrs.MaintenanceSchedules != nil {
		row.MaintenanceSchedules = make([]fleetstatus.MaintenanceSchedule, 0, len(attrs.MaintenanceSchedules))
		for i := range attrs.MaintenanceSchedules {
			item := attrs.MaintenanceSchedules[i]
			row.MaintenanceSchedules = append(row.MaintenanceSchedules, fleetstatus.MaintenanceSchedule{
				FleetStatusID:  row.ID,
				InspectionType: strings.TrimSpace(item.InspectionType),
				Type:           strings.TrimSpace(item.Type),
				Due:            strings.TrimSpace(item.Due),
				Ext:            strings.TrimSpace(item.Ext),
			})
		}
	} else {
		row.MaintenanceSchedules = existing.MaintenanceSchedules
	}
	if attrs.Files != nil {
		row.Files = make([]fleetstatus.FleetStatusFile, 0, len(attrs.Files))
		for i := range attrs.Files {
			fileID, parseErr := uuidv7.ParseString(strings.TrimSpace(attrs.Files[i].FileID))
			if parseErr != nil {
				return nil, parseErr
			}
			row.Files = append(row.Files, fleetstatus.FleetStatusFile{
				FleetStatusID: row.ID,
				FileID:        fileID,
				Category:      strings.TrimSpace(attrs.Files[i].Category),
				IsMandatory:   attrs.Files[i].IsMandatory != nil && *attrs.Files[i].IsMandatory,
			})
		}
	} else {
		row.Files = existing.Files
	}

	return row, nil
}

func fleetStatusMaintenanceSchedules(schedules []fleetstatus.MaintenanceSchedule) []shareddto.MaintenanceScheduleAttributes {
	byType := make(map[string][]fleetstatus.MaintenanceSchedule, len(schedules))
	for i := range schedules {
		t := schedules[i].InspectionType
		byType[t] = append(byType[t], schedules[i])
	}

	out := make([]shareddto.MaintenanceScheduleAttributes, 0, len(fleetstatus.InspectionTypeOrder)+len(schedules))
	for _, t := range fleetstatus.InspectionTypeOrder {
		items := byType[t]
		if len(items) == 0 {
			out = append(out, shareddto.MaintenanceScheduleAttributes{InspectionType: t})
			continue
		}
		for i := range items {
			out = append(out, maintenanceScheduleAttributes(items[i]))
		}
		delete(byType, t)
	}

	for i := range schedules {
		items, ok := byType[schedules[i].InspectionType]
		if !ok {
			continue
		}
		for j := range items {
			out = append(out, maintenanceScheduleAttributes(items[j]))
		}
		delete(byType, schedules[i].InspectionType)
	}
	return out
}

func maintenanceScheduleAttributes(s fleetstatus.MaintenanceSchedule) shareddto.MaintenanceScheduleAttributes {
	return shareddto.MaintenanceScheduleAttributes{
		InspectionType: s.InspectionType,
		Type:           s.Type,
		Due:            s.Due,
		Ext:            s.Ext,
	}
}

func fleetStatusFiles(ctx context.Context, storage fileManagerDownloadURLStorage, files []fleetstatus.FleetStatusFile) []responsedto.FleetStatusFile {
	out := make([]responsedto.FleetStatusFile, 0, len(files))
	for i := range files {
		item := responsedto.FleetStatusFile{
			ID:               idString(files[i].ID),
			FileAttachmentID: idString(files[i].FileAttachmentID),
			Category:         files[i].Category,
			IsMandatory:      files[i].IsMandatory,
		}
		if files[i].Attachment != nil && files[i].Attachment.File != nil {
			file := files[i].Attachment.File
			item.Name = strings.TrimSpace(file.Name)
			asset := resolveFileManagerDownloadURLs(ctx, storage, file)
			item.DownloadURL = strings.TrimSpace(asset.OriginalURL)
			item.ThumbnailDownloadURL = strings.TrimSpace(asset.ThumbnailURL)
			item.SizeBytes = int64Ptr(asset.OriginalSizeBytes)
		}
		out = append(out, item)
	}
	return out
}

func fleetStatusResource(ctx context.Context, svc fleetstatus.Service, storage fileManagerDownloadURLStorage, row *fleetstatus.FleetStatus, helicopterStatus string) responsedto.FleetStatusResource {
	res := responsedto.FleetStatusResource{Type: "fleet_status", ID: idString(row.ID), Attributes: responsedto.FleetStatusAttributes{Helicopter: fleetStatusHelicopterResource(row.Helicopter, helicopterStatus)}}
	res.Attributes.Status = row.Status
	res.Attributes.ServicedAt = timeStringOrEmpty(row.ServicedAt)
	res.Attributes.Summary = fleetStatusSummary(ctx, svc, row)
	res.Attributes.MaintenanceSchedules = fleetStatusMaintenanceSchedules(row.MaintenanceSchedules)
	res.Attributes.Files = fleetStatusFiles(ctx, storage, row.Files)
	res.Attributes.CreatedAt = row.CreatedAt.UTC().Format(time.RFC3339)
	res.Attributes.CreatedBy = bytesUUIDToString(row.CreatedBy)
	res.Attributes.UpdatedAt = row.UpdatedAt.UTC().Format(time.RFC3339)
	res.Attributes.UpdatedBy = bytesUUIDToString(row.UpdatedBy)
	return res
}

func (h *FleetStatusHandler) FMReportFleetStatus(ctx context.Context, helicopterID []byte) *responsedto.FleetStatusResource {
	if h == nil || h.svc == nil || len(helicopterID) != 16 {
		return nil
	}
	latest, err := h.svc.LatestByHelicopterIDs(ctx, [][]byte{helicopterID})
	if err != nil {
		return nil
	}
	row := latest[string(helicopterID)]
	if row == nil {
		return nil
	}
	status := h.deriveHelicopterStatus(ctx, row.Helicopter, row.HelicopterID)
	res := fleetStatusResource(ctx, h.svc, h.storage, row, status)
	res.Attributes.EASARelease = resolveEASARelease(ctx, h.easaSvc, row.HelicopterID)
	return &res
}

func fleetStatusDetailResource(ctx context.Context, h *FleetStatusHandler, row *fleetstatus.FleetStatus, helicopterStatus string) responsedto.FleetStatusResource {
	res := fleetStatusResource(ctx, h.svc, h.storage, row, helicopterStatus)
	if h == nil {
		return res
	}
	// Current active flight (when in use / booked) so the fleet page can create an EASA
	// release scoped to the flight.
	if af, err := h.helicopterSvc.ActiveFlightIDByAircraftIDs(ctx, [][]byte{row.HelicopterID}); err == nil {
		if fid := af[string(row.HelicopterID)]; len(fid) == 16 && res.Attributes.Helicopter != nil {
			res.Attributes.Helicopter.FlightID = idString(fid)
		}
	}
	// Surface open defects regardless of the derived status label: a complaint must show
	// even when the aircraft reads MCF/available. MCF is checked before AOG in
	// DeriveStatus (so a grounding complaint can be masked by MCF), and NSR-deferred /
	// MEL-within-grace items don't ground at all yet are still open hold items.
	// Every OPEN defect, plus the FIXED defects from this helicopter's latest flight (so a
	// just-resolved defect stays visible as the last state until a newer flight reports).
	// Same rule as the fleet list — one source of truth in OpenByHelicopterIDs.
	complaints := make([]complaint.Complaint, 0)
	if rows, err := h.complaintSvc.OpenByHelicopterIDs(ctx, [][]byte{row.HelicopterID}); err == nil {
		complaints = rows
	}
	if len(complaints) > 0 {
		res.Attributes.Complaints = mapComplaintListToFleetStatusDTO(ctx, h.complaintSvc, h.signoffSvc, complaints)
	}
	// The release form + status reasons accompany a defect (or a non-complaint AOG such
	// as a maintenance next-due); skip the extra lookups for a clean, non-AOG aircraft.
	if len(complaints) > 0 || helicopterStatus == helicopter.StatusAOG {
		res.Attributes.EASARelease = resolveEASARelease(ctx, h.easaSvc, row.HelicopterID)
		if row.Helicopter != nil {
			overdue, oErr := h.svc.OverdueInspections(ctx, row.HelicopterID)
			if oErr != nil {
				slog.WarnContext(ctx, "fleet status: overdue inspections lookup failed", slog.Any("error", oErr))
			}
			res.Attributes.Helicopter.StatusReasons = buildHelicopterStatusReasons(row.Helicopter, complaints, overdue, time.Now().UTC())
		}
	}
	return res
}

func mapComplaintListToFleetStatusDTO(ctx context.Context, svc complaintLister, signoffSvc complaintSignoffLookup, rows []complaint.Complaint) []shareddto.FleetStatusComplaint {
	out := make([]shareddto.FleetStatusComplaint, 0, len(rows))
	names := map[string]string{}
	if svc != nil && len(rows) > 0 {
		if resolved, err := svc.ResolveUserNames(ctx, rows); err == nil {
			names = resolved
		}
	}
	signoffs := resolveSignoffsByComplaint(ctx, signoffSvc, rows, names)
	for i := range rows {
		out = append(out, mapComplaintToFleetStatusDTO(ctx, &rows[i], names, signoffs))
	}
	return out
}

func mapComplaintToFleetStatusDTO(ctx context.Context, c *complaint.Complaint, names map[string]string, signoffs map[string]*actionsignoff.ActionSignoff) shareddto.FleetStatusComplaint {
	if c == nil {
		return shareddto.FleetStatusComplaint{}
	}
	signoff := signoffs[string(c.ID)]
	dto := shareddto.FleetStatusComplaint{
		ID:                    idString(c.ID),
		FlightID:              idString(c.FlightID),
		Description:           c.Description,
		Status:                c.Status(),
		ReportedAt:            c.ReportedAt.UTC().Format(time.RFC3339),
		AircraftHoursAtReport: c.AircraftHoursAtReport,
		MEL:                   shareddto.FleetStatusComplaintMEL{Severity: c.MELSeverity},
		NSR:                   shareddto.FleetStatusComplaintNSR{IsNSR: c.IsNSR},
		Action:                shareddto.FleetStatusComplaintAction{Taken: c.ActionTaken, AircraftHoursAtFix: c.AircraftHoursAtFix},
	}
	dto.ReportedBy = idString(c.ReportedBy)
	dto.ReportedByName = names[string(c.ReportedBy)]
	dto.ReportedByShort = userctx.GetShortName(ctx, c.ReportedBy)
	dto.MEL.ClassifiedBy = idString(c.MELClassifiedBy)
	dto.MEL.ClassifiedByName = names[string(c.MELClassifiedBy)]
	dto.MEL.ClassifiedByShort = userctx.GetShortName(ctx, c.MELClassifiedBy)
	dto.MEL.ClassifiedAt = timePtrOrNil(c.MELClassifiedAt)
	// Only label severity once classified — pending complaints (mel_classified_at NULL)
	// keep severity 0 as a default, which is not a "Non-MEL" decision.
	if c.MELClassifiedAt != nil {
		if label := complaintSeverityLabel(c.MELSeverity); label != "" {
			dto.MEL.SeverityLabel = &label
		}
	}
	if dl := c.MELDeadline(); dl != nil {
		v := dl.UTC().Format(time.RFC3339)
		dto.MEL.Deadline = &v
	}
	dto.NSR.DecidedAt = timePtrOrNil(c.NSRDecidedAt)
	dto.NSR.DecidedBy = idString(c.NSRDecidedBy)
	dto.NSR.DecidedByName = names[string(c.NSRDecidedBy)]
	dto.NSR.DecidedByShort = userctx.GetShortName(ctx, c.NSRDecidedBy)
	dto.NSR.Reason = c.NSRReason
	if signoff != nil {
		dto.Action.SignedAt = timePtrOrNil(signoff.SignedAt)
		dto.Action.SignedBy = idString(signoff.SignedBy)
		dto.Action.SignedByName = names[string(signoff.SignedBy)]
		dto.Action.SignedByShort = userctx.GetShortName(ctx, signoff.SignedBy)
	}
	return dto
}

func resolveEASARelease(ctx context.Context, svc easaReleaseLister, helicopterID []byte) *shareddto.FleetStatusEASARelease {
	if svc == nil || len(helicopterID) != 16 {
		return nil
	}
	rel, err := svc.GetLatestByHelicopter(ctx, helicopterID)
	if err == nil && rel != nil {
		return mapEASAReleaseToDTO(ctx, rel)
	}
	return nil
}

func mapEASAReleaseToDTO(ctx context.Context, r *easarelease.EASARelease) *shareddto.FleetStatusEASARelease {
	if r == nil {
		return nil
	}
	dto := &shareddto.FleetStatusEASARelease{
		ID:                         idString(r.ID),
		IsSigned:                   r.IsSigned(),
		EASADate:                   timeDatePtrOrNil(r.EASADate),
		EASAACHours:                r.EASAACHours,
		EASALocation:               r.EASALocation,
		EASAWONo:                   r.EASAWONo,
		EASAMaintManualRevAirframe: r.EASAMaintManualRevAirframe,
		EASAMaintManualRevEngine:   r.EASAMaintManualRevEngine,
		EASASignerID:               r.EASASignerID,
		EASAPermitNo:               r.EASAPermitNo,
		SignedAt:                   timePtrOrNil(r.SignedAt),
		SignedBy:                   idString(r.SignedBy),
		SignedByName:               userctx.GetDisplayName(ctx, r.SignedBy),
		SignedByShort:              userctx.GetShortName(ctx, r.SignedBy),
		MissingSignFields:          r.MissingSignFields(),
	}
	return dto
}

func buildHelicopterStatusReasons(h *helicopter.Helicopter, complaints []complaint.Complaint, overdue []fleetstatus.OverdueInspection, now time.Time) []shareddto.FleetStatusHelicopterReason {
	reasons := make([]shareddto.FleetStatusHelicopterReason, 0)
	if h != nil && h.AirOnGround {
		src := strings.TrimSpace(h.AOGSource)
		if src != helicopter.AOGSourceComplaint && src != helicopter.AOGSourceAuto {
			detail := strings.TrimSpace(h.AOGReason)
			if detail == "" {
				detail = "Manual AOG"
			}
			reasons = append(reasons, shareddto.FleetStatusHelicopterReason{Code: shareddto.ReasonCodeManualAOG, Detail: detail})
		}
	}
	for i := range complaints {
		c := complaints[i]
		if !c.IsGrounding(now) {
			continue
		}
		short := shortenText(c.Description, 40)
		if c.MELSeverity > 0 {
			reason := shareddto.FleetStatusHelicopterReason{
				Code:   shareddto.ReasonCodeMELExpired,
				Detail: fmt.Sprintf("MEL %s for %q overdue", complaintSeverityLabel(c.MELSeverity), short),
			}
			if deadline := c.MELDeadline(); deadline != nil {
				since := deadline.UTC().Format(time.RFC3339)
				reason.Since = &since
			}
			reasons = append(reasons, reason)
			continue
		}
		since := c.ReportedAt.UTC().Format(time.RFC3339)
		reasons = append(reasons, shareddto.FleetStatusHelicopterReason{
			Code:   shareddto.ReasonCodeNonMELPending,
			Detail: fmt.Sprintf("NON-MEL %q pending technician sign", short),
			Since:  &since,
		})
	}
	for i := range overdue {
		reasons = append(reasons, shareddto.FleetStatusHelicopterReason{
			Code:   shareddto.ReasonCodeInspectionOverdue,
			Detail: overdue[i].Detail,
		})
	}
	if len(reasons) == 0 {
		return nil
	}
	return reasons
}

func complaintSeverityLabel(severity int8) string {
	switch severity {
	case complaint.MELSeverityNonMEL:
		return "NON-MEL"
	case complaint.MELSeverityA:
		return "A"
	case complaint.MELSeverityB:
		return "B"
	case complaint.MELSeverityC:
		return "C"
	case complaint.MELSeverityD:
		return "D"
	default:
		return ""
	}
}

func shortenText(v string, n int) string {
	v = strings.TrimSpace(v)
	if n <= 0 || len(v) <= n {
		return v
	}
	return v[:n]
}

func timePtrOrNil(v *time.Time) *string {
	if v == nil || v.IsZero() {
		return nil
	}
	s := v.UTC().Format(time.RFC3339)
	return &s
}

func timeDatePtrOrNil(v *time.Time) *string {
	if v == nil || v.IsZero() {
		return nil
	}
	s := v.UTC().Format("2006-01-02")
	return &s
}

func fleetStatusListResourceWithSummaryCache(ctx context.Context, svc fleetstatus.Service, storage fileManagerDownloadURLStorage, row *fleetstatus.FleetStatus, helicopterStatus string, cache map[string]shareddto.FleetStatusSummary) responsedto.FleetStatusResource {
	if row == nil {
		return responsedto.FleetStatusResource{}
	}
	resource := responsedto.FleetStatusResource{Type: "fleet_status", ID: idString(row.ID), Attributes: responsedto.FleetStatusAttributes{Helicopter: fleetStatusHelicopterResource(row.Helicopter, helicopterStatus)}}
	resource.Attributes.Status = row.Status
	resource.Attributes.ServicedAt = timeStringOrEmpty(row.ServicedAt)
	resource.Attributes.Summary = fleetStatusSummaryWithCache(ctx, svc, row, cache)
	resource.Attributes.MaintenanceSchedules = fleetStatusMaintenanceSchedules(row.MaintenanceSchedules)
	resource.Attributes.Files = fleetStatusFiles(ctx, storage, row.Files)
	resource.Attributes.CreatedAt = row.CreatedAt.UTC().Format(time.RFC3339)
	resource.Attributes.CreatedBy = bytesUUIDToString(row.CreatedBy)
	resource.Attributes.UpdatedAt = row.UpdatedAt.UTC().Format(time.RFC3339)
	resource.Attributes.UpdatedBy = bytesUUIDToString(row.UpdatedBy)
	return resource
}

func fleetStatusSummaryWithCache(ctx context.Context, svc fleetstatus.Service, row *fleetstatus.FleetStatus, cache map[string]shareddto.FleetStatusSummary) shareddto.FleetStatusSummary {
	if svc == nil || row == nil || len(row.HelicopterID) != 16 {
		return shareddto.FleetStatusSummary{}
	}
	key := idString(row.HelicopterID)
	if cache != nil {
		if cached, ok := cache[key]; ok {
			return cached
		}
	}
	summary := fleetStatusSummary(ctx, svc, row)
	if cache != nil {
		cache[key] = summary
	}
	return summary
}

func fleetStatusSummary(ctx context.Context, svc fleetstatus.Service, row *fleetstatus.FleetStatus) shareddto.FleetStatusSummary {
	if svc == nil || row == nil || len(row.HelicopterID) != 16 {
		return shareddto.FleetStatusSummary{}
	}
	summary, err := svc.GetSummaryByHelicopterID(ctx, row.HelicopterID)
	if err != nil || summary == nil {
		return shareddto.FleetStatusSummary{}
	}
	return *summary
}

func fleetStatusServiceLogResource(row *fleetstatus.FleetStatusServiceLog) responsedto.FleetStatusServiceLogResource {
	return responsedto.FleetStatusServiceLogResource{
		Type: "fleet_status_service_log",
		ID:   idString(row.ID),
		Attributes: responsedto.FleetStatusServiceLogAttributes{
			FleetStatusID:     idString(row.FleetStatusID),
			HelicopterID:      idString(row.HelicopterID),
			Helicopter:        fleetStatusHelicopterResource(row.Helicopter, ""),
			FleetStatusStatus: fleetstatus.StatusServiced,
			InspectionType:    row.InspectionType,
			Type:              row.Type,
			Due:               row.Due,
			Ext:               row.Ext,
			ServicedAt:        row.ServicedAt.UTC().Format(time.RFC3339),
			CreatedAt:         row.CreatedAt.UTC().Format(time.RFC3339),
			CreatedBy:         bytesUUIDToString(row.CreatedBy),
		},
	}
}

func fleetStatusHelicopterResource(row *helicopter.Helicopter, status string) *shareddto.FleetStatusHelicopter {
	if row == nil {
		return nil
	}
	return &shareddto.FleetStatusHelicopter{
		ID:          idString(row.ID),
		Designation: row.Designation,
		Identifier:  row.Identifier,
		Type:        row.Type,
		Status:      status,
	}
}

// openComplaintsByHelicopter loads every open (fixed_at IS NULL) complaint for the fleet
// in a single query and groups them per helicopter (raw-bytes string key), so the list
// view can fill the Complaints column for each aircraft without an N+1. Returns an empty
// map on a nil service or query error.
func openComplaintsByHelicopter(ctx context.Context, svc complaintLister, ids [][]byte) map[string][]complaint.Complaint {
	out := map[string][]complaint.Complaint{}
	if svc == nil || len(ids) == 0 {
		return out
	}
	rows, err := svc.OpenByHelicopterIDs(ctx, ids)
	if err != nil {
		slog.WarnContext(ctx, "fleet status list: open complaints lookup failed", slog.Any("error", err))
		return out
	}
	for i := range rows {
		k := string(rows[i].HelicopterID)
		out[k] = append(out[k], rows[i])
	}
	return out
}

func (h *FleetStatusHandler) fleetStatusResourcesForHelicopters(ctx context.Context, helis []helicopter.Helicopter) ([]responsedto.FleetStatusResource, error) {
	ids := make([][]byte, 0, len(helis))
	for i := range helis {
		ids = append(ids, helis[i].ID)
	}
	latest, err := h.svc.LatestByHelicopterIDs(ctx, ids)
	if err != nil {
		return nil, err
	}
	inUse, err := h.helicopterSvc.InUseByAircraftIDs(ctx, ids)
	if err != nil {
		slog.WarnContext(ctx, "fleet status list: in-use lookup failed", slog.Any("error", err))
	}
	// Current active flight per in-use aircraft — exposed so the fleet page can create an
	// EASA release scoped to the flight (e.g. a standalone release for a booked aircraft).
	activeFlight, err := h.helicopterSvc.ActiveFlightIDByAircraftIDs(ctx, ids)
	if err != nil {
		slog.WarnContext(ctx, "fleet status list: active-flight lookup failed", slog.Any("error", err))
	}
	grounded := groundingHelicopterSet(ctx, h.complaintSvc, ids, time.Now().UTC())
	mcfPending := mcfPendingHelicopterSet(ctx, h.mcfSvc, ids)
	mcfLatest, err := h.mcfSvc.LatestByHelicopterIDs(ctx, ids)
	if err != nil {
		slog.WarnContext(ctx, "fleet status list: latest-mcf lookup failed", slog.Any("error", err))
		mcfLatest = map[string]*mcf.MaintenanceCheckFlight{}
	}
	// One batch query for the whole fleet's open defects, grouped per helicopter, so the
	// Complaints column can be filled for every aircraft that has one — not just AOG ones.
	openByHeli := openComplaintsByHelicopter(ctx, h.complaintSvc, ids)

	cache := make(map[string]shareddto.FleetStatusSummary, len(helis))
	render := fleetStatusRender{svc: h.svc, storage: h.storage, cache: cache}
	now := time.Now().UTC()
	out := make([]responsedto.FleetStatusResource, 0, len(helis))
	for i := range helis {
		heli := &helis[i]
		key := string(heli.ID)
		status := heli.DeriveStatus(helicopter.StatusInput{IsBook: inUse[key], IsMCF: mcfPending[key], ComplaintGrounded: grounded[key]})
		res := fleetStatusResourceForHelicopter(ctx, render, heli, latest[key], status)
		if fid := activeFlight[key]; len(fid) == 16 && res.Attributes.Helicopter != nil {
			res.Attributes.Helicopter.FlightID = idString(fid)
		}
		res.Attributes.MCF = buildFleetMCF(ctx, mcfLatest[key], res.Attributes.Summary)
		open := openByHeli[key]
		if len(open) > 0 {
			res.Attributes.Complaints = mapComplaintListToFleetStatusDTO(ctx, h.complaintSvc, h.signoffSvc, open)
		}
		// Load the release form + status reasons when there is a defect, or a non-complaint
		// AOG (e.g. maintenance next-due). Clean, non-AOG aircraft skip the extra lookups.
		if len(open) > 0 || status == helicopter.StatusAOG {
			res.Attributes.EASARelease = resolveEASARelease(ctx, h.easaSvc, heli.ID)
			overdue, oErr := h.svc.OverdueInspections(ctx, heli.ID)
			if oErr != nil {
				slog.WarnContext(ctx, "fleet status: overdue inspections lookup failed", slog.Any("error", oErr))
			}
			if res.Attributes.Helicopter != nil {
				res.Attributes.Helicopter.StatusReasons = buildHelicopterStatusReasons(heli, open, overdue, now)
			}
		}
		out = append(out, res)
	}
	return out, nil
}

type fleetStatusRender struct {
	svc     fleetstatus.Service
	storage fileManagerDownloadURLStorage
	cache   map[string]shareddto.FleetStatusSummary
}

func fleetStatusResourceForHelicopter(ctx context.Context, r fleetStatusRender, heli *helicopter.Helicopter, latest *fleetstatus.FleetStatus, status string) responsedto.FleetStatusResource {
	if latest != nil {
		res := fleetStatusListResourceWithSummaryCache(ctx, r.svc, r.storage, latest, status, r.cache)
		res.Attributes.Helicopter = fleetStatusHelicopterResource(heli, status)
		return res
	}
	return responsedto.FleetStatusResource{
		Type: "fleet_status",
		Attributes: responsedto.FleetStatusAttributes{
			Helicopter:           fleetStatusHelicopterResource(heli, status),
			MaintenanceSchedules: fleetStatusMaintenanceSchedules(nil),
			Files:                fleetStatusFiles(ctx, r.storage, nil),
		},
	}
}

// buildFleetMCF renders the helicopter's MCF for the fleet page. When there is a real
// MCF it uses its values (sign = completed). When there is none it returns a placeholder
// (empty id, unsigned) pre-filled from the aircraft's current airframe hours / landings.
func buildFleetMCF(ctx context.Context, row *mcf.MaintenanceCheckFlight, summary shareddto.FleetStatusSummary) *shareddto.FleetStatusMCF {
	// The fleet shows the helicopter's LATEST MCF. When none exists — or the MCF is still a
	// draft — af_hours / landing_count pre-fill from the aircraft's current (flight-derived)
	// totals (like legacy). A signed (completed) MCF stays visible with its own recorded
	// snapshot + signer, until a new MCF replaces it.
	// af_hours / landing_count are the SIGNED check-flight values — only present once the MCF
	// is signed. Unsigned states (none / draft / cancelled) omit them.
	out := &shareddto.FleetStatusMCF{}
	if row == nil {
		return out
	}
	// Surface the record (id + date) for any real MCF — including a cancelled one — so the
	// fleet distinguishes it from "no MCF".
	out.ID = idString(row.ID)
	if row.Date != nil && !row.Date.IsZero() {
		d := row.Date.UTC().Format("2006-01-02")
		out.Date = &d
	}
	// A cancelled MCF ("tidak jadi") releases the status — active:false, sign:false — but the
	// record stays visible (id/date) so it shows as a cancelled MCF, not "no MCF".
	if row.IsCancelled() {
		return out
	}
	out.Sign = row.IsComplete()
	// Active only while the MCF is an open draft. Once signed it is considered done
	// (regardless of pass/fail) and the helicopter leaves MCF → not active.
	out.Active = row.IsPending()
	if row.IsComplete() {
		if af := strings.TrimSpace(row.AFHours); af != "" {
			out.AFHours = &af
		}
		lc := row.LandingCount
		out.LandingCount = &lc
		out.SignedBy = idString(row.CompletedBy)
		if n := userctx.GetDisplayName(ctx, row.CompletedBy); n != "" {
			out.SignedByName = &n
		}
		if s := userctx.GetShortName(ctx, row.CompletedBy); s != "" {
			out.SignedByShort = &s
		}
	}
	return out
}


func (h *FleetStatusHandler) deriveHelicopterStatus(ctx context.Context, heli *helicopter.Helicopter, helicopterID []byte) string {
	if heli == nil {
		return ""
	}
	inUse := false
	if len(helicopterID) == 16 {
		got, err := h.helicopterSvc.IsInUse(ctx, helicopterID)
		if err != nil {
			slog.WarnContext(ctx, "fleet status: in-use lookup failed",
				slog.Any("error", err),
				slog.String("helicopter_id",
					idString(helicopterID)))
		}
		inUse = got
	}
	grounded := false
	mcfPending := false
	if len(helicopterID) == 16 {
		grounded = groundingHelicopterSet(ctx, h.complaintSvc, [][]byte{helicopterID}, time.Now().UTC())[string(helicopterID)]
		mcfPending = mcfPendingHelicopterSet(ctx, h.mcfSvc, [][]byte{helicopterID})[string(helicopterID)]
	}
	return heli.DeriveStatus(helicopter.StatusInput{IsBook: inUse, IsMCF: mcfPending, ComplaintGrounded: grounded})
}
