package service

import (
	"errors"
	"sort"

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

// effCandidate is one (engineer, interval) candidate competing in the overlay
// stack: baseline (overlay 0, from history or live computation) and overrides
// (overlay > 0, from ShiftInterval).
type effCandidate struct {
	start, end int64
	email      string
	engID      uint
	engName    string
	enName     string
	overlay    int
	source     string
}

// GetEffectiveScheduleV2 resolves the effective schedule for [from, to):
//   - baseline: recorded history for the past, live layer computation for the
//     rest; overrides (ShiftInterval overlay>0) stacked on top, highest wins.
//
// includeEmpty additionally emits engineer_id=0 rows for periods with no one
// on call (manual clears keep their overlay; natural gaps are "no-oncall").
//
// Output rows match GetEffectiveOverlayScheduleMulti's shape.
func (s *ScheduleService) GetEffectiveScheduleV2(teamIDs []uint, from, to int64, isBackup bool, includeEmpty bool) ([]map[string]interface{}, error) {
	if from <= 0 {
		return nil, errors.New("invalid range")
	}
	if to <= from {
		to = from + 1
	}

	uniq := make(map[uint]struct{}, len(teamIDs))
	ids := make([]uint, 0, len(teamIDs))
	for _, id := range teamIDs {
		if id == 0 {
			continue
		}
		if _, ok := uniq[id]; ok {
			continue
		}
		uniq[id] = struct{}{}
		ids = append(ids, id)
	}
	if len(ids) == 0 {
		return nil, errors.New("missing team_ids")
	}
	sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })

	// Batch all per-team reads into a constant number of queries (avoid N+1
	// across teams on the overview).
	var cats []models.Team
	if err := db.DB.Where("id IN ?", ids).Find(&cats).Error; err != nil {
		return nil, err
	}
	catByID := make(map[uint]models.Team, len(cats))
	for _, c := range cats {
		catByID[c.ID] = c
	}

	var allLayers []models.TeamLayer
	if err := db.DB.Where("team_id IN ?", ids).Order("priority desc, id asc").Find(&allLayers).Error; err != nil {
		return nil, err
	}
	layersByCat := make(map[uint][]models.TeamLayer)
	for _, l := range allLayers {
		layersByCat[l.TeamID] = append(layersByCat[l.TeamID], l)
	}

	var recorded []models.RealizedShift
	if err := db.DB.Where("team_id IN ? AND is_backup = ? AND end_time > ? AND start_time < ?",
		ids, isBackup, from, to).Order("start_time asc").Find(&recorded).Error; err != nil {
		return nil, err
	}
	recByCat := make(map[uint][]models.RealizedShift)
	for _, r := range recorded {
		recByCat[r.TeamID] = append(recByCat[r.TeamID], r)
	}

	var overrides []models.ShiftInterval
	if err := db.DB.Preload("Engineer").
		Where("team_id IN ? AND is_backup = ? AND overlay > 0 AND end_time > ? AND start_time < ?",
			ids, isBackup, from, to).Find(&overrides).Error; err != nil {
		return nil, err
	}
	ovByCat := make(map[uint][]models.ShiftInterval)
	for _, o := range overrides {
		ovByCat[o.TeamID] = append(ovByCat[o.TeamID], o)
	}

	var engs []models.Engineer
	if err := db.DB.Find(&engs).Error; err != nil {
		return nil, err
	}
	engByEmail := make(map[string]models.Engineer, len(engs))
	for _, e := range engs {
		engByEmail[normalizeEmail(e.Email)] = e
	}

	// Resolve every referenced team's primary timeline ONCE for the whole batch
	// (keyed by source team id, shared across all teams below) instead of calling
	// refTimelinesFor per team — which fanned out into O(#reference layers)
	// full GetEffectiveScheduleV2 calls, each re-scanning the engineers table.
	// The backup lane looks back by up to maxLayerPeriod+25h (see computeBackupWindow),
	// so widen the prefetch range on the backup pass to cover it.
	refFrom := from
	if isBackup {
		if back := from - (maxLayerPeriod(allLayers) + 25*3600); back < refFrom {
			refFrom = back
		}
		if refFrom < 1 {
			refFrom = 1
		}
	}
	refTL := s.batchRefTimelines(allLayers, refFrom, to)

	res := make([]map[string]interface{}, 0)
	for _, catID := range ids {
		cat, ok := catByID[catID]
		if !ok {
			continue
		}
		// One path for all teams: frozen baseline (history) + live materialization
		// + overrides. Reference layers are resolved read-through via refTL.
		res = append(res, s.buildTeamEffective(cat, layersByCat[catID], recByCat[catID], ovByCat[catID], engByEmail, refTL, from, to, isBackup, includeEmpty)...)
	}
	return res, nil
}

// buildTeamEffective resolves one team's effective rows. refTimelines holds the
// referenced teams' primary timelines (empty for people-only teams).
func (s *ScheduleService) buildTeamEffective(cat models.Team, layers []models.TeamLayer, recorded []models.RealizedShift, overrides []models.ShiftInterval, engByEmail map[string]models.Engineer, refTimelines map[uint][]refRow, from, to int64, isBackup bool, includeEmpty bool) []map[string]interface{} {
	catID := cat.ID
	liveBaseline := func(a, b int64) []ComputedSegment {
		if b <= a {
			return nil
		}
		if isBackup {
			if !cat.BackupEnabled {
				return nil
			}
			return s.computeBackupWindow(catID, layers, a, b, refTimelines)
		}
		return computeWindowFromLayers(catID, layers, a, b, refTimelines)
	}

	candidates := make([]effCandidate, 0)
	baselineEmails := map[string]struct{}{}
	addComputed := func(a, b int64) {
		for _, seg := range liveBaseline(a, b) {
			for _, email := range seg.Emails {
				candidates = append(candidates, effCandidate{start: seg.StartTime, end: seg.EndTime, email: email, overlay: 0, source: "layer"})
				baselineEmails[email] = struct{}{}
			}
		}
	}

	// recorded history baseline for this lane (preloaded, sorted by start)
	if len(recorded) > 0 {
		minStart, maxEnd := recorded[0].StartTime, recorded[0].EndTime
		for _, r := range recorded {
			if r.StartTime < minStart {
				minStart = r.StartTime
			}
			if r.EndTime > maxEnd {
				maxEnd = r.EndTime
			}
			st, en := r.StartTime, r.EndTime
			if st < from {
				st = from
			}
			if en > to {
				en = to
			}
			email := normalizeEmail(r.EngineerEmail)
			candidates = append(candidates, effCandidate{start: st, end: en, email: email, overlay: 0, source: "history"})
			baselineEmails[email] = struct{}{}
		}
		if minStart > from {
			addComputed(from, minStart)
		}
		if maxEnd < to {
			addComputed(maxEnd, to)
		}
	} else {
		addComputed(from, to)
	}

	// resolve baseline emails -> engineer id/name (engByEmail preloaded)
	_ = baselineEmails
	for i := range candidates {
		if candidates[i].overlay == 0 {
			if e, ok := engByEmail[candidates[i].email]; ok {
				candidates[i].engID = e.ID
				candidates[i].engName = e.Name
				candidates[i].enName = e.EnName
			}
		}
	}

	// overrides (overlay > 0) for this lane (preloaded)
	for _, o := range overrides {
		st, en := o.StartTime, o.EndTime
		if st < from {
			st = from
		}
		if en > to {
			en = to
		}
		candidates = append(candidates, effCandidate{
			start: st, end: en,
			email:   normalizeEmail(o.Engineer.Email),
			engID:   o.EngineerID,
			engName: o.Engineer.Name,
			enName:  o.Engineer.EnName,
			overlay: o.Overlay,
			source:  "manual-edit",
		})
	}

	return resolveCandidates(cat, candidates, from, to, isBackup, includeEmpty)
}

// resolveCandidates runs the cut-point + top-overlay resolution and emits rows.
func resolveCandidates(cat models.Team, candidates []effCandidate, from, to int64, isBackup bool, includeEmpty bool) []map[string]interface{} {
	cutset := map[int64]struct{}{from: {}, to: {}}
	for _, c := range candidates {
		if c.start > from && c.start < to {
			cutset[c.start] = struct{}{}
		}
		if c.end > from && c.end < to {
			cutset[c.end] = struct{}{}
		}
	}
	points := make([]int64, 0, len(cutset))
	for p := range cutset {
		points = append(points, p)
	}
	sort.Slice(points, func(i, j int) bool { return points[i] < points[j] })

	lane := "primary"
	if isBackup {
		lane = "backup"
	}

	out := make([]map[string]interface{}, 0)
	for i := 0; i+1 < len(points); i++ {
		a, b := points[i], points[i+1]
		if b <= a {
			continue
		}
		maxOv := -1
		var winners []effCandidate
		for _, c := range candidates {
			if !(c.start <= a && c.end > a) {
				continue
			}
			if c.engID == 0 && c.overlay == 0 && c.email == "" {
				// truly empty baseline → nothing on call
				continue
			}
			if c.overlay > maxOv {
				maxOv = c.overlay
				winners = winners[:0]
				winners = append(winners, c)
			} else if c.overlay == maxOv {
				winners = append(winners, c)
			}
		}
		sort.Slice(winners, func(i, j int) bool { return winners[i].email < winners[j].email })
		emitted := false
		for _, w := range winners {
			// A manual override (overlay > 0) with no engineer is an explicit
			// clear → emit no row. A baseline person without an engineer record
			// yet is still shown (by email) so Schedule matches the preview.
			if w.engID == 0 && w.overlay > 0 {
				continue
			}
			emitted = true
			name := w.engName
			if name == "" {
				name = w.email
			}
			if n := len(out); n > 0 {
				prev := out[n-1]
				if prev["engineer_email"].(string) == w.email && prev["overlay"].(int) == w.overlay &&
					prev["end_time"].(int64) == a {
					prev["end_time"] = b
					continue
				}
			}
			out = append(out, map[string]interface{}{
				"team_id":          cat.ID,
				"team_name":        cat.Name,
				"engineer_id":      w.engID,
				"engineer_name":    name,
				"engineer_en_name": w.enName,
				"engineer_email":   w.email,
				"start_time":       a,
				"end_time":         b,
				"overlay":          w.overlay,
				"is_backup":        isBackup,
				"lane":             lane,
				"source_type":      w.source,
				"effective":        true,
			})
		}
		if includeEmpty && !emitted {
			// No one on call in [a, b). Winners with engID==0 can only be manual
			// clears (overlay > 0; unassigned baselines are skipped above).
			ov, src := 0, "no-oncall"
			if maxOv > 0 {
				ov, src = maxOv, "manual-edit"
			}
			if n := len(out); n > 0 {
				prev := out[n-1]
				if prev["engineer_email"].(string) == "" && prev["overlay"].(int) == ov &&
					prev["end_time"].(int64) == a {
					prev["end_time"] = b
					continue
				}
			}
			out = append(out, map[string]interface{}{
				"team_id":        cat.ID,
				"team_name":      cat.Name,
				"engineer_id":    uint(0),
				"engineer_name":  "",
				"engineer_email": "",
				"start_time":     a,
				"end_time":       b,
				"overlay":        ov,
				"is_backup":      isBackup,
				"lane":           lane,
				"source_type":    src,
				"effective":      true,
			})
		}
	}
	return out
}
