package controllers

import (
	"encoding/json"
	"errors"
	"fmt"
	"oc-datacenter/infrastructure"
	"time"

	oclib "cloud.o-forge.io/core/oc-lib"
	"cloud.o-forge.io/core/oc-lib/dbs"
	"cloud.o-forge.io/core/oc-lib/models/booking"
	b "cloud.o-forge.io/core/oc-lib/models/booking"
	"cloud.o-forge.io/core/oc-lib/tools"
	beego "github.com/beego/beego/v2/server/web"
	"go.mongodb.org/mongo-driver/bson/primitive"
)

// Operations about workspace
type BookingController struct {
	beego.Controller
}

var BookingExample booking.Booking

// @Title Search
// @Description search bookings by execution
// @Param	id		path 	string	true		"id execution"
// @Param	is_draft		query 	string	false		"draft wished"
// @Success 200 {workspace} models.workspace
// @router /search/execution/:id [get]
func (o *BookingController) ExecutionSearch() {
	/*
	* This is a sample of how to use the search function
	* The search function is used to search for data in the database
	* The search function takes in a filter and a data type
	* The filter is a struct that contains the search parameters
	* The data type is an enum that specifies the type of data to search for
	* The search function returns a list of data that matches the filter
	* The data is then returned as a json object
	 */
	// store and return Id or post with UUID
	user, peerID, groups := oclib.ExtractTokenInfo(*o.Ctx.Request)
	id := o.Ctx.Input.Param(":id")
	isDraft := o.Ctx.Input.Query("is_draft")
	f := dbs.Filters{
		Or: map[string][]dbs.Filter{ // filter by name if no filters are provided
			"execution_id": {{Operator: dbs.EQUAL.String(), Value: id}},
		},
	}
	o.Data["json"] = oclib.NewRequest(oclib.LibDataEnum(oclib.BOOKING), user, peerID, groups, nil).Search(&f, "", isDraft == "true")
	o.ServeJSON()
}

// @Title Search
// @Description search bookings
// @Param	start_date		path 	string	true		"the word search you want to get"
// @Param	end_date		path 	string	true		"the word search you want to get"
// @Param	is_draft		query 	string	false		"draft wished"
// @Success 200 {workspace} models.workspace
// @router /search/:start_date/:end_date [get]
func (o *BookingController) Search() {
	/*
	* This is a sample of how to use the search function
	* The search function is used to search for data in the database
	* The search function takes in a filter and a data type
	* The filter is a struct that contains the search parameters
	* The data type is an enum that specifies the type of data to search for
	* The search function returns a list of data that matches the filter
	* The data is then returned as a json object
	 */
	// store and return Id or post with UUID
	user, peerID, groups := oclib.ExtractTokenInfo(*o.Ctx.Request)
	start_date, _ := time.Parse("2006-01-02", o.Ctx.Input.Param(":start_date"))
	end_date, _ := time.Parse("2006-01-02", o.Ctx.Input.Param(":end_date"))
	isDraft := o.Ctx.Input.Query("is_draft")
	sd := primitive.NewDateTimeFromTime(start_date)
	ed := primitive.NewDateTimeFromTime(end_date)
	f := dbs.Filters{
		And: map[string][]dbs.Filter{
			"execution_date": {{Operator: "gte", Value: sd}, {Operator: "lte", Value: ed}},
		},
	}
	o.Data["json"] = oclib.NewRequest(oclib.LibDataEnum(oclib.BOOKING), user, peerID, groups, nil).Search(&f, "", isDraft == "true")
	o.ServeJSON()
}

// @Title GetAll
// @Description find booking by id
// @Param	is_draft		query 	string	false		"draft wished"
// @Success 200 {booking} models.booking
// @router / [get]
func (o *BookingController) GetAll() {
	user, peerID, groups := oclib.ExtractTokenInfo(*o.Ctx.Request)
	isDraft := o.Ctx.Input.Query("is_draft")
	o.Data["json"] = oclib.NewRequest(oclib.LibDataEnum(oclib.BOOKING), user, peerID, groups, nil).LoadAll(isDraft == "true")
	o.ServeJSON()
}

// @Title Get
// @Description find booking by id
// @Param	id		path 	string	true		"the id you want to get"
// @Success 200 {booking} models.booking
// @router /:id [get]
func (o *BookingController) Get() {
	user, peerID, groups := oclib.ExtractTokenInfo(*o.Ctx.Request)
	id := o.Ctx.Input.Param(":id")
	o.Data["json"] = oclib.NewRequest(oclib.LibDataEnum(oclib.BOOKING), user, peerID, groups, nil).LoadOne(id)
	o.ServeJSON()
}

// @Title Update
// @Description create computes
// @Param	id		path 	string	true		"the compute id you want to get"
// @Param	body		body 	models.compute	true		"The compute content"
// @Success 200 {compute} models.compute
// @router /:id [put]
func (o *BookingController) Put() {
	user, peerID, groups := oclib.ExtractTokenInfo(*o.Ctx.Request)
	// store and return Id or post with UUID
	var res map[string]interface{}
	id := o.Ctx.Input.Param(":id")
	book := oclib.NewRequest(oclib.LibDataEnum(oclib.BOOKING), user, peerID, groups, nil).LoadOne(id)
	if book.Code != 200 {
		o.Data["json"] = map[string]interface{}{
			"data":  nil,
			"code":  book.Code,
			"error": book.Err,
		}
		o.ServeJSON()
		return
	}
	booking := book.Data.(*b.Booking)
	if time.Now().After(booking.ExpectedStartDate) {
		o.Data["json"] = oclib.NewRequest(oclib.LibDataEnum(oclib.BOOKING), user, peerID, groups, nil).UpdateOne(res, id)
	} else {
		o.Data["json"] = map[string]interface{}{
			"data":  nil,
			"code":  409,
			"error": "booking is not already started",
		}
	}
	o.ServeJSON()
}

// @Title Check
// @Description check booking
// @Param	id	path 	string		"id of the datacenter"
// @Param	start_date		path 	string		"the booking start date" format "2006-01-02T15:04:05"
// @Param	end_date		path 	string		"the booking end date" format "2006-01-02T15:04:05"
// @Param	is_draft		query 	string	false		"draft wished"
// @Success 200 {object} models.object
// @router /check/:id/:start_date/:end_date [get]
func (o *BookingController) Check() {
	/*
	 * This function is used to check if a booking is available for a specific datacenter.
	 * It takes the following parameters:
	 * - id: the id of the datacenter
	 * - start_date: the start date of the booking/search/execution/:id
	 * - end_date: the end date of the booking
	 */
	id := o.Ctx.Input.Param(":id")
	date, err := time.Parse("2006-01-02T15:04:05", o.Ctx.Input.Param(":start_date"))
	date2, err2 := time.Parse("2006-01-02T15:04:05", o.Ctx.Input.Param(":end_date"))
	if err != nil || err2 != nil {
		o.Data["json"] = map[string]interface{}{
			"data": map[string]interface{}{
				"is_available": false,
			},
			"code":  400,
			"error": errors.New("invalid date format"),
		}
	} else {
		booking := &b.Booking{}                                 // create a new booking object
		isAvailable, err2 := booking.Check(id, date, &date2, 1) // check if the booking is available
		fmt.Println(isAvailable, err2)
		code := 200
		err := ""
		if !isAvailable {
			code = 409
			err = "booking not available"
			if err2 != nil {
				err += " - " + err2.Error()
			}
		}
		o.Data["json"] = map[string]interface{}{
			"data": map[string]interface{}{
				"is_available": isAvailable,
			},
			"code":  code,
			"error": err,
		}
	}
	o.ServeJSON()
}

// @Title Post.
// @Description create booking
// @Param	booking		body 	string	true		"the booking you want to post"
// @Param	is_draft		query 	string	false		"draft wished"
// @Success 200 {object} models.object
// @router / [post]
func (o *BookingController) Post() {
	/*
	 * This function is used to create a booking.
	 * It takes the following parameters:
	 * - booking: the booking you want to post
	 * The booking is a JSON object that contains the following fields:
	 * - datacenter_resource_id: the id of the datacenter
	 * - workflow_execution: the workflow execution
	 */
	var resp booking.Booking
	user, peerID, groups := oclib.ExtractTokenInfo(*o.Ctx.Request)
	err := json.Unmarshal(o.Ctx.Input.CopyBody(10000000), &resp)
	if err != nil {
		o.Data["json"] = map[string]interface{}{
			"data":  nil,
			"code":  422,
			"error": err,
		}
		o.ServeJSON()
		return
	}
	if resp.ResourceType == tools.COMPUTE_RESOURCE {
		// later should check... health for any such as docker...
		res := oclib.NewRequest(oclib.LibDataEnum(oclib.COMPUTE_RESOURCE), user, peerID, groups, nil).LoadOne(resp.ResourceID)
		if res.Err != "" {
			o.Data["json"] = map[string]interface{}{
				"data":  nil,
				"code":  res.Code,
				"error": res.Err,
			}
			o.ServeJSON()
			return
		}
		serv, err := infrastructure.NewServiceByType(res.ToComputeResource().Infrastructure.String())
		if err != nil {
			o.Data["json"] = map[string]interface{}{
				"data":  nil,
				"code":  500,
				"error": err,
			}
			o.ServeJSON()
			return
		}
		if err := serv.CheckHealth(); err != nil {
			o.Data["json"] = map[string]interface{}{
				"data":  nil,
				"code":  500,
				"error": err,
			}
			o.ServeJSON()
			return
		}
	}
	// delete all previous bookings
	isDraft := o.Ctx.Input.Query("is_draft")
	res := oclib.NewRequest(oclib.LibDataEnum(oclib.BOOKING), user, peerID, groups, nil).Search(&dbs.Filters{And: map[string][]dbs.Filter{
		"workflow_id": {{Operator: dbs.EQUAL.String(), Value: resp.WorkflowID}},
		"resource_id": {{Operator: dbs.EQUAL.String(), Value: resp.ResourceID}},
	}}, "", isDraft == "true")
	if res.Code != 200 {
		o.Data["json"] = map[string]interface{}{
			"data":  nil,
			"code":  res.Code,
			"error": res.Err,
		}
		o.ServeJSON()
		return
	}
	for _, b := range res.Data { // delete all previous bookings
		oclib.NewRequest(oclib.LibDataEnum(oclib.BOOKING), user, peerID, groups, nil).DeleteOne(b.GetID())
	}
	b := oclib.NewRequest(oclib.LibDataEnum(oclib.BOOKING), user, peerID, groups, nil).StoreOne(resp.Serialize(&resp))
	if b.Code != 200 {
		o.Data["json"] = map[string]interface{}{
			"data":  nil,
			"code":  b.Code,
			"error": b.Err,
		}
		o.ServeJSON()
		return
	}
	if b.Data.(*booking.Booking).ResourceType == tools.COMPUTE_RESOURCE {
		go func() {
			time.Sleep(time.Until(b.Data.(*booking.Booking).ExpectedStartDate))
			infrastructure.NewPrometheusService().Stream(b.Data.GetID(), b.Data.(*booking.Booking).ExpectedEndDate, 1*time.Second, nil, nil)
		}()
	}

	if err := o.createNamespace(resp.ExecutionsID); err != nil {
		fmt.Println(err.Error())
	}

	o.Data["json"] = map[string]interface{}{
		"data":  []interface{}{b},
		"code":  200,
		"error": "",
	}
	o.ServeJSON()
}

func (o *BookingController) createNamespace(ns string) error {
	/*
	 * This function is used to create a namespace.
	 * It takes the following parameters:
	 * - ns: the namespace you want to create
	 */
	serv, err := infrastructure.NewService()
	if err != nil {
		return nil
	}
	err = serv.CreateNamespace(o.Ctx.Request.Context(), ns)
	if err != nil {
		return err
	}
	err = serv.CreateServiceAccount(o.Ctx.Request.Context(), ns)
	if err != nil {
		return err
	}
	role := "argo-role"
	err = serv.CreateRole(o.Ctx.Request.Context(), ns, role,
		[][]string{
			{"coordination.k8s.io"},
			{""},
			{""}},
		[][]string{
			{"leases"},
			{"secrets"},
			{"pods"}},
		[][]string{
			{"get", "create", "update"},
			{"get"},
			{"patch"}})
	if err != nil {
		return err
	}
	return serv.CreateRoleBinding(o.Ctx.Request.Context(), ns, "argo-role-binding", role)
}
