package mongo

import (
	"strings"

	"github.com/fiskerinc/cloud-services/pkg/common"
	"github.com/fiskerinc/cloud-services/pkg/db/queries"
	"go.mongodb.org/mongo-driver/bson/primitive"

	"github.com/fiskerinc/cloud-services/pkg/utils/elptr"
	"github.com/pkg/errors"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/mongo"
)

// NewFleetsCollection is the constructor for Fleets and utilizes the same mongo connection
func NewFleetsCollection(c CollectionInterface) FleetsCollectionInterface {
	return &FleetsCollection{c}
}

type FleetsCollectionInterface interface {
	AddFleet(fleet *Fleet) error
	FindFleet(filter *Fleet) (*Fleet, error)
	UpdateFleet(filter *Fleet, fleet *Fleet) error
	DeleteFleet(fleet *Fleet) error

	SelectFleets(filter *Fleet, options *queries.PageQueryOptions) ([]Fleet, error)
	GetFleetCount(filter *Fleet) (int64, error)

	GetVehiclesForFleet(name, search string, options *queries.PageQueryOptions) ([]string, error)
	GetVehiclesForFleetCount(name, search string) (int64, error)
	AddVehiclesToFleet(name string, vins []string) error
	DeleteVehiclesFromFleet(name string, vins []string) error

	GetFiltersForFleet(name string, options *queries.PageQueryOptions) ([]common.CANFilter, error)
	GetFiltersForFleetCount(name string) (int64, error)
	AddFilterToFleet(name string, filter *common.CANFilter) error
	UpdateFilterForFleet(name string, id string, filter *common.CANFilter) error
	DeleteFilterFromFleet(name string, id string) error

	GetCANBusForVehicle(vin string) (*Fleet, error)

	CollectionInterface
}

// FleetsCollection is the client for the collection
type FleetsCollection struct {
	CollectionInterface
}

// Fleet is an embedded object within Vehicle
type Fleet struct {
	Name          string          `json:"name" bson:"name"`
	LogLevel      common.LogLevel `json:"log_level" bson:"log_level,omitempty"`
	CANBus        common.CANBus   `json:"canbus" bson:"canbus,omitempty"`
	IDPSEnabled   bool            `json:"idps_enabled" bson:"idps_enabled"`
	DebugMask     string          `json:"debug_mask,omitempty" bson:"debug_mask,omitempty"`
	Tags          []string        `json:"tags" bson:"tags,omitempty"`
	Vehicles      []string        `json:"vehicles" bson:"vehicles,omitempty"`
	VehiclesCount int             `json:"vehicles_count" bson:"vehicles_count,omitempty"`
	search        string          `json:"-" bson:"-"`
}

func (f *Fleet) SetSearchQuery(search string) {
	f.search = search
}

func (f *Fleet) SearchQuery() string {
	return f.search
}

func (f *Fleet) AsExpr() bson.M {
	if f == nil || len(strings.Trim(f.search, " ")) == 0 {
		return bson.M{}
	}

	return bson.M{"$text": bson.M{"$search": f.search}}
}

func (c *FleetsCollection) AddFleet(fleet *Fleet) error {
	_, err := c.InsertOne(fleet)
	if err != nil {
		return err
	}

	return nil
}

func (c *FleetsCollection) FindFleet(filter *Fleet) (*Fleet, error) {
	m := bson.M{"name": filter.Name}
	projection := bson.D{{"vehicles", 0}}

	f := &Fleet{}
	err := c.FindOne(m, f, projection)
	if err != nil {
		if errors.Is(err, mongo.ErrNoDocuments) {
			return nil, nil
		}
		return nil, err
	}

	return f, nil
}

func (c *FleetsCollection) UpdateFleet(filter *Fleet, fleet *Fleet) error {
	if fleet.CANBus.DTCEnabled == nil {
		fleet.CANBus.DTCEnabled = elptr.ElPtr(false)
	}
	m := bson.M{"name": filter.Name}
	u := bson.M{"$set": bson.M{
		"name":                  fleet.Name,
		"canbus.enabled":        fleet.CANBus.Enabled,
		"canbus.data_logger":    fleet.CANBus.DataLogger,
		"canbus.dtc_enabled":    fleet.CANBus.DTCEnabled,
		"canbus.mem_buff_size":  fleet.CANBus.MemBuffSize,
		"canbus.disk_buff_size": fleet.CANBus.DiskBuffSize,
		"idps_enabled":          fleet.IDPSEnabled,
		"log_level":             fleet.LogLevel,
		"debug_mask":            fleet.DebugMask,
	}}

	_, err := c.UpdateOne(m, u)
	if err != nil {
		return err
	}

	return nil
}

func (c *FleetsCollection) DeleteFleet(fleet *Fleet) error {
	m := bson.M{"name": fleet.Name}

	err := c.DeleteOne(m)
	if err != nil {
		return err
	}

	return nil
}

func (c *FleetsCollection) SelectFleets(filter *Fleet, options *queries.PageQueryOptions) ([]Fleet, error) {
	m, f := filter.AsExpr(), make([]Fleet, 0, 0)

	if err := c.Find(m, &f, options); err != nil {
		return nil, err
	}

	for x := range f {
		if f[x].CANBus.DTCEnabled == nil {
			f[x].CANBus.DTCEnabled = elptr.ElPtr(false)
		}
	}

	return f, nil
}

func (c *FleetsCollection) GetFleetCount(filter *Fleet) (int64, error) {
	m := filter.AsExpr()

	if len(filter.Tags) > 0 {
		m["tags"] = bson.M{"$all": filter.Tags}
	}

	return c.Count(m)
}

func (c *FleetsCollection) GetVehiclesForFleet(name, search string, options *queries.PageQueryOptions) ([]string, error) {
	preProc := mongo.Pipeline{
		{{"$match", bson.D{{"name", name}}}},
		{{"$project", bson.D{{
			"vin", bson.D{{
				"$cond", bson.A{bson.D{{"$isArray", "$vehicles"}}, "$vehicles", []string{}},
			}},
		}}}},
		{{"$unwind", "$vin"}},
	}

	if search != "" {
		preProc = append(preProc, bson.D{
			{"$match", bson.M{"vin": primitive.Regex{
				Pattern: search,
				Options: "i",
			}}}})
	}

	preProc = append(preProc, bson.D{{"$sort", bson.D{{"vin", 1}}}})

	postProc := mongo.Pipeline{
		{{"$group", bson.D{{"_id", "$_id"}, {"vehicles", bson.D{{"$push", "$vin"}}}}}},
	}

	pipe := mongo.Pipeline{}
	pipe = append(pipe, preProc...)
	if options.Offset > 0 {
		pipe = append(pipe, bson.D{{"$skip", options.Offset}})
	}
	if options.Limit > 0 {
		pipe = append(pipe, bson.D{{"$limit", options.Limit}})
	}
	pipe = append(pipe, postProc...)

	var fleets []Fleet
	err := c.Aggregate(pipe, &fleets)
	if err != nil {
		return nil, err
	}

	if len(fleets) != 1 {
		return []string{}, nil
	}
	return fleets[0].Vehicles, nil
}

type count struct {
	Total int64 `bson:"total"`
}

func (c *FleetsCollection) GetVehiclesForFleetCount(name, search string) (int64, error) {
	pipe := mongo.Pipeline{
		{{"$match", bson.D{{"name", name}}}},
	}
	if search == "" {
		pipe = append(pipe, bson.D{{"$project", bson.D{{
			"total", bson.D{{
				"$size", bson.D{{
					"$cond", bson.A{
						bson.D{{"$isArray", "$vehicles"}}, "$vehicles", []string{}},
				}},
			}}}},
		}})
	} else {
		pipe = append(pipe,
			bson.D{{"$project",
				bson.D{{"vin",
					bson.D{{"$cond",
						bson.A{
							bson.D{{"$isArray", "$vehicles"}}, "$vehicles", []string{}}}}}}}},
			bson.D{{"$unwind", "$vin"}},
			bson.D{{"$match", bson.M{"vin": primitive.Regex{
				Pattern: search,
				Options: "i",
			}}}},
			bson.D{{"$count", "total"}},
		)
	}

	var counter []count

	err := c.Aggregate(pipe, &counter)
	if err != nil {
		return 0, err
	}

	if len(counter) != 1 {
		return 0, nil
	}

	return counter[0].Total, nil
}

// Creates a Mongodb pipeline to accomplish several steps
// 1. Get document by name
// 2. Merge vins as sets
// 3. Calculate an accurate count
// 4. Store result in db
func createPipelineForFleetVehicles(name string, vins []string, mergeStrategy string) mongo.Pipeline {
	return mongo.Pipeline{
		{{"$match", bson.D{{"name", name}}}},
		{{"$addFields", bson.D{
			{"vehicles", bson.D{
				{mergeStrategy, bson.A{
					bson.D{{"$ifNull", bson.A{"$vehicles", bson.A{}}}},
					vins,
				}},
			}},
		}}},
		{{"$set", bson.D{
			{"vehicles_count", bson.D{
				{"$size", bson.D{
					{"$ifNull", bson.A{"$vehicles", bson.A{}}},
				}},
			}},
		}}},
		{{"$merge", bson.D{{"into", "fleets"}}}},
	}
}

func (c *FleetsCollection) AddVehiclesToFleet(name string, vins []string) error {
	if len(vins) == 0 {
		return errors.Errorf("No VINs to add to fleet")
	}

	pipe := createPipelineForFleetVehicles(name, vins, "$setUnion")

	var result []interface{}
	err := c.Aggregate(pipe, &result)

	return err
}

func (c *FleetsCollection) DeleteVehiclesFromFleet(name string, vins []string) error {
	if len(vins) == 0 {
		return errors.Errorf("No VINs to remove from fleet")
	}

	pipe := createPipelineForFleetVehicles(name, vins, "$setDifference")

	var result []interface{}
	err := c.Aggregate(pipe, &result)

	return err
}

func (c *FleetsCollection) GetFiltersForFleet(name string, options *queries.PageQueryOptions) ([]common.CANFilter, error) {
	preProc := mongo.Pipeline{
		{{"$match", bson.D{{"name", name}}}},
		{{"$project", bson.D{{
			"canbus.filters", bson.D{{
				"$cond", bson.A{bson.D{{"$isArray", "$canbus.filters"}}, "$canbus.filters", []common.CANFilter{}},
			}},
		}}}},
		{{"$unwind", "$canbus.filters"}},
		{{"$sort", bson.D{{"canbus.filters.can_id", 1}}}},
	}
	postProc := mongo.Pipeline{
		{{"$group", bson.D{{"_id", "$_id"}, {"filters", bson.D{{"$push", "$canbus.filters"}}}}}},
		{{"$project", bson.D{{"canbus.filters", "$filters"}}}},
	}

	pipe := mongo.Pipeline{}
	pipe = append(pipe, preProc...)
	if options.Offset > 0 {
		pipe = append(pipe, bson.D{{"$skip", options.Offset}})
	}
	if options.Limit > 0 {
		pipe = append(pipe, bson.D{{"$limit", options.Limit}})
	}
	pipe = append(pipe, postProc...)

	var fleets []Fleet
	err := c.Aggregate(pipe, &fleets)
	if err != nil {
		return nil, err
	}

	if len(fleets) != 1 {
		return []common.CANFilter{}, nil
	}
	return fleets[0].CANBus.Filters, nil
}

func (c *FleetsCollection) GetFiltersForFleetCount(name string) (int64, error) {
	pipe := mongo.Pipeline{
		{{"$match", bson.D{{"name", name}}}},
		{{"$project", bson.D{{
			"total", bson.D{{
				"$size", bson.D{{
					"$cond", bson.A{bson.D{{"$isArray", "$canbus.filters"}}, "$canbus.filters", []common.CANBus{}},
				}},
			}}}},
		}},
	}

	var counter []count

	err := c.Aggregate(pipe, &counter)
	if err != nil {
		return 0, err
	}

	if len(counter) != 1 {
		return 0, nil
	}

	return counter[0].Total, nil
}

func (c *FleetsCollection) AddFilterToFleet(name string, filter *common.CANFilter) error {
	m := bson.M{"name": name}
	u := bson.M{"$addToSet": bson.M{"canbus.filters": filter}}

	_, err := c.UpdateOne(m, u)
	if err != nil {
		return err
	}

	return nil
}

func (c *FleetsCollection) UpdateFilterForFleet(name string, id string, filter *common.CANFilter) error {
	m := bson.M{
		"name":                  name,
		"canbus.filters.can_id": id,
	}
	u := bson.M{"$set": bson.M{
		"canbus.filters.$.interval":  filter.Interval,
		"canbus.filters.$.edge_mask": filter.EdgeMask}}

	_, err := c.UpdateOne(m, u)
	if err != nil {
		return err
	}

	return nil
}

func (c *FleetsCollection) DeleteFilterFromFleet(name string, id string) error {
	m := bson.M{"name": name}
	u := bson.M{"$pull": bson.M{"canbus.filters": bson.M{"can_id": id}}}

	_, err := c.UpdateOne(m, u)
	if err != nil {
		return err
	}

	return nil
}

func (c *FleetsCollection) GetCANBusForVehicle(vin string) (*Fleet, error) {
	pipe := mongo.Pipeline{
		{{"$match", bson.D{{"vehicles", vin}}}},
		{{"$unwind", bson.D{{"path", "$canbus.filters"}, {"preserveNullAndEmptyArrays", true}}}},
		{{"$group", bson.D{
			{"_id", nil},
			{"filters", bson.D{{"$push", "$canbus.filters"}}},
			{"enabled", bson.D{{"$max", "$canbus.enabled"}}},
			{"data_logger", bson.D{{"$max", "$canbus.data_logger"}}},
			{"mem_buff_size", bson.D{{"$min", "$canbus.mem_buff_size"}}},
			{"disk_buff_size", bson.D{{"$min", "$canbus.disk_buff_size"}}},
		}}},
		{{"$project", bson.D{
			{"canbus.filters", "$filters"},
			{"canbus.enabled", "$enabled"},
			{"canbus.data_logger", "$data_logger"},
			{"canbus.mem_buff_size", "$mem_buff_size"},
			{"canbus.disk_buff_size", "$disk_buff_size"},
			{"canbus.dtc_enabled", "$dtc_enabled"},
		}}},
	}

	var fleets []Fleet
	err := c.Aggregate(pipe, &fleets)
	if err != nil {
		return nil, err
	}

	if len(fleets) != 1 {
		return nil, nil
	}
	theFleet := &fleets[0]
	if theFleet.CANBus.DTCEnabled == nil {
		theFleet.CANBus.DTCEnabled = elptr.ElPtr(false)
	}
	return theFleet, nil
}
