// WEB 用户服务，负责用户认证、账户资料、积分查询和兑换码业务。
package service

import (
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"strings"
	"time"
	"unicode/utf8"

	"juhe-factory/api/internal/billing"
	"juhe-factory/api/internal/model"
	"juhe-factory/api/internal/security"

	"github.com/google/uuid"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

type Web struct {
	DB         *gorm.DB
	Passwords  security.PasswordHasher
	Tokens     security.WebTokenService
	RefreshTTL time.Duration
}

var (
	ErrInvalidUsername  = errors.New("用户名长度必须为 5～12 个字符")
	ErrUsernameExists   = errors.New("用户名已被使用")
	ErrOriginalPassword = errors.New("原密码错误")
	ErrAccountDisabled  = errors.New("账户已被禁用")
)

type WebTokenPair struct {
	AccessToken  string         `json:"access_token"`
	RefreshToken string         `json:"refresh_token"`
	ExpiresAt    time.Time      `json:"expires_at"`
	User         map[string]any `json:"user"`
}

func (s *Web) Login(username, password string) (*WebTokenPair, error) {
	var user model.WebUser
	if err := s.DB.Where("account = ? AND deleted_at IS NULL", strings.TrimSpace(username)).First(&user).Error; err != nil {
		return nil, ErrUnauthorized
	}
	if !user.Enabled {
		return nil, ErrAccountDisabled
	}
	if !s.Passwords.Verify(user.PasswordHash, password) {
		return nil, ErrUnauthorized
	}
	if user.LastOnlineAt == nil || user.LastOnlineAt.Before(time.Now().Add(-5*time.Minute)) {
		_ = s.DB.Model(&user).Update("last_online_at", time.Now()).Error
	}
	return s.issue(&user)
}

func (s *Web) Refresh(refreshToken string) (*WebTokenPair, error) {
	var record model.WebRefreshToken
	if err := s.DB.Where("token_hash = ? AND revoked_at IS NULL AND expires_at > ?", security.HashToken(refreshToken), time.Now()).First(&record).Error; err != nil {
		return nil, ErrUnauthorized
	}
	var user model.WebUser
	if err := s.DB.Where("id = ? AND enabled = true AND deleted_at IS NULL", record.UserID).First(&user).Error; err != nil || user.SessionVersion != record.SessionVersion {
		return nil, ErrUnauthorized
	}
	now := time.Now()
	if err := s.DB.Model(&record).Update("revoked_at", now).Error; err != nil {
		return nil, err
	}
	return s.issue(&user)
}

func (s *Web) Logout(refreshToken string) error {
	if refreshToken == "" {
		return nil
	}
	return s.DB.Model(&model.WebRefreshToken{}).Where("token_hash = ? AND revoked_at IS NULL", security.HashToken(refreshToken)).Update("revoked_at", time.Now()).Error
}

func (s *Web) Authenticate(raw string) (*model.WebUser, error) {
	claims, err := s.Tokens.ParseAccessToken(raw)
	if err != nil {
		return nil, ErrUnauthorized
	}
	id, err := uuid.Parse(claims.UserID)
	if err != nil {
		return nil, ErrUnauthorized
	}
	var user model.WebUser
	if err := s.DB.Where("id = ? AND enabled = true AND deleted_at IS NULL", id).First(&user).Error; err != nil || user.SessionVersion != claims.SessionVersion {
		return nil, ErrUnauthorized
	}
	return &user, nil
}

func (s *Web) issue(user *model.WebUser) (*WebTokenPair, error) {
	access, expires, err := s.Tokens.NewAccessToken(user.ID.String(), user.UID, user.Account, user.SessionVersion)
	if err != nil {
		return nil, err
	}
	opaque := security.TokenService{RefreshTTL: s.RefreshTTL}
	refresh, hash, refreshExpires, err := opaque.NewRefreshToken()
	if err != nil {
		return nil, err
	}
	record := model.WebRefreshToken{UserID: user.ID, TokenHash: hash, SessionVersion: user.SessionVersion, ExpiresAt: refreshExpires}
	if err := s.DB.Create(&record).Error; err != nil {
		return nil, err
	}
	return &WebTokenPair{AccessToken: access, RefreshToken: refresh, ExpiresAt: expires, User: map[string]any{"id": user.ID, "uid": user.UID, "username": user.Username, "account": user.Account, "avatar_url": user.AvatarURL}}, nil
}

// Account 返回用户资料、永久积分余额、每日限额和当日消耗数据。
func (s *Web) Account(userID uuid.UUID) (map[string]any, error) {
	result := map[string]any{}
	err := s.DB.Raw(`SELECT id,uid,username,account,avatar_url,point_balance,daily_limit,last_online_at,
		coalesce((SELECT sum(-hold.change_amount) FROM point_ledger hold
			WHERE hold.user_id=web_users.id AND hold.business_type='generation_hold'
			AND hold.created_at>=CURRENT_DATE AND hold.created_at<CURRENT_DATE+INTERVAL '1 day'
			AND NOT EXISTS (SELECT 1 FROM point_ledger refund WHERE refund.idempotency_key='generation:refund:'||hold.business_id)),0) AS today_consumption
		FROM web_users WHERE id=? AND deleted_at IS NULL`, userID).Scan(&result).Error
	return result, err
}

func (s *Web) UpdateProfile(userID uuid.UUID, username string) (map[string]any, error) {
	username = strings.TrimSpace(username)
	if count := utf8.RuneCountInString(username); count < 5 || count > 12 {
		return nil, ErrInvalidUsername
	}
	var duplicates int64
	if err := s.DB.Table("web_users").Where("username = ? AND id <> ?", username, userID).Count(&duplicates).Error; err != nil {
		return nil, err
	}
	if duplicates > 0 {
		return nil, ErrUsernameExists
	}
	if err := s.DB.Table("web_users").Where("id = ? AND deleted_at IS NULL", userID).Update("username", username).Error; err != nil {
		var raced int64
		if s.DB.Table("web_users").Where("username = ? AND id <> ?", username, userID).Count(&raced).Error == nil && raced > 0 {
			return nil, ErrUsernameExists
		}
		return nil, err
	}
	return s.Account(userID)
}

func (s *Web) ChangePassword(userID uuid.UUID, originalPassword, newPassword string) error {
	if len(newPassword) < 8 || len(newPassword) > 128 {
		return errors.New("新密码长度必须为 8～128 位")
	}
	var user model.WebUser
	if err := s.DB.Where("id = ? AND enabled = true AND deleted_at IS NULL", userID).First(&user).Error; err != nil {
		return ErrUnauthorized
	}
	if !s.Passwords.Verify(user.PasswordHash, originalPassword) {
		return ErrOriginalPassword
	}
	hash, err := s.Passwords.Hash(newPassword)
	if err != nil {
		return err
	}
	return s.DB.Model(&user).Update("password_hash", hash).Error
}

func (s *Web) AvatarKey(userID uuid.UUID) (string, error) {
	var key string
	err := s.DB.Raw("SELECT coalesce(avatar_key, '') FROM web_users WHERE id = ? AND deleted_at IS NULL", userID).Scan(&key).Error
	return key, err
}

func (s *Web) UpdateAvatar(userID uuid.UUID, url, key string) error {
	return s.DB.Table("web_users").Where("id = ? AND deleted_at IS NULL", userID).Updates(map[string]any{"avatar_url": url, "avatar_key": key}).Error
}

func (s *Web) Usage30Days(userID uuid.UUID) ([]map[string]any, error) {
	items := make([]map[string]any, 0)
	err := s.DB.Raw(`WITH days AS (SELECT generate_series((CURRENT_DATE-INTERVAL '29 days')::date,CURRENT_DATE::date,'1 day')::date AS day),
		stats AS (SELECT hold.created_at::date AS day,coalesce(sum(-hold.change_amount),0) AS consumption
			FROM point_ledger hold WHERE hold.user_id=? AND hold.business_type='generation_hold'
			AND hold.created_at>=CURRENT_DATE-INTERVAL '29 days'
			AND NOT EXISTS (SELECT 1 FROM point_ledger refund WHERE refund.idempotency_key='generation:refund:'||hold.business_id)
			GROUP BY hold.created_at::date)
		SELECT days.day,coalesce(stats.consumption,0) AS consumption FROM days LEFT JOIN stats USING(day) ORDER BY days.day`, userID).Scan(&items).Error
	return items, err
}

// ConsumptionRecordPage 描述用户积分流水的服务端分页结果。
type ConsumptionRecordPage struct {
	Items    []map[string]any `json:"items"`
	Total    int64            `json:"total"`
	Page     int              `json:"page"`
	PageSize int              `json:"page_size"`
}

// normalizeConsumptionPagination 约束积分流水页码和每页数量，避免无效或过大的查询。
func normalizeConsumptionPagination(page, pageSize int) (int, int) {
	if page < 1 {
		page = 1
	}
	if pageSize < 1 {
		pageSize = 10
	}
	if pageSize > 100 {
		pageSize = 100
	}
	return page, pageSize
}

// ConsumptionRecords 返回用户消费、退款、兑换及后台发放形成的积分变动分页流水。
func (s *Web) ConsumptionRecords(userID uuid.UUID, page, pageSize int) (ConsumptionRecordPage, error) {
	page, pageSize = normalizeConsumptionPagination(page, pageSize)
	result := ConsumptionRecordPage{Items: make([]map[string]any, 0), Page: page, PageSize: pageSize}
	ledger := s.DB.Table("point_ledger").
		Where("user_id=? AND business_type IN ?", userID, []string{"generation_hold", "generation_refund", "redemption", "admin_grant"})
	if err := ledger.Count(&result.Total).Error; err != nil {
		return result, err
	}
	offset := (page - 1) * pageSize
	err := s.DB.Raw(`SELECT id,abs(change_amount)::text AS amount,change_amount::text AS change_amount,
		balance_after::text AS balance_after,business_type,remark,created_at
		FROM point_ledger
		WHERE user_id=? AND business_type IN ('generation_hold','generation_refund','redemption','admin_grant')
		ORDER BY created_at DESC,id DESC LIMIT ? OFFSET ?`, userID, pageSize, offset).Scan(&result.Items).Error
	return result, err
}

// Redeem 校验兑换码并将对应积分以永久有效批次计入用户账户。
func (s *Web) Redeem(userID uuid.UUID, code string) (map[string]any, error) {
	normalized := strings.ReplaceAll(strings.TrimSpace(code), "-", "")
	if normalized == "" {
		return nil, errors.New("请输入兑换码")
	}
	sum := sha256.Sum256([]byte(normalized))
	hash := hex.EncodeToString(sum[:])
	result := map[string]any{}
	err := s.DB.Transaction(func(tx *gorm.DB) error {
		var record struct {
			ID        uuid.UUID
			Points    string
			Status    string
			ExpiresAt *time.Time
		}
		if err := tx.Table("redemption_codes").Clauses(clause.Locking{Strength: "UPDATE"}).Select("id,points::text AS points,status,expires_at").Where("code_hash=?", hash).Take(&record).Error; err != nil {
			return errors.New("兑换码不存在")
		}
		if record.Status != "unused" {
			return errors.New("兑换码已使用或已过期")
		}
		if record.ExpiresAt != nil && !record.ExpiresAt.After(time.Now()) {
			if err := tx.Table("redemption_codes").Where("id=?", record.ID).Update("status", "expired").Error; err != nil {
				return err
			}
			return errors.New("兑换码已过期")
		}
		redeemedAt := time.Now()
		balance, credited, err := billing.CreditRedemptionPoints(tx, userID, record.ID, record.Points)
		if err != nil {
			return err
		}
		if !credited {
			return errors.New("兑换码积分已入账")
		}
		if err := tx.Table("redemption_codes").Where("id=?", record.ID).Updates(map[string]any{"status": "used", "redeemed_by": userID, "redeemed_at": redeemedAt}).Error; err != nil {
			return err
		}
		result = map[string]any{"points": record.Points, "balance": balance}
		return nil
	})
	return result, err
}
