package mysql

import (
	"context"
	"database/sql/driver"
	"errors"
	"fmt"
	"strings"
	"time"

	"gorm.io/gorm"
	"gorm.io/gorm/clause"

	"wucher/internal/domain/base"
	"wucher/internal/shared/pkg/sortkey"
	"wucher/internal/shared/pkg/uuidv7"
)

type BaseRepository struct {
	db *gorm.DB
}

type binaryValue struct {
	data []byte
}

func (b binaryValue) Value() (driver.Value, error) {
	if len(b.data) == 0 {
		return nil, nil
	}
	return append([]byte(nil), b.data...), nil
}

func NewBaseRepository(db *gorm.DB) *BaseRepository {
	return &BaseRepository{db: db}
}

func (r *BaseRepository) CreateBase(ctx context.Context, row *base.Base, categoryType string) error {
	categoryID, err := r.categoryIDByType(ctx, categoryType)
	if err != nil {
		return err
	}
	row.BaseCategoryID = categoryID
	if actor := actorUserIDFromContext(ctx); len(actor) > 0 {
		row.CreatedBy = actor
		row.UpdatedBy = actor
	}
	row.DefaultStartTimeType = normalizeBaseShiftTimeType(row.DefaultStartTimeType)
	row.DefaultEndTimeType = normalizeBaseShiftTimeType(row.DefaultEndTimeType)
	row.DefaultShiftStart = baseClockStorageValue(row.DefaultShiftStart)
	row.DefaultShiftEnd = baseClockStorageValue(row.DefaultShiftEnd)
	requestedIsActive := row.IsActive
	operationalShiftTimes := row.OperationalShiftTimes
	row.OperationalShiftTimes = nil
	db := r.db.WithContext(ctx)
	return db.Transaction(func(tx *gorm.DB) error {
		if err := tx.Omit(clause.Associations).Create(row).Error; err != nil {
			return err
		}
		if err := tx.Model(&base.Base{}).Where("id = ?", row.ID).UpdateColumn("is_active", requestedIsActive).Error; err != nil {
			return err
		}
		if err := r.replaceBaseContactRolesTx(ctx, tx, row.ID, row.HEMSEDCContactIDs, row.MedPaxContactIDs, row.ResponsiblePilotContactIDs); err != nil {
			return err
		}
		return r.replaceBaseOperationalShiftTimesTx(ctx, tx, row.ID, operationalShiftTimes)
	})
}

func (r *BaseRepository) UpdateBase(ctx context.Context, row *base.Base, categoryType string) error {
	categoryID, err := r.categoryIDByType(ctx, categoryType)
	if err != nil {
		return err
	}
	row.BaseCategoryID = categoryID
	if actor := actorUserIDFromContext(ctx); len(actor) > 0 {
		row.UpdatedBy = actor
	}
	row.DefaultStartTimeType = normalizeBaseShiftTimeType(row.DefaultStartTimeType)
	row.DefaultEndTimeType = normalizeBaseShiftTimeType(row.DefaultEndTimeType)
	row.DefaultShiftStart = baseClockStorageValue(row.DefaultShiftStart)
	row.DefaultShiftEnd = baseClockStorageValue(row.DefaultShiftEnd)
	return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		// Use an explicit UPDATE statement so the base shift columns are stored
		// in the same datetime shape that exists in the local schema while still
		// accepting clock-only inputs from the API layer.
		if err := tx.Exec(
			fmt.Sprintf(`UPDATE bases
			 SET base = ?,
			     base_category_id = ?,
			     foto_attachment_id = ?,
			     base_abbreviation = ?,
			     address = ?,
			     latitude = ?,
			     longitude = ?,
			     landline_number = ?,
			     mobile_number = ?,
			     email = ?,
			     sortkey = ?,
			     sms_alert = ?,
			     checklist = ?,
			     leg_time = ?,
			     default_start_time_type = ?,
			     default_end_time_type = ?,
			     default_shift_start = ?,
			     default_shift_end = ?,
			     utc = ?,
			     dry = ?,
			     control_center = ?,
			     dul = ?,
			     notes = ?,
			     is_active = ?,
			     updated_by = ?,
			     updated_at = ?
			 WHERE id = ? AND deleted_at IS NULL`),
			row.BaseName,
			binaryValue{data: row.BaseCategoryID},
			binaryValue{data: row.FotoAttachmentID},
			row.BaseAbbreviation,
			row.Address,
			row.Latitude,
			row.Longitude,
			row.LandlineNumber,
			row.MobileNumber,
			row.Email,
			row.SortKey,
			row.SMSAlert,
			row.Checklist,
			row.LegTime,
			row.DefaultStartTimeType,
			row.DefaultEndTimeType,
			row.DefaultShiftStart,
			row.DefaultShiftEnd,
			row.UTC,
			row.Dry,
			row.ControlCenter,
			row.DUL,
			row.Notes,
			row.IsActive,
			binaryValue{data: row.UpdatedBy},
			time.Now().UTC(),
			binaryValue{data: row.ID},
		).Error; err != nil {
			return err
		}
		if err := r.replaceBaseContactRolesTx(ctx, tx, row.ID, row.HEMSEDCContactIDs, row.MedPaxContactIDs, row.ResponsiblePilotContactIDs); err != nil {
			return err
		}
		if row.OperationalShiftTimes == nil {
			return nil
		}
		return r.replaceBaseOperationalShiftTimesTx(ctx, tx, row.ID, row.OperationalShiftTimes)
	})
}

func (r *BaseRepository) DeleteBase(ctx context.Context, id []byte, categoryType string) error {
	categoryID, err := r.categoryIDByType(ctx, categoryType)
	if err != nil {
		return err
	}
	if err := ensureNoReferenceBeforeDelete(ctx, r.db, "bases", id); err != nil {
		return err
	}
	now := time.Now().UTC()
	updates := map[string]any{
		"deleted_at": now,
	}
	if actor := actorUserIDFromContext(ctx); len(actor) > 0 {
		updates["deleted_by"] = actor
		updates["updated_by"] = actor
	}
	return mapDeleteConstraintError(r.db.WithContext(ctx).
		Model(&base.Base{}).
		Where("id = ? AND base_category_id = ? AND deleted_at IS NULL", id, categoryID).
		Updates(updates).Error)
}

func (r *BaseRepository) GetBaseByID(ctx context.Context, id []byte, categoryType string) (*base.Base, error) {
	categoryID, err := r.categoryIDByType(ctx, categoryType)
	if err != nil {
		return nil, err
	}

	var row base.Base
	err = r.db.WithContext(ctx).
		Preload("FotoAttachment").
		Preload("FotoAttachment.File").
		Preload("OperationalShiftTimes", preloadBaseOperationalShiftTimes).
		Where("id = ? AND base_category_id = ? AND deleted_at IS NULL", id, categoryID).
		First(&row).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil
	}
	if err == nil {
		if loadErr := r.loadBaseContactRoles(ctx, []*base.Base{&row}); loadErr != nil {
			return nil, loadErr
		}
	}
	return &row, err
}

func (r *BaseRepository) ListBases(ctx context.Context, filter, sort string, limit, offset int, categoryType string, dul *bool) ([]base.Base, int64, error) {
	var rows []base.Base
	var total int64

	baseQuery := r.db.WithContext(ctx).
		Model(&base.Base{}).
		Where("deleted_at IS NULL")
	if strings.TrimSpace(categoryType) != "" {
		categoryID, err := r.categoryIDByType(ctx, categoryType)
		if err != nil {
			return nil, 0, err
		}
		baseQuery = baseQuery.Where("base_category_id = ?", categoryID)
	}
	if filter != "" {
		like := "%" + filter + "%"
		baseQuery = baseQuery.Where(
			"base LIKE ? OR base_abbreviation LIKE ? OR address LIKE ? OR email LIKE ? OR landline_number LIKE ? OR mobile_number LIKE ?",
			like,
			like,
			like,
			like,
			like,
			like,
		)
	}
	if dul != nil {
		baseQuery = baseQuery.Where("dul = ?", *dul)
	}

	query := baseQuery
	if sort != "" {
		query = query.Order(sort)
	} else {
		for _, clause := range sortkey.ActivePositiveSortClauses("bases", "is_active", "sortkey", "base", false) {
			query = query.Order(clause)
		}
	}
	if err := baseQuery.Count(&total).Error; err != nil {
		return nil, 0, err
	}
	if limit > 0 {
		query = query.Limit(limit).Offset(offset)
	}
	query = query.Preload("BaseCategory")
	query = query.Preload("FotoAttachment").Preload("FotoAttachment.File")
	query = query.Preload("OperationalShiftTimes", preloadBaseOperationalShiftTimes)
	if err := query.Find(&rows).Error; err != nil {
		return nil, 0, err
	}
	ptrs := make([]*base.Base, 0, len(rows))
	for i := range rows {
		ptrs = append(ptrs, &rows[i])
	}
	if err := r.loadBaseContactRoles(ctx, ptrs); err != nil {
		return nil, 0, err
	}

	return rows, total, nil
}

func preloadBaseOperationalShiftTimes(db *gorm.DB) *gorm.DB {
	if db == nil {
		return db
	}
	q := db.Where("deleted_at IS NULL").Order("date_start ASC, date_end ASC")
	if db.Dialector != nil && strings.EqualFold(db.Dialector.Name(), "mysql") {
		q = q.Select(`
			id,
			base_id,
			date_start,
			date_end,
			start_time_type,
			end_time_type,
			CASE WHEN shift_start IS NULL THEN '' ELSE TIME_FORMAT(shift_start, '%H:%i:%s') END AS shift_start,
			CASE WHEN shift_end IS NULL THEN '' ELSE TIME_FORMAT(shift_end, '%H:%i:%s') END AS shift_end,
			created_at,
			created_by,
			updated_at,
			updated_by,
			deleted_at,
			deleted_by
		`)
	}
	return q
}

func (r *BaseRepository) replaceBaseContactRolesTx(
	ctx context.Context,
	tx *gorm.DB,
	baseID []byte,
	hemsEDCIDs [][]byte,
	medPaxIDs [][]byte,
	pilotIDs [][]byte,
) error {
	if len(baseID) != 16 {
		return errors.New("invalid base id")
	}
	all := append(copyIDs(hemsEDCIDs), medPaxIDs...)
	all = append(all, pilotIDs...)
	if err := ensureContactsExistTx(tx, all); err != nil {
		return err
	}
	if err := tx.Where("base_id = ?", baseID).Delete(&base.BaseContactRole{}).Error; err != nil {
		return err
	}
	rows := make([]base.BaseContactRole, 0, len(hemsEDCIDs)+len(medPaxIDs)+len(pilotIDs))
	actor := actorUserIDFromContext(ctx)
	for _, contactID := range hemsEDCIDs {
		if len(contactID) != 16 {
			return fmt.Errorf("invalid hems_edc contact id")
		}
		rows = append(rows, base.BaseContactRole{
			BaseID:    baseID,
			ContactID: contactID,
			RoleCode:  base.BaseContactRoleHEMSEDC,
			CreatedBy: actor,
			UpdatedBy: actor,
		})
	}
	for _, contactID := range medPaxIDs {
		if len(contactID) != 16 {
			return fmt.Errorf("invalid med_pax contact id")
		}
		rows = append(rows, base.BaseContactRole{
			BaseID:    baseID,
			ContactID: contactID,
			RoleCode:  base.BaseContactRoleMedPax,
			CreatedBy: actor,
			UpdatedBy: actor,
		})
	}
	for _, contactID := range pilotIDs {
		if len(contactID) != 16 {
			return fmt.Errorf("invalid responsible_pilot contact id")
		}
		rows = append(rows, base.BaseContactRole{
			BaseID:    baseID,
			ContactID: contactID,
			RoleCode:  base.BaseContactRoleResponsiblePilot,
			CreatedBy: actor,
			UpdatedBy: actor,
		})
	}
	if len(rows) == 0 {
		return nil
	}
	return tx.Create(&rows).Error
}

func (r *BaseRepository) replaceBaseOperationalShiftTimesTx(ctx context.Context, tx *gorm.DB, baseID []byte, items []base.BaseOperationalShiftTime) error {
	if len(baseID) != 16 {
		return errors.New("invalid base id")
	}
	if items == nil {
		return nil
	}
	actor := actorUserIDFromContext(ctx)
	if err := tx.Where("base_id = ?", baseID).Delete(&base.BaseOperationalShiftTime{}).Error; err != nil {
		return err
	}
	if len(items) == 0 {
		return nil
	}
	now := time.Now().UTC()
	for i := range items {
		row := items[i]
		if row.DateStart == nil || row.DateEnd == nil || row.DateStart.IsZero() || row.DateEnd.IsZero() {
			return errors.New("invalid operational shift date range")
		}
		if row.DateEnd.Before(*row.DateStart) {
			return errors.New("operational shift date_end must be >= date_start")
		}
		if len(row.ID) == 0 {
			row.ID = uuidv7.MustBytes()
		}
		dateStart := row.DateStart.UTC().Format("2006-01-02")
		dateEnd := row.DateEnd.UTC().Format("2006-01-02")
		var createdBy any
		var updatedBy any
		if len(actor) > 0 {
			createdBy = binaryValue{data: actor}
			updatedBy = binaryValue{data: actor}
		}
		shiftStart := shiftTimeValue(row.ShiftStart)
		shiftEnd := shiftTimeValue(row.ShiftEnd)
		if err := tx.Exec(
			`INSERT INTO base_operational_shift_times
			(id, base_id, date_start, date_end, start_time_type, end_time_type, shift_start, shift_end, created_at, created_by, updated_at, updated_by, deleted_at, deleted_by)
			VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
			binaryValue{data: row.ID},
			binaryValue{data: baseID},
			dateStart,
			dateEnd,
			row.StartTimeType,
			row.EndTimeType,
			shiftStart,
			shiftEnd,
			now,
			createdBy,
			now,
			updatedBy,
			nil,
			nil,
		).Error; err != nil {
			return err
		}
	}
	return nil
}

func shiftTimeValue(raw string) any {
	clock, ok := extractClockValue(raw)
	if !ok {
		return nil
	}
	return clock
}

func baseClockStorageValue(raw string) string {
	clock, ok := extractClockValue(raw)
	if !ok {
		return strings.TrimSpace(raw)
	}
	return "2000-01-01 " + clock
}

func normalizeBaseShiftTimeType(raw string) string {
	switch strings.ToUpper(strings.TrimSpace(raw)) {
	case "", base.ShiftTimeTypeFixed:
		return base.ShiftTimeTypeFixed
	case base.ShiftTimeTypeBMCT:
		return base.ShiftTimeTypeBMCT
	case base.ShiftTimeTypeECET:
		return base.ShiftTimeTypeECET
	default:
		return strings.ToUpper(strings.TrimSpace(raw))
	}
}

func extractClockValue(raw string) (string, bool) {
	s := strings.TrimSpace(raw)
	if s == "" {
		return "", false
	}
	layouts := []string{
		"15:04:05",
		"15:04",
		"2006-01-02 15:04:05",
		"2006-01-02 15:04",
		time.RFC3339,
		time.RFC3339Nano,
	}
	for _, layout := range layouts {
		if parsed, err := time.Parse(layout, s); err == nil {
			return parsed.UTC().Format("15:04:05"), true
		}
	}
	return "", false
}

func ensureContactsExistTx(tx *gorm.DB, ids [][]byte) error {
	valid := make([][]byte, 0, len(ids))
	seen := map[string]struct{}{}
	for _, id := range ids {
		if len(id) != 16 {
			return errors.New("invalid contact id")
		}
		key := string(id)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		valid = append(valid, id)
	}
	if len(valid) == 0 {
		return nil
	}
	var count int64
	if err := tx.Table("users").Where("id IN ?", valid).Count(&count).Error; err != nil {
		return err
	}
	if count != int64(len(valid)) {
		return errors.New("one or more contact ids not found")
	}
	return nil
}

func (r *BaseRepository) loadBaseContactRoles(ctx context.Context, bases []*base.Base) error {
	if len(bases) == 0 {
		return nil
	}
	baseIDs := make([][]byte, 0, len(bases))
	index := make(map[string]*base.Base, len(bases))
	for _, b := range bases {
		if b == nil || len(b.ID) != 16 {
			continue
		}
		baseIDs = append(baseIDs, b.ID)
		index[string(b.ID)] = b
		b.HEMSEDCContactIDs = nil
		b.MedPaxContactIDs = nil
		b.ResponsiblePilotContactIDs = nil
		b.HEMSEDCs = nil
		b.MedPax = nil
		b.ResponsiblePilots = nil
	}
	if len(baseIDs) == 0 {
		return nil
	}
	var rels []base.BaseContactRole
	if err := r.db.WithContext(ctx).
		Where("base_id IN ?", baseIDs).
		Order("created_at ASC").
		Find(&rels).Error; err != nil {
		return err
	}
	for i := range rels {
		b := index[string(rels[i].BaseID)]
		if b == nil {
			continue
		}
		switch rels[i].RoleCode {
		case base.BaseContactRoleHEMSEDC:
			b.HEMSEDCContactIDs = append(b.HEMSEDCContactIDs, append([]byte(nil), rels[i].ContactID...))
		case base.BaseContactRoleMedPax:
			b.MedPaxContactIDs = append(b.MedPaxContactIDs, append([]byte(nil), rels[i].ContactID...))
		case base.BaseContactRoleResponsiblePilot:
			b.ResponsiblePilotContactIDs = append(b.ResponsiblePilotContactIDs, append([]byte(nil), rels[i].ContactID...))
		}
	}
	var hemsEDCContactIDs [][]byte
	var medPaxContactIDs [][]byte
	var pilotContactIDs [][]byte
	for _, b := range bases {
		for i := range b.HEMSEDCContactIDs {
			hemsEDCContactIDs = append(hemsEDCContactIDs, b.HEMSEDCContactIDs[i])
		}
		for i := range b.MedPaxContactIDs {
			medPaxContactIDs = append(medPaxContactIDs, b.MedPaxContactIDs[i])
		}
		for i := range b.ResponsiblePilotContactIDs {
			pilotContactIDs = append(pilotContactIDs, b.ResponsiblePilotContactIDs[i])
		}
	}
	allContactIDs := append(append(hemsEDCContactIDs, medPaxContactIDs...), pilotContactIDs...)
	contactMap, err := r.contactNameMap(ctx, allContactIDs)
	if err != nil {
		return err
	}
	for _, b := range bases {
		for i := range b.HEMSEDCContactIDs {
			id := b.HEMSEDCContactIDs[i]
			p := contactMap[string(id)]
			p.ContactID = append([]byte(nil), id...)
			b.HEMSEDCs = append(b.HEMSEDCs, p)
		}
		for i := range b.MedPaxContactIDs {
			id := b.MedPaxContactIDs[i]
			p := contactMap[string(id)]
			p.ContactID = append([]byte(nil), id...)
			b.MedPax = append(b.MedPax, p)
		}
		for i := range b.ResponsiblePilotContactIDs {
			id := b.ResponsiblePilotContactIDs[i]
			p := contactMap[string(id)]
			p.ContactID = append([]byte(nil), id...)
			b.ResponsiblePilots = append(b.ResponsiblePilots, p)
		}
	}
	return nil
}

func (r *BaseRepository) contactNameMap(ctx context.Context, contactIDs [][]byte) (map[string]base.BaseContactPerson, error) {
	uniq := make([][]byte, 0, len(contactIDs))
	seen := map[string]struct{}{}
	for _, id := range contactIDs {
		if len(id) != 16 {
			continue
		}
		key := string(id)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		uniq = append(uniq, id)
	}
	if len(uniq) == 0 {
		return map[string]base.BaseContactPerson{}, nil
	}
	var rows []struct {
		ID        []byte `gorm:"column:id"`
		FirstName string `gorm:"column:first_name"`
		LastName  string `gorm:"column:last_name"`
	}
	if err := r.db.WithContext(ctx).
		Table("users").
		Select("id, first_name, last_name").
		Where("id IN ?", uniq).
		Find(&rows).Error; err != nil {
		return nil, err
	}
	out := make(map[string]base.BaseContactPerson, len(rows))
	for i := range rows {
		out[string(rows[i].ID)] = base.BaseContactPerson{
			FirstName: rows[i].FirstName,
			LastName:  rows[i].LastName,
		}
	}
	return out, nil
}

func copyIDs(src [][]byte) [][]byte {
	out := make([][]byte, 0, len(src))
	for _, id := range src {
		out = append(out, id)
	}
	return out
}

func (r *BaseRepository) categoryIDByType(ctx context.Context, categoryType string) ([]byte, error) {
	normalized, ok := base.NormalizeCategoryType(categoryType)
	if !ok {
		return nil, errors.New("invalid base category type")
	}
	return r.categoryIDByKey(ctx, normalized)
}

func (r *BaseRepository) categoryIDByKey(ctx context.Context, categoryKey string) ([]byte, error) {
	var row struct {
		ID []byte `gorm:"column:id"`
	}
	if err := r.db.WithContext(ctx).
		Table("base_categories").
		Select("id").
		Where("`key` = ?", strings.ToLower(strings.TrimSpace(categoryKey))).
		Take(&row).Error; err != nil {
		return nil, err
	}
	return append([]byte(nil), row.ID...), nil
}
