package repository

import (
	"context"
	"strings"

	app "haixun-backend/internal/library/errors"
	"haixun-backend/internal/library/errors/code"
	"haixun-backend/internal/model/copy_draft/domain/entity"
	domrepo "haixun-backend/internal/model/copy_draft/domain/repository"

	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
	"go.mongodb.org/mongo-driver/mongo/writeconcern"
)

type mongoRepository struct {
	collection *mongo.Collection
}

func NewMongoRepository(db *mongo.Database) domrepo.Repository {
	if db == nil {
		return &mongoRepository{}
	}
	return &mongoRepository{collection: db.Collection(entity.CollectionName)}
}

func (r *mongoRepository) EnsureIndexes(ctx context.Context) error {
	if r.collection == nil {
		return nil
	}
	_, err := r.collection.Indexes().CreateMany(ctx, []mongo.IndexModel{
		{
			Keys: bson.D{
				{Key: "tenant_id", Value: 1},
				{Key: "owner_uid", Value: 1},
				{Key: "persona_id", Value: 1},
				{Key: "create_at", Value: -1},
			},
		},
		{
			Keys: bson.D{
				{Key: "tenant_id", Value: 1},
				{Key: "owner_uid", Value: 1},
				{Key: "persona_id", Value: 1},
				{Key: "content_plan_id", Value: 1},
				{Key: "create_at", Value: -1},
			},
		},
		{
			Keys: bson.D{
				{Key: "tenant_id", Value: 1},
				{Key: "owner_uid", Value: 1},
				{Key: "persona_id", Value: 1},
				{Key: "copy_mission_id", Value: 1},
				{Key: "sort_order", Value: 1},
			},
		},
	})
	return err
}

func personaFilter(tenantID, ownerUID, personaID string) bson.M {
	return bson.M{
		"tenant_id":  strings.TrimSpace(tenantID),
		"owner_uid":  strings.TrimSpace(ownerUID),
		"persona_id": strings.TrimSpace(personaID),
	}
}

func (r *mongoRepository) CreateMany(ctx context.Context, drafts []*entity.CopyDraft) error {
	if r.collection == nil {
		return app.For(code.Persona).DBUnavailable("Mongo is not configured")
	}
	if len(drafts) == 0 {
		return nil
	}
	docs := make([]any, 0, len(drafts))
	for _, draft := range drafts {
		if draft == nil {
			continue
		}
		docs = append(docs, draft)
	}
	if len(docs) == 0 {
		return nil
	}
	_, err := r.collection.InsertMany(ctx, docs)
	return err
}

func (r *mongoRepository) Create(ctx context.Context, draft *entity.CopyDraft) error {
	if r.collection == nil {
		return app.For(code.Persona).DBUnavailable("Mongo is not configured")
	}
	if draft == nil {
		return app.For(code.Persona).InputMissingRequired("draft is required")
	}
	_, err := r.collection.InsertOne(ctx, draft)
	return err
}

func inboxStatusFilter(status string) bson.M {
	switch strings.TrimSpace(status) {
	case "draft":
		return bson.M{
			"$and": []bson.M{
				{"$or": []bson.M{{"publish_queue_id": ""}, {"publish_queue_id": bson.M{"$exists": false}}}},
				{"$or": []bson.M{{"published_at": 0}, {"published_at": bson.M{"$exists": false}}}},
			},
		}
	case "scheduled":
		return bson.M{
			"publish_queue_id": bson.M{"$ne": ""},
			"$or":              []bson.M{{"published_at": 0}, {"published_at": bson.M{"$exists": false}}},
		}
	case "published":
		return bson.M{"published_at": bson.M{"$gt": 0}}
	default:
		return bson.M{}
	}
}

func normalizeInboxPage(page, pageSize int) (int, int) {
	if page <= 0 {
		page = 1
	}
	if pageSize <= 0 {
		pageSize = 12
	}
	if pageSize > 100 {
		pageSize = 100
	}
	return page, pageSize
}

func (r *mongoRepository) ListInbox(ctx context.Context, filter domrepo.InboxFilter) (*domrepo.InboxListResult, error) {
	if r.collection == nil {
		return nil, app.For(code.Persona).DBUnavailable("Mongo is not configured")
	}
	page, pageSize := normalizeInboxPage(filter.Page, filter.PageSize)
	mongoFilter := personaFilter(filter.TenantID, filter.OwnerUID, filter.PersonaID)
	if extra := inboxStatusFilter(filter.Status); len(extra) > 0 {
		for k, v := range extra {
			mongoFilter[k] = v
		}
	}
	if filter.RangeStart > 0 || filter.RangeEnd > 0 {
		rangeClauses := make([]bson.M, 0, 3)
		createRange := bson.M{}
		if filter.RangeStart > 0 {
			createRange["$gte"] = filter.RangeStart
		}
		if filter.RangeEnd > 0 {
			createRange["$lte"] = filter.RangeEnd
		}
		if len(createRange) > 0 {
			rangeClauses = append(rangeClauses, bson.M{"create_at": createRange})
		}
		publishedRange := bson.M{}
		if filter.RangeStart > 0 {
			publishedRange["$gte"] = filter.RangeStart
		}
		if filter.RangeEnd > 0 {
			publishedRange["$lte"] = filter.RangeEnd
		}
		if len(publishedRange) > 0 {
			rangeClauses = append(rangeClauses, bson.M{"published_at": publishedRange})
		}
		if len(rangeClauses) > 0 {
			mongoFilter["$or"] = rangeClauses
		}
	}
	total, err := r.collection.CountDocuments(ctx, mongoFilter)
	if err != nil {
		return nil, err
	}
	opts := options.Find().
		SetSort(bson.D{{Key: "create_at", Value: -1}}).
		SetSkip(int64((page - 1) * pageSize)).
		SetLimit(int64(pageSize))
	cur, err := r.collection.Find(ctx, mongoFilter, opts)
	if err != nil {
		return nil, err
	}
	defer cur.Close(ctx)
	var out []entity.CopyDraft
	if err := cur.All(ctx, &out); err != nil {
		return nil, err
	}
	return &domrepo.InboxListResult{Items: out, Total: total}, nil
}

func (r *mongoRepository) List(ctx context.Context, tenantID, ownerUID, personaID string, limit int) ([]entity.CopyDraft, error) {
	if r.collection == nil {
		return nil, app.For(code.Persona).DBUnavailable("Mongo is not configured")
	}
	if limit <= 0 {
		limit = 50
	}
	if limit > 200 {
		limit = 200
	}
	opts := options.Find().
		SetSort(bson.D{{Key: "sort_order", Value: 1}, {Key: "create_at", Value: -1}}).
		SetLimit(int64(limit))
	cur, err := r.collection.Find(ctx, personaFilter(tenantID, ownerUID, personaID), opts)
	if err != nil {
		return nil, err
	}
	defer cur.Close(ctx)
	var out []entity.CopyDraft
	if err := cur.All(ctx, &out); err != nil {
		return nil, err
	}
	return out, nil
}

func (r *mongoRepository) Get(ctx context.Context, tenantID, ownerUID, personaID, draftID string) (*entity.CopyDraft, error) {
	if r.collection == nil {
		return nil, app.For(code.Persona).DBUnavailable("Mongo is not configured")
	}
	draftID = strings.TrimSpace(draftID)
	if draftID == "" {
		return nil, app.For(code.Persona).InputMissingRequired("draft_id is required")
	}
	filter := personaFilter(tenantID, ownerUID, personaID)
	filter["_id"] = draftID
	var out entity.CopyDraft
	err := r.collection.FindOne(ctx, filter).Decode(&out)
	if err != nil {
		if err == mongo.ErrNoDocuments {
			return nil, app.For(code.Persona).ResNotFound("copy draft not found")
		}
		return nil, err
	}
	return &out, nil
}

func (r *mongoRepository) Update(ctx context.Context, tenantID, ownerUID, personaID, draftID string, patch map[string]interface{}) (*entity.CopyDraft, error) {
	if r.collection == nil {
		return nil, app.For(code.Persona).DBUnavailable("Mongo is not configured")
	}
	if len(patch) == 0 {
		return nil, app.For(code.Persona).InputMissingRequired("patch is required")
	}
	draftID = strings.TrimSpace(draftID)
	filter := personaFilter(tenantID, ownerUID, personaID)
	filter["_id"] = draftID
	opts := options.FindOneAndUpdate().SetReturnDocument(options.After)
	var out entity.CopyDraft
	err := r.collection.FindOneAndUpdate(ctx, filter, bson.M{"$set": patch}, opts).Decode(&out)
	if err != nil {
		if err == mongo.ErrNoDocuments {
			return nil, app.For(code.Persona).ResNotFound("copy draft not found")
		}
		return nil, err
	}
	return &out, nil
}

func (r *mongoRepository) Delete(ctx context.Context, tenantID, ownerUID, personaID, draftID string) error {
	if r.collection == nil {
		return app.For(code.Persona).DBUnavailable("Mongo is not configured")
	}
	draftID = strings.TrimSpace(draftID)
	if draftID == "" {
		return app.For(code.Persona).InputMissingRequired("draft_id is required")
	}
	filter := personaFilter(tenantID, ownerUID, personaID)
	filter["_id"] = draftID
	res, err := r.collection.DeleteOne(ctx, filter)
	if err != nil {
		return err
	}
	if res.DeletedCount == 0 {
		return app.For(code.Persona).ResNotFound("copy draft not found")
	}
	return nil
}

func (r *mongoRepository) DeleteMany(
	ctx context.Context,
	tenantID, ownerUID, personaID, missionID, draftType string,
	draftIDs []string,
) (int64, error) {
	if r.collection == nil {
		return 0, app.For(code.Persona).DBUnavailable("Mongo is not configured")
	}
	missionID = strings.TrimSpace(missionID)
	if missionID == "" {
		return 0, app.For(code.Persona).InputMissingRequired("copy_mission_id is required")
	}
	filter := personaFilter(tenantID, ownerUID, personaID)
	filter["copy_mission_id"] = missionID
	if draftType = strings.TrimSpace(draftType); draftType != "" {
		filter["draft_type"] = draftType
	}
	ids := make([]string, 0, len(draftIDs))
	for _, id := range draftIDs {
		id = strings.TrimSpace(id)
		if id == "" {
			continue
		}
		ids = append(ids, id)
	}
	if len(ids) > 0 {
		filter["_id"] = bson.M{"$in": ids}
	}
	res, err := r.collection.DeleteMany(ctx, filter)
	if err != nil {
		return 0, err
	}
	return res.DeletedCount, nil
}

func (r *mongoRepository) DeleteByMission(ctx context.Context, tenantID, ownerUID, personaID, missionID string) error {
	if r.collection == nil {
		return app.For(code.Persona).DBUnavailable("Mongo is not configured")
	}
	missionID = strings.TrimSpace(missionID)
	if missionID == "" {
		return app.For(code.Persona).InputMissingRequired("copy_mission_id is required")
	}
	filter := personaFilter(tenantID, ownerUID, personaID)
	filter["copy_mission_id"] = missionID
	_, err := r.collection.DeleteMany(ctx, filter)
	return err
}

func (r *mongoRepository) DeleteByMissionAndType(ctx context.Context, tenantID, ownerUID, personaID, missionID, draftType string) error {
	if r.collection == nil {
		return app.For(code.Persona).DBUnavailable("Mongo is not configured")
	}
	missionID = strings.TrimSpace(missionID)
	draftType = strings.TrimSpace(draftType)
	if missionID == "" || draftType == "" {
		return app.For(code.Persona).InputMissingRequired("copy_mission_id and draft_type are required")
	}
	filter := personaFilter(tenantID, ownerUID, personaID)
	filter["copy_mission_id"] = missionID
	filter["draft_type"] = draftType
	_, err := r.collection.DeleteMany(ctx, filter)
	return err
}

func matrixDraftFilter(tenantID, ownerUID, personaID, missionID string) bson.M {
	filter := personaFilter(tenantID, ownerUID, personaID)
	filter["copy_mission_id"] = strings.TrimSpace(missionID)
	filter["draft_type"] = entity.DraftTypeMatrix
	return filter
}

func (r *mongoRepository) ReplaceMissionMatrix(
	ctx context.Context,
	tenantID, ownerUID, personaID, missionID string,
	drafts []*entity.CopyDraft,
) error {
	if r.collection == nil {
		return app.For(code.Persona).DBUnavailable("Mongo is not configured")
	}
	missionID = strings.TrimSpace(missionID)
	if missionID == "" {
		return app.For(code.Persona).InputMissingRequired("copy_mission_id is required")
	}
	if err := r.replaceMissionMatrixWithTransaction(ctx, tenantID, ownerUID, personaID, missionID, drafts); err == nil {
		return nil
	}
	return r.replaceMissionMatrixInsertFirst(ctx, tenantID, ownerUID, personaID, missionID, drafts)
}

func (r *mongoRepository) replaceMissionMatrixWithTransaction(
	ctx context.Context,
	tenantID, ownerUID, personaID, missionID string,
	drafts []*entity.CopyDraft,
) error {
	client := r.collection.Database().Client()
	session, err := client.StartSession()
	if err != nil {
		return err
	}
	defer session.EndSession(ctx)

	wc := writeconcern.Majority()
	txnOpts := options.Transaction().SetWriteConcern(wc)
	_, err = session.WithTransaction(ctx, func(sessCtx mongo.SessionContext) (any, error) {
		filter := matrixDraftFilter(tenantID, ownerUID, personaID, missionID)
		if _, err := r.collection.DeleteMany(sessCtx, filter); err != nil {
			return nil, err
		}
		if len(drafts) == 0 {
			return nil, nil
		}
		docs := make([]any, 0, len(drafts))
		for _, draft := range drafts {
			if draft == nil {
				continue
			}
			docs = append(docs, draft)
		}
		if len(docs) == 0 {
			return nil, nil
		}
		if _, err := r.collection.InsertMany(sessCtx, docs); err != nil {
			return nil, err
		}
		return nil, nil
	}, txnOpts)
	return err
}

func (r *mongoRepository) replaceMissionMatrixInsertFirst(
	ctx context.Context,
	tenantID, ownerUID, personaID, missionID string,
	drafts []*entity.CopyDraft,
) error {
	batchID := ""
	if len(drafts) > 0 {
		batchID = strings.TrimSpace(drafts[0].MatrixBatchID)
	}
	if batchID == "" {
		filter := matrixDraftFilter(tenantID, ownerUID, personaID, missionID)
		if _, err := r.collection.DeleteMany(ctx, filter); err != nil {
			return err
		}
		if len(drafts) == 0 {
			return nil
		}
		return r.CreateMany(ctx, drafts)
	}
	if err := r.CreateMany(ctx, drafts); err != nil {
		return err
	}
	filter := matrixDraftFilter(tenantID, ownerUID, personaID, missionID)
	filter["matrix_batch_id"] = bson.M{"$ne": batchID}
	_, err := r.collection.DeleteMany(ctx, filter)
	return err
}

func pendingIdleDraftFilter(tenantID, ownerUID, personaID string) bson.M {
	filter := personaFilter(tenantID, ownerUID, personaID)
	filter["$and"] = []bson.M{
		{"$or": []bson.M{{"publish_queue_id": ""}, {"publish_queue_id": bson.M{"$exists": false}}}},
		{"$or": []bson.M{{"published_at": 0}, {"published_at": bson.M{"$exists": false}}}},
		{"$or": []bson.M{{"copy_mission_id": ""}, {"copy_mission_id": bson.M{"$exists": false}}}},
	}
	return filter
}

func (r *mongoRepository) PruneOldestPendingBeyond(ctx context.Context, tenantID, ownerUID, personaID string, keep int) (int64, error) {
	if r.collection == nil {
		return 0, app.For(code.Persona).DBUnavailable("Mongo is not configured")
	}
	if keep <= 0 {
		keep = 40
	}
	filter := pendingIdleDraftFilter(tenantID, ownerUID, personaID)
	total, err := r.collection.CountDocuments(ctx, filter)
	if err != nil {
		return 0, err
	}
	excess := total - int64(keep)
	if excess <= 0 {
		return 0, nil
	}
	opts := options.Find().
		SetSort(bson.D{{Key: "create_at", Value: 1}}).
		SetLimit(excess).
		SetProjection(bson.M{"_id": 1})
	cur, err := r.collection.Find(ctx, filter, opts)
	if err != nil {
		return 0, err
	}
	defer cur.Close(ctx)
	ids := make([]string, 0, excess)
	for cur.Next(ctx) {
		var row struct {
			ID string `bson:"_id"`
		}
		if err := cur.Decode(&row); err != nil {
			return 0, err
		}
		if row.ID != "" {
			ids = append(ids, row.ID)
		}
	}
	if err := cur.Err(); err != nil {
		return 0, err
	}
	if len(ids) == 0 {
		return 0, nil
	}
	deleteFilter := personaFilter(tenantID, ownerUID, personaID)
	deleteFilter["_id"] = bson.M{"$in": ids}
	res, err := r.collection.DeleteMany(ctx, deleteFilter)
	if err != nil {
		return 0, err
	}
	return res.DeletedCount, nil
}

func (r *mongoRepository) ListByMission(ctx context.Context, tenantID, ownerUID, personaID, missionID string, limit int) ([]entity.CopyDraft, error) {
	if r.collection == nil {
		return nil, app.For(code.Persona).DBUnavailable("Mongo is not configured")
	}
	missionID = strings.TrimSpace(missionID)
	if missionID == "" {
		return nil, app.For(code.Persona).InputMissingRequired("copy_mission_id is required")
	}
	if limit <= 0 {
		limit = 50
	}
	if limit > 200 {
		limit = 200
	}
	filter := personaFilter(tenantID, ownerUID, personaID)
	filter["copy_mission_id"] = missionID
	opts := options.Find().
		SetSort(bson.D{{Key: "sort_order", Value: 1}, {Key: "create_at", Value: -1}}).
		SetLimit(int64(limit))
	cur, err := r.collection.Find(ctx, filter, opts)
	if err != nil {
		return nil, err
	}
	defer cur.Close(ctx)
	var out []entity.CopyDraft
	if err := cur.All(ctx, &out); err != nil {
		return nil, err
	}
	return out, nil
}
