package upc

import (
	"time"

	lp "mgit.msbls.de/m/paliad/pkg/litigationplanner"
)

// SnapshotHoliday is the embedded holiday row shape. Mirrors
// paliad.holidays + the generator's output. Country and Regime are
// optional pointers — at least one of them is non-empty on every
// row (matches paliad's CHECK).
type SnapshotHoliday struct {
	Date        string  `json:"date"` // YYYY-MM-DD
	Name        string  `json:"name"`
	Country     *string `json:"country,omitempty"`
	Regime      *string `json:"regime,omitempty"`
	State       *string `json:"state,omitempty"`
	HolidayType string  `json:"holiday_type"`
}

func (h SnapshotHoliday) appliesTo(country, regime string) bool {
	if h.Country != nil && country != "" && *h.Country == country {
		return true
	}
	if h.Regime != nil && regime != "" && *h.Regime == regime {
		return true
	}
	return false
}

func (h SnapshotHoliday) isVacation() bool { return h.HolidayType == "vacation" }

// isClosure accepts both "public_holiday" and "closure" so the
// embedded calendar matches paliad's HolidayService.IsClosure
// reconciliation (internal/services/holidays.go ~L132). Live DB rows
// use "public_holiday"; "closure" is kept as a legacy synonym so old
// hand-crafted snapshots still parse correctly.
func (h SnapshotHoliday) isClosure() bool {
	return h.HolidayType == "public_holiday" || h.HolidayType == "closure"
}

// SnapshotHolidayCalendar serves HolidayCalendar against the embedded
// holiday slice. The semantics mirror paliad's HolidayService:
//
//   - IsNonWorkingDay = weekend OR a closure row matching the
//     (country, regime) pair. "Vacation" rows are informational only
//     and do not block — see t-paliad-121 / IsNonWorkingDay godoc.
//   - AdjustForNonWorkingDays = walk forward day-by-day until
//     IsNonWorkingDay returns false (bounded at 60 iters)
//   - AdjustForNonWorkingDaysBackward = same but stepping -1 day
//   - AdjustForNonWorkingDaysWithReason = forward walk + structured
//     reason payload (vacation > public_holiday > weekend) — vacation
//     kind fires only when a vacation row overlaps a weekend or
//     closure that is doing the rolling.
type SnapshotHolidayCalendar struct {
	byDate map[string][]SnapshotHoliday // keyed by YYYY-MM-DD
}

// NewHolidayCalendar parses the embedded holidays.json and returns a
// ready-to-use calendar.
func NewHolidayCalendar() (*SnapshotHolidayCalendar, error) {
	var holidays []SnapshotHoliday
	if err := readJSON("holidays.json", &holidays); err != nil {
		return nil, err
	}
	cal := &SnapshotHolidayCalendar{byDate: make(map[string][]SnapshotHoliday, len(holidays))}
	for _, h := range holidays {
		cal.byDate[h.Date] = append(cal.byDate[h.Date], h)
	}
	return cal, nil
}

// IsNonWorkingDay returns true on weekends or closure-type holidays
// applicable to the given (country, regime).
//
// "Vacation" entries (today: UPC summer + winter judicial vacations
// per the UPC AC decision on judicial vacations 2023-05-26) are
// deliberately excluded — the Court continues to operate during them
// and they do not extend procedural deadlines (RoP / AC decision-on-
// judicial-vacation). They stay in holidays.json as informational
// metadata so callers can still surface "this date overlaps with UPC
// vacation" if they want. Mirrors HolidayService.IsNonWorkingDay in
// internal/services — see t-paliad-121 for the policy decision and
// t-paliad-332 for the snapshot-side alignment.
func (c *SnapshotHolidayCalendar) IsNonWorkingDay(date time.Time, country, regime string) bool {
	if wd := date.Weekday(); wd == time.Saturday || wd == time.Sunday {
		return true
	}
	key := date.Format("2006-01-02")
	for _, h := range c.byDate[key] {
		if !h.appliesTo(country, regime) {
			continue
		}
		if h.isClosure() {
			return true
		}
	}
	return false
}

func (c *SnapshotHolidayCalendar) holidayMatch(date time.Time, country, regime string) *SnapshotHoliday {
	key := date.Format("2006-01-02")
	for _, h := range c.byDate[key] {
		if !h.appliesTo(country, regime) {
			continue
		}
		hh := h
		return &hh
	}
	return nil
}

// AdjustForNonWorkingDays walks forward until the date lands on a
// working day. Bound = 60 iters (same as paliad — generous safety
// margin past any vacation run).
func (c *SnapshotHolidayCalendar) AdjustForNonWorkingDays(date time.Time, country, regime string) (adjusted, original time.Time, wasAdjusted bool) {
	original = date
	adjusted = date
	for i := 0; i < 60 && c.IsNonWorkingDay(adjusted, country, regime); i++ {
		adjusted = adjusted.AddDate(0, 0, 1)
		wasAdjusted = true
	}
	return adjusted, original, wasAdjusted
}

// AdjustForNonWorkingDaysBackward walks backward until the date lands
// on a working day. Same bound.
func (c *SnapshotHolidayCalendar) AdjustForNonWorkingDaysBackward(date time.Time, country, regime string) (adjusted, original time.Time, wasAdjusted bool) {
	original = date
	adjusted = date
	for i := 0; i < 60 && c.IsNonWorkingDay(adjusted, country, regime); i++ {
		adjusted = adjusted.AddDate(0, 0, -1)
		wasAdjusted = true
	}
	return adjusted, original, wasAdjusted
}

// AdjustForNonWorkingDaysWithReason is the structured-explanation
// counterpart to AdjustForNonWorkingDays. Reason kind precedence
// (longest cause wins): vacation > public_holiday > weekend. Reason
// is nil when wasAdjusted is false.
func (c *SnapshotHolidayCalendar) AdjustForNonWorkingDaysWithReason(date time.Time, country, regime string) (adjusted, original time.Time, wasAdjusted bool, reason *lp.AdjustmentReason) {
	original = date
	adjusted = date

	var holidaysHit []lp.HolidayDTO
	seen := map[string]bool{}
	var sawWeekend, sawVacation, sawPublicHoliday bool
	var vacationName string

	for i := 0; i < 60 && c.IsNonWorkingDay(adjusted, country, regime); i++ {
		if wd := adjusted.Weekday(); wd == time.Saturday || wd == time.Sunday {
			sawWeekend = true
		}
		if h := c.holidayMatch(adjusted, country, regime); h != nil {
			if h.isVacation() {
				sawVacation = true
				if vacationName == "" {
					vacationName = h.Name
				}
			} else if h.isClosure() {
				sawPublicHoliday = true
			}
			key := h.Date + "|" + h.Name
			if !seen[key] {
				holidaysHit = append(holidaysHit, lp.HolidayDTO{
					Date:       h.Date,
					Name:       h.Name,
					IsVacation: h.isVacation(),
					IsClosure:  h.isClosure(),
				})
				seen[key] = true
			}
		}
		adjusted = adjusted.AddDate(0, 0, 1)
		wasAdjusted = true
	}
	if !wasAdjusted {
		return adjusted, original, false, nil
	}
	r := &lp.AdjustmentReason{Holidays: holidaysHit}
	switch {
	case sawVacation:
		r.Kind = "vacation"
		r.VacationName = vacationName
		if vs, ve, ok := c.findVacationBlock(original, country, regime); ok {
			r.VacationStart = vs.Format("2006-01-02")
			r.VacationEnd = ve.Format("2006-01-02")
		}
	case sawPublicHoliday:
		r.Kind = "public_holiday"
	default:
		r.Kind = "weekend"
	}
	if sawWeekend && r.Kind == "weekend" {
		r.OriginalWeekday = original.Weekday().String()
	}
	return adjusted, original, true, r
}

// findVacationBlock scans outward from date through non-working days
// to locate the first/last IsVacation entries. Weekends inside the
// run are traversed but don't extend the reported span — start/end
// are always real vacation entries.
func (c *SnapshotHolidayCalendar) findVacationBlock(date time.Time, country, regime string) (start, end time.Time, ok bool) {
	cur := date
	for i := 0; i < 60; i++ {
		if !c.IsNonWorkingDay(cur, country, regime) {
			break
		}
		if h := c.holidayMatch(cur, country, regime); h != nil && h.isVacation() {
			start = cur
			ok = true
			break
		}
		cur = cur.AddDate(0, 0, -1)
	}
	if !ok {
		return
	}
	cur = date
	for i := 0; i < 60; i++ {
		if !c.IsNonWorkingDay(cur, country, regime) {
			break
		}
		if h := c.holidayMatch(cur, country, regime); h != nil && h.isVacation() {
			end = cur
		}
		cur = cur.AddDate(0, 0, 1)
	}
	return start, end, true
}

// Compile-time assertion that SnapshotHolidayCalendar satisfies
// lp.HolidayCalendar.
var _ lp.HolidayCalendar = (*SnapshotHolidayCalendar)(nil)
