package service

import (
	"gorm.io/gorm"
	"gorm.io/gorm/clause"

	"github.com/tidbcloud/aisupport/services/oncall/models"
)

// historyRetentionSeconds bounds how far back edit-time recording reaches. Older
// history is rarely queried and, absent edits over it, recomputes correctly.
const historyRetentionSeconds = int64(365 * 24 * 3600)

func maxLayerPeriod(layers []models.TeamLayer) int64 {
	m := int64(0)
	for _, l := range layers {
		if l.RotateEverySeconds > m {
			m = l.RotateEverySeconds
		}
	}
	if m <= 0 {
		m = 7 * 24 * 3600
	}
	return m
}

// teamNextBoundary returns the end of the on-call segment containing `at`
// (the next handover).
func (s *ScheduleService) teamNextBoundary(teamID uint, layers []models.TeamLayer, at int64) int64 {
	horizon := maxLayerPeriod(layers) + 25*3600
	tl := s.refTimelinesFor(layers, at, at+horizon)
	segs := computeWindowFromLayers(teamID, layers, at, at+horizon, tl)
	if len(segs) > 0 && segs[0].StartTime <= at && segs[0].EndTime > at {
		return segs[0].EndTime
	}
	return at
}

// computeBackupWindow derives the backup lane over [from, to): backup at any
// instant = the primary on-call of the immediately preceding shift. No-oncall
// gaps (closed windows, placeholder slots) carry no backup of their own and are
// skipped when looking back, so the lane resumes with the last staffed shift.
// refTL, when non-nil, is a prefetched source-team-id -> timeline map that must
// already cover [from-buffer, to]; callers that don't have one pass nil and it is
// resolved here (used by single-team paths; the batched overview passes its own).
func (s *ScheduleService) computeBackupWindow(teamID uint, layers []models.TeamLayer, from, to int64, refTL map[uint][]refRow) []ComputedSegment {
	if to <= from || len(layers) == 0 {
		return nil
	}
	buffer := maxLayerPeriod(layers) + 25*3600
	tl := refTL
	if tl == nil {
		tl = s.refTimelinesFor(layers, from-buffer, to)
	}
	primary := computeWindowFromLayers(teamID, layers, from-buffer, to, tl)
	out := make([]ComputedSegment, 0)
	for i := 1; i < len(primary); i++ {
		st := primary[i].StartTime
		if st < from {
			st = from
		}
		en := primary[i].EndTime
		if en > to {
			en = to
		}
		if en <= st {
			continue
		}
		emails := primary[i-1].Emails
		if len(emails) == 0 {
			continue
		}
		if len(out) > 0 {
			last := &out[len(out)-1]
			if last.EndTime == st && emailsEqual(last.Emails, emails) {
				last.EndTime = en
				continue
			}
		}
		out = append(out, ComputedSegment{TeamID: teamID, StartTime: st, EndTime: en, Emails: emails})
	}
	return out
}

func realizedRowsFromSegments(segs []ComputedSegment, isBackup bool, frozenAt int64) []models.RealizedShift {
	rows := make([]models.RealizedShift, 0)
	for _, seg := range segs {
		for _, email := range seg.Emails {
			rows = append(rows, models.RealizedShift{
				TeamID:        seg.TeamID,
				StartTime:     seg.StartTime,
				EndTime:       seg.EndTime,
				EngineerEmail: email,
				IsBackup:      isBackup,
				FrozenAt:      frozenAt,
			})
		}
	}
	return rows
}

// recordTeamHistoryTx persists the as-yet-unrecorded past baseline (and
// backup, if enabled) for a team, up to `now`, so that a subsequent rule edit
// in the same transaction cannot rewrite the past. New layers (e.g. overlays)
// take effect immediately rather than being deferred to the current shift's
// boundary. Idempotent.
func (s *ScheduleService) recordTeamHistoryTx(tx *gorm.DB, teamID uint, now int64) error {
	var cat models.Team
	if err := tx.First(&cat, teamID).Error; err != nil {
		return err
	}
	var layers []models.TeamLayer
	if err := tx.Where("team_id = ?", teamID).Order("priority desc, id asc").Find(&layers).Error; err != nil {
		return err
	}
	if len(layers) == 0 {
		return nil
	}

	var lastEnd int64
	if err := tx.Model(&models.RealizedShift{}).
		Where("team_id = ? AND is_backup = ?", teamID, false).
		Select("COALESCE(MAX(end_time), 0)").Scan(&lastEnd).Error; err != nil {
		return err
	}
	from := lastEnd
	if floor := now - historyRetentionSeconds; from < floor {
		from = floor
	}
	to := now
	if to <= from {
		return nil
	}

	tl := s.refTimelinesFor(layers, from, to)
	rows := realizedRowsFromSegments(computeWindowFromLayers(teamID, layers, from, to, tl), false, now)
	if cat.BackupEnabled {
		rows = append(rows, realizedRowsFromSegments(s.computeBackupWindow(teamID, layers, from, to, nil), true, now)...)
	}
	if len(rows) == 0 {
		return nil
	}
	return tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&rows).Error
}
