package main

import (
	"bytes"
	"context"
	"time"

	"git.dev.alexdunmow.com/block/pluginsdk/blocks"
	"git.dev.alexdunmow.com/block/pluginsdk/settings"
)

// renderSettings is the SettingsManager available to CalcomBookingFunc, which
// renders during the public page pipeline where no HTTP request (and thus no
// visitor timezone) exists. Set once by NewCalcomRouter when the plugin's HTTP
// routes are mounted at startup — before any page render. Nil in editor-only
// or test contexts, where the grid falls back to the server clock.
var renderSettings *SettingsManager

// BookingConfig holds the configuration for rendering the booking widget
type BookingConfig struct {
	BlockID       string
	Username      string
	EventTypeSlug string
	Title         string
	Description   string
	ShowTimezone  bool
	WeeksToShow   int
	WeekStart     string // YYYY-MM-DD — first date in the current grid window
	Today         string // YYYY-MM-DD — today in the resolved timezone, for past-clamping
	TimeZone      string // IANA name shown in the "All times shown in" label
	RangeLabel    string // e.g. "May 27 – Jun 10, 2026" — populated by handler
	CanGoBack     bool   // false when the window starts today (no point going earlier)
	Dates         []DateCell
	// UnavailableMessage is the admin-authored copy shown when an online booking
	// can't be completed for a non-fixable reason (Tier-2). Emitted as a hidden
	// form input so the POST /book handler can read it back; defaults to
	// DefaultUnavailableMessage when the admin leaves it blank.
	UnavailableMessage string
}

// DateCell represents a single date in the calendar grid
type DateCell struct {
	Date       string // YYYY-MM-DD format
	DayOfMonth string // e.g., "15"
	DayOfWeek  string // e.g., "Mon"
	IsToday    bool
	IsPast     bool
	IsSelected bool // server-rendered selected state — set by HandleGetSlots
}

// CalcomBookingFunc renders the Cal.com booking widget
func CalcomBookingFunc(ctx context.Context, content map[string]any) string {
	// Extract block ID from context or content
	blockID := settings.GetStringOr(content, "_block_id", "")
	if blockID == "" {
		// Generate a simple ID if not provided
		blockID = "calcom-" + time.Now().Format("150405")
	}

	config := BookingConfig{
		BlockID:            blockID,
		Username:           settings.GetStringOr(content, "username", ""),
		EventTypeSlug:      settings.GetStringOr(content, "eventTypeSlug", ""),
		Title:              settings.GetStringOr(content, "title", ""),
		Description:        settings.GetStringOr(content, "description", ""),
		ShowTimezone:       settings.GetBoolOr(content, "showTimezone", true),
		WeeksToShow:        settings.GetIntOr(content, "weeksToShow", 2),
		UnavailableMessage: settings.GetStringOr(content, "unavailableMessage", DefaultUnavailableMessage),
	}

	// Check for missing required configuration
	if config.Username == "" || config.EventTypeSlug == "" {
		if blocks.IsEditor(ctx) {
			return renderEditorPlaceholder()
		}
		return renderConfigError()
	}

	// Generate date grid for the calendar — initial window starts today on
	// the MEETING's calendar: the cached event timezone when warm (filled by
	// the first HTMX hit against the plugin endpoints), else the site
	// Localization setting. Page render never blocks on a Cal.com round-trip.
	// Without this, a container running UTC starts the grid on yesterday's
	// date for a Perth schedule until 08:00 AWST.
	loc, tzName := time.UTC, "UTC"
	if renderSettings != nil {
		loc, tzName = renderSettings.resolveLocation(ctx, peekEventTZ(config.Username, config.EventTypeSlug))
	}
	now := time.Now().In(loc)
	today := now.Format("2006-01-02")
	config.WeekStart = today
	config.Today = today
	config.TimeZone = tzName
	config.Dates = generateDateGridFrom(now, config.WeeksToShow, today)
	config.RangeLabel = formatRangeLabel(now, config.WeeksToShow)
	config.CanGoBack = false // initial window is already at today

	// Render the booking widget
	var buf bytes.Buffer
	if err := CalcomBookingWidget(config).Render(ctx, &buf); err != nil {
		return renderError("Failed to render booking widget: " + err.Error())
	}

	return buf.String()
}

// formatRangeLabel renders the human-readable label for the date grid header,
// e.g. "May 27 – Jun 10, 2026". If start and end fall in the same month, the
// label collapses to "May 27 – 31, 2026".
func formatRangeLabel(start time.Time, weeks int) string {
	if weeks <= 0 {
		weeks = 1
	}
	end := start.AddDate(0, 0, weeks*7-1)
	if start.Year() == end.Year() && start.Month() == end.Month() {
		return start.Format("Jan 2") + " – " + end.Format("2, 2006")
	}
	if start.Year() == end.Year() {
		return start.Format("Jan 2") + " – " + end.Format("Jan 2, 2006")
	}
	return start.Format("Jan 2, 2006") + " – " + end.Format("Jan 2, 2006")
}

// prevWeekStart returns the YYYY-MM-DD string for the previous date-grid
// window — weeks*7 days before the current weekStart, but clamped to today
// (the visitor can't book in the past). Used by the date-grid prev button.
// today is the YYYY-MM-DD date in the resolved timezone (BookingConfig.Today)
// — not the server clock's, which can be a day behind the visitor's.
func prevWeekStart(currentStart string, weeks int, today string) string {
	if weeks <= 0 {
		weeks = 1
	}
	t, err := time.Parse("2006-01-02", currentStart)
	if err != nil || today > currentStart {
		return today
	}
	prev := t.AddDate(0, 0, -weeks*7)
	if prev.Format("2006-01-02") < today {
		return today
	}
	return prev.Format("2006-01-02")
}

// nextWeekStart returns the YYYY-MM-DD string for the next date-grid window —
// weeks*7 days after the current weekStart.
func nextWeekStart(currentStart string, weeks int) string {
	if weeks <= 0 {
		weeks = 1
	}
	t, err := time.Parse("2006-01-02", currentStart)
	if err != nil {
		t = time.Now()
	}
	return t.AddDate(0, 0, weeks*7).Format("2006-01-02")
}

// generateDateGridFrom produces `weeks*7` consecutive day cells starting at
// `start`. Days strictly before `today` (YYYY-MM-DD in the resolved timezone)
// are flagged IsPast so the templ can render them disabled.
func generateDateGridFrom(start time.Time, weeks int, today string) []DateCell {
	if weeks <= 0 {
		weeks = 1
	}
	dates := make([]DateCell, 0, weeks*7)
	for i := 0; i < weeks*7; i++ {
		date := start.AddDate(0, 0, i)
		dateStr := date.Format("2006-01-02")
		dates = append(dates, DateCell{
			Date:       dateStr,
			DayOfMonth: date.Format("2"),
			DayOfWeek:  date.Format("Mon"),
			IsToday:    dateStr == today,
			IsPast:     dateStr < today,
		})
	}
	return dates
}

// leadingBlanks returns how many empty spacer cells must precede the first
// date so it lands under its weekday column header in the 7-column grid
// (Sun = column 1). Without these, a window starting mid-week pours into
// column 1 and every date renders under the wrong weekday — the bidbuddy
// "tapped Tue, got Friday" incident.
func leadingBlanks(dates []DateCell) int {
	if len(dates) == 0 {
		return 0
	}
	t, err := time.Parse("2006-01-02", dates[0].Date)
	if err != nil {
		return 0
	}
	return int(t.Weekday())
}

func renderEditorPlaceholder() string {
	return `<div class="p-8 border-2 border-dashed border-border rounded-lg text-center">
		<svg class="h-12 w-12 mx-auto mb-4 text-muted-foreground opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
			<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
		</svg>
		<p class="text-lg font-medium text-foreground">Cal.com Booking</p>
		<p class="text-sm text-muted-foreground">Select an event type in the editor</p>
	</div>`
}

func renderConfigError() string {
	return `<div class="p-6 bg-destructive/10 border border-destructive/20 rounded-lg text-center">
		<p class="text-sm text-destructive">Cal.com booking block is not configured properly.</p>
	</div>`
}

func renderError(msg string) string {
	return `<div class="p-4 bg-destructive/10 border border-destructive/20 rounded-lg">
		<p class="text-sm text-destructive">` + msg + `</p>
	</div>`
}

// inputTypeFor maps a Cal.com bookingField type to an HTML <input type> value.
// Unknown types fall through to `text`.
func inputTypeFor(t string) string {
	switch t {
	case "email":
		return "email"
	case "phone":
		return "tel"
	case "number":
		return "number"
	default:
		return "text"
	}
}
