package mysql

import (
	"context"
	"errors"
	"log"
	"time"

	"gorm.io/gorm"

	flightdata "wucher/internal/domain/flight_data"
	"wucher/internal/shared/pkg/txctx"
	"wucher/internal/shared/pkg/uuidv7"
)

type FlightDataRepository struct {
	db                    *gorm.DB
	hasHesloFlightsTable  bool
	hasHesloSlingsTable   bool
	hasLoggingSlingsTable bool
	hasHecFlightsTable    bool
	hasHecSlingsTable     bool
	hasHecLoadsTable      bool
}

func NewFlightDataRepository(db *gorm.DB) *FlightDataRepository {
	schema := newSchemaCache(db)
	return &FlightDataRepository{
		db:                    db,
		hasHesloFlightsTable:  schema.HasTable("heslo_flights"),
		hasHesloSlingsTable:   schema.HasTable("heslo_slings"),
		hasLoggingSlingsTable: schema.HasTable("logging_slings"),
		hasHecFlightsTable:    schema.HasTable("hec_flights"),
		hasHecSlingsTable:     schema.HasTable("hec_slings"),
		hasHecLoadsTable:      schema.HasTable("hec_loads"),
	}
}

func (r *FlightDataRepository) Create(ctx context.Context, row *flightdata.FlightData) error {
	if row == nil {
		return nil
	}
	now := time.Now().UTC()
	if len(row.ID) != 16 {
		row.ID = uuidv7.MustBytes()
	}
	if row.CreatedAt.IsZero() {
		row.CreatedAt = now
	}
	if row.UpdatedAt.IsZero() {
		row.UpdatedAt = row.CreatedAt
	}
	return txctx.DB(ctx, r.db).Table("flight_data").Create(flightDataPersistenceValues(row)).Error
}

func (r *FlightDataRepository) Update(ctx context.Context, row *flightdata.FlightData) error {
	if row == nil {
		return nil
	}
	row.UpdatedAt = time.Now().UTC()
	updates := flightDataPersistenceValues(row)
	delete(updates, "id")
	delete(updates, "created_at")
	delete(updates, "created_by")
	return r.db.WithContext(ctx).
		Model(&flightdata.FlightData{}).
		Where("id = ?", row.ID).
		Updates(updates).Error
}

func (r *FlightDataRepository) Delete(ctx context.Context, id []byte, deletedBy []byte) error {
	now := time.Now().UTC()
	updates := map[string]any{
		"deleted_at": now,
		"deleted_by": deletedBy,
		"updated_by": deletedBy,
	}
	return r.db.WithContext(ctx).
		Model(&flightdata.FlightData{}).
		Where("id = ? AND deleted_at IS NULL", id).
		Updates(updates).Error
}

func (r *FlightDataRepository) GetByID(ctx context.Context, id []byte) (*flightdata.FlightData, error) {
	var row flightdata.FlightData
	err := r.db.WithContext(ctx).
		Preload("Mission").
		Preload("Mission.Flight").
		Preload("Mission.Flight.Takeover").
		Preload("Mission.Flight.Takeover.RosterCrews").
		Preload("Mission.Flight.Takeover.OtherPeople").
		Preload("CoPilot").
		Preload("FromICAO").
		Preload("FromHospital").
		Preload("ToICAO").
		Preload("ToHospital").
		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 *FlightDataRepository) GetByMissionID(ctx context.Context, missionID []byte) (*flightdata.FlightData, error) {
	var row flightdata.FlightData
	err := r.db.WithContext(ctx).
		Preload("Mission").
		Preload("Mission.Flight").
		Preload("Mission.Flight.Takeover").
		Preload("Mission.Flight.Takeover.RosterCrews").
		Preload("Mission.Flight.Takeover.OtherPeople").
		Preload("CoPilot").
		Preload("FromICAO").
		Preload("FromHospital").
		Preload("ToICAO").
		Preload("ToHospital").
		Where("mission_id = ? AND deleted_at IS NULL", missionID).
		Order("created_at DESC").
		First(&row).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil
	}
	return &row, err
}

func (r *FlightDataRepository) ListByMissionID(ctx context.Context, missionID []byte) ([]flightdata.FlightData, error) {
	rows := make([]flightdata.FlightData, 0)
	err := r.db.WithContext(ctx).
		Preload("Mission").
		Preload("Mission.Flight").
		Preload("Mission.Flight.Takeover").
		Preload("Mission.Flight.Takeover.RosterCrews").
		Preload("Mission.Flight.Takeover.OtherPeople").
		Preload("CoPilot").
		Preload("FromICAO").
		Preload("FromHospital").
		Preload("ToICAO").
		Preload("ToHospital").
		Where("mission_id = ? AND deleted_at IS NULL", missionID).
		Order("created_at ASC, id ASC").
		Find(&rows).Error
	return rows, err
}

func (r *FlightDataRepository) GetByFlightID(ctx context.Context, flightID []byte) (*flightdata.FlightData, error) {
	var row flightdata.FlightData
	err := r.db.WithContext(ctx).
		Preload("Mission").
		Preload("CoPilot").
		Preload("FromICAO").
		Preload("FromHospital").
		Preload("ToICAO").
		Preload("ToHospital").
		Where("mission_id IN (SELECT id FROM missions WHERE flight_id = ? AND deleted_at IS NULL) AND deleted_at IS NULL", flightID).
		Order("created_at DESC").
		First(&row).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil
	}
	return &row, err
}

func (r *FlightDataRepository) List(ctx context.Context, date string, missionID, flightDataID []byte, limit, offset int) ([]flightdata.FlightData, int64, error) {
	rows := make([]flightdata.FlightData, 0)
	var total int64

	base := r.db.WithContext(ctx).
		Model(&flightdata.FlightData{}).
		Preload("Mission").
		Preload("Mission.Flight").
		Preload("Mission.Flight.Takeover").
		Preload("Mission.Flight.Takeover.RosterCrews").
		Preload("Mission.Flight.Takeover.OtherPeople").
		Preload("CoPilot").
		Preload("FromICAO").
		Preload("FromHospital").
		Preload("ToICAO").
		Preload("ToHospital").
		Where("deleted_at IS NULL")
	if date != "" {
		base = base.Where("DATE(take_off) = ?", date)
	}
	if len(missionID) == 16 {
		base = base.Where("mission_id = ?", missionID)
	}
	if len(flightDataID) == 16 {
		base = base.Where("id = ?", flightDataID)
	}

	if err := base.Count(&total).Error; err != nil {
		return nil, 0, err
	}

	query := base.Order("take_off DESC, created_at DESC")
	if limit > 0 {
		query = query.Limit(limit).Offset(offset)
	}
	if err := query.Find(&rows).Error; err != nil {
		return nil, 0, err
	}

	return rows, total, nil
}

func (r *FlightDataRepository) GetSPODetailsByFlightDataID(ctx context.Context, flightDataID []byte) (*flightdata.SPODetails, error) {
	details := &flightdata.SPODetails{
		HESLO: flightdata.SPOHESLODetails{
			Flights: make([]flightdata.SPOHESLOFlight, 0),
			Slings:  make([]flightdata.SPOHESLOSling, 0),
		},
		Logging: flightdata.SPOLoggingDetails{
			Slings: make([]flightdata.SPOLoggingSling, 0),
		},
		HEC: flightdata.SPOHECDetails{
			Flights: make([]flightdata.SPOHECFlight, 0),
			Slings:  make([]flightdata.SPOHECSling, 0),
			Loads:   make([]flightdata.SPOHECLoad, 0),
		},
	}

	if len(flightDataID) != 16 {
		return details, nil
	}

	db := r.db.WithContext(ctx)

	if r.hasHesloFlightsTable {
		type hesloFlightRow struct {
			ID               []byte `gorm:"column:id"`
			ROT              int    `gorm:"column:rot"`
			FuelTruckID      []byte `gorm:"column:fuel_truck_id"`
			FuelTruckName    string `gorm:"column:fuel_truck_name"`
			LoggingSlingID   []byte `gorm:"column:logging_sling_id"`
			LoggingSlingName string `gorm:"column:logging_sling_name"`
		}
		rows := make([]hesloFlightRow, 0)
		if err := db.Table("heslo_flights hf").
			Select("hf.id, hf.rot, hf.fuel_truck_id, fuel.name AS fuel_truck_name, hf.logging_sling_id, log.name AS logging_sling_name").
			Joins("LEFT JOIN facilities fuel ON fuel.id = hf.fuel_truck_id").
			Joins("LEFT JOIN facilities log ON log.id = hf.logging_sling_id").
			Where("hf.flight_data_id = ?", flightDataID).
			Order("hf.created_at ASC").
			Scan(&rows).Error; err != nil {
			return nil, err
		}
		for i := range rows {
			details.HESLO.Flights = append(details.HESLO.Flights, flightdata.SPOHESLOFlight{
				ID:               rows[i].ID,
				ROT:              rows[i].ROT,
				FuelTruckID:      rows[i].FuelTruckID,
				FuelTruckName:    rows[i].FuelTruckName,
				LoggingSlingID:   rows[i].LoggingSlingID,
				LoggingSlingName: rows[i].LoggingSlingName,
			})
		}
	}

	if r.hasHesloSlingsTable && r.hasHesloFlightsTable {
		type hesloSlingRow struct {
			ID            []byte `gorm:"column:id"`
			HESLOFlightID []byte `gorm:"column:heslo_flight_id"`
			SlingID       []byte `gorm:"column:facility_sling_id"`
			SlingName     string `gorm:"column:sling_name"`
		}
		rows := make([]hesloSlingRow, 0)
		if err := db.Table("heslo_slings hs").
			Select("hs.id, hs.heslo_flight_id, hs.facility_sling_id, sling.name AS sling_name").
			Joins("JOIN heslo_flights hf ON hf.id = hs.heslo_flight_id").
			Joins("LEFT JOIN facilities sling ON sling.id = hs.facility_sling_id").
			Where("hf.flight_data_id = ?", flightDataID).
			Order("hs.created_at ASC").
			Scan(&rows).Error; err != nil {
			return nil, err
		}
		for i := range rows {
			details.HESLO.Slings = append(details.HESLO.Slings, flightdata.SPOHESLOSling{
				ID:            rows[i].ID,
				HESLOFlightID: rows[i].HESLOFlightID,
				SlingID:       rows[i].SlingID,
				SlingName:     rows[i].SlingName,
			})
		}
	}

	if r.hasLoggingSlingsTable {
		type loggingSlingRow struct {
			ID        []byte `gorm:"column:id"`
			SlingID   []byte `gorm:"column:facility_sling_id"`
			SlingName string `gorm:"column:sling_name"`
		}
		rows := make([]loggingSlingRow, 0)
		if err := db.Table("logging_slings ls").
			Select("ls.id, ls.facility_sling_id, sling.name AS sling_name").
			Joins("LEFT JOIN facilities sling ON sling.id = ls.facility_sling_id").
			Where("ls.flight_data_id = ?", flightDataID).
			Order("ls.created_at ASC").
			Scan(&rows).Error; err != nil {
			return nil, err
		}
		for i := range rows {
			details.Logging.Slings = append(details.Logging.Slings, flightdata.SPOLoggingSling{
				ID:        rows[i].ID,
				SlingID:   rows[i].SlingID,
				SlingName: rows[i].SlingName,
			})
		}
	}

	if r.hasHecFlightsTable {
		type hecFlightRow struct {
			ID            []byte `gorm:"column:id"`
			ROT           int    `gorm:"column:rot"`
			HECCycle      int    `gorm:"column:hec_cycle"`
			EquipmentID   []byte `gorm:"column:equipment_id"`
			EquipmentName string `gorm:"column:equipment_name"`
		}
		rows := make([]hecFlightRow, 0)
		if err := db.Table("hec_flights hf").
			Select("hf.id, hf.rot, hf.hec_cycle, hf.equipment_id, eq.name AS equipment_name").
			Joins("LEFT JOIN facilities eq ON eq.id = hf.equipment_id").
			Where("hf.flight_data_id = ?", flightDataID).
			Order("hf.created_at ASC").
			Scan(&rows).Error; err != nil {
			return nil, err
		}
		for i := range rows {
			details.HEC.Flights = append(details.HEC.Flights, flightdata.SPOHECFlight{
				ID:            rows[i].ID,
				ROT:           rows[i].ROT,
				HECCycle:      rows[i].HECCycle,
				EquipmentID:   rows[i].EquipmentID,
				EquipmentName: rows[i].EquipmentName,
			})
		}
	}

	if r.hasHecSlingsTable {
		type hecSlingRow struct {
			ID        []byte `gorm:"column:id"`
			SlingID   []byte `gorm:"column:hec_sling_id"`
			SlingName string `gorm:"column:sling_name"`
		}
		rows := make([]hecSlingRow, 0)
		if err := db.Table("hec_slings hs").
			Select("hs.id, hs.hec_sling_id, sling.name AS sling_name").
			Joins("LEFT JOIN facilities sling ON sling.id = hs.hec_sling_id").
			Where("hs.flight_data_id = ?", flightDataID).
			Order("hs.created_at ASC").
			Scan(&rows).Error; err != nil {
			return nil, err
		}
		for i := range rows {
			details.HEC.Slings = append(details.HEC.Slings, flightdata.SPOHECSling{
				ID:        rows[i].ID,
				SlingID:   rows[i].SlingID,
				SlingName: rows[i].SlingName,
			})
		}
	}

	if r.hasHecLoadsTable {
		type hecLoadRow struct {
			ID           []byte `gorm:"column:id"`
			LoadCategory string `gorm:"column:load_category"`
			Quantity     int    `gorm:"column:quantity"`
		}
		rows := make([]hecLoadRow, 0)
		if err := db.Table("hec_loads").
			Select("id, load_category, quantity").
			Where("flight_data_id = ?", flightDataID).
			Order("created_at ASC").
			Scan(&rows).Error; err != nil {
			return nil, err
		}
		for i := range rows {
			details.HEC.Loads = append(details.HEC.Loads, flightdata.SPOHECLoad{
				ID:           rows[i].ID,
				LoadCategory: rows[i].LoadCategory,
				Quantity:     rows[i].Quantity,
			})
		}
	}

	return details, nil
}

func (r *FlightDataRepository) UpsertSPODetails(ctx context.Context, flightDataID []byte, data *flightdata.SPOUpsertData) error {
	if len(flightDataID) != 16 || data == nil {
		return nil
	}
	log.Printf("[UpsertSPODetails] start: heslo=%v logging=%v hec=%v hasHesloFlights=%v hasLogging=%v hasHecFlights=%v",
		data.HESLO != nil, data.Logging != nil, data.HEC != nil,
		r.hasHesloFlightsTable, r.hasLoggingSlingsTable, r.hasHecFlightsTable)

	txErr := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		now := time.Now().UTC()

		if data.HESLO != nil && r.hasHesloFlightsTable {
			if r.hasHesloSlingsTable {
				if err := tx.Exec(
					"DELETE hs FROM heslo_slings hs JOIN heslo_flights hf ON hf.id = hs.heslo_flight_id WHERE hf.flight_data_id = ?",
					flightDataID,
				).Error; err != nil {
					return err
				}
			}
			if err := tx.Exec("DELETE FROM heslo_flights WHERE flight_data_id = ?", flightDataID).Error; err != nil {
				return err
			}
			for _, f := range data.HESLO.Flights {
				hf := &flightdata.HESLOFlight{
					ID:                  uuidv7.MustBytes(),
					FlightDataID:        flightDataID,
					ROT:                 f.ROT,
					FacilityFuelTruckID: f.FuelTruckID,
					LoggingSlingID:      f.LoggingSlingID,
					CreatedAt:           now,
				}
				if err := tx.Omit("FlightData", "FacilityFuelTruck", "LoggingSling").Create(hf).Error; err != nil {
					return err
				}
				if r.hasHesloSlingsTable {
					for _, s := range f.Slings {
						if len(s.SlingID) != 16 {
							continue
						}
						hs := &flightdata.HESLOSling{
							ID:              uuidv7.MustBytes(),
							HESLOFlightID:   hf.ID,
							FacilitySlingID: s.SlingID,
							CreatedAt:       now,
						}
						if err := tx.Omit("HESLOFlight", "FacilitySling").Create(hs).Error; err != nil {
							return err
						}
					}
				}
			}
		}

		if data.Logging != nil && r.hasLoggingSlingsTable {
			if err := tx.Exec("DELETE FROM logging_slings WHERE flight_data_id = ?", flightDataID).Error; err != nil {
				return err
			}
			for _, s := range data.Logging.Slings {
				if len(s.SlingID) != 16 {
					continue
				}
				ls := &flightdata.LoggingSling{
					ID:              uuidv7.MustBytes(),
					FlightDataID:    flightDataID,
					FacilitySlingID: s.SlingID,
					CreatedAt:       now,
				}
				if err := tx.Omit("FlightData", "FacilitySling").Create(ls).Error; err != nil {
					return err
				}
			}
		}

		if data.HEC != nil {
			if r.hasHecSlingsTable {
				if err := tx.Exec("DELETE FROM hec_slings WHERE flight_data_id = ?", flightDataID).Error; err != nil {
					return err
				}
			}
			if r.hasHecLoadsTable {
				if err := tx.Exec("DELETE FROM hec_loads WHERE flight_data_id = ?", flightDataID).Error; err != nil {
					return err
				}
			}
			if r.hasHecFlightsTable {
				if err := tx.Exec("DELETE FROM hec_flights WHERE flight_data_id = ?", flightDataID).Error; err != nil {
					return err
				}
				for _, f := range data.HEC.Flights {
					hf := &flightdata.HECFlight{
						ID:           uuidv7.MustBytes(),
						FlightDataID: flightDataID,
						ROT:          f.ROT,
						HECCycle:     f.HECCycle,
						EquipmentID:  f.EquipmentID,
						CreatedAt:    now,
					}
					if err := tx.Omit("FlightData", "Equipment").Create(hf).Error; err != nil {
						return err
					}
				}
			}
			if r.hasHecSlingsTable {
				for _, s := range data.HEC.Slings {
					if len(s.SlingID) != 16 {
						continue
					}
					hs := &flightdata.HECSling{
						ID:           uuidv7.MustBytes(),
						FlightDataID: flightDataID,
						HECSlingID:   s.SlingID,
						CreatedAt:    now,
					}
					if err := tx.Omit("FlightData", "HECSling").Create(hs).Error; err != nil {
						return err
					}
				}
			}
			if r.hasHecLoadsTable {
				for _, l := range data.HEC.Loads {
					hl := &flightdata.HECLoads{
						ID:           uuidv7.MustBytes(),
						FlightDataID: flightDataID,
						LoadCategory: l.LoadCategory,
						Quantity:     l.Quantity,
						CreatedAt:    now,
					}
					if err := tx.Omit("FlightData").Create(hl).Error; err != nil {
						return err
					}
				}
			}
		}

		return nil
	})
	log.Printf("[UpsertSPODetails] done: err=%v", txErr)
	return txErr
}

func flightDataPersistenceValues(row *flightdata.FlightData) map[string]any {
	if row == nil {
		return map[string]any{}
	}
	return map[string]any{
		"id":                      nullableBytesValue(row.ID),
		"mission_id":              nullableBytesValue(row.MissionID),
		"co_pilot_id":             nullableBytesValue(row.CoPilotID),
		"from_icao_id":            nullableBytesValue(row.FromICAOID),
		"from_hospital_id":        nullableBytesValue(row.FromHospitalID),
		"to_icao_id":              nullableBytesValue(row.ToICAOID),
		"to_hospital_id":          nullableBytesValue(row.ToHospitalID),
		"take_off":                nullableTimeValue(row.FlightTakeOff),
		"landing":                 nullableTimeValue(row.FlightLanding),
		"duration":                row.FlightDuration,
		"flight_type":             row.FlightType,
		"red":                     row.FlightRED,
		"max_n1":                  row.MaxN1,
		"max_n2":                  row.MaxN2,
		"pax_count":               row.PaxCount,
		"ticket_no":               row.TicketNo,
		"engine":                  row.Engine,
		"landing_count":           row.LandingCount,
		"rotor_brake_cycle":       row.RotorBrakeCycle,
		"hook_releases":           row.HookReleases,
		"delivery_note_number":    row.DeliveryNoteNumber,
		"customer_name":           row.CustomerName,
		"flight_plan_distance":    row.FlightPlanDistance,
		"flight_plan_time":        row.FlightPlanTime,
		"flight_plan_true_course": row.FlightPlanTrueCourse,
		"fuel_before_flight":      row.FuelBeforeFlight,
		"fuel_upload":             row.FuelUpload,
		"fuel_after_flight":       row.FuelAfterFlight,
		"fuel_planning":           row.FuelPlanning,
		"flight_position":         row.FlightPositioning,
		"other_information":       row.OtherInformation,
		"status":                  row.Status,
		"created_at":              row.CreatedAt,
		"created_by":              nullableBytesValue(row.CreatedBy),
		"updated_at":              row.UpdatedAt,
		"updated_by":              nullableBytesValue(row.UpdatedBy),
		"deleted_at":              nullableTimePtrValue(row.DeletedAt),
		"deleted_by":              nullableBytesValue(row.DeletedBy),
	}
}

func nullableBytesValue(v []byte) any {
	if len(v) == 0 {
		return nil
	}
	return v
}

func nullableTimeValue(v time.Time) any {
	if v.IsZero() {
		return nil
	}
	return v
}

func nullableTimePtrValue(v *time.Time) any {
	if v == nil {
		return nil
	}
	return *v
}
