package resources

import (
	"encoding/json"
	"errors"
	"fmt"
	"slices"
	"time"

	"cloud.o-forge.io/core/oc-lib/config"
	"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/models"
	"cloud.o-forge.io/core/oc-lib/models/common/pricing"
	"cloud.o-forge.io/core/oc-lib/models/live"
	"cloud.o-forge.io/core/oc-lib/models/peer"
	"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/tools"
	"github.com/biter777/countries"
	"github.com/google/uuid"
)

func FiltersFromFlatMap(flatMap map[string]interface{}, target interface{}) *dbs.Filters {
	return dbs.FiltersFromFlatMap(flatMap, target)
}

// AbstractResource is the struct containing all of the attributes commons to all ressources
type AbstractResource struct {
	utils.AbstractObject                                                   // AbstractObject contains the basic fields of an object (id, name)
	PurchaseID           string                                            `json:"purchase_id,omitempty"`                                          // is_buy precise if a resource is buy or not
	Type                 string                                            `json:"type,omitempty" bson:"type,omitempty"`                           // Type is the type of the resource
	Logo                 string                                            `json:"logo,omitempty" bson:"logo,omitempty"`                           // Logo is the logo of the resource
	Description          string                                            `json:"description,omitempty" bson:"description,omitempty"`             // Description is the description of the resource
	ShortDescription     string                                            `json:"short_description,omitempty" bson:"short_description,omitempty"` // ShortDescription is the short description of the resource
	Owners               []utils.Owner                                     `json:"owners,omitempty" bson:"owners,omitempty"`                       // Owners is the list of owners of the resource
	UsageRestrictions    string                                            `bson:"usage_restrictions,omitempty" json:"usage_restrictions,omitempty"`
	AllowedBookingModes  map[booking.BookingMode]*pricing.PricingVariation `bson:"allowed_booking_modes" json:"allowed_booking_modes"`

	Env     []models.Param `json:"env,omitempty" bson:"env,omitempty"`
	Inputs  []models.Param `json:"inputs,omitempty" bson:"inputs,omitempty"`
	Outputs []models.Param `json:"outputs,omitempty" bson:"outputs,omitempty"`

	// License is the usage licence of the resource (SPDX identifier or free-text).
	// Displayed prominently in the catalog detail view.
	License string `json:"license,omitempty" bson:"license,omitempty"`

	// ExploitationAuthorizations (AEs) are coupling and peer-usage constraints
	// issued by the resource owner.  Stored embedded in the resource document,
	// NOT in a separate collection.
	// Visibility-filtered per requesting peer before any response is sent.
	ExploitationAuthorizations []ExploitationAuthorization `json:"exploitation_authorizations,omitempty" bson:"exploitation_authorizations,omitempty"`
}

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

func (abs *AbstractResource) VerifyBuy() {
	p := &purchase_resource.PurchaseResource{}
	access := p.GetAccessor(&tools.APIRequest{Admin: true})

	purchase, _, _ := access.Search(&dbs.Filters{
		And: map[string][]dbs.Filter{
			"resource_id": {{Operator: dbs.EQUAL.String(), Value: abs.GetID()}},
		},
	}, "", false, 0, 1)
	if len(purchase) > 0 {
		abs.PurchaseID = purchase[0].GetID()
	}
}

func (abs *AbstractResource) GetEnv() []models.Param {
	return abs.Env
}
func (abs *AbstractResource) GetInputs() []models.Param {
	return abs.Inputs
}
func (abs *AbstractResource) GetOutputs() []models.Param {
	return abs.Outputs
}

func (abs *AbstractResource) FilterPeer(peerID string) *dbs.Filters {
	return nil
}

// GetExploitationAuthorizations returns all AEs attached to this resource.
// Used by oc-schedulerd's CheckWorkflowAE via structural interface assertion.
func (r *AbstractResource) GetExploitationAuthorizations() []ExploitationAuthorization {
	return r.ExploitationAuthorizations
}

// FilterExploitationAuthorizations removes AEs that are not visible to peerID.
// Must be called before serializing the resource for a consumer peer.
// The resource owner (CreatorID) always sees all AEs unfiltered.
func (r *AbstractResource) FilterExploitationAuthorizations(peerID string, isAdmin bool) {
	if isAdmin {
		return // admin or owner: no filtering
	}
	filtered := r.ExploitationAuthorizations[:0]
	for _, ae := range r.ExploitationAuthorizations {
		if ae.IsVisibleTo(peerID, r.CreatorID) {
			filtered = append(filtered, ae)
		}
	}
	r.ExploitationAuthorizations = filtered
}

func (ri *AbstractResource) ClearEnv() utils.DBObject {
	ri.Env = []models.Param{}
	ri.Inputs = []models.Param{}
	ri.Outputs = []models.Param{}
	return ri
}

func (r *AbstractResource) GetBookingModes() map[booking.BookingMode]*pricing.PricingVariation {
	if len(r.AllowedBookingModes) == 0 {
		return map[booking.BookingMode]*pricing.PricingVariation{
			booking.PLANNED: {
				Percentage: 0,
			}, booking.WHEN_POSSIBLE: {
				Percentage: 0,
			},
		}
	}
	return r.AllowedBookingModes
}

func (r *AbstractResource) GetType() string {
	return tools.INVALID.String()
}

func (r *AbstractResource) StoreDraftDefault() {
	//r.IsDraft = true pour le moment on passe outre.
}

func (r *AbstractResource) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
	fmt.Println("IsDrafted", r.IsDraft, set.IsDrafted())
	return r.IsDraft || set.IsDrafted(), set
}

type AbstractInstanciatedResource[T ResourceInstanceITF] struct {
	AbstractResource // AbstractResource contains the basic fields of an object (id, name)

	Instances []T `json:"instances,omitempty" bson:"instances,omitempty"`
}

func (abs *AbstractInstanciatedResource[T]) AddInstances(instance ResourceInstanceITF) {
	abs.Instances = append(abs.Instances, instance.(T))
}

func ConvertToPricedResource[T pricing.PricingProfileITF](t tools.DataType,
	selectedInstance *int, selectedPartnership *int, selectedBuyingStrategy *int, selectedStrategy *int,
	selectedBookingModeIndex *int, abs ResourceInterface, request *tools.APIRequest) (pricing.PricedItemITF, error) {
	instances := map[string]string{}
	var profile pricing.PricingProfileITF
	var inst ResourceInstanceITF
	if t := abs.GetSelectedInstance(selectedInstance); t != nil {
		inst = t
		instances[t.GetID()] = t.GetName()
		profile = t.GetProfile(request.PeerID, selectedPartnership, selectedBuyingStrategy, selectedStrategy)
	} else {
		for i, instance := range abs.SetAllowedInstances(request) { // TODO why it crush before ?
			if i == 0 {
				inst = instance
			}
			instances[instance.GetID()] = instance.GetName()
			profiles := instance.GetPricingsProfiles(request.PeerID, request.Groups)
			if len(profiles) > 0 {
				profile = profiles[0]
				break
			}
		}
	}
	if profile == nil {
		/*if ok, _ := utils.IsMySelf(request.PeerID, (&peer.Peer{}).GetAccessor(&tools.APIRequest{
			Admin: true,
		})); ok {*/
		profile = pricing.GetDefaultPricingProfile()
		/*} else {
			return nil, errors.New("no pricing profile found")
		}*/
	}
	variations := []*pricing.PricingVariation{}
	if selectedBookingModeIndex != nil && abs.GetBookingModes()[booking.BookingMode(*selectedBookingModeIndex)] != nil {
		variations = append(variations, abs.GetBookingModes()[booking.BookingMode(*selectedBookingModeIndex)])
	}
	// Seed the booking configuration with the instance's historical average duration
	// so GetExplicitDurationInS() returns a realistic default out of the box.
	var bc *BookingConfiguration
	if inst != nil {
		if avg := inst.GetAverageDurationS(); avg > 0 {
			bc = &BookingConfiguration{ExplicitBookingDurationS: avg}
		}
	}
	instanceID := ""
	if inst != nil {
		instanceID = inst.GetID()
	}
	selectedPricing, _ := profile.(T)
	return &PricedResource[T]{
		Name:                 abs.GetName(),
		ResourceID:           abs.GetID(),
		InstanceID:           instanceID,
		ResourceType:         t,
		Quantity:             1,
		InstancesRefs:        instances,
		SelectedPricing:      selectedPricing,
		Variations:           variations,
		CreatorID:            abs.GetCreatorID(),
		BookingConfiguration: bc,
	}, nil
}

func (r *AbstractInstanciatedResource[T]) GetSelectedInstance(selected *int) ResourceInstanceITF {
	if selected != nil && len(r.Instances) > *selected {
		return r.Instances[*selected]
	}
	if len(r.Instances) > 0 {
		return r.Instances[0]
	}
	return nil
}

func (abs *AbstractInstanciatedResource[T]) SetAllowedInstances(request *tools.APIRequest, instanceID ...string) []ResourceInstanceITF {
	if !((request != nil && request.PeerID == abs.CreatorID && request.PeerID != "") || request.Admin) {
		abs.Instances = VerifyAuthAction(abs.Instances, request, instanceID...)
		// Filter AEs: only return AEs visible to the requesting peer.
		if request != nil {
			abs.FilterExploitationAuthorizations(request.PeerID, request.Admin)
		}
	}
	inst := []ResourceInstanceITF{}
	for _, i := range abs.Instances {
		inst = append(inst, i)
	}
	return inst
}

func (abs *AbstractInstanciatedResource[T]) VerifyAuth(callName string, request *tools.APIRequest) bool {
	return len(VerifyAuthAction(abs.Instances, request)) > 0 || abs.AbstractObject.VerifyAuth(callName, request)
}

func VerifyAuthAction[T ResourceInstanceITF](baseInstance []T, request *tools.APIRequest, instanceID ...string) []T {
	instances := []T{}
	for _, instance := range baseInstance {
		if len(instanceID) > 0 && !slices.Contains(instanceID, instance.GetID()) {
			continue
		}
		// Structurally peerless instances (no creator, no partnerships, non-empty Ref)
		// are freely accessible by any requester.
		if instance.IsPeerless() {
			instances = append(instances, instance)
			continue
		}
		_, peerGroups := instance.GetPeerGroups()
		for _, peers := range peerGroups {
			if request == nil {
				continue
			}
			_, allOK := peers["*"]
			if grps, ok := peers[request.PeerID]; ok || allOK || config.GetConfig().Whitelist {
				if allOK || (ok && slices.Contains(grps, "*")) || (!ok && config.GetConfig().Whitelist) {
					instance.FilterInstance(request.PeerID)
					instances = append(instances, instance)
					// TODO filter Partners + Profiles...
					continue
				}
				for _, grp := range grps {
					if slices.Contains(request.Groups, grp) {
						instance.FilterInstance(request.PeerID)
						instances = append(instances, instance)
					}
				}
			}
		}
	}
	return instances
}

type ResourceInstance[T ResourcePartnerITF] struct {
	utils.AbstractObject
	ContentType    string                `json:"content_type,omitempty" bson:"content_type,omitempty"`
	LastUpdate     time.Time             `json:"last_update,omitempty" bson:"last_update,omitempty"`
	Origin         OriginMeta            `json:"origin,omitempty" bson:"origin,omitempty"`
	Location       live.GeoPoint         `json:"location,omitempty" bson:"location,omitempty"`
	Country        countries.CountryCode `json:"country,omitempty" bson:"country,omitempty"`
	AccessProtocol string                `json:"access_protocol,omitempty" bson:"access_protocol,omitempty"`

	Partnerships []T `json:"partnerships,omitempty" bson:"partnerships,omitempty"`

	AverageDurationS       float64 `json:"average_duration_s,omitempty" bson:"average_duration_s,omitempty"`
	AverageDurationSamples int     `json:"average_duration_samples,omitempty" bson:"average_duration_samples,omitempty"`
}

// TODO should kicks all selection

func NewInstance[T ResourcePartnerITF](name string) *ResourceInstance[T] {
	return &ResourceInstance[T]{
		AbstractObject: utils.AbstractObject{
			UUID: uuid.New().String(),
			Name: name,
		},
		Partnerships: []T{},
	}
}

func (ri *ResourceInstance[T]) GetOrigin() OriginMeta {
	return ri.Origin
}

// IsPeerless returns true when the instance has no owning peer and a non-empty
// registry reference. This is derived from structural invariants — NOT from the
// self-declared Origin.Type field — to prevent auth bypass via metadata manipulation:
//
//	CreatorID == "" ∧ len(Partnerships) == 0 ∧ Origin.Ref != ""
func (ri *ResourceInstance[T]) IsPeerless() bool {
	return ri.CreatorID == "" && len(ri.Partnerships) == 0 && ri.Origin.Ref != ""
}

func (ri *ResourceInstance[T]) FilterInstance(peerID string) {
	partnerships := []T{}
	for _, p := range ri.Partnerships {
		if p.GetPeerGroups()["*"] != nil || p.GetPeerGroups()[peerID] != nil {
			p.FilterPartnership(peerID)
			partnerships = append(partnerships, p)
		}
	}
	ri.Partnerships = partnerships
}

func (ri *ResourceInstance[T]) GetProfile(peerID string, partnershipIndex *int, buyingIndex *int, strategyIndex *int) pricing.PricingProfileITF {
	if ri.IsPeerless() {
		return pricing.GetDefaultPricingProfile()
	}
	if partnershipIndex != nil && len(ri.Partnerships) > *partnershipIndex {
		prts := ri.Partnerships[*partnershipIndex]
		return prts.GetProfile(buyingIndex, strategyIndex)
	}
	if ok, _ := utils.IsMySelf(peerID, (&peer.Peer{}).GetAccessor(&tools.APIRequest{
		Admin: true,
	})); ok {
		return pricing.GetDefaultPricingProfile()
	}
	return nil
}

func (ri *ResourceInstance[T]) GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF {
	if ri.IsPeerless() {
		return []pricing.PricingProfileITF{pricing.GetDefaultPricingProfile()}
	}
	pricings := []pricing.PricingProfileITF{}
	for _, p := range ri.Partnerships {
		pricings = append(pricings, p.GetPricingsProfiles(peerID, groups)...)
	}
	if len(pricings) == 0 {
		if ok, _ := utils.IsMySelf(peerID, (&peer.Peer{}).GetAccessor(&tools.APIRequest{
			Admin: true,
		})); ok {
			pricings = append(pricings, pricing.GetDefaultPricingProfile())
		}
	}
	return pricings
}

func (ri *ResourceInstance[T]) GetPeerGroups() ([]ResourcePartnerITF, []map[string][]string) {
	// Structurally peerless: universally accessible — wildcard on all peers.
	if ri.IsPeerless() {
		return []ResourcePartnerITF{}, []map[string][]string{{"*": {"*"}}}
	}
	groups := []map[string][]string{}
	partners := []ResourcePartnerITF{}
	for _, p := range ri.Partnerships {
		partners = append(partners, p)
		groups = append(groups, p.GetPeerGroups())
	}
	if len(groups) == 0 {
		pp, err := utils.GetMySelf((&peer.Peer{}).GetAccessor(&tools.APIRequest{
			Admin: true,
		}))
		if err != nil || pp == nil {
			return partners, groups
		}
		groups = []map[string][]string{
			{
				pp.GetID(): {"*"},
			},
		}
		// TODO make allow all only for self.
	}
	return partners, groups
}

func (ri *ResourceInstance[T]) ClearPeerGroups() {
	for _, p := range ri.Partnerships {
		p.ClearPeerGroups()
	}
}

func (ri *ResourceInstance[T]) GetAverageDurationS() float64 {
	return ri.AverageDurationS
}

func (ri *ResourceInstance[T]) UpdateAverageDuration(actualS float64) {
	buffered := actualS * 1.20
	n := float64(ri.AverageDurationSamples)
	ri.AverageDurationS = (ri.AverageDurationS*n + buffered) / (n + 1)
	ri.AverageDurationSamples++
}

func (ri *ResourceInstance[T]) GetPartnerships() []ResourcePartnerITF {
	rt := []ResourcePartnerITF{}
	for _, p := range ri.Partnerships {
		rt = append(rt, p)
	}
	return rt
}

type ResourcePartnerShip[T pricing.PricingProfileITF] struct {
	Namespace       string              `json:"namespace" bson:"namespace" default:"default-namespace"`
	PeerGroups      map[string][]string `json:"peer_groups,omitempty" bson:"peer_groups,omitempty"`
	PricingProfiles map[int]map[int]T   `json:"pricing_profiles,omitempty" bson:"pricing_profiles,omitempty"`
	// to upgrade pricing profiles. to be a map BuyingStrategy, map of Strategy
}

func (ri *ResourcePartnerShip[T]) FilterPartnership(peerID string) {
	if ri.PeerGroups["*"] == nil && ri.PeerGroups[peerID] == nil {
		ri.PeerGroups = map[string][]string{
			"*": {"*"},
		}
	} else {
		ri.PeerGroups = map[string][]string{}
		if ri.PeerGroups["*"] != nil {
			ri.PeerGroups["*"] = ri.PeerGroups["*"]
		}
		if ri.PeerGroups[peerID] != nil {
			ri.PeerGroups[peerID] = ri.PeerGroups[peerID]
		}
	}
}

func (ri *ResourcePartnerShip[T]) GetProfile(buying *int, strategy *int) pricing.PricingProfileITF {
	if buying != nil && strategy != nil {
		if strat, ok := ri.PricingProfiles[*buying]; ok {
			if profile, ok := strat[*strategy]; ok {
				return profile
			}
		}
	}

	return nil
}

/*
Le pricing doit être selectionné lors d'un scheduling...
le type de paiement défini le type de stratégie de paiement
note : il faut rajouté - une notion de facturation
Une order est l'ensemble de la commande... un booking une réservation, une purchase un acte d'achat.
Une bill (facture) représente alors... l'emission d'une facture à un instant T en but d'être honorée, envoyée ... etc.
*/
func (ri *ResourcePartnerShip[T]) GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF {
	profiles := []pricing.PricingProfileITF{}
	if ri.PeerGroups["*"] == nil && ri.PeerGroups[peerID] == nil {
		return profiles
	}
	if ri.PeerGroups["*"] != nil {
		for _, ri := range ri.PricingProfiles {
			for _, i := range ri {
				profiles = append(profiles, i)
			}
		}
		return profiles
	}
	for _, p := range ri.PeerGroups[peerID] {
		if slices.Contains(groups, p) || slices.Contains(groups, "*") {
			for _, ri := range ri.PricingProfiles {
				for _, i := range ri {
					profiles = append(profiles, i)
				}
			}
			return profiles
		}
	}
	if len(profiles) == 0 {
		if ok, _ := utils.IsMySelf(peerID, (&peer.Peer{}).GetAccessor(&tools.APIRequest{
			Admin: true,
		})); ok {
			profiles = append(profiles, pricing.GetDefaultPricingProfile())
		}
	}
	return profiles
}

func (rp *ResourcePartnerShip[T]) GetPeerGroups() map[string][]string {
	if len(rp.PeerGroups) == 0 {
		pp, err := utils.GetMySelf((&peer.Peer{}).GetAccessor(&tools.APIRequest{
			Admin: true,
		}))
		if err != nil || pp == nil {
			return rp.PeerGroups
		}
		return map[string][]string{
			"*":        {"*"},
			pp.GetID(): {"*"},
		}
	}
	return rp.PeerGroups
}

func (rp *ResourcePartnerShip[T]) ClearPeerGroups() {
	rp.PeerGroups = map[string][]string{}
}

func ToResource(
	dt int,
	payload []byte,
) (ResourceInterface, error) {
	switch dt {
	case tools.PROCESSING_RESOURCE.EnumIndex():
		var data ProcessingResource
		if err := json.Unmarshal(payload, &data); err != nil {
			return nil, err
		}
		return &data, nil
	case tools.WORKFLOW_RESOURCE.EnumIndex():
		var data WorkflowResource
		if err := json.Unmarshal(payload, &data); err != nil {
			return nil, err
		}
		return &data, nil
	case tools.DATA_RESOURCE.EnumIndex():
		var data DataResource
		if err := json.Unmarshal(payload, &data); err != nil {
			return nil, err
		}
		return &data, nil
	case tools.STORAGE_RESOURCE.EnumIndex():
		var data StorageResource
		if err := json.Unmarshal(payload, &data); err != nil {
			return nil, err
		}
		return &data, nil
	case tools.COMPUTE_RESOURCE.EnumIndex():
		var data ComputeResource
		if err := json.Unmarshal(payload, &data); err != nil {
			return nil, err
		}
		return &data, nil
	case tools.SERVICE_RESOURCE.EnumIndex():
		var data ServiceResource
		if err := json.Unmarshal(payload, &data); err != nil {
			return nil, err
		}
		return &data, nil
	}
	return nil, errors.New("can't found any data resources matching")
}

type ResourceAccess struct {
	Source    *models.PathSource `json:"source,omitempty" bson:"source,omitempty"`
	Container *models.Container  `json:"container,omitempty" bson:"container,omitempty"` // Container is the container
}

// HasSource returns true when the access is source-based (no embedded container).
func (a *ResourceAccess) HasSource() bool {
	return a != nil && a.Container == nil && a.Source != nil
}

// HasContainer returns true when an explicit container image is provided.
func (a *ResourceAccess) HasContainer() bool {
	return a != nil && a.Container != nil
}
