package repository

import (
	"context"

	libmongo "apps/backend/internal/lib/mongo"
	"apps/backend/internal/module/scout/domain"

	"github.com/zeromicro/go-zero/core/stores/mon"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/mongo/options"
)

type MonStore struct {
	brands   *mon.Model
	products *mon.Model
	active   *mon.Model
	posts    *mon.Model
	runs     *mon.Model
	seen     *mon.Model
	hw       *mon.Model
	crawler  *mon.Model
}

func NewMonStore(uri, database string) *MonStore {
	uri = libmongo.MustMongoURI(uri)
	return &MonStore{
		brands:   mon.MustNewModel(uri, database, "scout_brands"),
		products: mon.MustNewModel(uri, database, "scout_products"),
		active:   mon.MustNewModel(uri, database, "scout_active_brand"),
		posts:    mon.MustNewModel(uri, database, "scout_posts"),
		runs:     mon.MustNewModel(uri, database, "scout_runs"),
		seen:     mon.MustNewModel(uri, database, "scout_seen_identities"),
		hw:       mon.MustNewModel(uri, database, "scout_homework"),
		crawler:  mon.MustNewModel(uri, database, "scout_crawler_session"),
	}
}

func (s *MonStore) SaveBrand(ctx context.Context, b *domain.Brand) error {
	_, err := s.brands.ReplaceOne(ctx, bson.M{"_id": b.ID}, b, options.Replace().SetUpsert(true))
	return err
}
func (s *MonStore) GetBrand(ctx context.Context, id string) (*domain.Brand, error) {
	var b domain.Brand
	err := s.brands.FindOne(ctx, &b, bson.M{"_id": id})
	if err != nil {
		if err == mon.ErrNotFound {
			return nil, domain.ErrNotFound
		}
		return nil, err
	}
	return &b, nil
}
func (s *MonStore) ListBrands(ctx context.Context, ownerUID int64) ([]*domain.Brand, error) {
	var list []*domain.Brand
	err := s.brands.Find(ctx, &list, bson.M{"owner_uid": ownerUID})
	return list, err
}
func (s *MonStore) DeleteBrand(ctx context.Context, id string) error {
	res, err := s.brands.DeleteOne(ctx, bson.M{"_id": id})
	if err != nil {
		return err
	}
	if res == 0 {
		return domain.ErrNotFound
	}
	return nil
}

func (s *MonStore) SaveProduct(ctx context.Context, p *domain.Product) error {
	_, err := s.products.ReplaceOne(ctx, bson.M{"_id": p.ID}, p, options.Replace().SetUpsert(true))
	return err
}
func (s *MonStore) GetProduct(ctx context.Context, id string) (*domain.Product, error) {
	var p domain.Product
	err := s.products.FindOne(ctx, &p, bson.M{"_id": id})
	if err != nil {
		if err == mon.ErrNotFound {
			return nil, domain.ErrNotFound
		}
		return nil, err
	}
	return &p, nil
}
func (s *MonStore) ListProducts(ctx context.Context, ownerUID int64, brandID string) ([]*domain.Product, error) {
	filter := bson.M{"owner_uid": ownerUID}
	if brandID != "" {
		filter["brand_id"] = brandID
	}
	var list []*domain.Product
	err := s.products.Find(ctx, &list, filter)
	return list, err
}
func (s *MonStore) DeleteProduct(ctx context.Context, id string) error {
	res, err := s.products.DeleteOne(ctx, bson.M{"_id": id})
	if err != nil {
		return err
	}
	if res == 0 {
		return domain.ErrNotFound
	}
	return nil
}

func (s *MonStore) GetActiveBrandID(ctx context.Context, ownerUID int64) (string, error) {
	var a domain.ActiveBrand
	err := s.active.FindOne(ctx, &a, bson.M{"_id": ownerUID})
	if err != nil {
		if err == mon.ErrNotFound {
			return "", nil
		}
		return "", err
	}
	return a.BrandID, nil
}
func (s *MonStore) SetActiveBrandID(ctx context.Context, ownerUID int64, brandID string) error {
	doc := &domain.ActiveBrand{OwnerUID: ownerUID, BrandID: brandID, UpdatedAt: domain.NowNano()}
	_, err := s.active.ReplaceOne(ctx, bson.M{"_id": ownerUID}, doc, options.Replace().SetUpsert(true))
	return err
}

func (s *MonStore) SavePost(ctx context.Context, p *domain.Post) error {
	_, err := s.posts.ReplaceOne(ctx, bson.M{"_id": p.ID}, p, options.Replace().SetUpsert(true))
	return err
}
func (s *MonStore) GetPost(ctx context.Context, id string) (*domain.Post, error) {
	var p domain.Post
	err := s.posts.FindOne(ctx, &p, bson.M{"_id": id})
	if err != nil {
		if err == mon.ErrNotFound {
			return nil, domain.ErrNotFound
		}
		return nil, err
	}
	return &p, nil
}
func (s *MonStore) ListPosts(ctx context.Context, ownerUID int64, brandID string) ([]*domain.Post, error) {
	filter := bson.M{"owner_uid": ownerUID}
	if brandID != "" {
		filter["brand_id"] = brandID
	}
	var list []*domain.Post
	// score 是品質主排序；時間只作相同分數的 deterministic tie-breaker。
	err := s.posts.Find(ctx, &list, filter, options.Find().SetSort(bson.D{
		{Key: "score", Value: -1},
		{Key: "posted_at", Value: -1},
		{Key: "created_at", Value: -1},
		{Key: "_id", Value: -1},
	}))
	return list, err
}
func (s *MonStore) DeletePost(ctx context.Context, id string) error {
	res, err := s.posts.DeleteOne(ctx, bson.M{"_id": id})
	if err != nil {
		return err
	}
	if res == 0 {
		return domain.ErrNotFound
	}
	return nil
}
func (s *MonStore) DeletePostsByTheme(ctx context.Context, ownerUID int64, themeKey string) error {
	_, err := s.posts.DeleteMany(ctx, bson.M{"owner_uid": ownerUID, "theme_key": themeKey})
	return err
}

func (s *MonStore) SaveHomework(ctx context.Context, h *domain.Homework) error {
	// composite: store theme as id, filter by owner
	docID := formatKey(h.OwnerUID, h.ThemeKey)
	type hwDoc struct {
		ID string `bson:"_id"`
		*domain.Homework
	}
	_, err := s.hw.ReplaceOne(ctx, bson.M{"_id": docID}, &hwDoc{ID: docID, Homework: h}, options.Replace().SetUpsert(true))
	return err
}
func (s *MonStore) GetHomework(ctx context.Context, ownerUID int64, themeKey string) (*domain.Homework, error) {
	type hwDoc struct {
		ID              string `bson:"_id"`
		domain.Homework `bson:",inline"`
	}
	var doc hwDoc
	err := s.hw.FindOne(ctx, &doc, bson.M{"_id": formatKey(ownerUID, themeKey)})
	if err != nil {
		if err == mon.ErrNotFound {
			return nil, domain.ErrNotFound
		}
		return nil, err
	}
	h := doc.Homework
	return &h, nil
}
func (s *MonStore) ListHomework(ctx context.Context, ownerUID int64) ([]*domain.Homework, error) {
	var list []*domain.Homework
	err := s.hw.Find(ctx, &list, bson.M{"owner_uid": ownerUID})
	return list, err
}
func (s *MonStore) DeleteHomework(ctx context.Context, ownerUID int64, themeKey string) error {
	res, err := s.hw.DeleteOne(ctx, bson.M{"_id": formatKey(ownerUID, themeKey)})
	if err != nil {
		return err
	}
	if res == 0 {
		return domain.ErrNotFound
	}
	return nil
}

func (s *MonStore) GetCrawlerSession(ctx context.Context, ownerUID int64) (*domain.CrawlerSession, error) {
	var c domain.CrawlerSession
	err := s.crawler.FindOne(ctx, &c, bson.M{"_id": ownerUID})
	if err != nil {
		if err == mon.ErrNotFound {
			return nil, domain.ErrNotFound
		}
		return nil, err
	}
	if c.StorageStateEnc == "" {
		return nil, domain.ErrNotFound
	}
	return &c, nil
}
func (s *MonStore) SetCrawlerSession(ctx context.Context, sess *domain.CrawlerSession) error {
	_, err := s.crawler.ReplaceOne(ctx, bson.M{"_id": sess.OwnerUID}, sess, options.Replace().SetUpsert(true))
	return err
}
func (s *MonStore) ClearCrawlerSession(ctx context.Context, ownerUID int64) error {
	_, err := s.crawler.DeleteOne(ctx, bson.M{"_id": ownerUID})
	return err
}

func (s *MonStore) CreateRun(ctx context.Context, r *domain.Run) error {
	if r == nil || r.ID == "" || r.OwnerUID == 0 {
		return domain.ErrValidation
	}
	_, err := s.runs.InsertOne(ctx, r)
	return err
}

func (s *MonStore) GetRun(ctx context.Context, ownerUID int64, id string) (*domain.Run, error) {
	var r domain.Run
	err := s.runs.FindOne(ctx, &r, bson.M{"_id": id, "owner_uid": ownerUID})
	if err != nil {
		if err == mon.ErrNotFound {
			return nil, domain.ErrNotFound
		}
		return nil, err
	}
	return &r, nil
}

func (s *MonStore) ListRuns(ctx context.Context, filter domain.RunFilter) (domain.RunPage, error) {
	page := domain.NormalizePage(filter.Page, filter.PageSize)
	q := bson.M{"owner_uid": filter.OwnerUID}
	if filter.BrandID != "" {
		q["brand_id"] = filter.BrandID
	}
	if filter.Mode != "" {
		q["mode"] = filter.Mode
	}
	total, err := s.runs.CountDocuments(ctx, q)
	if err != nil {
		return domain.RunPage{}, err
	}
	page = page.WithTotal(total)
	var list []*domain.Run
	err = s.runs.Find(ctx, &list, q, options.Find().SetSort(bson.D{
		{Key: "created_at", Value: -1}, {Key: "_id", Value: -1},
	}).SetSkip(int64((page.Page-1)*page.PageSize)).SetLimit(int64(page.PageSize)))
	return domain.RunPage{Items: list, Pagination: page}, err
}

func (s *MonStore) ListRunPosts(ctx context.Context, ownerUID int64, runID string, requestedPage, requestedSize int) (domain.RunPostPage, error) {
	r, err := s.GetRun(ctx, ownerUID, runID)
	if err != nil {
		return domain.RunPostPage{}, err
	}
	page := domain.NormalizePage(requestedPage, requestedSize)
	// 與 memory store 一致的可見性屏障：run 成功前的候選是暫存資料，不得外流。
	if r.Status != domain.RunSucceeded {
		return domain.RunPostPage{Run: r, Items: []*domain.Post{}, Pagination: page.WithTotal(0)}, nil
	}
	q := bson.M{"owner_uid": ownerUID, "run_id": runID}
	total, err := s.posts.CountDocuments(ctx, q)
	if err != nil {
		return domain.RunPostPage{}, err
	}
	page = page.WithTotal(total)
	var list []*domain.Post
	err = s.posts.Find(ctx, &list, q, options.Find().SetSort(bson.D{
		{Key: "score", Value: -1}, {Key: "posted_at", Value: -1}, {Key: "created_at", Value: -1}, {Key: "_id", Value: -1},
	}).SetSkip(int64((page.Page-1)*page.PageSize)).SetLimit(int64(page.PageSize)))
	return domain.RunPostPage{Run: r, Items: list, Pagination: page}, err
}

func (s *MonStore) ReplaceRunGuarded(ctx context.Context, ownerUID int64, id string, expected []string, replacement *domain.Run) error {
	if replacement == nil || replacement.ID != id {
		return domain.ErrValidation
	}
	filter := bson.M{"_id": id, "owner_uid": ownerUID}
	if len(expected) > 0 {
		filter["status"] = bson.M{"$in": expected}
	}
	res, err := s.runs.ReplaceOne(ctx, filter, replacement)
	if err != nil {
		return err
	}
	if res.MatchedCount == 0 {
		if _, getErr := s.GetRun(ctx, ownerUID, id); getErr != nil {
			return getErr
		}
		return domain.ErrIllegalRunStatus
	}
	return nil
}

func (s *MonStore) DeleteRun(ctx context.Context, ownerUID int64, id string) error {
	res, err := s.runs.DeleteOne(ctx, bson.M{"_id": id, "owner_uid": ownerUID})
	if err != nil {
		return err
	}
	if res == 0 {
		return domain.ErrNotFound
	}
	_, err = s.posts.DeleteMany(ctx, bson.M{"owner_uid": ownerUID, "run_id": id})
	return err
}

func (s *MonStore) PublishRunPosts(ctx context.Context, ownerUID int64, runID string, posts []*domain.Post) error {
	if _, err := s.GetRun(ctx, ownerUID, runID); err != nil {
		return err
	}
	for _, p := range posts {
		if p == nil || p.ID == "" || p.OwnerUID != ownerUID || p.RunID != runID {
			return domain.ErrValidation
		}
	}
	for _, p := range posts {
		if _, err := s.posts.ReplaceOne(ctx, bson.M{"_id": p.ID, "owner_uid": ownerUID}, p, options.Replace().SetUpsert(true)); err != nil {
			return err
		}
	}
	return nil
}

func (s *MonStore) HasSeenIdentity(ctx context.Context, ownerUID int64, identity string) (bool, error) {
	if identity == "" {
		return false, domain.ErrValidation
	}
	var doc struct {
		ID string `bson:"_id"`
	}
	err := s.seen.FindOne(ctx, &doc, bson.M{"_id": identityKey(ownerUID, identity)})
	if err == mon.ErrNotFound {
		return false, nil
	}
	return err == nil, err
}

func (s *MonStore) MarkSeenIdentity(ctx context.Context, ownerUID int64, identity, postID string, seenAt int64) error {
	if identity == "" {
		return domain.ErrValidation
	}
	doc := bson.M{"_id": identityKey(ownerUID, identity), "owner_uid": ownerUID, "identity": identity, "post_id": postID, "seen_at": seenAt}
	_, err := s.seen.ReplaceOne(ctx, bson.M{"_id": identityKey(ownerUID, identity)}, doc, options.Replace().SetUpsert(true))
	return err
}

func formatKey(uid int64, theme string) string {
	return formatUID(uid) + "|" + theme
}

var _ domain.Repository = (*MonStore)(nil)
