package billing

import (
	"encoding/json"
	"sync"
	"time"

	"cloud.o-forge.io/core/oc-lib/dbs"
	"cloud.o-forge.io/core/oc-lib/models/billing/subscription"
	"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/order"
	"cloud.o-forge.io/core/oc-lib/models/peer"
	"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/tools"
	"github.com/google/uuid"
)

type Bill struct {
	utils.AbstractObject
	OrderID   string                `json:"order_id" bson:"order_id" validate:"required"`
	Status    enum.CompletionStatus `json:"status" bson:"status" default:"0"`
	SubOrders map[string]*PeerOrder `json:"sub_orders" bson:"sub_orders"`
	Total     float64               `json:"total" bson:"total" validate:"required"`
}

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

// IsFullySettled retourne vrai quand chaque ligne de chaque peer-order est réglée.
func (b *Bill) IsFullySettled() bool {
	for _, po := range b.SubOrders {
		for _, item := range po.Items {
			if !item.Settled {
				return false
			}
		}
	}
	return true
}

// SettledTotal retourne le montant total des lignes déjà réglées.
func (b *Bill) SettledTotal() float64 {
	total := 0.0
	for _, po := range b.SubOrders {
		for _, item := range po.Items {
			if item.Settled {
				total += item.UnitPriceHT * float64(item.Quantity)
			}
		}
	}
	return total
}

// MarkItemSettled marque une ligne comme réglée d'après son itemID
// et propage le statut PAID sur le PeerOrder si toutes ses lignes sont réglées.
func (b *Bill) MarkItemSettled(itemID string) bool {
	now := time.Now().UTC()
	for _, po := range b.SubOrders {
		for _, item := range po.Items {
			if item.ItemID == itemID {
				item.Settled = true
				item.SettledAt = &now
				// propage le statut PAID si toutes les lignes du peer sont réglées
				if po.IsFullySettled() {
					po.Status = enum.PAID
				}
				return true
			}
		}
	}
	return false
}

func GenerateBill(ord *order.Order, request *tools.APIRequest) (*Bill, error) {
	return &Bill{
		AbstractObject: utils.AbstractObject{
			Name:    "bill_" + request.PeerID + "_" + time.Now().UTC().Format("2006-01-02T15:04:05"),
			IsDraft: false,
		},
		OrderID: ord.UUID,
		Status:  enum.PENDING,
	}, nil
}

// DraftFirstBill crée le premier brouillon de facture pour un order.
// Règle :
//   - Calcul total indépendant du mode de paiement (photo du coût réel).
//   - Les purchases sont toujours BILL_ONCE / PAY_ONCE → réglées immédiatement.
//   - Les bookings avec BillingStrategy != BILL_ONCE génèrent des Subscription.
//   - Chaque ligne reçoit un ItemID unique pour le suivi de règlement.
func DraftFirstBill(ord *order.Order, request *tools.APIRequest) (*Bill, error) {
	peers := map[string][]*PeerItemOrder{}

	// Purchases : facturation immédiate, pas de subscription
	for _, p := range ord.Purchases {
		if _, ok := peers[p.DestPeerID]; !ok {
			peers[p.DestPeerID] = []*PeerItemOrder{}
		}
		peers[p.DestPeerID] = append(peers[p.DestPeerID], &PeerItemOrder{
			ItemID:          uuid.New().String(),
			ResourceType:    p.ResourceType,
			Purchase:        p,
			Item:            p.PricedItem,
			Quantity:        1,
			BillingStrategy: pricing.BILL_ONCE,
			PaymentType:     pricing.PAY_ONCE,
		})
	}

	// Bookings : exclure les ressources déjà achetées (purchase_resource existant)
	purchasedIDs := map[string]bool{}
	for _, p := range ord.Purchases {
		purchasedIDs[p.ResourceID] = true
	}
	for _, b := range ord.Bookings {
		if purchasedIDs[b.ResourceID] {
			continue
		}
		if _, ok := peers[b.DestPeerID]; !ok {
			peers[b.DestPeerID] = []*PeerItemOrder{}
		}
		peers[b.DestPeerID] = append(peers[b.DestPeerID], &PeerItemOrder{
			ItemID:          uuid.New().String(),
			ResourceType:    b.ResourceType,
			Quantity:        1,
			Item:            b.PricedItem,
			BillingStrategy: b.BillingStrategy,
			PaymentType:     b.PaymentType,
		})
	}

	// Résolution des adresses de facturation peer
	peerOrders := map[string]*PeerOrder{}
	for peerID, items := range peers {
		pr, _, err := peer.NewAccessor(request).LoadOne(peerID)
		if err != nil {
			return nil, err
		}
		peerOrders[peerID] = &PeerOrder{
			PeerID:         peerID,
			BillingAddress: pr.(*peer.Peer).WalletAddress,
			Items:          items,
		}
	}

	bill := &Bill{
		AbstractObject: utils.AbstractObject{
			Name:    "bill_" + request.PeerID + "_" + time.Now().UTC().Format("2006-01-02T15:04:05"),
			IsDraft: true,
		},
		OrderID:   ord.UUID,
		Status:    enum.PENDING,
		SubOrders: peerOrders,
	}

	// 1. Calcul des totaux (indépendant du mode de paiement)
	if _, err := bill.SumUpBill(request); err != nil {
		return bill, err
	}

	// 2. Création des subscriptions pour les lignes récurrentes
	subIDs, err := createRecurringSubscriptions(bill, request)
	if err != nil {
		return bill, err
	}

	// 3. Liaison des subscription IDs à l'order pour traçabilité
	if len(subIDs) > 0 {
		ord.SubscriptionIDs = append(ord.SubscriptionIDs, subIDs...)
	}

	// 4. Persistance du brouillon de facture (les UnitPriceHT et SubscriptionID sont déjà set)
	stored, _, err := NewAccessor(request).StoreOne(bill)
	if err != nil {
		return bill, err
	}
	return stored.(*Bill), nil
}

// createRecurringSubscriptions crée les Subscription pour chaque groupe
// (peer × BillingStrategy) dont la stratégie est récurrente.
// Modifie les PeerItemOrder en place (SubscriptionID).
// Retourne les IDs de subscriptions créées.
func createRecurringSubscriptions(b *Bill, request *tools.APIRequest) ([]string, error) {
	subIDs := []string{}

	for peerID, po := range b.SubOrders {
		// Groupe les items récurrents par BillingStrategy
		byStrategy := map[pricing.BillingStrategy][]*PeerItemOrder{}
		for _, item := range po.Items {
			if item.BillingStrategy == pricing.BILL_ONCE {
				continue
			}
			byStrategy[item.BillingStrategy] = append(byStrategy[item.BillingStrategy], item)
		}

		for strategy, items := range byStrategy {
			subItems := make([]*subscription.SubscriptionItem, 0, len(items))
			totalAmount := 0.0
			for _, item := range items {
				subItems = append(subItems, &subscription.SubscriptionItem{
					ResourceType: item.ResourceType,
					Quantity:     item.Quantity,
					UnitPrice:    item.UnitPriceHT,
				})
				totalAmount += item.UnitPriceHT * float64(item.Quantity)
			}

			var sub *subscription.Subscription
			switch strategy {
			case pricing.BILL_PER_YEAR:
				sub = subscription.NewYearlySubscription(request.PeerID, peerID, subItems, totalAmount, "EUR")
			case pricing.BILL_PER_WEEK:
				sub = subscription.NewWeeklySubscription(request.PeerID, peerID, subItems, totalAmount, "EUR")
			default: // BILL_PER_MONTH et tout autre cas récurrent
				sub = subscription.NewMonthlySubscription(request.PeerID, peerID, subItems, totalAmount, "EUR")
			}
			sub.IsDraft = true

			res, _, err := subscription.NewAccessor(request).StoreOne(sub)
			if err != nil {
				return subIDs, err
			}
			storedSub := res.(*subscription.Subscription)
			subIDs = append(subIDs, storedSub.GetID())

			// Lie le SubscriptionID à chaque ligne concernée
			for _, item := range items {
				item.SubscriptionID = storedSub.GetID()
			}
		}
	}
	return subIDs, nil
}

func (d *Bill) GetAccessor(request *tools.APIRequest) utils.Accessor {
	return NewAccessor(request)
}

func (r *Bill) StoreDraftDefault() {
	r.IsDraft = true
}

func (r *Bill) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
	if !r.IsDraft && r.Status != set.(*Bill).Status {
		return true, &Bill{Status: set.(*Bill).Status}
	}
	return r.IsDraft, set
}

func (r *Bill) CanDelete() bool {
	return r.IsDraft
}

func (d *Bill) SumUpBill(request *tools.APIRequest) (*Bill, error) {
	for _, b := range d.SubOrders {
		err := b.SumUpBill(request)
		if err != nil {
			return d, err
		}
		d.Total += b.Total
	}
	return d, nil
}

// ---------------------------------------------------------------------------
// PeerOrder
// ---------------------------------------------------------------------------

type PeerOrder struct {
	Error          string                `json:"error,omitempty" bson:"error,omitempty"`
	PeerID         string                `json:"peer_id,omitempty" bson:"peer_id,omitempty"`
	Status         enum.CompletionStatus `json:"status" bson:"status" default:"0"`
	BillingAddress string                `json:"billing_address,omitempty" bson:"billing_address,omitempty"`
	Items          []*PeerItemOrder      `json:"items,omitempty" bson:"items,omitempty"`
	Total          float64               `json:"total,omitempty" bson:"total,omitempty"`
}

// IsFullySettled retourne vrai si toutes les lignes de ce peer sont réglées.
func (po *PeerOrder) IsFullySettled() bool {
	for _, item := range po.Items {
		if !item.Settled {
			return false
		}
	}
	return true
}

func PricedByType(dt tools.DataType) pricing.PricedItemITF {
	switch dt {
	case tools.PROCESSING_RESOURCE:
		return &resources.PricedProcessingResource{}
	case tools.STORAGE_RESOURCE:
		return &resources.PricedStorageResource{}
	case tools.DATA_RESOURCE:
		return &resources.PricedDataResource{}
	case tools.COMPUTE_RESOURCE:
		return &resources.PricedComputeResource{}
	case tools.WORKFLOW_RESOURCE:
		return &resources.PricedResource[*pricing.ExploitPricingProfile[pricing.TimePricingStrategy]]{}
	}
	return nil
}

func (d *PeerOrder) Pay(request *tools.APIRequest, response chan *PeerOrder, wg *sync.WaitGroup) {
	d.Status = enum.PENDING
	go func() {
		// DO SOMETHING TO PAY ON BLOCKCHAIN OR WHATEVER — UPDATE STATUS ON RETURN
		d.Status = enum.PAID // TO REMOVE LATER IT'S A MOCK
		if d.Status == enum.PAID {
			now := time.Now().UTC()
			for _, b := range d.Items {
				priced := PricedByType(b.ResourceType)
				bb, _ := json.Marshal(b.Item)
				json.Unmarshal(bb, priced)
				if !priced.IsPurchasable() {
					continue
				}
				accessor := purchase_resource.NewAccessor(request)
				accessor.StoreOne(&purchase_resource.PurchaseResource{
					ResourceID:   priced.GetID(),
					ResourceType: priced.GetType(),
					EndDate:      priced.GetLocationEnd(),
				})
				// Marque la ligne comme réglée
				b.Settled = true
				b.SettledAt = &now
			}
		}
		if d.Status != enum.PENDING {
			response <- d
		}
		wg.Done()
	}()
}

func (d *PeerOrder) SumUpBill(request *tools.APIRequest) error {
	for _, b := range d.Items {
		tot, err := b.GetPriceHT(request)
		if err != nil {
			return err
		}
		d.Total += tot
	}
	return nil
}

// ---------------------------------------------------------------------------
// PeerItemOrder
// ---------------------------------------------------------------------------

// PeerItemOrder est une ligne de facture pour un peer donné.
type PeerItemOrder struct {
	// ItemID identifie de manière unique cette ligne pour le suivi de règlement.
	ItemID       string                              `json:"item_id,omitempty" bson:"item_id,omitempty"`
	ResourceType tools.DataType                      `json:"datatype,omitempty" bson:"datatype,omitempty"`
	Quantity     int                                 `json:"quantity,omitempty" bson:"quantity,omitempty"`
	Purchase     *purchase_resource.PurchaseResource `json:"purchase,omitempty" bson:"purchase,omitempty"`
	Item         map[string]interface{}              `json:"item,omitempty" bson:"item,omitempty"`

	BillingStrategy pricing.BillingStrategy `json:"billing_strategy" bson:"billing_strategy"`
	PaymentType     pricing.PaymentType     `json:"payment_type" bson:"payment_type"`

	// UnitPriceHT est le prix unitaire HT calculé par SumUpBill/GetPriceHT.
	// Utilisé pour la création des subscriptions sans recalcul.
	UnitPriceHT float64 `json:"unit_price_ht,omitempty" bson:"unit_price_ht,omitempty"`

	// SubscriptionID référence la Subscription créée pour les lignes récurrentes.
	// Vide pour les lignes BILL_ONCE.
	SubscriptionID string `json:"subscription_id,omitempty" bson:"subscription_id,omitempty"`

	// Settled indique si cette ligne a été réglée (paiement confirmé).
	Settled   bool       `json:"settled" bson:"settled"`
	SettledAt *time.Time `json:"settled_at,omitempty" bson:"settled_at,omitempty"`
}

func (d *PeerItemOrder) GetPriceHT(request *tools.APIRequest) (float64, error) {
	if d.Purchase == nil {
		return 0, nil
	}
	priced := PricedByType(d.ResourceType)
	b, _ := json.Marshal(d.Item)
	err := json.Unmarshal(b, priced)
	if err != nil {
		return 0, err
	}
	accessor := purchase_resource.NewAccessor(request)
	search, code, _ := accessor.Search(&dbs.Filters{
		And: map[string][]dbs.Filter{
			"resource_id": {{Operator: dbs.EQUAL.String(), Value: priced.GetID()}},
		},
	}, "", d.Purchase.IsDraft, 0, 10000)
	if code == 200 && len(search) > 0 {
		for _, s := range search {
			if s.(*purchase_resource.PurchaseResource).EndDate == nil ||
				time.Now().UTC().After(*s.(*purchase_resource.PurchaseResource).EndDate) {
				return 0, nil
			}
		}
	}
	unitPrice, err := priced.GetPriceHT()
	if err != nil {
		return 0, err
	}
	d.UnitPriceHT = unitPrice // cache pour createRecurringSubscriptions
	return unitPrice * float64(d.Quantity), nil
}
