package litigationplanner

import (
	"fmt"
	"sort"

	"github.com/google/uuid"
)

// SortDeadlinesByDurationWithinTriggerGroup is the public form of
// sortDeadlinesByDurationWithinTriggerGroup. Exported so paliad's
// test suite (which historically reached the helper directly) can
// keep invoking it via a tiny wrapper.
func SortDeadlinesByDurationWithinTriggerGroup(
	deadlines []TimelineEntry,
	ruleByID map[uuid.UUID]Rule,
) {
	sortDeadlinesByDurationWithinTriggerGroup(deadlines, ruleByID)
}

// sortDeadlinesByDurationWithinTriggerGroup walks consecutive runs of
// deadlines whose underlying rule shares the same trigger group
// (parent_id + trigger_event_id) and reorders each run in place by
// duration ascending. Different trigger groups keep their original
// proceeding-sequence position — the walk only ever permutes adjacent
// same-group rows.
//
// Sort key (within a run):
//  1. Conditional / court-set rows (no concrete date in the duration
//     ladder) sort LAST, tiebroken by submission_code.
//  2. duration_unit weight ASC: days/working_days < weeks < months < years
//  3. duration_value ASC
//  4. submission_code ASC (deterministic tiebreak)
//
// Issue: m/paliad#128 — post-decision optional events (R.151/R.353
// 1-month before R.118.4/R.220.1 2-month) were rendering in catalog
// order instead of likely-sequence order. (t-paliad-296)
func sortDeadlinesByDurationWithinTriggerGroup(
	deadlines []TimelineEntry,
	ruleByID map[uuid.UUID]Rule,
) {
	if len(deadlines) < 2 {
		return
	}
	n := len(deadlines)
	i := 0
	for i < n {
		gid := triggerGroupKey(deadlines[i], ruleByID)
		j := i + 1
		for j < n && triggerGroupKey(deadlines[j], ruleByID) == gid {
			j++
		}
		// Root rules (no parent and no trigger_event) get gid="" and
		// would otherwise collapse into one big run. Skip the sort for
		// the "root" pseudo-group — each root rule represents its own
		// anchor (SoC, oral hearing, decision …) and the proceeding-
		// sequence order between them must be preserved.
		if j-i > 1 && gid != "" {
			chunk := deadlines[i:j]
			sort.SliceStable(chunk, func(a, b int) bool {
				return durationLessForSort(chunk[a], chunk[b], ruleByID)
			})
		}
		i = j
	}
}

// triggerGroupKey returns a string key identifying which trigger group
// a deadline belongs to. Same key = same group = candidates for sort.
// Empty string means "root" (no parent, no trigger_event) — used as a
// sentinel by the caller to skip sorting roots against each other.
func triggerGroupKey(d TimelineEntry, ruleByID map[uuid.UUID]Rule) string {
	rid, err := uuid.Parse(d.RuleID)
	if err != nil {
		return ""
	}
	r, ok := ruleByID[rid]
	if !ok {
		return ""
	}
	if r.ParentID != nil {
		return "p:" + r.ParentID.String()
	}
	if r.TriggerEventID != nil {
		return fmt.Sprintf("t:%d", *r.TriggerEventID)
	}
	return ""
}

// durationLessForSort compares two deadlines for the duration-ascending
// sort. Court-set / conditional rows (no concrete date) sort LAST
// regardless of duration — they don't fit the duration ladder.
func durationLessForSort(
	a, b TimelineEntry,
	ruleByID map[uuid.UUID]Rule,
) bool {
	aLast := a.IsCourtSet || a.IsConditional
	bLast := b.IsCourtSet || b.IsConditional
	if aLast != bLast {
		return !aLast
	}
	if aLast && bLast {
		return a.Code < b.Code
	}

	ra := lookupRuleFromDeadline(a, ruleByID)
	rb := lookupRuleFromDeadline(b, ruleByID)

	wa := durationUnitWeight(ra.DurationUnit)
	wb := durationUnitWeight(rb.DurationUnit)
	if wa != wb {
		return wa < wb
	}
	if ra.DurationValue != rb.DurationValue {
		return ra.DurationValue < rb.DurationValue
	}
	return a.Code < b.Code
}

func lookupRuleFromDeadline(
	d TimelineEntry,
	ruleByID map[uuid.UUID]Rule,
) Rule {
	if d.RuleID == "" {
		return Rule{}
	}
	rid, err := uuid.Parse(d.RuleID)
	if err != nil {
		return Rule{}
	}
	return ruleByID[rid]
}

// durationUnitWeight maps a duration unit to its sort weight so the
// trigger-group sort can order shorter durations first. days and
// working_days share weight 0 (both are sub-week granularities);
// unknown units sort to the end so they're visible as a tail rather
// than silently winning.
func durationUnitWeight(unit string) int {
	switch unit {
	case "days", "working_days":
		return 0
	case "weeks":
		return 1
	case "months":
		return 2
	case "years":
		return 3
	}
	return 4
}
