package workflow_execution

import (
	"encoding/json"
	"strings"
	"time"

	"cloud.o-forge.io/core/oc-lib/dbs"
	"cloud.o-forge.io/core/oc-lib/models/booking"
	"cloud.o-forge.io/core/oc-lib/models/common/enum"
	"cloud.o-forge.io/core/oc-lib/models/common/pricing"
	"cloud.o-forge.io/core/oc-lib/models/resources"
	"cloud.o-forge.io/core/oc-lib/models/resources/purchase_resource"
	"cloud.o-forge.io/core/oc-lib/models/utils"
	"cloud.o-forge.io/core/oc-lib/models/workflow"
	"cloud.o-forge.io/core/oc-lib/tools"
	"github.com/google/uuid"
	"go.mongodb.org/mongo-driver/bson/primitive"
)

// BookingState tracks the reservation and completion status of a single booking
// within a workflow execution.
//   - IsBooked: true while the resource is actively reserved (set on WORKFLOW_STARTED_EVENT,
//     cleared on WORKFLOW_STEP_DONE_EVENT / WORKFLOW_DONE_EVENT).
//   - IsDone: true once the booking has been confirmed by the remote peer (CONSIDERS_EVENT)
//     or completed (WORKFLOW_STEP_DONE_EVENT / WORKFLOW_DONE_EVENT).
type BookingState struct {
	IsBooked bool `json:"is_booked" bson:"is_booked"`
	IsDone   bool `json:"is_done" bson:"is_done"`
}

/*
* WorkflowExecution is a struct that represents a list of workflow executions
* Warning: No user can write (del, post, put) a workflow execution, it is only used by the system
* workflows generate their own executions
 */
type WorkflowExecution struct {
	utils.AbstractObject                                // AbstractObject contains the basic fields of an object (id, name)
	Priority             int                            `json:"priority" bson:"priority"`                                         // will best effort on default                                    // Will Best Effort on priority
	PeerBuyByGraph       map[string]map[string][]string `json:"peer_buy_by_graph,omitempty" bson:"peer_buy_by_graph,omitempty"`   // BookByResource is a map of the resource id and the list of the booking id
	PeerBookByGraph      map[string]map[string][]string `json:"peer_book_by_graph,omitempty" bson:"peer_book_by_graph,omitempty"` // BookByResource is a map of the resource id and the list of the booking id
	ExecutionsID         string                         `json:"executions_id,omitempty" bson:"executions_id,omitempty"`
	ExecDate             time.Time                      `json:"execution_date,omitempty" bson:"execution_date,omitempty" validate:"required"` // ExecDate is the execution date of the workflow, is required
	EndDate              *time.Time                     `json:"end_date,omitempty" bson:"end_date,omitempty"`                                 // EndDate is the end date of the workflow
	State                enum.BookingStatus             `json:"state" bson:"state" default:"0"`                                               // TEMPORARY TODO DEFAULT 1 -> 0 State is the state of the workflow
	WorkflowID           string                         `json:"workflow_id" bson:"workflow_id,omitempty"`                                     // WorkflowID is the ID of the workflow

	BookingsState  map[string]BookingState `json:"bookings_state" bson:"bookings_state,omitempty"`   // booking_id → reservation+completion status
	PurchasesState map[string]bool         `json:"purchases_state" bson:"purchases_state,omitempty"` // purchase_id → confirmed

	// Graph is a lightweight, real-time summary of the workflow execution graph.
	// Keyed by workflow graph item ID; updated by oc-scheduler on each step-done event.
	// Consumed by oc-front to render the live execution panel via websocket updates.
	Graph ExecutionGraph `json:"graph,omitempty" bson:"graph,omitempty"`

	SelectedInstances    workflow.ConfigItem `json:"selected_instances"`
	SelectedPartnerships workflow.ConfigItem `json:"selected_partnerships"`
	SelectedBuyings      workflow.ConfigItem `json:"selected_buyings"`
	SelectedStrategies   workflow.ConfigItem `json:"selected_strategies"`
	SelectedPaymentMode  workflow.ConfigItem `json:"selected_payment_mode"`

	// SelectedBillingStrategy est la fréquence de facturation globale choisie par l'utilisateur
	// (BILL_ONCE, BILL_PER_WEEK, BILL_PER_MONTH, BILL_PER_YEAR).
	// Propagée depuis WorkflowSchedule.SelectedBillingStrategy par GenerateExecutions().
	SelectedBillingStrategy pricing.BillingStrategy `json:"selected_billing_strategy" bson:"selected_billing_strategy"`

	// SelectedEmbeddedStorages records which storage capability was activated on
	// each compute unit graph node (key = compute graph node ID).
	// Populated by oc-scheduler, consumed by oc-monitord's argo builder.
	SelectedEmbeddedStorages map[string]*resources.EmbeddedStorageSelection `json:"selected_embedded_storages,omitempty" bson:"selected_embedded_storages,omitempty"`
}

func (ri *WorkflowExecution) Extend(typ ...string) map[string][]tools.DataType {
	ext := ri.AbstractObject.Extend(typ...)
	for _, t := range typ {
		switch t {
		case "workflow":
			if _, ok := ext[t]; !ok {
				ext[t] = []tools.DataType{}
			}
			ext[t] = append(ext[t], tools.PEER)
		}
	}
	return ext
}

func (r *WorkflowExecution) StoreDraftDefault() {
	r.IsDraft = true
	r.State = enum.DRAFT
}

func (r *WorkflowExecution) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
	if r.State != set.(*WorkflowExecution).State {
		return true, &WorkflowExecution{State: set.(*WorkflowExecution).State} // only state can be updated
	}
	return !r.IsDraft, set // only draft buying can be updated
}

func (r *WorkflowExecution) CanDelete() bool {
	return r.IsDraft // only draft bookings can be deleted
}

func (wfa *WorkflowExecution) Equals(we *WorkflowExecution) bool {
	return wfa.ExecDate.Equal(we.ExecDate) && wfa.WorkflowID == we.WorkflowID
}

func (ws *WorkflowExecution) PurgeDraft(request *tools.APIRequest) error {
	if ws.EndDate == nil {
		// if no end... then Book like a savage
		e := ws.ExecDate.UTC().Add(time.Hour)
		ws.EndDate = &e
	}
	accessor := ws.GetAccessor(request)
	res, code, err := accessor.Search(&dbs.Filters{
		And: map[string][]dbs.Filter{ // check if there is a booking on the same compute resource by filtering on the compute_resource_id, the state and the execution date
			"state":       {{Operator: dbs.EQUAL.String(), Value: enum.DRAFT.EnumIndex()}},
			"workflow_id": {{Operator: dbs.EQUAL.String(), Value: ws.WorkflowID}},
			"execution_date": {
				{Operator: dbs.LTE.String(), Value: primitive.NewDateTimeFromTime(*ws.EndDate)},
				{Operator: dbs.GTE.String(), Value: primitive.NewDateTimeFromTime(ws.ExecDate)},
			},
		},
	}, "", ws.IsDraft, 0, 10000)
	if code != 200 || err != nil {
		return err
	}
	for _, r := range res {
		accessor.DeleteOne(r.GetID())
	}
	return nil
}

// tool to transform the argo status to a state
func (wfa *WorkflowExecution) ArgoStatusToState(status string) *WorkflowExecution {
	status = strings.ToLower(status)
	switch status {
	case "succeeded": // Succeeded
		wfa.State = enum.SUCCESS
	case "pending": // Pending
		wfa.State = enum.SCHEDULED
	case "running": // Running
		wfa.State = enum.STARTED
	default: // Failed
		wfa.State = enum.FAILURE
	}
	return wfa
}

func (r *WorkflowExecution) GenerateID() {
	if r.UUID == "" {
		r.UUID = uuid.New().String()
	}
}

func (d *WorkflowExecution) GetName() string {
	return d.UUID + "_" + d.ExecDate.String()
}

func (d *WorkflowExecution) GetAccessor(request *tools.APIRequest) utils.Accessor {
	return NewAccessor(request) // Create a new instance of the accessor
}

func (d *WorkflowExecution) VerifyAuth(callName string, request *tools.APIRequest) bool {
	return true
}

/*
booking is an activity reserved for not a long time investment.
... purchase is dependant of a one time buying.
use of a datacenter or storage can't be buy for permanent access.
*/
func (d *WorkflowExecution) Buy(bs pricing.BillingStrategy, executionsID string, wfID string, priceds map[tools.DataType]map[string]pricing.PricedItemITF) []*purchase_resource.PurchaseResource {
	purchases := d.buyEach(bs, executionsID, wfID, tools.PROCESSING_RESOURCE, priceds[tools.PROCESSING_RESOURCE])
	purchases = append(purchases, d.buyEach(bs, executionsID, wfID, tools.DATA_RESOURCE, priceds[tools.DATA_RESOURCE])...)
	purchases = append(purchases, d.buyEach(bs, executionsID, wfID, tools.SERVICE_RESOURCE, priceds[tools.SERVICE_RESOURCE])...)
	d.PurchasesState = map[string]bool{}
	for _, p := range purchases {
		d.PurchasesState[p.GetID()] = false
	}
	return purchases
}

func (d *WorkflowExecution) buyEach(bs pricing.BillingStrategy, executionsID string, wfID string, dt tools.DataType, priceds map[string]pricing.PricedItemITF) []*purchase_resource.PurchaseResource {
	items := []*purchase_resource.PurchaseResource{}
	for itemID, priced := range priceds {
		if !priced.IsPurchasable() || bs != pricing.BILL_ONCE { // buy only that must be buy
			continue
		}
		if d.PeerBuyByGraph == nil {
			d.PeerBuyByGraph = map[string]map[string][]string{}
		}
		if d.PeerBuyByGraph[priced.GetCreatorID()] == nil {
			d.PeerBuyByGraph[priced.GetCreatorID()] = map[string][]string{}
		}
		if d.PeerBuyByGraph[priced.GetCreatorID()][itemID] == nil {
			d.PeerBuyByGraph[priced.GetCreatorID()][itemID] = []string{}
		}
		start := d.ExecDate
		if s := priced.GetLocationStart(); s != nil && s.After(time.Now()) {
			start = *s
		}
		var m map[string]interface{}
		b, _ := json.Marshal(priced)
		json.Unmarshal(b, &m)
		var endDate *time.Time
		if durS := priced.GetExplicitDurationInS(); durS > 0 {
			e := start.Add(time.Duration(durS) * time.Second)
			endDate = &e
		}
		bookingItem := &purchase_resource.PurchaseResource{
			AbstractObject: utils.AbstractObject{
				UUID:    uuid.New().String(),
				Name:    d.GetName() + "_" + executionsID + "_" + wfID,
				IsDraft: true,
			},
			PricedItem:   m,
			ExecutionID:  d.GetID(),
			ExecutionsID: executionsID,
			DestPeerID:   priced.GetCreatorID(),
			ResourceID:   priced.GetID(),
			InstanceID:   priced.GetInstanceID(),
			ResourceType: dt,
			EndDate:      endDate,
		}
		items = append(items, bookingItem)
		d.PeerBuyByGraph[priced.GetCreatorID()][itemID] = append(
			d.PeerBuyByGraph[priced.GetCreatorID()][itemID], bookingItem.GetID())
	}
	return items
}

func (d *WorkflowExecution) Book(executionsID string, wfID string, priceds map[tools.DataType]map[string]pricing.PricedItemITF) []*booking.Booking {
	booking := d.bookEach(executionsID, wfID, tools.STORAGE_RESOURCE, priceds[tools.STORAGE_RESOURCE])
	booking = append(booking, d.bookEach(executionsID, wfID, tools.PROCESSING_RESOURCE, priceds[tools.PROCESSING_RESOURCE])...)
	booking = append(booking, d.bookEach(executionsID, wfID, tools.SERVICE_RESOURCE, priceds[tools.SERVICE_RESOURCE])...)
	booking = append(booking, d.bookEach(executionsID, wfID, tools.COMPUTE_RESOURCE, priceds[tools.COMPUTE_RESOURCE])...)
	booking = append(booking, d.bookEach(executionsID, wfID, tools.DATA_RESOURCE, priceds[tools.DATA_RESOURCE])...)
	for _, p := range booking {
		if d.BookingsState == nil {
			d.BookingsState = map[string]BookingState{}
		}
		d.BookingsState[p.GetID()] = BookingState{}
	}
	return booking
}

func (d *WorkflowExecution) bookEach(executionsID string, wfID string, dt tools.DataType, priceds map[string]pricing.PricedItemITF) []*booking.Booking {
	items := []*booking.Booking{}
	for itemID, priced := range priceds {
		if !priced.IsBooked() { // book only that must be booked
			continue
		}
		if d.PeerBookByGraph == nil {
			d.PeerBookByGraph = map[string]map[string][]string{}
		}
		if d.PeerBookByGraph[priced.GetCreatorID()] == nil {
			d.PeerBookByGraph[priced.GetCreatorID()] = map[string][]string{}
		}
		if d.PeerBookByGraph[priced.GetCreatorID()][itemID] == nil {
			d.PeerBookByGraph[priced.GetCreatorID()][itemID] = []string{}
		}
		start := d.ExecDate
		if s := priced.GetLocationStart(); s != nil && s.After(time.Now()) {
			start = *s
		}
		// Prefer LocationEnd set by Planify; fall back to ExplicitDurationInS only
		// when Planify did not compute an end (open-ended / service resources).
		var endDate *time.Time
		if locEnd := priced.GetLocationEnd(); locEnd != nil {
			endDate = locEnd
		} else if durationS := priced.GetExplicitDurationInS(); durationS > 0 {
			e := start.Add(time.Duration(durationS) * time.Second)
			endDate = &e
		}
		// durationS < 0 means the resource is a service (runs indefinitely):
		// leave ExpectedEndDate nil so the booking is open-ended.
		var m map[string]interface{}
		b, _ := json.Marshal(priced)
		json.Unmarshal(b, &m)
		name := priced.GetName()
		if len(executionsID) > 8 {
			name += " " + executionsID[:8]
		}
		// Résout le mode de paiement spécifique à cette ressource depuis SelectedPaymentMode.
		// SelectedPaymentMode est un ConfigItem (map[string]int) dont les clés sont les IDs
		// de nœuds du graph ; on tente itemID puis l'ID de la ressource comme fallback.
		paymentType := pricing.PAY_ONCE
		if v, ok := d.SelectedPaymentMode[itemID]; ok {
			paymentType = pricing.PaymentType(v)
		} else if v, ok := d.SelectedPaymentMode[priced.GetID()]; ok {
			paymentType = pricing.PaymentType(v)
		}

		bookingItem := &booking.Booking{
			AbstractObject: utils.AbstractObject{
				UUID:    uuid.New().String(),
				Name:    name + " " + start.Format("2006-01-02 15:04"),
				IsDraft: true,
			},
			PricedItem:        m,
			ExecutionsID:      executionsID,
			State:             enum.DRAFT,
			ResourceID:        priced.GetID(),
			InstanceID:        priced.GetInstanceID(),
			ResourceType:      dt,
			DestPeerID:        priced.GetCreatorID(),
			Peerless:          priced.GetCreatorID() == "",
			WorkflowID:        wfID,
			ExecutionID:       d.GetID(),
			ExpectedStartDate: start,
			ExpectedEndDate:   endDate,
			BillingStrategy:   d.SelectedBillingStrategy,
			PaymentType:       paymentType,
		}
		items = append(items, bookingItem)
		d.PeerBookByGraph[priced.GetCreatorID()][itemID] = append(
			d.PeerBookByGraph[priced.GetCreatorID()][itemID], bookingItem.GetID())
	}
	return items
}
