package services

import (
	"time"

	"mgit.msbls.de/m/paliad/internal/models"
)

// CalculatedDeadline is one computed deadline (rule + due date + adjustment info).
type CalculatedDeadline struct {
	RuleCode        string `json:"rule_code"`
	RuleID          string `json:"rule_id"`
	Title           string `json:"title"`
	DueDate         string `json:"due_date"`          // YYYY-MM-DD, after holiday/weekend adjust
	OriginalDueDate string `json:"original_due_date"` // YYYY-MM-DD, before adjust
	WasAdjusted     bool   `json:"was_adjusted"`
}

// DeadlineCalculator turns rules into dates given a trigger event.
type DeadlineCalculator struct {
	holidays *HolidayService
}

// NewDeadlineCalculator wires the calculator to the holiday service.
func NewDeadlineCalculator(holidays *HolidayService) *DeadlineCalculator {
	return &DeadlineCalculator{holidays: holidays}
}

// CalculateEndDate applies a single rule's duration + timing to the event date,
// then bumps off non-working days for the given (country, regime). For
// rules with both a primary and an alt duration (alt_duration_value/_unit)
// and a combine_op of 'max' or 'min', both legs are computed independently
// and combined per the operator — this implements RoP R.198 / R.213
// ("31 days OR 20 working days, whichever is longer") and the equivalent
// shape under EPC. Returns (adjusted, original, didAdjust).
//
// Snap direction follows timing: 'after' snaps forward to the next
// working day (RoP R.300.b — period extends to the next working day),
// 'before' snaps *backward* to the preceding working day so the
// statutory cut-off is not pushed past its hard limit.
//
// duration_unit='working_days' walks day-by-day via the holiday service
// (skipping weekends + court holidays), so its result is always already a
// working day — no post-arithmetic snap needed for that leg.
//
// Per Tier 3 Primitives §10 of docs/research-deadlines-completeness-2026-05-25.md
// (m's 2026-05-25 15:29 steer: build the full primitives, no workarounds).
func (c *DeadlineCalculator) CalculateEndDate(eventDate time.Time, rule models.DeadlineRule, country, regime string) (time.Time, time.Time, bool) {
	timing := "after"
	if rule.Timing != nil {
		timing = *rule.Timing
	}

	adjusted, raw, wasAdjusted := c.computeLeg(eventDate, rule.DurationValue, rule.DurationUnit, timing, country, regime)

	// combine_op + alt_duration_*: compute the alt leg independently,
	// then pick the later (max) or earlier (min) of the two adjusted
	// end-dates. Live use case is UPC RoP R.198 / R.213 (31 calendar
	// days vs. 20 working days, whichever is longer).
	if rule.CombineOp != nil && rule.AltDurationValue != nil && rule.AltDurationUnit != nil {
		altAdj, altRaw, altWasAdj := c.computeLeg(eventDate, *rule.AltDurationValue, *rule.AltDurationUnit, timing, country, regime)
		switch *rule.CombineOp {
		case "max":
			if altAdj.After(adjusted) {
				adjusted, raw, wasAdjusted = altAdj, altRaw, altWasAdj
			}
		case "min":
			if altAdj.Before(adjusted) {
				adjusted, raw, wasAdjusted = altAdj, altRaw, altWasAdj
			}
		}
	}

	return adjusted, raw, wasAdjusted
}

// computeLeg evaluates a single (value, unit) duration against the event
// date in the given timing direction and snap-adjusts the result. Returns
// the snap-adjusted end-date, the pre-snap end-date, and whether a snap
// occurred. working_days arithmetic never needs a snap (the walker lands
// on a working day by construction).
func (c *DeadlineCalculator) computeLeg(eventDate time.Time, value int, unit string, timing string, country, regime string) (adjusted, raw time.Time, wasAdjusted bool) {
	sign := 1
	if timing == "before" {
		sign = -1
	}
	raw = c.addDuration(eventDate, value, unit, sign, country, regime)
	if unit == "working_days" {
		return raw, raw, false
	}
	if timing == "before" {
		return c.holidays.AdjustForNonWorkingDaysBackward(raw, country, regime)
	}
	return c.holidays.AdjustForNonWorkingDays(raw, country, regime)
}

// addDuration adds `sign * value` of the given unit to eventDate. For
// 'working_days' it walks day-by-day skipping weekends and court
// holidays via the holiday service.
func (c *DeadlineCalculator) addDuration(eventDate time.Time, value int, unit string, sign int, country, regime string) time.Time {
	switch unit {
	case "days":
		return eventDate.AddDate(0, 0, sign*value)
	case "weeks":
		return eventDate.AddDate(0, 0, sign*value*7)
	case "months":
		return eventDate.AddDate(0, sign*value, 0)
	case "working_days":
		return c.addWorkingDays(eventDate, sign*value, country, regime)
	}
	return eventDate
}

// addWorkingDays walks `n` business days from `date` (negative `n` walks
// backward). The event day itself is never counted; we step first, then
// skip past non-working days, repeated n times. Result is always a
// working day for the given (country, regime). Matches UPC RoP R.300.b's
// "the day on which the event happens shall not be counted" convention
// applied to the business-day axis.
//
// Bound: each business-day step is bounded by a 60-day inner cap so a
// misconfigured holiday table can never spin forever. The longest
// real-world non-working run between adjacent business days is the
// Christmas Eve → Neujahr window (~6 days), so 60 is over-provisioned.
func (c *DeadlineCalculator) addWorkingDays(date time.Time, n int, country, regime string) time.Time {
	if n == 0 {
		return date
	}
	step := 1
	count := n
	if n < 0 {
		step = -1
		count = -n
	}
	cur := date
	for i := 0; i < count; i++ {
		cur = cur.AddDate(0, 0, step)
		for j := 0; j < 60 && c.holidays.IsNonWorkingDay(cur, country, regime); j++ {
			cur = cur.AddDate(0, 0, step)
		}
	}
	return cur
}

// CalculateFromRules calculates deadlines for a slice of rules using the
// given (country, regime) for non-working-day adjustment. Rules with
// duration == 0 (court-set hearings, decisions) return the event date itself.
func (c *DeadlineCalculator) CalculateFromRules(eventDate time.Time, rules []models.DeadlineRule, country, regime string) []CalculatedDeadline {
	results := make([]CalculatedDeadline, 0, len(rules))
	for _, r := range rules {
		var adjusted, original time.Time
		var wasAdjusted bool

		if r.DurationValue > 0 {
			adjusted, original, wasAdjusted = c.CalculateEndDate(eventDate, r, country, regime)
		} else {
			adjusted, original = eventDate, eventDate
		}

		code := ""
		if r.SubmissionCode != nil {
			code = *r.SubmissionCode
		}

		results = append(results, CalculatedDeadline{
			RuleCode:        code,
			RuleID:          r.ID.String(),
			Title:           r.Name,
			DueDate:         adjusted.Format("2006-01-02"),
			OriginalDueDate: original.Format("2006-01-02"),
			WasAdjusted:     wasAdjusted,
		})
	}
	return results
}
