package server

import (
	"context"

	"github.com/google/uuid"
	"go.uber.org/zap"

	"scrabble/backend/internal/account"
	"scrabble/backend/internal/ads"
	"scrabble/backend/internal/notify"
)

// bannerDTO is the advertising-banner block attached to an eligible viewer's
// profile: the campaigns to rotate (each already resolved to the viewer's bot
// language) and the global display timings the client rotator reads. It is
// absent for a viewer who should see no banner.
type bannerDTO struct {
	Campaigns []bannerCampaignDTO `json:"campaigns"`
	Timings   bannerTimingsDTO    `json:"timings"`
}

// bannerCampaignDTO is one campaign in the rotation feed: its GCD-reduced show
// weight (for the client's smooth weighted round-robin) and its messages, in
// display order, already resolved to one language. The override_* colours are
// present only for a campaign that carries a colour override: override_bg/fg/link
// paint every theme, and the *_dark trio further overrides the dark theme (the
// client resolves the cascade). They are absent for a plain campaign, which keeps
// the neutral theme tokens.
type bannerCampaignDTO struct {
	Weight           int      `json:"weight"`
	Messages         []string `json:"messages"`
	OverrideBg       string   `json:"override_bg,omitempty"`
	OverrideFg       string   `json:"override_fg,omitempty"`
	OverrideLink     string   `json:"override_link,omitempty"`
	OverrideBgDark   string   `json:"override_bg_dark,omitempty"`
	OverrideFgDark   string   `json:"override_fg_dark,omitempty"`
	OverrideLinkDark string   `json:"override_link_dark,omitempty"`
}

// bannerTimingsDTO mirrors ads.Timings on the wire.
type bannerTimingsDTO struct {
	HoldMs         int `json:"hold_ms"`
	EdgePauseMs    int `json:"edge_pause_ms"`
	ScrollPxPerSec int `json:"scroll_px_per_sec"`
	FadeOutMs      int `json:"fade_out_ms"`
	GapMs          int `json:"gap_ms"`
	FadeInMs       int `json:"fade_in_ms"`
}

// profileResponse builds the account's profile DTO with the advertising-banner block attached when
// the viewer is eligible. Every endpoint returning the caller's own profile (get, update, link)
// uses it, so the banner never drops off the client's profile after a non-get profile change (e.g.
// a language switch).
func (s *Server) profileResponse(ctx context.Context, acc account.Account) profileResponse {
	r := profileResponseFor(acc)
	r.Banner = s.bannerFor(ctx, acc)
	s.fillLinkedIdentities(ctx, &r, acc.ID)
	return r
}

// fillLinkedIdentities sets the profile's confirmed email address and platform-linked
// flags from the account's identities, so the client offers the right link / unlink /
// change-email controls. A read failure leaves them zero (no controls), logged as a
// warning so the profile response still succeeds.
func (s *Server) fillLinkedIdentities(ctx context.Context, r *profileResponse, accountID uuid.UUID) {
	ids, err := s.accounts.Identities(ctx, accountID)
	if err != nil {
		s.log.Warn("profile: identities read failed", zap.String("account", accountID.String()), zap.Error(err))
		return
	}
	for _, id := range ids {
		switch id.Kind {
		case account.KindEmail:
			if id.Confirmed {
				r.Email = id.ExternalID
			}
		case account.KindTelegram:
			r.TelegramLinked = true
		case account.KindVK:
			r.VkLinked = true
		}
	}
}

// bannerFor builds the advertising-banner block for the account's profile, or
// nil when the ads service is not configured or the viewer is not eligible to
// see a banner. The message language follows the account's bot (service)
// language, falling back to its interface language and then English. A failure
// reading roles or campaigns is logged and treated as "no banner" so the profile
// response still succeeds.
func (s *Server) bannerFor(ctx context.Context, acc account.Account) *bannerDTO {
	if s.ads == nil {
		return nil
	}
	set, timings, urgent, err := s.ads.ActiveSet(ctx, bannerLang(acc))
	if err != nil {
		s.log.Warn("banner: active set failed", zap.String("account", acc.ID.String()), zap.Error(err))
		return nil
	}
	// An urgent campaign is shown to every viewer; otherwise the normal eligibility
	// gate applies (a paid account, a non-empty hint wallet or the no_banner role
	// hides the banner).
	if !urgent {
		hasNoBanner, err := s.accounts.HasRole(ctx, acc.ID, account.RoleNoBanner)
		if err != nil {
			s.log.Warn("banner: role check failed", zap.String("account", acc.ID.String()), zap.Error(err))
			return nil
		}
		if !ads.Eligible(acc.PaidAccount, acc.HintBalance, hasNoBanner) {
			return nil
		}
	}
	campaigns := make([]bannerCampaignDTO, 0, len(set))
	for _, c := range set {
		campaigns = append(campaigns, bannerCampaignFromActive(c))
	}
	return &bannerDTO{
		Campaigns: campaigns,
		Timings: bannerTimingsDTO{
			HoldMs:         timings.HoldMs,
			EdgePauseMs:    timings.EdgePauseMs,
			ScrollPxPerSec: timings.ScrollPxPerSec,
			FadeOutMs:      timings.FadeOutMs,
			GapMs:          timings.GapMs,
			FadeInMs:       timings.FadeInMs,
		},
	}
}

// bannerCampaignFromActive flattens a resolved campaign into its wire DTO,
// projecting each optional colour set into its three "#rrggbb" fields (empty when
// the set is absent, so JSON omitempty drops them).
func bannerCampaignFromActive(c ads.ActiveCampaign) bannerCampaignDTO {
	d := bannerCampaignDTO{Weight: c.Weight, Messages: c.Messages}
	if c.OverrideAll != nil {
		d.OverrideBg, d.OverrideFg, d.OverrideLink = c.OverrideAll.Bg, c.OverrideAll.Fg, c.OverrideAll.Link
	}
	if c.OverrideDark != nil {
		d.OverrideBgDark, d.OverrideFgDark, d.OverrideLinkDark = c.OverrideDark.Bg, c.OverrideDark.Fg, c.OverrideDark.Link
	}
	return d
}

// bannerLang resolves the message language for a viewer: their interface language
// (Russian, or English by default).
func bannerLang(acc account.Account) string {
	if acc.PreferredLanguage == "ru" {
		return "ru"
	}
	return "en"
}

// publishBannerChange emits the in-app "banner eligibility may have changed"
// re-poll signal to the account, so an open client re-fetches profile.get and
// shows or hides the banner without a reload. Best-effort (notify.Nop when no
// notifier is wired).
func (s *Server) publishBannerChange(id uuid.UUID) {
	s.notifier.Publish(notify.BannerChanged(id))
}
