package services

import (
	"context"
	"errors"
	"time"

	"quyun/v2/app/http/tenant/dto"
	"quyun/v2/app/requests"
	"quyun/v2/database"
	"quyun/v2/database/models"
	"quyun/v2/pkg/consts"

	pkgerrors "github.com/pkg/errors"
	"github.com/samber/lo"
	log "github.com/sirupsen/logrus"
	"go.ipao.vip/gen"
	"gorm.io/gorm"
)

// content 实现内容域相关的业务能力（创建/更新/定价/授权等）。
//
// @provider
type content struct{}

// ContentDetailResult 为内容详情的内部结果（供 controller 组合返回）。
type ContentDetailResult struct {
	// Content 内容实体。
	Content *models.Content
	// Price 定价信息（可能为 nil，表示未设置价格）。
	Price *models.ContentPrice
	// HasAccess 当前用户是否拥有主资源访问权限。
	HasAccess bool
}

func (s *content) Create(ctx context.Context, tenantID, userID int64, form *dto.ContentCreateForm) (*models.Content, error) {
	log.WithFields(log.Fields{
		"tenant_id": tenantID,
		"user_id":   userID,
	}).Info("services.content.create")

	// 关键默认值：未传可见性时默认“租户内可见”。
	visibility := form.Visibility
	if visibility == "" {
		visibility = consts.ContentVisibilityTenantOnly
	}

	// 试看策略：默认固定时长；并强制不允许下载。
	previewSeconds := consts.DefaultContentPreviewSeconds
	if form.PreviewSeconds != nil && *form.PreviewSeconds > 0 {
		previewSeconds = *form.PreviewSeconds
	}

	m := &models.Content{
		TenantID:            tenantID,
		UserID:              userID,
		Title:               form.Title,
		Description:         form.Description,
		Status:              consts.ContentStatusDraft,
		Visibility:          visibility,
		PreviewSeconds:      previewSeconds,
		PreviewDownloadable: false,
	}
	if err := m.Create(ctx); err != nil {
		return nil, pkgerrors.Wrap(err, "create content failed")
	}
	return m, nil
}

func (s *content) Update(ctx context.Context, tenantID, userID, contentID int64, form *dto.ContentUpdateForm) (*models.Content, error) {
	log.WithFields(log.Fields{
		"tenant_id":  tenantID,
		"user_id":    userID,
		"content_id": contentID,
	}).Info("services.content.update")

	tbl, query := models.ContentQuery.QueryContext(ctx)
	m, err := query.Where(
		tbl.TenantID.Eq(tenantID),
		tbl.ID.Eq(contentID),
	).First()
	if err != nil {
		return nil, pkgerrors.Wrap(err, "content not found")
	}

	if form.Title != nil {
		m.Title = *form.Title
	}
	if form.Description != nil {
		m.Description = *form.Description
	}
	if form.Visibility != nil {
		m.Visibility = *form.Visibility
	}
	if form.PreviewSeconds != nil && *form.PreviewSeconds > 0 {
		m.PreviewSeconds = *form.PreviewSeconds
		m.PreviewDownloadable = false
	}
	if form.Status != nil {
		m.Status = *form.Status
		// 发布动作：首次发布时补齐发布时间，便于后续排序/检索与审计。
		if m.Status == consts.ContentStatusPublished && m.PublishedAt.IsZero() {
			m.PublishedAt = time.Now()
		}
	}

	if _, err := m.Update(ctx); err != nil {
		return nil, pkgerrors.Wrap(err, "update content failed")
	}
	return m, nil
}

func (s *content) UpsertPrice(ctx context.Context, tenantID, userID, contentID int64, form *dto.ContentPriceUpsertForm) (*models.ContentPrice, error) {
	log.WithFields(log.Fields{
		"tenant_id":  tenantID,
		"user_id":    userID,
		"content_id": contentID,
		"amount":     form.PriceAmount,
	}).Info("services.content.upsert_price")

	currency := form.Currency
	if currency == "" {
		currency = consts.CurrencyCNY
	}
	discountType := form.DiscountType
	if discountType == "" {
		discountType = consts.DiscountTypeNone
	}

	startAt := time.Time{}
	if form.DiscountStartAt != nil {
		startAt = *form.DiscountStartAt
	}
	endAt := time.Time{}
	if form.DiscountEndAt != nil {
		endAt = *form.DiscountEndAt
	}

	tbl, query := models.ContentPriceQuery.QueryContext(ctx)
	m, err := query.Where(
		tbl.TenantID.Eq(tenantID),
		tbl.ContentID.Eq(contentID),
	).First()
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, pkgerrors.Wrap(err, "find content price failed")
	}
	if errors.Is(err, gorm.ErrRecordNotFound) {
		m = &models.ContentPrice{
			TenantID:        tenantID,
			UserID:          userID,
			ContentID:       contentID,
			Currency:        currency,
			PriceAmount:     form.PriceAmount,
			DiscountType:    discountType,
			DiscountValue:   form.DiscountValue,
			DiscountStartAt: startAt,
			DiscountEndAt:   endAt,
		}
		if err := m.Create(ctx); err != nil {
			return nil, pkgerrors.Wrap(err, "create content price failed")
		}
		return m, nil
	}

	m.UserID = userID
	m.Currency = currency
	m.PriceAmount = form.PriceAmount
	m.DiscountType = discountType
	m.DiscountValue = form.DiscountValue
	m.DiscountStartAt = startAt
	m.DiscountEndAt = endAt

	if _, err := m.Update(ctx); err != nil {
		return nil, pkgerrors.Wrap(err, "update content price failed")
	}
	return m, nil
}

func (s *content) AttachAsset(ctx context.Context, tenantID, userID, contentID, assetID int64, role consts.ContentAssetRole, sort int32, now time.Time) (*models.ContentAsset, error) {
	log.WithFields(log.Fields{
		"tenant_id":  tenantID,
		"user_id":    userID,
		"content_id": contentID,
		"asset_id":   assetID,
		"role":       role,
		"sort":       sort,
	}).Info("services.content.attach_asset")

	m := &models.ContentAsset{
		TenantID:  tenantID,
		UserID:    userID,
		ContentID: contentID,
		AssetID:   assetID,
		Role:      role,
		Sort:      sort,
		CreatedAt: now,
		UpdatedAt: now,
	}
	if err := m.Create(ctx); err != nil {
		return nil, pkgerrors.Wrap(err, "attach content asset failed")
	}
	return m, nil
}

func (s *content) ListPublished(ctx context.Context, tenantID, userID int64, filter *dto.ContentListFilter) (*requests.Pager, error) {
	log.WithFields(log.Fields{
		"tenant_id": tenantID,
		"user_id":   userID,
		"page":      filter.Page,
		"limit":     filter.Limit,
	}).Info("services.content.list_published")

	tbl, query := models.ContentQuery.QueryContext(ctx)

	conds := []gen.Condition{
		tbl.TenantID.Eq(tenantID),
		tbl.Status.Eq(consts.ContentStatusPublished),
		tbl.Visibility.In(consts.ContentVisibilityPublic, consts.ContentVisibilityTenantOnly),
	}
	if filter.Keyword != nil && *filter.Keyword != "" {
		conds = append(conds, tbl.Title.Like(database.WrapLike(*filter.Keyword)))
	}

	filter.Pagination.Format()
	items, total, err := query.Where(conds...).Order(tbl.ID.Desc()).FindByPage(int(filter.Offset()), int(filter.Limit))
	if err != nil {
		return nil, err
	}

	contentIDs := lo.Map(items, func(item *models.Content, _ int) int64 { return item.ID })

	priceByContent, err := s.contentPriceMapping(ctx, tenantID, contentIDs)
	if err != nil {
		return nil, err
	}

	accessSet, err := s.accessSet(ctx, tenantID, userID, contentIDs)
	if err != nil {
		return nil, err
	}

	respItems := lo.Map(items, func(model *models.Content, _ int) *dto.ContentItem {
		price := priceByContent[model.ID]
		free := price == nil || price.PriceAmount == 0
		has := free || accessSet[model.ID] || model.UserID == userID
		return &dto.ContentItem{
			Content:   model,
			Price:     price,
			HasAccess: has,
		}
	})

	return &requests.Pager{
		Pagination: filter.Pagination,
		Total:      total,
		Items:      respItems,
	}, nil
}

func (s *content) Detail(ctx context.Context, tenantID, userID, contentID int64) (*ContentDetailResult, error) {
	log.WithFields(log.Fields{
		"tenant_id":  tenantID,
		"user_id":    userID,
		"content_id": contentID,
	}).Info("services.content.detail")

	tbl, query := models.ContentQuery.QueryContext(ctx)
	model, err := query.Where(
		tbl.TenantID.Eq(tenantID),
		tbl.ID.Eq(contentID),
	).First()
	if err != nil {
		return nil, pkgerrors.Wrapf(err, "content not found, tenantID=%d, contentID=%d", tenantID, contentID)
	}

	if model.Status != consts.ContentStatusPublished && model.UserID != userID {
		return nil, errors.New("content is not published")
	}

	price, err := s.contentPrice(ctx, tenantID, contentID)
	if err != nil {
		return nil, err
	}
	free := price == nil || price.PriceAmount == 0

	canView := false
	switch model.Visibility {
	case consts.ContentVisibilityPublic, consts.ContentVisibilityTenantOnly:
		canView = true
	case consts.ContentVisibilityPrivate:
		canView = model.UserID == userID
	default:
		canView = false
	}

	hasAccess := model.UserID == userID || free
	if !hasAccess {
		ok, err := s.HasAccess(ctx, tenantID, userID, contentID)
		if err != nil {
			return nil, err
		}
		hasAccess = ok
		canView = canView || ok
	}

	if !canView {
		return nil, errors.New("content is private")
	}

	return &ContentDetailResult{
		Content:   model,
		Price:     price,
		HasAccess: hasAccess,
	}, nil
}

func (s *content) HasAccess(ctx context.Context, tenantID, userID, contentID int64) (bool, error) {
	log.WithFields(log.Fields{
		"tenant_id":  tenantID,
		"user_id":    userID,
		"content_id": contentID,
	}).Info("services.content.has_access")

	tbl, query := models.ContentAccessQuery.QueryContext(ctx)
	_, err := query.Where(
		tbl.TenantID.Eq(tenantID),
		tbl.UserID.Eq(userID),
		tbl.ContentID.Eq(contentID),
		tbl.Status.Eq(consts.ContentAccessStatusActive),
	).First()
	if err != nil {
		return false, nil
	}
	return true, nil
}

func (s *content) AssetsByRole(ctx context.Context, tenantID, contentID int64, role consts.ContentAssetRole) ([]*models.MediaAsset, error) {
	log.WithFields(log.Fields{
		"tenant_id":  tenantID,
		"content_id": contentID,
		"role":       role,
	}).Info("services.content.assets_by_role")

	maTbl, maQuery := models.MediaAssetQuery.QueryContext(ctx)
	caTbl, _ := models.ContentAssetQuery.QueryContext(ctx)

	assets, err := maQuery.
		LeftJoin(caTbl, caTbl.AssetID.EqCol(maTbl.ID)).
		Select(maTbl.ALL).
		Where(
			maTbl.TenantID.Eq(tenantID),
			caTbl.TenantID.Eq(tenantID),
			caTbl.ContentID.Eq(contentID),
			caTbl.Role.Eq(role),
		).
		Order(caTbl.Sort.Asc()).
		Find()
	if err != nil {
		return nil, err
	}
	return assets, nil
}

func (s *content) contentPrice(ctx context.Context, tenantID, contentID int64) (*models.ContentPrice, error) {
	tbl, query := models.ContentPriceQuery.QueryContext(ctx)
	m, err := query.Where(
		tbl.TenantID.Eq(tenantID),
		tbl.ContentID.Eq(contentID),
	).First()
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, nil
		}
		return nil, err
	}
	return m, nil
}

func (s *content) contentPriceMapping(ctx context.Context, tenantID int64, contentIDs []int64) (map[int64]*models.ContentPrice, error) {
	if len(contentIDs) == 0 {
		return map[int64]*models.ContentPrice{}, nil
	}

	tbl, query := models.ContentPriceQuery.QueryContext(ctx)
	items, err := query.Where(
		tbl.TenantID.Eq(tenantID),
		tbl.ContentID.In(contentIDs...),
	).Find()
	if err != nil {
		return nil, err
	}

	return lo.SliceToMap(items, func(item *models.ContentPrice) (int64, *models.ContentPrice) {
		return item.ContentID, item
	}), nil
}

func (s *content) accessSet(ctx context.Context, tenantID, userID int64, contentIDs []int64) (map[int64]bool, error) {
	if len(contentIDs) == 0 {
		return map[int64]bool{}, nil
	}

	tbl, query := models.ContentAccessQuery.QueryContext(ctx)
	items, err := query.Where(
		tbl.TenantID.Eq(tenantID),
		tbl.UserID.Eq(userID),
		tbl.ContentID.In(contentIDs...),
		tbl.Status.Eq(consts.ContentAccessStatusActive),
	).Find()
	if err != nil {
		return nil, err
	}

	out := make(map[int64]bool, len(items))
	for _, item := range items {
		out[item.ContentID] = true
	}
	return out, nil
}
