package server

import (
	"errors"
	"strconv"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"

	"scrabble/backend/internal/adminconsole"
	"scrabble/backend/internal/ads"
)

// bannersBack is the campaign-list path the banner console actions return to.
const bannersBack = "/_gm/banners"

// localTimeLayout is the datetime-local input/round-trip form of a validity-window
// bound; the operator's entry is interpreted as UTC.
const localTimeLayout = "2006-01-02T15:04"

// Neutral banner theme tokens, mirrored from ui/src/app.css (--ad-bg,
// --text-muted, --accent for the light and dark themes). They seed the console's
// colour pickers and the preview fallback when a campaign has no override, so the
// operator always edits against the real neutral colours.
const (
	tokenLightBg, tokenLightFg, tokenLightLink = "#e3e7ee", "#6b7280", "#2f6df6"
	tokenDarkBg, tokenDarkFg, tokenDarkLink    = "#272f3c", "#9aa3b2", "#5b8cff"
)

// consoleBanners lists the advertising campaigns (default first), each with its
// weight, validity window, enabled flag, message count and live-now status.
func (s *Server) consoleBanners(c *gin.Context) {
	campaigns, err := s.ads.ListCampaigns(c.Request.Context())
	if err != nil {
		s.consoleError(c, err)
		return
	}
	now := time.Now().UTC()
	var view adminconsole.BannersView
	for _, cm := range campaigns {
		view.Items = append(view.Items, adminconsole.BannerCampaignRow{
			ID:        cm.ID.String(),
			Name:      cm.Name,
			Weight:    cm.Weight,
			IsDefault: cm.IsDefault,
			Enabled:   cm.Enabled,
			Window:    windowLabel(cm),
			Messages:  len(cm.Messages),
			ActiveNow: cm.ActiveAt(now),
		})
	}
	s.renderConsole(c, "banners", "banners", "Banners", view)
}

// consoleBannerDetail renders one campaign's edit form and its messages.
func (s *Server) consoleBannerDetail(c *gin.Context) {
	id, ok := s.consoleUUID(c, bannersBack)
	if !ok {
		return
	}
	cm, err := s.ads.Campaign(c.Request.Context(), id)
	if err != nil {
		s.bannerError(c, err, bannersBack)
		return
	}
	view := adminconsole.BannerDetailView{
		ID:             cm.ID.String(),
		Name:           cm.Name,
		Weight:         cm.Weight,
		IsDefault:      cm.IsDefault,
		Enabled:        cm.Enabled,
		StartsAt:       localInput(cm.StartsAt),
		EndsAt:         localInput(cm.EndsAt),
		Urgent:         cm.Urgent,
		OverrideAllOn:  cm.OverrideAll != nil,
		OverrideDarkOn: cm.OverrideDark != nil,
	}
	view.AllBg, view.AllFg, view.AllLink = seedColors(cm.OverrideAll, tokenLightBg, tokenLightFg, tokenLightLink)
	view.DarkBg, view.DarkFg, view.DarkLink = seedColors(cm.OverrideDark, tokenDarkBg, tokenDarkFg, tokenDarkLink)
	for i, m := range cm.Messages {
		view.Messages = append(view.Messages, adminconsole.BannerMessageRow{
			ID:     m.ID.String(),
			BodyEn: m.BodyEn,
			BodyRu: m.BodyRu,
			First:  i == 0,
			Last:   i == len(cm.Messages)-1,
		})
	}
	s.renderConsole(c, "banner_detail", "banners", cm.Name, view)
}

// consoleCreateBanner creates a new time-limited campaign.
func (s *Server) consoleCreateBanner(c *gin.Context) {
	starts, ends, err := parseWindow(c)
	if err != nil {
		s.renderConsoleMessage(c, "Invalid window", err.Error(), bannersBack)
		return
	}
	id, err := s.ads.CreateCampaign(c.Request.Context(), ads.Campaign{
		Name:     trimForm(c, "name"),
		Weight:   atoiForm(c, "weight"),
		Enabled:  c.PostForm("enabled") != "",
		StartsAt: starts,
		EndsAt:   ends,
	})
	if err != nil {
		s.bannerError(c, err, bannersBack)
		return
	}
	s.renderConsoleMessage(c, "Created", "campaign created", "/_gm/banners/"+id.String())
}

// consoleUpdateBanner saves a campaign's editable fields. For the default
// campaign the service ignores everything but the name.
func (s *Server) consoleUpdateBanner(c *gin.Context) {
	id, ok := s.consoleUUID(c, bannersBack)
	if !ok {
		return
	}
	back := "/_gm/banners/" + id.String()
	starts, ends, err := parseWindow(c)
	if err != nil {
		s.renderConsoleMessage(c, "Invalid window", err.Error(), back)
		return
	}
	err = s.ads.UpdateCampaign(c.Request.Context(), ads.Campaign{
		ID:           id,
		Name:         trimForm(c, "name"),
		Weight:       atoiForm(c, "weight"),
		Enabled:      c.PostForm("enabled") != "",
		StartsAt:     starts,
		EndsAt:       ends,
		Urgent:       c.PostForm("urgent") != "",
		OverrideAll:  colorSetForm(c, "override_all_on", "override_bg", "override_fg", "override_link"),
		OverrideDark: colorSetForm(c, "override_dark_on", "override_bg_dark", "override_fg_dark", "override_link_dark"),
	})
	if err != nil {
		s.bannerError(c, err, back)
		return
	}
	s.renderConsoleMessage(c, "Saved", "campaign updated", back)
}

// consoleDeleteBanner removes a campaign (the service refuses the default).
func (s *Server) consoleDeleteBanner(c *gin.Context) {
	id, ok := s.consoleUUID(c, bannersBack)
	if !ok {
		return
	}
	if err := s.ads.DeleteCampaign(c.Request.Context(), id); err != nil {
		s.bannerError(c, err, "/_gm/banners/"+id.String())
		return
	}
	s.renderConsoleMessage(c, "Deleted", "campaign deleted", bannersBack)
}

// consoleAddBannerMessage appends a bilingual message to a campaign.
func (s *Server) consoleAddBannerMessage(c *gin.Context) {
	id, ok := s.consoleUUID(c, bannersBack)
	if !ok {
		return
	}
	back := "/_gm/banners/" + id.String()
	if _, err := s.ads.AddMessage(c.Request.Context(), id, trimForm(c, "body_en"), trimForm(c, "body_ru")); err != nil {
		s.bannerError(c, err, back)
		return
	}
	s.renderConsoleMessage(c, "Added", "message added", back)
}

// consoleEditBannerMessage rewrites a message's bilingual bodies.
func (s *Server) consoleEditBannerMessage(c *gin.Context) {
	cid, mid, ok := s.bannerMessageIDs(c)
	if !ok {
		return
	}
	back := "/_gm/banners/" + cid.String()
	if err := s.ads.EditMessage(c.Request.Context(), cid, mid, trimForm(c, "body_en"), trimForm(c, "body_ru")); err != nil {
		s.bannerError(c, err, back)
		return
	}
	s.renderConsoleMessage(c, "Saved", "message updated", back)
}

// consoleDeleteBannerMessage removes a message (the service keeps the default's
// last one).
func (s *Server) consoleDeleteBannerMessage(c *gin.Context) {
	cid, mid, ok := s.bannerMessageIDs(c)
	if !ok {
		return
	}
	back := "/_gm/banners/" + cid.String()
	if err := s.ads.DeleteMessage(c.Request.Context(), cid, mid); err != nil {
		s.bannerError(c, err, back)
		return
	}
	s.renderConsoleMessage(c, "Deleted", "message deleted", back)
}

// consoleMoveBannerMessage reorders a message up or down within its campaign.
func (s *Server) consoleMoveBannerMessage(c *gin.Context) {
	cid, mid, ok := s.bannerMessageIDs(c)
	if !ok {
		return
	}
	back := "/_gm/banners/" + cid.String()
	dir := 1
	if trimForm(c, "dir") == "up" {
		dir = -1
	}
	if err := s.ads.MoveMessage(c.Request.Context(), cid, mid, dir); err != nil {
		s.bannerError(c, err, back)
		return
	}
	s.renderConsoleMessage(c, "Moved", "message reordered", back)
}

// consoleBannerSettings renders the global display-timings form.
func (s *Server) consoleBannerSettings(c *gin.Context) {
	t, err := s.ads.Settings(c.Request.Context())
	if err != nil {
		s.consoleError(c, err)
		return
	}
	s.renderConsole(c, "banner_settings", "banners", "Banner settings", adminconsole.BannerSettingsView{
		HoldMs:         t.HoldMs,
		EdgePauseMs:    t.EdgePauseMs,
		ScrollPxPerSec: t.ScrollPxPerSec,
		FadeOutMs:      t.FadeOutMs,
		GapMs:          t.GapMs,
		FadeInMs:       t.FadeInMs,
	})
}

// consoleUpdateBannerSettings saves the global display timings (clamped by the
// service into their safe ranges).
func (s *Server) consoleUpdateBannerSettings(c *gin.Context) {
	err := s.ads.UpdateSettings(c.Request.Context(), ads.Timings{
		HoldMs:         atoiForm(c, "hold_ms"),
		EdgePauseMs:    atoiForm(c, "edge_pause_ms"),
		ScrollPxPerSec: atoiForm(c, "scroll_px_per_sec"),
		FadeOutMs:      atoiForm(c, "fade_out_ms"),
		GapMs:          atoiForm(c, "gap_ms"),
		FadeInMs:       atoiForm(c, "fade_in_ms"),
	})
	if err != nil {
		s.consoleError(c, err)
		return
	}
	s.renderConsoleMessage(c, "Saved", "display timings updated", "/_gm/banner-settings")
}

// bannerMessageIDs parses the campaign :id and message :mid path parameters,
// rendering an invalid-id message and returning false when either is not a UUID.
func (s *Server) bannerMessageIDs(c *gin.Context) (campaignID, messageID uuid.UUID, ok bool) {
	cid, err := uuid.Parse(c.Param("id"))
	if err != nil {
		s.renderConsoleMessage(c, "Invalid id", "the campaign id in the URL is not valid", bannersBack)
		return uuid.Nil, uuid.Nil, false
	}
	mid, err := uuid.Parse(c.Param("mid"))
	if err != nil {
		s.renderConsoleMessage(c, "Invalid id", "the message id in the URL is not valid", "/_gm/banners/"+cid.String())
		return uuid.Nil, uuid.Nil, false
	}
	return cid, mid, true
}

// bannerError maps an ads validation/immutability/not-found error to a friendly
// console message, and anything else to the generic error page.
func (s *Server) bannerError(c *gin.Context, err error, back string) {
	switch {
	case errors.Is(err, ads.ErrValidation), errors.Is(err, ads.ErrDefaultImmutable):
		s.renderConsoleMessage(c, "Not saved", err.Error(), back)
	case errors.Is(err, ads.ErrNotFound):
		s.renderConsoleMessage(c, "Not found", "no such campaign or message", back)
	default:
		s.consoleError(c, err)
	}
}

// parseWindow reads the optional starts_at/ends_at datetime-local form fields,
// interpreting each as UTC; an empty field is an open-ended bound (nil).
func parseWindow(c *gin.Context) (starts, ends *time.Time, err error) {
	if starts, err = parseLocalTime(trimForm(c, "starts_at")); err != nil {
		return nil, nil, errors.New("the start time is not a valid date/time")
	}
	if ends, err = parseLocalTime(trimForm(c, "ends_at")); err != nil {
		return nil, nil, errors.New("the end time is not a valid date/time")
	}
	return starts, ends, nil
}

// parseLocalTime parses a datetime-local value as UTC, or returns nil for an
// empty string.
func parseLocalTime(s string) (*time.Time, error) {
	if s == "" {
		return nil, nil
	}
	t, err := time.Parse(localTimeLayout, s)
	if err != nil {
		return nil, err
	}
	t = t.UTC()
	return &t, nil
}

// localInput formats a validity-window bound for a datetime-local input, or ""
// when open-ended.
func localInput(t *time.Time) string {
	if t == nil {
		return ""
	}
	return t.UTC().Format(localTimeLayout)
}

// windowLabel builds the human-readable validity window for the campaign list.
func windowLabel(c ads.Campaign) string {
	if c.IsDefault {
		return "perpetual"
	}
	switch {
	case c.StartsAt == nil && c.EndsAt == nil:
		return "always"
	case c.StartsAt == nil:
		return "until " + fmtTimePtr(c.EndsAt)
	case c.EndsAt == nil:
		return "from " + fmtTimePtr(c.StartsAt)
	default:
		return fmtTimePtr(c.StartsAt) + " – " + fmtTimePtr(c.EndsAt)
	}
}

// atoiForm returns the integer value of a posted form field, or 0 when missing
// or non-numeric (the ads service clamps timings and validates weights).
func atoiForm(c *gin.Context, name string) int {
	n, _ := strconv.Atoi(trimForm(c, name))
	return n
}

// seedColors returns the three colour-input seed values for a campaign's optional
// override: the stored colours when the set is present, otherwise the given
// neutral theme tokens (so the native picker starts sensibly and the preview
// shows the real fallback).
func seedColors(cs *ads.ColorSet, tokenBg, tokenFg, tokenLink string) (bg, fg, link string) {
	if cs == nil {
		return tokenBg, tokenFg, tokenLink
	}
	return cs.Bg, cs.Fg, cs.Link
}

// colorSetForm reads an optional colour override from the form: the on flag gates
// it (an unchecked set is absent, nil), otherwise the three posted colours are
// returned for the ads service to validate. The colour inputs are disabled in the
// browser while the set is off, so they are not posted then.
func colorSetForm(c *gin.Context, on, bg, fg, link string) *ads.ColorSet {
	if c.PostForm(on) == "" {
		return nil
	}
	return &ads.ColorSet{
		Bg:   trimForm(c, bg),
		Fg:   trimForm(c, fg),
		Link: trimForm(c, link),
	}
}
