package mysql import ( "context" "errors" "fmt" "strings" "time" "gorm.io/gorm" "gorm.io/gorm/clause" flightdata "wucher/internal/domain/flight_data" helicopterusage "wucher/internal/domain/helicopter_usage" "wucher/internal/shared/pkg/metricparse" ) type HelicopterUsageRepository struct { db *gorm.DB } func NewHelicopterUsageRepository(db *gorm.DB) *HelicopterUsageRepository { return &HelicopterUsageRepository{db: db} } func (r *HelicopterUsageRepository) Create(ctx context.Context, row *helicopterusage.HelicopterUsage) error { return r.db.WithContext(ctx).Create(row).Error } func (r *HelicopterUsageRepository) Update(ctx context.Context, row *helicopterusage.HelicopterUsage) error { return r.db.WithContext(ctx).Save(row).Error } func (r *HelicopterUsageRepository) Delete(ctx context.Context, id []byte, deletedBy []byte) error { if err := ensureNoReferenceBeforeDelete(ctx, r.db, "helicopter_usage", id); err != nil { return err } now := time.Now().UTC() updates := map[string]any{ "deleted_at": now, "deleted_by": deletedBy, "updated_by": deletedBy, } return mapDeleteConstraintError(r.db.WithContext(ctx). Model(&helicopterusage.HelicopterUsage{}). Where("id = ? AND deleted_at IS NULL", id). Updates(updates).Error) } func (r *HelicopterUsageRepository) RefreshAll(ctx context.Context) error { now := time.Now().UTC() existing := map[string]helicopterusage.HelicopterUsage{} { var rows []helicopterusage.HelicopterUsage if err := r.db.WithContext(ctx). Select("helicopter_usage.*"). Where("helicopter_usage.deleted_at IS NULL"). Find(&rows).Error; err != nil { return err } for i := range rows { key, err := helicopterUsageKey(rows[i].HelicopterID) if err != nil { continue } existing[key] = rows[i] } } helicopterIDs, err := r.listActiveHelicopterIDs(ctx) if err != nil { return err } if len(helicopterIDs) == 0 { return nil } flightStats, err := r.flightStatsByHelicopter(ctx) if err != nil { return err } reportStats, err := r.reportStatsByHelicopter(ctx) if err != nil { return err } for _, helicopterID := range helicopterIDs { key, _ := helicopterUsageKey(helicopterID) row := &helicopterusage.HelicopterUsage{ HelicopterID: append([]byte(nil), helicopterID...), TotalLanding: flightStats[key].TotalLanding, TotalAirframeHours: flightStats[key].TotalAirframeHours, TotalAirframeCycles: flightStats[key].TotalAirframeCycles, TotalEngine1Hours: flightStats[key].TotalAirframeHours, TotalEngine2Hours: flightStats[key].TotalAirframeHours, TotalEngine1GpcNgN1: reportStats[key].TotalEngine1GpcNgN1, TotalEngine1PtcNfN2: reportStats[key].TotalEngine1PtcNfN2, TotalEngine2GpcNgN1: reportStats[key].TotalEngine2GpcNgN1, TotalEngine2PtcNfN2: reportStats[key].TotalEngine2PtcNfN2, TotalFlightReport: reportStats[key].TotalFlightReport, TotalHookRelease: flightStats[key].TotalHookRelease, TotalRotorBrakeCycle: flightStats[key].TotalRotorBrakeCycle, } if ex, ok := existing[key]; ok { row.ID = ex.ID row.CreatedAt = ex.CreatedAt row.CreatedBy = ex.CreatedBy row.UpdatedBy = ex.UpdatedBy row.TotalEngine1Ccc = ex.TotalEngine1Ccc row.TotalEngine2Ccc = ex.TotalEngine2Ccc row.ManualAirframeHours = ex.ManualAirframeHours row.ManualAirframeCycles = ex.ManualAirframeCycles row.ManualEngine1Hours = ex.ManualEngine1Hours row.ManualEngine1GpcNgN1 = ex.ManualEngine1GpcNgN1 row.ManualEngine1PtcNfN2 = ex.ManualEngine1PtcNfN2 row.ManualEngine2Hours = ex.ManualEngine2Hours row.ManualEngine2GpcNgN1 = ex.ManualEngine2GpcNgN1 row.ManualEngine2PtcNfN2 = ex.ManualEngine2PtcNfN2 row.ManualLanding = ex.ManualLanding row.ManualFlightReport = ex.ManualFlightReport row.ManualHookRelease = ex.ManualHookRelease row.ManualRotorBrakeCycle = ex.ManualRotorBrakeCycle row.TotalAirframeHours += ex.ManualAirframeHours row.TotalAirframeCycles += ex.ManualAirframeCycles row.TotalEngine1Hours += ex.ManualEngine1Hours row.TotalEngine1GpcNgN1 += ex.ManualEngine1GpcNgN1 row.TotalEngine1PtcNfN2 += ex.ManualEngine1PtcNfN2 row.TotalEngine2Hours += ex.ManualEngine2Hours row.TotalEngine2GpcNgN1 += ex.ManualEngine2GpcNgN1 row.TotalEngine2PtcNfN2 += ex.ManualEngine2PtcNfN2 row.TotalLanding += ex.ManualLanding row.TotalFlightReport += ex.ManualFlightReport row.TotalHookRelease += ex.ManualHookRelease row.TotalRotorBrakeCycle += ex.ManualRotorBrakeCycle } else { row.CreatedAt = now } row.UpdatedAt = now if err := r.upsertByHelicopterID(ctx, row); err != nil { return err } } return nil } func (r *HelicopterUsageRepository) GetByID(ctx context.Context, id []byte) (*helicopterusage.HelicopterUsage, error) { var row helicopterusage.HelicopterUsage err := r.db.WithContext(ctx). Preload("Helicopter"). Where("id = ? AND deleted_at IS NULL", id). First(&row).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } return &row, err } func (r *HelicopterUsageRepository) GetByHelicopterID(ctx context.Context, helicopterID []byte) (*helicopterusage.HelicopterUsage, error) { var row helicopterusage.HelicopterUsage err := r.db.WithContext(ctx). Preload("Helicopter"). Where("helicopter_id = ? AND deleted_at IS NULL", helicopterID). First(&row).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } return &row, err } type helicopterUsageFlightStats struct { TotalLanding float64 TotalAirframeHours float64 TotalAirframeCycles float64 TotalHookRelease float64 TotalRotorBrakeCycle float64 } type helicopterUsageReportStats struct { TotalFlightReport float64 TotalEngine1GpcNgN1 float64 TotalEngine1PtcNfN2 float64 TotalEngine2GpcNgN1 float64 TotalEngine2PtcNfN2 float64 } func (r *HelicopterUsageRepository) listActiveHelicopterIDs(ctx context.Context) ([][]byte, error) { type row struct { ID []byte `gorm:"column:id"` } rows := make([]row, 0) if err := r.db.WithContext(ctx). Table("helicopters"). Select("id"). Where("is_active = ?", true). Order("created_at ASC, id ASC"). Scan(&rows).Error; err != nil { return nil, err } out := make([][]byte, 0, len(rows)) for i := range rows { if len(rows[i].ID) != 16 { continue } out = append(out, append([]byte(nil), rows[i].ID...)) } return out, nil } func (r *HelicopterUsageRepository) flightStatsByHelicopter(ctx context.Context) (map[string]helicopterUsageFlightStats, error) { type row struct { HelicopterID []byte `gorm:"column:helicopter_id"` TotalLanding int64 `gorm:"column:total_landing"` TotalAirframeNS int64 `gorm:"column:total_airframe_ns"` TotalAirframeCycles int64 `gorm:"column:total_airframe_cycles"` TotalHookRelease int64 `gorm:"column:total_hook_release"` TotalRotorBrakeCycle int64 `gorm:"column:total_rotor_brake_cycle"` } rows := make([]row, 0) err := r.db.WithContext(ctx). Table("flight_data fd"). Select(` ra.helicopter_id, COALESCE(SUM(fd.landing_count), 0) AS total_landing, COALESCE(SUM(fd.duration), 0) AS total_airframe_ns, COUNT(fd.id) AS total_airframe_cycles, COALESCE(SUM(fd.hook_releases), 0) AS total_hook_release, COALESCE(SUM(fd.rotor_brake_cycle), 0) AS total_rotor_brake_cycle `). Joins("JOIN missions m ON m.id = fd.mission_id AND m.deleted_at IS NULL"). Joins("JOIN flights f ON f.id = m.flight_id AND f.deleted_at IS NULL"). Joins("JOIN takeover_acs ta ON ta.id = f.takeover_ac_id AND ta.deleted_at IS NULL"). Joins("JOIN reserve_acs ra ON ra.id = ta.reserve_ac_id AND ra.deleted_at IS NULL"). Where("fd.deleted_at IS NULL AND fd.status = ?", flightdata.StatusCompleted). Group("ra.helicopter_id"). Scan(&rows).Error if err != nil { return nil, err } out := make(map[string]helicopterUsageFlightStats, len(rows)) for i := range rows { key, err := helicopterUsageKey(rows[i].HelicopterID) if err != nil { continue } out[key] = helicopterUsageFlightStats{ TotalLanding: float64(rows[i].TotalLanding), TotalAirframeHours: float64(rows[i].TotalAirframeNS) / float64(time.Hour), TotalAirframeCycles: float64(rows[i].TotalAirframeCycles), TotalHookRelease: float64(rows[i].TotalHookRelease), TotalRotorBrakeCycle: float64(rows[i].TotalRotorBrakeCycle), } } return out, nil } func (r *HelicopterUsageRepository) reportStatsByHelicopter(ctx context.Context) (map[string]helicopterUsageReportStats, error) { type row struct { HelicopterID []byte `gorm:"column:helicopter_id"` Engine1GpcN1 *string `gorm:"column:engine1_gpc_n1"` Engine1PtcN2 *string `gorm:"column:engine1_ptc_n2"` Engine2GpcN1 *string `gorm:"column:engine2_gpc_n1"` Engine2PtcN2 *string `gorm:"column:engine2_ptc_n2"` } rows := make([]row, 0) if err := r.db.WithContext(ctx). Table("fm_reports"). Select("helicopter_id, engine1_gpc_n1, engine1_ptc_n2, engine2_gpc_n1, engine2_ptc_n2"). Where("deleted_at IS NULL AND helicopter_id IS NOT NULL"). Order("helicopter_id ASC, created_at ASC, id ASC"). Scan(&rows).Error; err != nil { return nil, err } out := make(map[string]helicopterUsageReportStats) for i := range rows { key, err := helicopterUsageKey(rows[i].HelicopterID) if err != nil { continue } acc := out[key] acc.TotalFlightReport++ acc.TotalEngine1GpcNgN1 += parseNumericMetric(rows[i].Engine1GpcN1) acc.TotalEngine1PtcNfN2 += parseNumericMetric(rows[i].Engine1PtcN2) acc.TotalEngine2GpcNgN1 += parseNumericMetric(rows[i].Engine2GpcN1) acc.TotalEngine2PtcNfN2 += parseNumericMetric(rows[i].Engine2PtcN2) out[key] = acc } return out, nil } func (r *HelicopterUsageRepository) upsertByHelicopterID(ctx context.Context, row *helicopterusage.HelicopterUsage) error { return r.db.WithContext(ctx). Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "helicopter_id"}}, DoUpdates: clause.AssignmentColumns([]string{ "total_landing", "total_airframe_hours", "total_airframe_cycles", "total_engine_1_hours", "total_engine_1_gpc_ng_n1", "total_engine_1_ptc_nf_n2", "total_engine_2_hours", "total_engine_2_gpc_ng_n1", "total_engine_2_ptc_nf_n2", "total_flight_report", "total_hook_release", "total_rotor_brake_cycle", "updated_at", "updated_by", }), }). Create(row).Error } func helicopterUsageKey(id []byte) (string, error) { if len(id) != 16 { return "", fmt.Errorf("invalid helicopter id") } return string(id), nil } func parseNumericMetric(v *string) float64 { return metricparse.Parse(v) } // FlightStatsByFlightID aggregates one flight's completed flight_data — the "today" // contribution of an FM report. Uses the same SUM/COUNT definitions as the cumulative // totals so prev = total - today reconciles exactly. func (r *HelicopterUsageRepository) FlightStatsByFlightID(ctx context.Context, flightID []byte) (helicopterusage.FlightDayStats, error) { if len(flightID) != 16 { return helicopterusage.FlightDayStats{}, nil } var row struct { TotalLanding int64 `gorm:"column:total_landing"` TotalAirframeNS int64 `gorm:"column:total_airframe_ns"` TotalAirframeCycles int64 `gorm:"column:total_airframe_cycles"` TotalHookRelease int64 `gorm:"column:total_hook_release"` TotalRotorBrakeCycle int64 `gorm:"column:total_rotor_brake_cycle"` } err := r.db.WithContext(ctx). Table("flight_data fd"). Select(` COALESCE(SUM(fd.landing_count), 0) AS total_landing, COALESCE(SUM(fd.duration), 0) AS total_airframe_ns, COUNT(fd.id) AS total_airframe_cycles, COALESCE(SUM(fd.hook_releases), 0) AS total_hook_release, COALESCE(SUM(fd.rotor_brake_cycle), 0) AS total_rotor_brake_cycle `). Joins("JOIN missions m ON m.id = fd.mission_id AND m.deleted_at IS NULL"). Where("fd.deleted_at IS NULL AND fd.status = ? AND m.flight_id = ?", flightdata.StatusCompleted, flightID). Scan(&row).Error if err != nil { return helicopterusage.FlightDayStats{}, err } return helicopterusage.FlightDayStats{ AirframeHours: float64(row.TotalAirframeNS) / float64(time.Hour), AirframeCycles: float64(row.TotalAirframeCycles), Landing: float64(row.TotalLanding), HookRelease: float64(row.TotalHookRelease), RotorBrakeCycle: float64(row.TotalRotorBrakeCycle), }, nil } func (r *HelicopterUsageRepository) List(ctx context.Context, filter, sort string, limit, offset int) ([]helicopterusage.HelicopterUsage, int64, error) { rows := make([]helicopterusage.HelicopterUsage, 0) var total int64 base := r.db.WithContext(ctx). Model(&helicopterusage.HelicopterUsage{}). Joins("LEFT JOIN helicopters h ON h.id = helicopter_usage.helicopter_id AND h.deleted_at IS NULL"). Where("helicopter_usage.deleted_at IS NULL") if strings.TrimSpace(filter) != "" { like := "%" + strings.ToLower(strings.TrimSpace(filter)) + "%" base = base.Where( "LOWER(HEX(helicopter_usage.id)) LIKE ? OR LOWER(HEX(helicopter_usage.helicopter_id)) LIKE ? OR LOWER(h.designation) LIKE ? OR LOWER(h.identifier) LIKE ?", like, like, like, like, ) } query := base if strings.TrimSpace(sort) != "" { query = query.Order(sort) } else { query = query.Order("helicopter_usage.created_at DESC") } if err := base.Count(&total).Error; err != nil { return nil, 0, err } if limit > 0 { query = query.Limit(limit).Offset(offset) } if err := query.Preload("Helicopter").Find(&rows).Error; err != nil { return nil, 0, err } return rows, total, nil }