// Port of web/kit/Calendar.tsx. Date math uses the stdlib time package.
package webui

import (
	"strconv"
	"time"

	"kjol/vdom"
)

// Day-of-week and month labels (module-level in the TSX). Prefixed to stay
// private to the Calendar/DatePicker files that share them.
var calDays = []string{"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"}

var calMonths = []string{
	"January", "February", "March", "April", "May", "June",
	"July", "August", "September", "October", "November", "December",
}

var calMonthsShort = []string{
	"Jan", "Feb", "Mar", "Apr", "May", "Jun",
	"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
}

// -- Shared Tailwind class constants (also used by datepicker.go). These are
// exported from Calendar.tsx; here they stay unexported with a cal prefix so the
// two ported files can share them without widening the package's public API. --
const calPickerRoot = "p-2 min-w-[240px]"
const calMonthRoot = "p-0 min-w-0 w-full bg-surface border border-line rounded-default shadow-sm overflow-hidden"

const calHeaderPicker = "flex items-center justify-between mb-2 gap-1"
const calHeaderMonth = "flex items-center justify-between gap-1 py-3 px-4 border-b border-line bg-surface-muted"

const calNavBtn = "bg-transparent border-0 p-1 cursor-pointer text-text-muted rounded-sm flex items-center justify-center hover:bg-surface-raised hover:text-text-body"

const calMyPicker = "text-sm font-semibold text-text-heading mx-3 whitespace-nowrap"
const calMyMonth = "text-lg font-heading mx-4 flex-1 text-center font-semibold text-text-heading whitespace-nowrap"

const calWeekdaysPicker = "grid grid-cols-7 gap-[2px] mb-1"
const calWeekdaysMonth = "grid grid-cols-7 border-b border-line"
const calWeekdayPicker = "text-center text-xs font-semibold text-text-muted p-1"
const calWeekdayMonth = "text-center text-xs font-semibold text-text-muted p-2 uppercase tracking-wider"

const calDaysPicker = "grid grid-cols-7 gap-[2px]"
const calDaysMonth = "grid grid-cols-7"

const calDayPickerBase = "aspect-square flex items-center justify-center text-sm bg-transparent border-0 rounded-sm cursor-pointer text-text-body p-0"
const calDayMonthBase = "min-h-[6.5rem] flex flex-col items-stretch justify-start p-1.5 border-r border-b border-line text-left gap-1 text-xs bg-transparent cursor-pointer"

const calSelect = "flex-1 py-1 px-2 text-sm font-semibold border border-line rounded-sm bg-surface text-text-heading cursor-pointer focus:outline-hidden focus:border-primary"

// Calendar variants.
const (
	CalendarVariantPicker = "picker"
	CalendarVariantMonth  = "month"
)

// calDaysInMonth returns the number of days in month (day 0 of the next month).
func calDaysInMonth(year int, month time.Month) int {
	return time.Date(year, month+1, 0, 0, 0, 0, 0, time.UTC).Day()
}

// calFirstWeekday returns the weekday of the 1st (Sunday=0 .. Saturday=6),
// matching JS getDay().
func calFirstWeekday(year int, month time.Month) int {
	return int(time.Date(year, month, 1, 0, 0, 0, 0, time.UTC).Weekday())
}

// calDateKey formats a date as the "YYYY-MM-DD" key the callbacks emit.
func calDateKey(t time.Time) string { return t.Format("2006-01-02") }

// calParseKey strictly parses a "YYYY-MM-DD" key.
//
// NOTE: the TSX passes selected/viewMonth through `new Date(...)`, which accepts
// many free-form strings; the Go API is documented as "YYYY-MM-DD" so parsing is
// narrowed to that layout.
func calParseKey(s string) (time.Time, bool) {
	if s == "" {
		return time.Time{}, false
	}
	t, err := time.Parse("2006-01-02", s)
	if err != nil {
		return time.Time{}, false
	}
	return t, true
}

func calSameDay(a, b time.Time) bool {
	return a.Year() == b.Year() && a.Month() == b.Month() && a.Day() == b.Day()
}

// calCell is one grid cell: a day, or an empty leading blank.
type calCell struct {
	date  time.Time
	empty bool
}

// calMonthCells builds the padded month grid (leading blanks + each day).
func calMonthCells(year int, month time.Month) []calCell {
	n := calDaysInMonth(year, month)
	first := calFirstWeekday(year, month)
	cells := make([]calCell, 0, first+n)
	for i := 0; i < first; i++ {
		cells = append(cells, calCell{empty: true})
	}
	for d := 1; d <= n; d++ {
		cells = append(cells, calCell{date: time.Date(year, month, d, 0, 0, 0, 0, time.UTC)})
	}
	return cells
}

// calWeekdaysRow renders the Su..Sa header row for the given grid/cell classes.
func calWeekdaysRow(rowCls, cellCls string) *vdom.VNode {
	mods := []vdom.Mod{vdom.Attr("class", rowCls)}
	for _, d := range calDays {
		mods = append(mods, vdom.Div(vdom.Attr("class", cellCls), vdom.Text(d)))
	}
	return vdom.Div(mods...)
}

func calDayClassPicker(empty, selected, today bool) string {
	c := calDayPickerBase
	if empty {
		c = cx(c, "cursor-default")
	} else {
		c = cx(c, "hover:bg-surface-raised")
	}
	if today {
		c = cx(c, "font-bold text-accent")
	}
	if selected {
		c = cx(c, "!bg-primary !text-white")
	}
	return c
}

// calDayClassMonth mirrors the month-variant class logic. idx%7==6 (last column)
// drops the right border since the Tailwind compiler can't do [&:nth-child(7n)].
func calDayClassMonth(idx int, empty, selected bool) string {
	c := calDayMonthBase
	if idx%7 == 6 {
		c = cx(c, "border-r-0")
	}
	if empty {
		c = cx(c, "bg-surface-muted cursor-default")
	} else {
		c = cx(c, "hover:bg-surface-muted")
	}
	if selected {
		c = cx(c, "bg-primary/10 text-text-body")
	}
	return c
}

func calDayNumberClassMonth(today bool) string {
	if today {
		return "bg-primary text-white rounded-full w-6 h-6 inline-flex items-center justify-center p-0 self-end text-sm font-semibold"
	}
	return "text-sm font-semibold text-text-muted self-end px-1 py-0.5"
}

// calPickerDaysGrid renders the picker-variant day grid. renderDay is optional.
// Shared by Calendar (picker) and the DatePicker date-of-birth calendar.
func calPickerDaysGrid(view, sel time.Time, hasSel bool, now time.Time, onSelect func(string), renderDay func(string, time.Time) *vdom.VNode) *vdom.VNode {
	cells := calMonthCells(view.Year(), view.Month())
	mods := []vdom.Mod{vdom.Attr("class", calDaysPicker)}
	for _, cell := range cells {
		selected := hasSel && !cell.empty && calSameDay(cell.date, sel)
		today := !cell.empty && calSameDay(cell.date, now)
		btnMods := []vdom.Mod{
			vdom.Attr("type", "button"),
			vdom.Attr("class", calDayClassPicker(cell.empty, selected, today)),
		}
		if cell.empty {
			btnMods = append(btnMods, vdom.Attr("disabled", "disabled"))
		} else {
			d := cell.date
			btnMods = append(btnMods, vdom.On(vdom.EVENT_CLICK, func() {
				if onSelect != nil {
					onSelect(calDateKey(d))
				}
			}))
		}
		num := ""
		if !cell.empty {
			num = strconv.Itoa(cell.date.Day())
		}
		btnMods = append(btnMods, vdom.Span(vdom.Attr("class", "leading-none"), vdom.Text(num)))
		if !cell.empty && renderDay != nil {
			if extra := renderDay(calDateKey(cell.date), cell.date); extra != nil {
				btnMods = append(btnMods, extra)
			}
		}
		mods = append(mods, vdom.Button(btnMods...))
	}
	return vdom.Div(mods...)
}

// calMonthDaysGrid renders the month-variant day grid.
func calMonthDaysGrid(view, sel time.Time, hasSel bool, now time.Time, onSelect func(string), renderDay func(string, time.Time) *vdom.VNode) *vdom.VNode {
	cells := calMonthCells(view.Year(), view.Month())
	mods := []vdom.Mod{vdom.Attr("class", calDaysMonth)}
	for idx, cell := range cells {
		selected := hasSel && !cell.empty && calSameDay(cell.date, sel)
		today := !cell.empty && calSameDay(cell.date, now)
		btnMods := []vdom.Mod{
			vdom.Attr("type", "button"),
			vdom.Attr("class", calDayClassMonth(idx, cell.empty, selected)),
		}
		if cell.empty {
			btnMods = append(btnMods, vdom.Attr("disabled", "disabled"))
		} else {
			d := cell.date
			btnMods = append(btnMods, vdom.On(vdom.EVENT_CLICK, func() {
				if onSelect != nil {
					onSelect(calDateKey(d))
				}
			}))
		}
		num := ""
		if !cell.empty {
			num = strconv.Itoa(cell.date.Day())
		}
		btnMods = append(btnMods, vdom.Span(vdom.Attr("class", calDayNumberClassMonth(today)), vdom.Text(num)))
		if !cell.empty && renderDay != nil {
			if extra := renderDay(calDateKey(cell.date), cell.date); extra != nil {
				btnMods = append(btnMods, extra)
			}
		}
		mods = append(mods, vdom.Button(btnMods...))
	}
	return vdom.Div(mods...)
}

// CalendarProps configures Calendar. It is a controlled component: the visible
// month is a plain value (ViewMonth) driven by the OnNavigate callback, and the
// selection is a "YYYY-MM-DD" key driven by OnSelect.
//
// NOTE: the TSX kept ViewMonth in an internal signal synced from props via
// effects, plus attr:key / _reset re-render hacks. Those have no equivalent in
// the neutral runtime; the month is modeled as an explicit prop + callback.
type CalendarProps struct {
	Selected   string                                       // selected date "YYYY-MM-DD" (empty = none)
	ViewMonth  time.Time                                    // visible month (any day within it); zero => Selected's month, else today
	Variant    string                                       // "picker" (default) or "month"
	OnSelect   func(key string)                             // clicked day, as "YYYY-MM-DD"
	OnNavigate func(month time.Time)                        // first-of-month requested by the prev/next buttons
	RenderDay  func(key string, date time.Time) *vdom.VNode // optional per-day content
	Class      string
}

// Calendar renders a month grid in either the compact "picker" layout or the
// large "month" layout.
func Calendar(p CalendarProps) *vdom.VNode {
	now := time.Now()
	sel, hasSel := calParseKey(p.Selected)

	view := p.ViewMonth
	if view.IsZero() {
		if hasSel {
			view = sel
		} else {
			view = now
		}
	}

	isMonth := p.Variant == CalendarVariantMonth

	rootCls := calPickerRoot
	headerCls := calHeaderPicker
	myCls := calMyPicker
	weekdaysCls := calWeekdaysPicker
	weekdayCls := calWeekdayPicker
	if isMonth {
		rootCls = calMonthRoot
		headerCls = calHeaderMonth
		myCls = calMyMonth
		weekdaysCls = calWeekdaysMonth
		weekdayCls = calWeekdayMonth
	}

	prev := time.Date(view.Year(), view.Month()-1, 1, 0, 0, 0, 0, time.UTC)
	next := time.Date(view.Year(), view.Month()+1, 1, 0, 0, 0, 0, time.UTC)
	navigate := func(m time.Time) func() {
		return func() {
			if p.OnNavigate != nil {
				p.OnNavigate(m)
			}
		}
	}

	header := vdom.Div(vdom.Attr("class", headerCls),
		vdom.Button(vdom.Attr("type", "button"),
			vdom.Attr("class", calNavBtn),
			vdom.On(vdom.EVENT_CLICK, navigate(prev)),
			Icon("chevron-left", 16, ""),
		),
		vdom.Span(vdom.Attr("class", myCls),
			vdom.Text(calMonths[int(view.Month())-1]+" "+strconv.Itoa(view.Year()))),
		vdom.Button(vdom.Attr("type", "button"),
			vdom.Attr("class", calNavBtn),
			vdom.On(vdom.EVENT_CLICK, navigate(next)),
			Icon("chevron-right", 16, ""),
		),
	)

	var daysGrid *vdom.VNode
	if isMonth {
		daysGrid = calMonthDaysGrid(view, sel, hasSel, now, p.OnSelect, p.RenderDay)
	} else {
		daysGrid = calPickerDaysGrid(view, sel, hasSel, now, p.OnSelect, p.RenderDay)
	}

	return vdom.Div(vdom.Attr("class", cx(rootCls, p.Class)),
		header,
		calWeekdaysRow(weekdaysCls, weekdayCls),
		daysGrid,
	)
}
