package mongo

import (
	"strings"

	"github.com/fiskerinc/cloud-services/pkg/common"
	"github.com/fiskerinc/cloud-services/pkg/db/queries"

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

// Vehicle serves as the schema for Vehicles collection
type Vehicle struct {
	VIN         string          `json:"vin" bson:"vin"`
	LogLevel    common.LogLevel `json:"log_level" bson:"log_level"`
	DLTEnabled  bool            `json:"dlt_enabled" bson:"dlt_enabled"`
	DLTLevel    int             `json:"dlt_level,omitempty" bson:"dlt_level,omitempty"`
	CANBus      common.CANBus   `json:"canbus" bson:"canbus"`
	IDPSEnabled bool            `json:"idps_enabled" bson:"idps_enabled"`
	DebugMask   string          `json:"debug_mask,omitempty" bson:"debug_mask,omitempty"`
	Fleets      []Fleet         `json:"fleets" bson:"fleets,omitempty"`
	search      string          `json:"-" bson:"-"`
}

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

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

type VehicleCANBusWithFleet struct {
	CANBus struct {
		Filters []common.CANFilterWithFleet `json:"filters,omitempty" bson:"filters,omitempty"`
	} `json:"canbus" bson:"canbus"`
}

// NewVehiclesCollection is the construct for Vehicles and utilizes the same mongo connection
func NewVehiclesCollection(c CollectionInterface) VehiclesCollectionInterface {
	return &VehiclesCollection{c}
}

type VehiclesCollectionInterface interface {
	AddVehicle(vehicle *Vehicle) error
	FindVehicle(filter *Vehicle) (*Vehicle, error)
	UpdateVehicle(vehicle *Vehicle) error
	DeleteVehicle(filter *Vehicle) error

	AddFilterToVehicle(vin string, filter *common.CANFilter) error
	GetFiltersForVehicle(vin string, options *queries.PageQueryOptions) ([]common.CANFilterWithFleet, error)
	UpdateFilterForVehicle(vin string, id string, filter *common.CANFilter) error
	DeleteFilterForVehicle(vin string, id string) error

	GetFilterCountForVehicle(vin string) (int64, error)
	UpdateFiltersForVehicle(vin string, filters []common.CANFilter) error

	GetFleetsForVehicle(
		vin, fleetName string,
		options *queries.PageQueryOptions,
	) ([]string, error)
	GetFleetCountForVehicle(
		vin, fleetName string,
	) (int64, error)

	CollectionInterface
}

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

func (c *VehiclesCollection) AddVehicle(vehicle *Vehicle) error {
	vehicle.VIN = strings.ToUpper(vehicle.VIN)
	_, err := c.InsertOne(vehicle)
	if err != nil {
		return err
	}
	return nil
}

func (c *VehiclesCollection) FindVehicle(filter *Vehicle) (*Vehicle, error) {

	m := bson.M{"vin": filter.VIN}

	v := &Vehicle{}
	err := c.FindOne(m, v, nil)
	if err != nil {
		return nil, err
	}

	if v.CANBus.DTCEnabled == nil {
		v.CANBus.DTCEnabled = elptr.ElPtr(false)
	}

	return v, nil
}

func (c *VehiclesCollection) UpdateVehicle(vehicle *Vehicle) error {
	m := bson.M{"vin": vehicle.VIN}
	u := bson.M{"$set": bson.M{
		"canbus.enabled":        vehicle.CANBus.Enabled,
		"canbus.data_logger":    vehicle.CANBus.DataLogger,
		"canbus.dtc_enabled":    vehicle.CANBus.DTCEnabled,
		"canbus.mem_buff_size":  vehicle.CANBus.MemBuffSize,
		"canbus.disk_buff_size": vehicle.CANBus.DiskBuffSize,
		"log_level":             vehicle.LogLevel,
		"debug_mask":            vehicle.DebugMask,
		"dlt_enabled":           vehicle.DLTEnabled,
		"dlt_level":             vehicle.DLTLevel,
		"idps_enabled":          vehicle.IDPSEnabled,
	}}

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

	return nil
}

func (c *VehiclesCollection) DeleteVehicle(vehicle *Vehicle) error {
	m := bson.M{"vin": vehicle.VIN}

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

	return nil
}

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

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

	return nil
}

func (c *VehiclesCollection) filtersForVehiclePipe(
	vin string,
) mongo.Pipeline {
	// This pipeline works in a following way:
	// 1. matches the vin from vehicles.
	// 2. checks whether the canbus.filters is array, if not creates an empty one.
	// 3. does lookup in fleets collection
	//   3.1. checks whether fleets.vehicles is array and
	//   3.2. matches a current vin with the vins in fleets.vehicles
	//   3.3. checks if the canbus.filters of the matching fleet is array, if not creates an empty one
	//   3.4. names it as matches
	// 4. creates transformed matches as cfilters
	//   4.1. goes through every canbus filter of the matches
	//   4.2. concatenates them through creating a new object with a "fleet" field representing fleet name
	//   4.3. eventually named as cfilters
	// 5. merges cfilters and canbus.filters selected as "filters" eventually
	// 6. unwinds filters
	// 7. sorts the unwinded through filters.can_id
	return mongo.Pipeline{
		{{"$match", bson.D{{"vin", vin}}}},
		{{"$project", bson.D{
			{"canbus.filters", bson.D{
				{"$cond",
					bson.A{bson.D{{"$isArray", "$canbus.filters"}}, "$canbus.filters", bson.A{}},
				},
			}},
			{"vin", 1}}}},
		{{"$lookup",
			bson.D{
				{"from", "fleets"},
				{"let", bson.D{{"vin", "$vin"}}},
				{"pipeline", bson.A{bson.D{
					{"$match", bson.D{
						{"$expr", bson.D{
							{"$and", bson.A{
								bson.D{{"$isArray", "$vehicles"}},
								bson.D{{"$in", bson.A{"$$vin", "$vehicles"}}},
							}}}}}}}, bson.D{
					{"$project", bson.D{
						{"canbus.filters", bson.D{
							{"$cond", bson.A{
								bson.D{{"$isArray", "$canbus.filters"}},
								"$canbus.filters",
								bson.A{},
							}}}},
						{"name", 1},
					}}}}},
				{"as", "matches"}}}},
		{{"$addFields", bson.D{
			{"cfilters", bson.D{
				{"$reduce", bson.D{
					{"input", "$matches"},
					{"initialValue", bson.A{}},
					{"in", bson.D{
						{"$concatArrays", bson.A{
							"$$value", bson.D{
								{"$map", bson.D{
									{"input", "$$this.canbus.filters"},
									{"as", "cfilter"},
									{"in", bson.D{
										{"fleet", "$$this.name"},
										{"can_id", "$$cfilter.can_id"},
										{"interval", "$$cfilter.interval"},
										{"edge_mask", "$$cfilter.edge_mask"}}},
								}}}}}}}}}}}}}},
		{{"$project", bson.D{
			{"filters", bson.D{
				{"$concatArrays", bson.A{
					"$canbus.filters",
					"$cfilters",
				}}}}}}},
		{{"$unwind", "$filters"}},
		{{"$sort", bson.D{{"filters.can_id", 1}}}},
	}
}

func (c *VehiclesCollection) GetFiltersForVehicle(
	vin string,
	options *queries.PageQueryOptions,
) ([]common.CANFilterWithFleet, error) {
	preProc := c.filtersForVehiclePipe(vin)
	postProc := mongo.Pipeline{{
		{"$group", bson.D{
			{"_id", "$_id"},
			{"filters", bson.D{{"$push", "$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 vehicles []VehicleCANBusWithFleet
	err := c.Aggregate(pipe, &vehicles)
	if err != nil {
		return nil, err
	}

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

func (c *VehiclesCollection) UpdateFilterForVehicle(vin string, id string, filter *common.CANFilter) error {
	m := bson.M{
		"vin":                   vin,
		"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 *VehiclesCollection) DeleteFilterForVehicle(vin string, id string) error {
	m := bson.M{"vin": vin}
	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 *VehiclesCollection) GetFilterCountForVehicle(vin string) (int64, error) {
	pipe := append(c.filtersForVehiclePipe(vin), mongo.Pipeline{
		{{"$group", bson.D{
			{"_id", "$_id"},
			{"filters", bson.D{{"$push", "$filters"}}}}}},
		{{"$project", bson.D{{"canbus.filters", "$filters"}}}},
		{{"$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 *VehiclesCollection) UpdateFiltersForVehicle(vin string, filters []common.CANFilter) error {
	m := bson.M{"vin": vin}
	u := bson.M{"$set": bson.M{"canbus.filters": filters}}

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

	return nil
}

func (c *VehiclesCollection) getFleetsForVehiclePipe(vin, fleetName string) mongo.Pipeline {
	ofFleet := bson.E{}
	if fleetName != "" {
		ofFleet = bson.E{"name", fleetName}
	}

	return mongo.Pipeline{
		{{"$match", bson.D{{"vin", vin}}}},
		{{"$project", bson.D{{"vin", 1}}}},
		{{"$lookup", bson.D{
			{"from", "fleets"},
			{"let", bson.D{{"vin", "$vin"}}},
			{"pipeline", bson.A{bson.D{
				{"$match", bson.D{
					{"$expr", bson.D{
						{"$and", bson.A{
							bson.D{{"$isArray", "$vehicles"}},
							bson.D{{"$in", bson.A{"$$vin", "$vehicles"}}},
						}}}}, ofFleet,
				}}}, bson.D{
				{"$project", bson.D{{"name", 1}}}}}},
			{"as", "fleets"}}}},
		{{"$project", bson.D{
			{"fleets", bson.D{
				{"$map", bson.D{
					{"input", "$fleets"},
					{"as", "fleet"},
					{"in", "$$fleet.name"}}}}}}}},
		{{"$unwind", "$fleets"}}}
}

func (c *VehiclesCollection) GetFleetCountForVehicle(
	vin, fleetName string,
) (int64, error) {
	pipe := append(c.getFleetsForVehiclePipe(vin, fleetName), mongo.Pipeline{
		{{"$group", bson.D{
			{"_id", "$_id"},
			{"fleets", bson.D{{"$push", "$fleets"}}}}}},
		{{"$project", bson.D{{
			"total", bson.D{{
				"$size", bson.D{{
					"$cond", bson.A{bson.D{{"$isArray", "$fleets"}}, "$fleets", []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 *VehiclesCollection) GetFleetsForVehicle(
	vin, fleetName string,
	options *queries.PageQueryOptions,
) ([]string, error) {
	preProc := c.getFleetsForVehiclePipe(vin, fleetName)

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

	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 []struct {
		Fleets []string `bson:"fleets"`
	}
	err := c.Aggregate(pipe, &fleets)
	if err != nil {
		return nil, err
	}

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