package usage

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"time"

	"github.com/redis/go-redis/v9"
	"github.com/wangjia/pangolin/server/internal/apierr"
)

// nowFunc is overridable in tests to pin "today".
var nowFunc = time.Now

// utcToday returns the current UTC calendar date (time truncated).
func utcToday() time.Time {
	n := nowFunc().UTC()
	return time.Date(n.Year(), n.Month(), n.Day(), 0, 0, 0, 0, time.UTC)
}

// Service serves usage queries, the /v1/me usage summary, the ads-unlock flow,
// and the free-plan connect quota check.
type Service struct {
	store      *Store
	rdb        *redis.Client
	verifier   AdVerifier
	adNonceTTL time.Duration
}

// NewService builds a Service.  rdb may be nil (ad-token replay protection is
// then disabled — not recommended in production).  verifier may be nil if the
// ads-unlock endpoint is not mounted.  adNonceTTL <= 0 defaults to 1h.
func NewService(store *Store, rdb *redis.Client, verifier AdVerifier, adNonceTTL time.Duration) *Service {
	if adNonceTTL <= 0 {
		adNonceTTL = time.Hour
	}
	return &Service{store: store, rdb: rdb, verifier: verifier, adNonceTTL: adNonceTTL}
}

// UsagePoint is one day of the usage curve.
type UsagePoint struct {
	Date        string `json:"date"` // YYYY-MM-DD (UTC)
	BytesUp     uint64 `json:"bytes_up"`
	BytesDown   uint64 `json:"bytes_down"`
	MinutesUsed int    `json:"minutes_used"`
}

// UsageCurve returns the last `days` daily points for userID (UTC), with
// missing days zero-filled, oldest first.  days is clamped to [1, 90].
func (svc *Service) UsageCurve(ctx context.Context, userID int64, days int) ([]UsagePoint, *apierr.Error) {
	if days < 1 {
		days = 7
	}
	if days > 90 {
		days = 90
	}

	today := utcToday()
	from := today.AddDate(0, 0, -(days - 1))

	rows, err := svc.store.GetUsageRange(ctx, userID, from, today)
	if err != nil {
		return nil, apierr.ErrInternal
	}
	byDate := make(map[string]DailyUsage, len(rows))
	for _, r := range rows {
		byDate[r.Date.UTC().Format(dateLayout)] = r
	}

	points := make([]UsagePoint, 0, days)
	for i := 0; i < days; i++ {
		d := from.AddDate(0, 0, i).Format(dateLayout)
		if u, ok := byDate[d]; ok {
			points = append(points, UsagePoint{
				Date:        d,
				BytesUp:     u.BytesUp,
				BytesDown:   u.BytesDown,
				MinutesUsed: u.MinutesUsed,
			})
		} else {
			points = append(points, UsagePoint{Date: d})
		}
	}
	return points, nil
}

// TodaySummary is the /v1/me today_usage block.
type TodaySummary struct {
	MinutesUsed      int  `json:"minutes_used"`
	MinutesRemaining *int `json:"minutes_remaining"` // nil = unlimited (pro/team)
	AdUnlocked       bool `json:"ad_unlocked"`
}

// TodaySummary returns the current user's usage summary for today (UTC),
// reflecting plan limits and ad-unlock state.
func (svc *Service) TodaySummary(ctx context.Context, userID int64) (*TodaySummary, *apierr.Error) {
	plan, err := svc.store.EffectivePlan(ctx, userID)
	if err != nil {
		return nil, apierr.ErrInternal
	}
	day, err := svc.store.GetDay(ctx, userID, utcToday())
	if err != nil {
		return nil, apierr.ErrInternal
	}

	used := 0
	adUnlocked := false
	if day != nil {
		used = day.MinutesUsed
		adUnlocked = day.AdUnlockedAt.Valid
	}

	out := &TodaySummary{MinutesUsed: used, AdUnlocked: adUnlocked}
	if plan.DailyMinutes.Valid {
		remaining := int(plan.DailyMinutes.Int64) - used
		if remaining < 0 {
			remaining = 0
		}
		out.MinutesRemaining = &remaining
	}
	return out, nil
}

// UnlockAd verifies a rewarded-ad receipt and records the day's ad-unlock.
//
// Flow: validate inputs → verify the receipt with the ad network → consume the
// ad_token as a one-time nonce (replay-protected) → set ad_unlocked_at.  A
// second unlock on a day already unlocked is idempotent (alreadyUnlocked=true).
func (svc *Service) UnlockAd(ctx context.Context, userID int64, deviceID, adToken string) (alreadyUnlocked bool, apiErr *apierr.Error) {
	if deviceID == "" || adToken == "" {
		return false, apierr.ErrBadRequest
	}
	if svc.verifier == nil {
		return false, apierr.ErrInternal
	}

	// 1. Authenticate the receipt with the ad network.
	if err := svc.verifier.Verify(ctx, AdVerifyRequest{
		UserID:   userID,
		DeviceID: deviceID,
		AdToken:  adToken,
	}); err != nil {
		return false, apierr.ErrAdVerifyFailed
	}

	// 2. One-time nonce: a genuine receipt may only be redeemed once.
	if dup, err := svc.consumeAdNonce(ctx, adToken); err != nil {
		return false, apierr.ErrInternal
	} else if dup {
		return false, apierr.ErrAdReplay
	}

	// 3. Record the unlock (idempotent per UTC day).
	already, err := svc.store.MarkAdUnlocked(ctx, userID, utcToday())
	if err != nil {
		return false, apierr.ErrInternal
	}
	return already, nil
}

// consumeAdNonce atomically records the ad_token so it cannot be reused.
// Returns dup=true if the token was already consumed.  No-ops (dup=false) when
// Redis is not configured.
func (svc *Service) consumeAdNonce(ctx context.Context, adToken string) (bool, error) {
	if svc.rdb == nil {
		return false, nil
	}
	sum := sha256.Sum256([]byte(adToken))
	key := "ads:nonce:" + hex.EncodeToString(sum[:])
	set, err := svc.rdb.SetNX(ctx, key, "1", svc.adNonceTTL).Result()
	if err != nil {
		return false, err
	}
	return !set, nil
}
