package mongo

import (
	"context"
	"fmt"
	"heruvym/model"
	"time"

	"github.com/rs/xid"
	"github.com/themakers/hlog"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
	"go.mongodb.org/mongo-driver/mongo/readpref"
)

const (
	collMessages = "messages"
	collTickets  = "tickets"
)

type DAL struct {
	logger         hlog.Logger
	colMsg, colTck *mongo.Collection
	client         *mongo.Client
}

type ErrorConnectToDB struct {
	Err      error
	MongoURI string
}

type ErrorPingDB struct {
	Err      error
	MongoURI string
}

type InfoPing struct {
	MongoURI    string
	Nanoseconds int64
}

func New(
	ctx context.Context,
	mongoURI, database string,
	log hlog.Logger,
) (*DAL, error) {
	client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI))
	if err != nil {
		log.Emit(ErrorConnectToDB{
			Err:      err,
			MongoURI: mongoURI,
		})
		return nil, err
	}

	before := time.Now().Unix()

	if err := client.Ping(ctx, readpref.PrimaryPreferred()); err != nil {
		log.Emit(ErrorPingDB{
			Err:      err,
			MongoURI: mongoURI,
		})

		return nil, err
	}

	log.Emit(InfoPing{
		MongoURI:    mongoURI,
		Nanoseconds: time.Now().Unix() - before,
	})

	fmt.Println("dal0")
	dal := &DAL{
		client: client,
		colMsg: client.Database(database).Collection(collMessages),
		colTck: client.Database(database).Collection(collTickets),
		logger: log.Module("DAL"),
	}

	fmt.Println("dal1")
	// Будет ошибка создания индексов только если эти индексы изменить и предварительно не удалить старые
	err = dal.CreateTicketIndex(ctx)

	if err != nil {
		return nil, err
	}

	fmt.Println("dal2")
	err = dal.CreateMessageIndex(ctx)

	if err != nil {
		return nil, err
	}

	fmt.Println("dal3")
	return dal, nil
}

type ErrorInsert struct {
	Err               error
	UserID, SessionID string
}

type ErrorCreateIndex struct {
	Err error
}

func (d *DAL) CreateMessageIndex(ctx context.Context) error {
	keys := bson.D{{"$**", "text"}}

	opts := options.Index().SetDefaultLanguage("russian")

	_, err := d.colMsg.Indexes().CreateOne(ctx, mongo.IndexModel{Keys: keys, Options: opts})

	if err != nil {
		d.logger.Emit(ErrorCreateIndex{err})
	}

	return err
}

func (d *DAL) CreateTicketIndex(ctx context.Context) error {
	keys := bson.D{{"$**", "text"}}

	opts := options.Index().SetDefaultLanguage("russian")

	_, err := d.colTck.Indexes().CreateOne(ctx, mongo.IndexModel{Keys: keys, Options: opts})

	if err != nil {
		d.logger.Emit(ErrorCreateIndex{err})
	}

	return err
}

func (d *DAL) PutMessage(
	ctx context.Context,
	message, userID, sessionID, ticketID string,
	files []string,
) (*model.Message, error) {
	insertable := model.Message{
		ID:        xid.New().String(),
		UserID:    userID,
		SessionID: sessionID,
		TicketID:  ticketID,
		Message:   message,
		Files:     files,
		Shown:     map[string]int{},
		CreatedAt: time.Now(),
	}

	if _, err := d.colMsg.InsertOne(ctx, &insertable); err != nil {
		d.logger.Emit(ErrorInsert{
			Err:       err,
			UserID:    userID,
			SessionID: sessionID,
		})

		return nil, err
	}

	return &insertable, nil
}

func (d *DAL) PutSCResponse(
	ctx context.Context,
	message, userID, sessionID, ticketID string,
	files []string,
) (*model.Message, error) {
	insertable := model.Message{
		ID:                xid.New().String(),
		UserID:            userID,
		SessionID:         sessionID,
		TicketID:          ticketID,
		Message:           message,
		Files:             files,
		RequestScreenshot: "response",
		Shown:             map[string]int{},
		CreatedAt:         time.Now(),
	}

	if _, err := d.colMsg.InsertOne(ctx, &insertable); err != nil {
		d.logger.Emit(ErrorInsert{
			Err:       err,
			UserID:    userID,
			SessionID: sessionID,
		})

		return nil, err
	}

	return &insertable, nil
}

func (d *DAL) PutSCRequest(
	ctx context.Context,
	userID, sessionID, ticketID string,
) (*model.Message, error) {
	insertable := model.Message{
		ID:                xid.New().String(),
		UserID:            userID,
		SessionID:         sessionID,
		TicketID:          ticketID,
		Message:           "",
		Files:             []string{},
		Shown:             map[string]int{},
		RequestScreenshot: "acquisition",
		CreatedAt:         time.Now(),
	}

	if _, err := d.colMsg.InsertOne(ctx, &insertable); err != nil {
		d.logger.Emit(ErrorInsert{
			Err:       err,
			UserID:    userID,
			SessionID: sessionID,
		})

		return nil, err
	}

	return &insertable, nil
}

func (d *DAL) CreateTicket(
	ctx context.Context,
	userID,
	sessionID,
	title, message string,
	files []string,
) (string, error) {

	ticketID := xid.New().String()

	if _, err := d.colTck.InsertOne(ctx, &model.Ticket{
		ID:        ticketID,
		UserID:    userID,
		SessionID: sessionID,
		Title:     title,
		State:     model.StateOpen,
		CreatedAt: time.Now(),
		UpdatedAt: time.Now(),
		Rate:      -1,
		TopMessage: model.Message{
			ID:        xid.New().String(),
			UserID:    userID,
			SessionID: sessionID,
			TicketID:  ticketID,
			Message:   message,
			Files:     []string{},
			Shown:     map[string]int{},
			CreatedAt: time.Now(),
		},
	}); err != nil {
		d.logger.Emit(ErrorInsert{
			Err:       err,
			UserID:    userID,
			SessionID: sessionID,
		})

		return "", err
	}

	return ticketID, nil
}

func (d *DAL) GetTickets4Sess(ctx context.Context, sessID string) ([]model.Ticket, int64, error) {
	sort := bson.M{"State": -1, "UpdatedAt": 1}
	opts := options.Find().SetSort(sort)

	query := bson.M{
		"SessionID": sessID,
	}

	cur, err := d.colTck.Find(ctx, query, opts)
	if err != nil {
		return nil, 0, err
	}

	var result []model.Ticket
	if err := cur.All(ctx, &result); err != nil {
		return nil, 0, err
	}

	col, err := d.colTck.CountDocuments(ctx, query)
	if err != nil {
		return nil, 0, err
	}

	return result, col, nil
}

func (d *DAL) GetTicket4Sess(
	ctx context.Context,
	ticketID,
	sessID string) (*model.Ticket, error) {

	var result model.Ticket
	if err := d.colTck.FindOne(ctx, bson.M{
		"_id":       ticketID,
		"SessionID": sessID,
	}).Decode(&result); err != nil {
		return nil, err
	}

	return &result, nil
}

func (d *DAL) GetTicket4User(
	ctx context.Context,
	ticketID,
	userID string) (*model.Ticket, error) {

	var result model.Ticket
	if err := d.colTck.FindOne(ctx, bson.M{
		"_id":    ticketID,
		"UserID": userID,
	}).Decode(&result); err != nil {
		return nil, err
	}

	return &result, nil
}

func (d *DAL) WatchTickets(
	ctx context.Context,
	userID string,
	yield func(ticket model.Ticket) error) error {
	operationTypes := []bson.D{{{"operationType", "insert"}},
		{{"operationType", "update"}}}

	matchStage := bson.M{
		"$and": bson.A{
			bson.M{"$or": operationTypes},
			bson.M{"fullDocument.UserID": userID},
		},
	}

	matchPipeline := mongo.Pipeline{
		bson.D{{"$match", matchStage}},
	}

	cs, err := d.colTck.Watch(ctx, matchPipeline,
		options.ChangeStream().SetFullDocument(options.UpdateLookup))
	if err != nil {
		fmt.Println("111", err, matchPipeline)
		return err
	}

	var piece model.Ticket

	var change Change
	for cs.Next(ctx) {
		if err := cs.Decode(&change); err != nil {
			return err
		}

		if err := bson.Unmarshal(change.FullDocument, &piece); err != nil {
			return err
		}

		if err := yield(piece); err != nil {
			return err
		}
	}

	return nil
}

func (d *DAL) YieldActiveTickets(
	ctx context.Context,
	yield func(ticket model.Ticket) error) error {
	cursor, err := d.colTck.Find(ctx, bson.M{
		"State": model.StateOpen,
	}, options.Find().SetLimit(20))
	if err != nil {
		return err
	}

	var piece model.Ticket
	for cursor.Next(ctx) {
		if err := cursor.Decode(&piece); err != nil {
			return err
		}

		if err := yield(piece); err != nil {
			return err
		}
	}

	return nil
}

func (d *DAL) YieldTickets(ctx context.Context, limit int64) ([]model.Ticket, int64, error) {
	sort := bson.D{{"State", -1}, {"UpdatedAt", 1}}

	cursor, err := d.colTck.Find(ctx, bson.M{}, options.Find().SetLimit(limit).SetSort(sort))
	if err != nil {
		return nil, 0, err
	}

	var result []model.Ticket

	err = cursor.All(ctx, &result)

	if err != nil {
		return nil, 0, err
	}

	col, err := d.colTck.CountDocuments(ctx, bson.M{})
	if err != nil {
		return nil, 0, err
	}

	return result, col, nil
}

func (d *DAL) YieldUserTickets(ctx context.Context, userID string, limit, offset int64) ([]model.Ticket, int64, error) {
	query := bson.M{
		"UserID": userID,
	}

	sort := bson.D{{"State", -1}, {"UpdatedAt", 1}}

	cursor, err := d.colTck.Find(ctx, query, options.Find().SetSort(sort).SetLimit(limit).SetSkip(offset))
	if err != nil {
		return nil, 0, err
	}

	var result []model.Ticket

	err = cursor.All(ctx, &result)

	if err != nil {
		return nil, 0, err
	}

	col, err := d.colTck.CountDocuments(ctx, bson.M{})
	if err != nil {
		return nil, 0, err
	}

	return result, col, nil
}

func (d *DAL) WatchAllTickets(ctx context.Context, yield func(ticket model.Ticket) error) error {
	operationTypes := []bson.D{
		{{"operationType", "insert"}},
		{{"operationType", "update"}},
	}

	matchStage := bson.D{{"$or", operationTypes}}

	matchPipeline := mongo.Pipeline{
		bson.D{
			{"$match", matchStage},
		},
	}

	cs, err := d.colTck.Watch(ctx, matchPipeline,
		options.ChangeStream().SetFullDocument(options.UpdateLookup))
	if err != nil {
		return err
	}

	var piece model.Ticket
	var change Change
	for cs.Next(ctx) {
		if err := cs.Decode(&change); err != nil {
			return err
		}

		if err := bson.Unmarshal(change.FullDocument, &piece); err != nil {
			return err
		}

		if err := yield(piece); err != nil {
			return err
		}
	}

	return nil
}

func (d *DAL) UpdateTopMessage(ctx context.Context, ticketID string, msg *model.Message) error {
	if err := d.colTck.FindOneAndUpdate(ctx, bson.M{
		"_id": ticketID,
	}, bson.M{
		"$set": bson.M{
			"UpdatedAt":  time.Now(),
			"TopMessage": msg,
		},
	}).Decode(&model.Ticket{}); err != nil {
		return err
	}

	return nil
}

func (d *DAL) YieldMessages(
	ctx context.Context,
	ticketID string,
	yield func(ticket model.Message) error) error {
	cursor, err := d.colMsg.Find(ctx, bson.M{
		"TicketID": ticketID,
	})
	if err != nil {
		return err
	}

	var piece model.Message
	for cursor.Next(ctx) {
		if err := cursor.Decode(&piece); err != nil {
			return err
		}

		if err := yield(piece); err != nil {
			return err
		}
	}

	return nil
}

type OpType string

const (
	OpInsert  OpType = "insert"
	OpDelete  OpType = "delete"
	OpReplace OpType = "replace"
	OpUpdate  OpType = "update"
)

type Change struct {
	ID struct {
		Data string `bson:"_data"`
	} `bson:"_id"`

	OperationType OpType `bson:"operationType"`

	FullDocument bson.Raw `bson:"fullDocument"`

	NS struct {
		DB   string `bson:"db"`
		Coll string `bson:"coll"`
	} `bson:"ns"`

	To *struct {
		DB   string `bson:"db"`
		Coll string `bson:"coll"`
	} `bson:"to"`

	DocumentKey bson.Raw `bson:"documentKey"`

	UpdateDescription *struct {
		UpdatedFields bson.Raw `bson:"updatedFields"`
		RemovedFields []string `bson:"removedFields"`
	} `bson:"updateDescription"`

	ClusterTime primitive.Timestamp `bson:"clusterTime"`

	TxnNumber int64 `bson:"txnNumber"`

	LSID bson.Raw `bson:"lsid"`

	//LSID *struct {
	//	ID  primitive.ObjectID `bson:"id"`
	//	UID primitive.Binary   `bson:"uid"`
	//} `bson:"lsid"`
}

func (d *DAL) WatchMessages(
	ctx context.Context, ticketID string, yield func(ticket model.Message) error) error {
	operationTypes := []bson.D{{{"operationType", "insert"}},
		{{"operationType", "update"}}}

	matchStage := bson.D{{"$and", []bson.D{
		{
			{"$or", operationTypes},
		},
		{
			{
				"fullDocument.TicketID", ticketID,
			},
		},
	}}}

	matchPipeline := mongo.Pipeline{
		bson.D{{"$match", matchStage}},
	}

	cs, err := d.colMsg.Watch(ctx, matchPipeline,
		options.ChangeStream().SetFullDocument(options.UpdateLookup))
	if err != nil {
		return err
	}

	var piece model.Message
	var change Change
	for cs.Next(ctx) {
		if err := cs.Decode(&change); err != nil {
			return err
		}

		bson.Unmarshal(change.FullDocument, &piece)

		if err := yield(piece); err != nil {
			return err
		}
	}

	return nil
}

func (d *DAL) SetShown(ctx context.Context, messageID, userID string) error {
	if err := d.colMsg.FindOneAndUpdate(ctx, bson.M{
		"_id": messageID,
	}, bson.M{
		"$set": bson.M{
			fmt.Sprintf("Shown.%s", userID): 1,
		},
	}).Decode(&model.Message{}); err != nil {
		return err
	}

	return nil
}

func (d *DAL) GetTicketPage(
	ctx context.Context,
	state, srch string,
	limit, skip int64,
) (*[]model.Ticket, int64, error) {
	query := bson.M{}

	if state != "" {
		query["State"] = state
	}

	if srch != "" {
		query["$text"] = bson.M{
			"$search": srch,
		}
	}

	sort := bson.D{{"State", -1}, {"UpdatedAt", -1}}

	cur, err := d.colTck.Find(ctx, query, options.Find().SetSort(sort).SetLimit(limit).SetSkip(skip*limit))
	if err != nil {
		return nil, 0, err
	}

	var result []model.Ticket

	if err := cur.All(ctx, &result); err != nil {
		return nil, 0, err
	}

	col, err := d.colTck.CountDocuments(ctx, query)
	if err != nil {
		return nil, 0, err
	}

	return &result, col, nil
}

func (d *DAL) GetMessagesPage(ctx context.Context,
	search, ticketID string,
	limit, offset int64) ([]model.Message, error) {

	var (
		query  bson.M
		result []model.Message
	)

	if ticketID != "" {
		query = bson.M{
			"TicketID": ticketID,
		}
	}

	if search != "" {
		query = bson.M{
			"TicketID": ticketID,
			"$text": bson.M{
				"$search": search,
			},
		}
	}

	sort := bson.D{{"UpdatedAt", 1}}

	cur, err := d.colMsg.Find(ctx, query, options.Find().SetLimit(limit).SetSkip(limit*offset).SetSort(sort))
	if err != nil {
		return nil, err
	}

	if err := cur.All(ctx, &result); err != nil {
		return nil, err
	}

	return result, nil
}

func (d *DAL) SetTicketStatus(ctx context.Context,
	ticket, status string) error {
	if _, err := d.colTck.UpdateByID(ctx, ticket, bson.M{
		"$set": bson.M{
			"State":     status,
			"UpdatedAt": time.Now(),
		},
	}); err != nil {
		return err
	}

	return nil
}

func (d *DAL) SetRate(ctx context.Context, ticket string, rate int) error {
	if _, err := d.colTck.UpdateByID(ctx, ticket, bson.M{"$set": bson.M{
		"Rate":      rate,
		"UpdatedAt": time.Now(),
	}}); err != nil {
		return err
	}

	return nil
}

func (d *DAL) SetAnswerer(ctx context.Context, ticket, answerer string) error {
	if _, err := d.colTck.UpdateByID(ctx, ticket, bson.M{"$set": bson.M{
		"AnswererID": answerer,
		"UpdatedAt":  time.Now(),
	}}); err != nil {
		return err
	}

	return nil
}

type Additional struct {
	Email string
	Uid   int64
}
type Identites struct {
	ID        string     `bson:"_id"`
	Identites []Identity `bson:"Identities"`
}
type Identity struct {
	Name     string `bson:"Name"`
	Identity string `bson:"Identity"`
}
type UidTechs struct {
	DisplayID int64  `bson:"display_id"`
	ID        string `bson:"_id"`
}

func (d *DAL) GetAdditionalData(ctx context.Context, id string) (*Additional, error) {
	result := Additional{}
	idens := Identites{}
	if err := d.client.Database("bb-identity").Collection("tp-users").FindOne(ctx, bson.M{
		"_id": id,
	}, options.FindOne().SetProjection(bson.D{{
		"Identities", 1,
	}})).Decode(&idens); err != nil {
		return nil, err
	}

	for _, iden := range idens.Identites {
		fmt.Println(iden.Name, iden)
		if iden.Name == "email" {
			result.Email = iden.Identity
		}
	}

	u := UidTechs{}
	if err := d.client.Database("profile").Collection("profile").FindOne(ctx, bson.M{
		"_id": id,
	}, options.FindOne().SetProjection(bson.D{{
		"display_id", 1,
	}})).Decode(&u); err != nil {
		return nil, err
	}
	result.Uid = u.DisplayID
	return &result, nil
}
