package product

import (
	"context"
	"encoding/json"
	"errors"
	"strings"
	"time"

	"bindbox-game/internal/pkg/logger"
	"bindbox-game/internal/repository/mysql"
	"bindbox-game/internal/repository/mysql/dao"
	"bindbox-game/internal/repository/mysql/model"
)

type Service interface {
	CreateCategory(ctx context.Context, in CreateCategoryInput) (*model.ProductCategories, error)
	ModifyCategory(ctx context.Context, id int64, in ModifyCategoryInput) error
	DeleteCategory(ctx context.Context, id int64) error
	ListCategories(ctx context.Context, in ListCategoriesInput) (items []*model.ProductCategories, total int64, err error)

	CreateProduct(ctx context.Context, in CreateProductInput) (*model.Products, error)
	ModifyProduct(ctx context.Context, id int64, in ModifyProductInput) error
	DeleteProduct(ctx context.Context, id int64) error
	ListProducts(ctx context.Context, in ListProductsInput) (items []*model.Products, total int64, err error)
	BatchUpdate(ctx context.Context, ids []int64, stock *int64, status *int32) (int64, error)
	ListForApp(ctx context.Context, in AppListInput) (items []AppListItem, total int64, err error)
	GetDetailForApp(ctx context.Context, id int64) (*AppDetail, error)
	ListProductsByIDs(ctx context.Context, ids []int64) ([]*model.Products, error)
}

type service struct {
	logger      logger.CustomLogger
	readDB      *dao.Query
	writeDB     *dao.Query
	listCache   map[string]cachedList
	detailCache map[int64]cachedDetail
}

func New(l logger.CustomLogger, db mysql.Repo) Service {
	return &service{logger: l, readDB: dao.Use(db.GetDbR()), writeDB: dao.Use(db.GetDbW()), listCache: make(map[string]cachedList), detailCache: make(map[int64]cachedDetail)}
}

type CreateCategoryInput struct {
	Name     string
	ParentID int64
	Status   int32
}

type ModifyCategoryInput struct {
	Name     *string
	ParentID *int64
	Status   *int32
}

type ListCategoriesInput struct {
	Name     string
	Status   *int32
	Page     int
	PageSize int
}

func (s *service) CreateCategory(ctx context.Context, in CreateCategoryInput) (*model.ProductCategories, error) {
	m := &model.ProductCategories{Name: in.Name, ParentID: in.ParentID, Status: in.Status}
	if err := s.writeDB.ProductCategories.WithContext(ctx).Create(m); err != nil {
		return nil, err
	}
	return m, nil
}

func (s *service) ModifyCategory(ctx context.Context, id int64, in ModifyCategoryInput) error {
	updater := s.writeDB.ProductCategories.WithContext(ctx).Where(s.writeDB.ProductCategories.ID.Eq(id))
	set := map[string]any{}
	if in.Name != nil {
		set["name"] = *in.Name
	}
	if in.ParentID != nil {
		set["parent_id"] = *in.ParentID
	}
	if in.Status != nil {
		set["status"] = *in.Status
	}
	if len(set) == 0 {
		return nil
	}
	_, err := updater.Updates(set)
	return err
}

func (s *service) DeleteCategory(ctx context.Context, id int64) error {
	_, err := s.writeDB.ProductCategories.WithContext(ctx).Where(s.writeDB.ProductCategories.ID.Eq(id)).Updates(map[string]any{"deleted_at": time.Now()})
	return err
}

func (s *service) ListCategories(ctx context.Context, in ListCategoriesInput) (items []*model.ProductCategories, total int64, err error) {
	if in.Page <= 0 {
		in.Page = 1
	}
	if in.PageSize <= 0 {
		in.PageSize = 20
	}
	q := s.readDB.ProductCategories.WithContext(ctx).ReadDB()
	if in.Name != "" {
		q = q.Where(s.readDB.ProductCategories.Name.Like("%" + in.Name + "%"))
	}
	if in.Status != nil {
		q = q.Where(s.readDB.ProductCategories.Status.Eq(*in.Status))
	}
	total, err = q.Count()
	if err != nil {
		return
	}
	items, err = q.Order(s.readDB.ProductCategories.ID.Desc()).Limit(in.PageSize).Offset((in.Page - 1) * in.PageSize).Find()
	return
}

type CreateProductInput struct {
	Name        string
	CategoryID  int64
	ImagesJSON  string
	Price       int64
	Stock       int64
	Status      int32
	Description string
}

type ModifyProductInput struct {
	Name        *string
	CategoryID  *int64
	ImagesJSON  *string
	Price       *int64
	Stock       *int64
	Status      *int32
	Description *string
}

type ListProductsInput struct {
	Name       string
	CategoryID *int64
	Status     *int32
	Page       int
	PageSize   int
}

type AppListInput struct {
	CategoryID *int64
	PriceMin   *int64
	PriceMax   *int64
	SalesMin   *int64
	InStock    *bool
	SortBy     string
	Order      string
	Page       int
	PageSize   int
}

type AppListItem struct {
	ID        int64
	Name      string
	MainImage string
	Price     int64
	Sales     int64
	InStock   bool
}

type AppDetail struct {
	ID              int64
	Name            string
	Album           []string
	Price           int64
	Sales           int64
	Stock           int64
	Description     string
	Service         []string
	Recommendations []AppListItem
}

func (s *service) CreateProduct(ctx context.Context, in CreateProductInput) (*model.Products, error) {
	m := &model.Products{Name: in.Name, CategoryID: in.CategoryID, ImagesJSON: normalizeJSON(in.ImagesJSON), Price: in.Price, Stock: in.Stock, Status: in.Status, Description: in.Description}
	if err := s.writeDB.Products.WithContext(ctx).Create(m); err != nil {
		return nil, err
	}
	return m, nil
}

func (s *service) ModifyProduct(ctx context.Context, id int64, in ModifyProductInput) error {
	updater := s.writeDB.Products.WithContext(ctx).Where(s.writeDB.Products.ID.Eq(id))
	set := map[string]any{}
	if in.Name != nil {
		set["name"] = *in.Name
	}
	if in.CategoryID != nil {
		set["category_id"] = *in.CategoryID
	}
	if in.ImagesJSON != nil {
		set["images_json"] = normalizeJSON(*in.ImagesJSON)
	}
	if in.Price != nil {
		set["price"] = *in.Price
	}
	if in.Stock != nil {
		set["stock"] = *in.Stock
	}
	if in.Status != nil {
		set["status"] = *in.Status
	}
	if in.Description != nil {
		set["description"] = *in.Description
	}
	if len(set) == 0 {
		return nil
	}
	_, err := updater.Updates(set)
	return err
}

func (s *service) DeleteProduct(ctx context.Context, id int64) error {
	_, err := s.writeDB.Products.WithContext(ctx).Where(s.writeDB.Products.ID.Eq(id)).Updates(map[string]any{"deleted_at": time.Now()})
	return err
}

func (s *service) ListProducts(ctx context.Context, in ListProductsInput) (items []*model.Products, total int64, err error) {
	if in.Page <= 0 {
		in.Page = 1
	}
	if in.PageSize <= 0 {
		in.PageSize = 20
	}
	q := s.readDB.Products.WithContext(ctx).ReadDB()
	if in.Name != "" {
		q = q.Where(s.readDB.Products.Name.Like("%" + in.Name + "%"))
	}
	if in.CategoryID != nil {
		q = q.Where(s.readDB.Products.CategoryID.Eq(*in.CategoryID))
	}
	if in.Status != nil {
		q = q.Where(s.readDB.Products.Status.Eq(*in.Status))
	}
	total, err = q.Count()
	if err != nil {
		return
	}
	items, err = q.Order(s.readDB.Products.ID.Desc()).Limit(in.PageSize).Offset((in.Page - 1) * in.PageSize).Find()
	return
}

func (s *service) ListForApp(ctx context.Context, in AppListInput) (items []AppListItem, total int64, err error) {
	key := s.listKey(in)
	if v, ok := s.listCache[key]; ok && time.Now().Before(v.expireAt) {
		return v.items, v.total, nil
	}
	if in.Page <= 0 {
		in.Page = 1
	}
	if in.PageSize <= 0 {
		in.PageSize = 20
	}
	q := s.readDB.Products.WithContext(ctx).ReadDB().Where(s.readDB.Products.Status.Eq(1))
	if in.CategoryID != nil {
		q = q.Where(s.readDB.Products.CategoryID.Eq(*in.CategoryID))
	}
	if in.PriceMin != nil {
		q = q.Where(s.readDB.Products.Price.Gte(*in.PriceMin))
	}
	if in.PriceMax != nil {
		q = q.Where(s.readDB.Products.Price.Lte(*in.PriceMax))
	}
	if in.SalesMin != nil {
		q = q.Where(s.readDB.Products.Sales.Gte(*in.SalesMin))
	}
	if in.InStock != nil && *in.InStock {
		q = q.Where(s.readDB.Products.Stock.Gt(0))
	}
	total, err = q.Count()
	if err != nil {
		return
	}
	orderDesc := true
	if strings.ToLower(in.Order) == "asc" {
		orderDesc = false
	}
	switch strings.ToLower(in.SortBy) {
	case "price":
		if orderDesc {
			q = q.Order(s.readDB.Products.Price.Desc())
		} else {
			q = q.Order(s.readDB.Products.Price.Asc())
		}
	case "createdat", "created_at":
		if orderDesc {
			q = q.Order(s.readDB.Products.CreatedAt.Desc())
		} else {
			q = q.Order(s.readDB.Products.CreatedAt.Asc())
		}
	default:
		if orderDesc {
			q = q.Order(s.readDB.Products.Sales.Desc())
		} else {
			q = q.Order(s.readDB.Products.Sales.Asc())
		}
	}
	rows, err := q.Limit(in.PageSize).Offset((in.Page - 1) * in.PageSize).Find()
	if err != nil {
		return
	}
	items = make([]AppListItem, len(rows))
	for i, it := range rows {
		items[i] = AppListItem{ID: it.ID, Name: it.Name, MainImage: firstImage(it.ImagesJSON), Price: it.Price, Sales: it.Sales, InStock: it.Stock > 0}
	}
	s.listCache[key] = cachedList{items: items, total: total, expireAt: time.Now().Add(30 * time.Second)}
	return
}

func (s *service) GetDetailForApp(ctx context.Context, id int64) (*AppDetail, error) {
	if v, ok := s.detailCache[id]; ok && time.Now().Before(v.expireAt) {
		return v.detail, nil
	}
	p, err := s.readDB.Products.WithContext(ctx).Where(s.readDB.Products.ID.Eq(id)).Take()
	if err != nil {
		return nil, err
	}
	if p.Status != 1 {
		return nil, errors.New("PRODUCT_OFFSHELF")
	}
	if p.Stock <= 0 {
		return nil, errors.New("PRODUCT_OUT_OF_STOCK")
	}
	album := splitImages(p.ImagesJSON)
	d := &AppDetail{ID: p.ID, Name: p.Name, Album: album, Price: p.Price, Sales: p.Sales, Stock: p.Stock, Description: p.Description, Service: []string{}, Recommendations: []AppListItem{}}
	recQ := s.readDB.Products.WithContext(ctx).ReadDB().Where(s.readDB.Products.Status.Eq(1)).Where(s.readDB.Products.ID.Neq(p.ID))
	if p.CategoryID > 0 {
		recQ = recQ.Where(s.readDB.Products.CategoryID.Eq(p.CategoryID))
	}
	recs, _ := recQ.Order(s.readDB.Products.Sales.Desc()).Limit(6).Find()
	for _, it := range recs {
		d.Recommendations = append(d.Recommendations, AppListItem{ID: it.ID, Name: it.Name, MainImage: firstImage(it.ImagesJSON), Price: it.Price, Sales: it.Sales, InStock: it.Stock > 0})
	}
	s.detailCache[id] = cachedDetail{detail: d, expireAt: time.Now().Add(60 * time.Second)}
	return d, nil
}

func (s *service) BatchUpdate(ctx context.Context, ids []int64, stock *int64, status *int32) (int64, error) {
	if len(ids) == 0 {
		return 0, nil
	}
	set := map[string]any{}
	if stock != nil {
		set["stock"] = *stock
	}
	if status != nil {
		set["status"] = *status
	}
	if len(set) == 0 {
		return 0, nil
	}
	updater := s.writeDB.Products.WithContext(ctx).Where(s.writeDB.Products.ID.In(ids...))
	result, err := updater.Updates(set)
	if err != nil {
		return 0, err
	}
	return result.RowsAffected, nil
}

func (s *service) ListProductsByIDs(ctx context.Context, ids []int64) ([]*model.Products, error) {
	if len(ids) == 0 {
		return nil, nil
	}
	return s.readDB.Products.WithContext(ctx).Where(s.readDB.Products.ID.In(ids...)).Find()
}

func normalizeJSON(s string) string {
	if strings.TrimSpace(s) == "" {
		return "[]"
	}
	var v any
	if json.Unmarshal([]byte(s), &v) != nil {
		return "[]"
	}
	return s
}

func (s *service) listKey(in AppListInput) string {
	b, _ := json.Marshal(in)
	return string(b)
}

func splitImages(s string) []string {
	var arr []string
	if strings.TrimSpace(s) == "" {
		return arr
	}
	var v []string
	if json.Unmarshal([]byte(s), &v) != nil {
		return arr
	}
	return v
}

func firstImage(s string) string {
	imgs := splitImages(s)
	if len(imgs) > 0 {
		return imgs[0]
	}
	return ""
}

type cachedList struct {
	items    []AppListItem
	total    int64
	expireAt time.Time
}

type cachedDetail struct {
	detail   *AppDetail
	expireAt time.Time
}
