package processor

import (
	"apskel-pos-be/internal/appcontext"
	"context"
	"fmt"
	"time"

	"apskel-pos-be/internal/contract"
	"apskel-pos-be/internal/entities"
	"apskel-pos-be/internal/mappers"
	"apskel-pos-be/internal/models"
	"apskel-pos-be/internal/repository"

	"github.com/google/uuid"
)

type InventoryProcessor interface {
	Create(ctx context.Context, req *models.CreateInventoryRequest, organizationID uuid.UUID) (*models.InventoryResponse, error)
	GetByID(ctx context.Context, id, organizationID uuid.UUID) (*models.InventoryResponse, error)
	GetByProductAndOutlet(ctx context.Context, productID, outletID, organizationID uuid.UUID) (*models.InventoryResponse, error)
	GetByOutlet(ctx context.Context, outletID, organizationID uuid.UUID) ([]*models.InventoryResponse, error)
	GetByProduct(ctx context.Context, productID, organizationID uuid.UUID) ([]*models.InventoryResponse, error)
	GetLowStock(ctx context.Context, outletID, organizationID uuid.UUID) ([]*models.InventoryResponse, error)
	GetZeroStock(ctx context.Context, outletID, organizationID uuid.UUID) ([]*models.InventoryResponse, error)
	Update(ctx context.Context, id uuid.UUID, req *models.UpdateInventoryRequest, organizationID uuid.UUID) (*models.InventoryResponse, error)
	Delete(ctx context.Context, id, organizationID uuid.UUID) error
	List(ctx context.Context, filters map[string]interface{}, limit, offset int, organizationID uuid.UUID) ([]*models.InventoryResponse, int64, error)
	AdjustQuantity(ctx context.Context, productID, outletID, organizationID uuid.UUID, delta int) (*models.InventoryResponse, error)
	SetQuantity(ctx context.Context, productID, outletID, organizationID uuid.UUID, quantity int) (*models.InventoryResponse, error)
	UpdateReorderLevel(ctx context.Context, id uuid.UUID, reorderLevel int, organizationID uuid.UUID) error
	RestockInventory(ctx context.Context, outletID uuid.UUID, items []contract.RestockItem, reason string) (*contract.RestockInventoryResponse, error)
	GetInventoryReportSummary(ctx context.Context, outletID, organizationID uuid.UUID, dateFrom, dateTo *time.Time) (*models.InventoryReportSummary, error)
	GetInventoryReportDetails(ctx context.Context, filter *models.InventoryReportFilter, organizationID uuid.UUID) (*models.InventoryReportDetail, error)
}

type InventoryProcessorImpl struct {
	inventoryRepo         repository.InventoryRepository
	productRepo           ProductRepository
	outletRepo            OutletRepository
	ingredientRepo        IngredientRepository
	inventoryMovementRepo repository.InventoryMovementRepository
}

func NewInventoryProcessorImpl(
	inventoryRepo repository.InventoryRepository,
	productRepo ProductRepository,
	outletRepo OutletRepository,
	ingredientRepo IngredientRepository,
	inventoryMovementRepo repository.InventoryMovementRepository,
) *InventoryProcessorImpl {
	return &InventoryProcessorImpl{
		inventoryRepo:         inventoryRepo,
		productRepo:           productRepo,
		outletRepo:            outletRepo,
		ingredientRepo:        ingredientRepo,
		inventoryMovementRepo: inventoryMovementRepo,
	}
}

// Create creates a new inventory record
func (p *InventoryProcessorImpl) Create(ctx context.Context, req *models.CreateInventoryRequest, organizationID uuid.UUID) (*models.InventoryResponse, error) {
	_, err := p.productRepo.GetByID(ctx, req.ProductID)
	if err != nil {
		return nil, fmt.Errorf("invalid product: %w", err)
	}

	// Validate outlet exists
	_, err = p.outletRepo.GetByID(ctx, req.OutletID)
	if err != nil {
		return nil, fmt.Errorf("invalid outlet: %w", err)
	}

	// Check if inventory already exists for this product-outlet combination
	existingInventory, err := p.inventoryRepo.GetByProductAndOutlet(ctx, req.ProductID, req.OutletID)
	if err == nil && existingInventory != nil {
		return nil, fmt.Errorf("inventory already exists for product %s in outlet %s", req.ProductID, req.OutletID)
	}

	// Map request to entity
	inventoryEntity := mappers.CreateInventoryRequestToEntity(req)

	// Create inventory
	if err := p.inventoryRepo.Create(ctx, inventoryEntity); err != nil {
		return nil, fmt.Errorf("failed to create inventory: %w", err)
	}

	// Get inventory with relations for response
	inventoryWithRelations, err := p.inventoryRepo.GetWithRelations(ctx, inventoryEntity.ID)
	if err != nil {
		return nil, fmt.Errorf("failed to retrieve created inventory: %w", err)
	}

	// Map entity to response model
	response := mappers.InventoryEntityToResponse(inventoryWithRelations)
	return response, nil
}

// Update updates an existing inventory record
func (p *InventoryProcessorImpl) Update(ctx context.Context, id uuid.UUID, req *models.UpdateInventoryRequest, organizationID uuid.UUID) (*models.InventoryResponse, error) {
	// Get existing inventory
	existingInventory, err := p.inventoryRepo.GetByID(ctx, id)
	if err != nil {
		return nil, fmt.Errorf("inventory not found: %w", err)
	}

	// Apply updates to entity
	mappers.UpdateInventoryEntityFromRequest(existingInventory, req)

	// Update inventory
	if err := p.inventoryRepo.Update(ctx, existingInventory); err != nil {
		return nil, fmt.Errorf("failed to update inventory: %w", err)
	}

	// Get updated inventory with relations for response
	inventoryWithRelations, err := p.inventoryRepo.GetWithRelations(ctx, id)
	if err != nil {
		return nil, fmt.Errorf("failed to retrieve updated inventory: %w", err)
	}

	// Map entity to response model
	response := mappers.InventoryEntityToResponse(inventoryWithRelations)
	return response, nil
}

// Delete deletes an inventory record
func (p *InventoryProcessorImpl) Delete(ctx context.Context, id uuid.UUID, organizationID uuid.UUID) error {
	// Check if inventory exists
	_, err := p.inventoryRepo.GetByID(ctx, id)
	if err != nil {
		return fmt.Errorf("inventory not found: %w", err)
	}

	// Delete inventory
	if err := p.inventoryRepo.Delete(ctx, id); err != nil {
		return fmt.Errorf("failed to delete inventory: %w", err)
	}

	return nil
}

// GetByID retrieves an inventory record by ID
func (p *InventoryProcessorImpl) GetByID(ctx context.Context, id, organizationID uuid.UUID) (*models.InventoryResponse, error) {
	inventory, err := p.inventoryRepo.GetWithRelations(ctx, id)
	if err != nil {
		return nil, fmt.Errorf("inventory not found: %w", err)
	}

	response := mappers.InventoryEntityToResponse(inventory)
	return response, nil
}

// GetByProductAndOutlet retrieves inventory by product and outlet
func (p *InventoryProcessorImpl) GetByProductAndOutlet(ctx context.Context, productID, outletID, organizationID uuid.UUID) (*models.InventoryResponse, error) {
	inventory, err := p.inventoryRepo.GetByProductAndOutlet(ctx, productID, outletID)
	if err != nil {
		return nil, fmt.Errorf("inventory not found: %w", err)
	}

	response := mappers.InventoryEntityToResponse(inventory)
	return response, nil
}

// GetByOutlet retrieves all inventory records for a specific outlet
func (p *InventoryProcessorImpl) GetByOutlet(ctx context.Context, outletID, organizationID uuid.UUID) ([]*models.InventoryResponse, error) {
	inventories, err := p.inventoryRepo.GetByOutlet(ctx, outletID)
	if err != nil {
		return nil, fmt.Errorf("failed to get inventory by outlet: %w", err)
	}

	var responses []*models.InventoryResponse
	for _, inventory := range inventories {
		response := mappers.InventoryEntityToResponse(inventory)
		responses = append(responses, response)
	}

	return responses, nil
}

// GetByProduct retrieves all inventory records for a specific product
func (p *InventoryProcessorImpl) GetByProduct(ctx context.Context, productID, organizationID uuid.UUID) ([]*models.InventoryResponse, error) {
	inventories, err := p.inventoryRepo.GetByProduct(ctx, productID)
	if err != nil {
		return nil, fmt.Errorf("failed to get inventory by product: %w", err)
	}

	var responses []*models.InventoryResponse
	for _, inventory := range inventories {
		response := mappers.InventoryEntityToResponse(inventory)
		responses = append(responses, response)
	}

	return responses, nil
}

// List retrieves inventory records with filtering and pagination
func (p *InventoryProcessorImpl) List(ctx context.Context, filters map[string]interface{}, limit, offset int, organizationID uuid.UUID) ([]*models.InventoryResponse, int64, error) {
	inventories, totalCount, err := p.inventoryRepo.List(ctx, filters, limit, offset)
	if err != nil {
		return nil, 0, fmt.Errorf("failed to list inventory: %w", err)
	}

	var responses []*models.InventoryResponse
	for _, inventory := range inventories {
		response := mappers.InventoryEntityToResponse(inventory)
		responses = append(responses, response)
	}

	return responses, totalCount, nil
}

// AdjustQuantity adjusts the quantity of an inventory item
func (p *InventoryProcessorImpl) AdjustQuantity(ctx context.Context, productID, outletID, organizationID uuid.UUID, delta int) (*models.InventoryResponse, error) {
	inventory, err := p.inventoryRepo.AdjustQuantity(ctx, productID, outletID, delta)
	if err != nil {
		return nil, fmt.Errorf("failed to adjust inventory quantity: %w", err)
	}

	response := mappers.InventoryEntityToResponse(inventory)
	return response, nil
}

// SetQuantity sets the quantity of an inventory item
func (p *InventoryProcessorImpl) SetQuantity(ctx context.Context, productID, outletID, organizationID uuid.UUID, quantity int) (*models.InventoryResponse, error) {
	inventory, err := p.inventoryRepo.SetQuantity(ctx, productID, outletID, quantity)
	if err != nil {
		return nil, fmt.Errorf("failed to set inventory quantity: %w", err)
	}

	response := mappers.InventoryEntityToResponse(inventory)
	return response, nil
}

// UpdateReorderLevel updates the reorder level of an inventory item
func (p *InventoryProcessorImpl) UpdateReorderLevel(ctx context.Context, id uuid.UUID, reorderLevel int, organizationID uuid.UUID) error {
	return p.inventoryRepo.UpdateReorderLevel(ctx, id, reorderLevel)
}

// GetLowStock retrieves low stock inventory items for a specific outlet
func (p *InventoryProcessorImpl) GetLowStock(ctx context.Context, outletID, organizationID uuid.UUID) ([]*models.InventoryResponse, error) {
	inventories, err := p.inventoryRepo.GetLowStock(ctx, outletID)
	if err != nil {
		return nil, fmt.Errorf("failed to get low stock inventory: %w", err)
	}

	var responses []*models.InventoryResponse
	for _, inventory := range inventories {
		response := mappers.InventoryEntityToResponse(inventory)
		responses = append(responses, response)
	}

	return responses, nil
}

// GetZeroStock retrieves zero stock inventory items for a specific outlet
func (p *InventoryProcessorImpl) GetZeroStock(ctx context.Context, outletID, organizationID uuid.UUID) ([]*models.InventoryResponse, error) {
	inventories, err := p.inventoryRepo.GetZeroStock(ctx, outletID)
	if err != nil {
		return nil, fmt.Errorf("failed to get zero stock inventory: %w", err)
	}

	var responses []*models.InventoryResponse
	for _, inventory := range inventories {
		response := mappers.InventoryEntityToResponse(inventory)
		responses = append(responses, response)
	}

	return responses, nil
}

func (p *InventoryProcessorImpl) GetInventoryReportSummary(ctx context.Context, outletID, organizationID uuid.UUID, dateFrom, dateTo *time.Time) (*models.InventoryReportSummary, error) {
	outlet, err := p.outletRepo.GetByID(ctx, outletID)
	if err != nil {
		return nil, fmt.Errorf("outlet not found: %w", err)
	}
	if outlet.OrganizationID != organizationID {
		return nil, fmt.Errorf("outlet does not belong to the organization")
	}

	summary, err := p.inventoryRepo.GetInventoryReportSummary(ctx, outletID, dateFrom, dateTo)
	if err != nil {
		return nil, fmt.Errorf("failed to get inventory report summary: %w", err)
	}

	return summary, nil
}

// GetInventoryReportDetails returns detailed inventory report with products and ingredients
func (p *InventoryProcessorImpl) GetInventoryReportDetails(ctx context.Context, filter *models.InventoryReportFilter, organizationID uuid.UUID) (*models.InventoryReportDetail, error) {
	if filter.OutletID == nil {
		return nil, fmt.Errorf("outlet_id is required for inventory report")
	}

	outlet, err := p.outletRepo.GetByID(ctx, *filter.OutletID)
	if err != nil {
		return nil, fmt.Errorf("outlet not found: %w", err)
	}
	if outlet.OrganizationID != organizationID {
		return nil, fmt.Errorf("outlet does not belong to the organization")
	}

	report, err := p.inventoryRepo.GetInventoryReportDetails(ctx, filter)
	if err != nil {
		return nil, fmt.Errorf("failed to get inventory report details: %w", err)
	}

	return report, nil
}

func (p *InventoryProcessorImpl) GetInventoryByID(ctx context.Context, id uuid.UUID) (*models.InventoryResponse, error) {
	inventoryEntity, err := p.inventoryRepo.GetWithRelations(ctx, id)
	if err != nil {
		return nil, fmt.Errorf("inventory not found: %w", err)
	}

	response := mappers.InventoryEntityToResponse(inventoryEntity)
	return response, nil
}

func (p *InventoryProcessorImpl) ListInventory(ctx context.Context, filters map[string]interface{}, page, limit int) ([]models.InventoryResponse, int, error) {
	offset := (page - 1) * limit

	inventoryEntities, total, err := p.inventoryRepo.List(ctx, filters, limit, offset)
	if err != nil {
		return nil, 0, fmt.Errorf("failed to list inventory: %w", err)
	}

	responses := make([]models.InventoryResponse, len(inventoryEntities))
	for i, entity := range inventoryEntities {
		response := mappers.InventoryEntityToResponse(entity)
		if response != nil {
			responses[i] = *response
		}
	}

	return responses, int(total), nil
}

func (p *InventoryProcessorImpl) AdjustInventory(ctx context.Context, productID, outletID uuid.UUID, req *models.InventoryAdjustmentRequest) (*models.InventoryResponse, error) {
	// Validate product exists
	_, err := p.productRepo.GetByID(ctx, productID)
	if err != nil {
		return nil, fmt.Errorf("invalid product: %w", err)
	}

	// Validate outlet exists
	_, err = p.outletRepo.GetByID(ctx, outletID)
	if err != nil {
		return nil, fmt.Errorf("invalid outlet: %w", err)
	}

	// Perform quantity adjustment
	adjustedInventory, err := p.inventoryRepo.AdjustQuantity(ctx, productID, outletID, req.Delta)
	if err != nil {
		return nil, fmt.Errorf("failed to adjust inventory quantity: %w", err)
	}

	// Get inventory with relations for response
	inventoryWithRelations, err := p.inventoryRepo.GetWithRelations(ctx, adjustedInventory.ID)
	if err != nil {
		return nil, fmt.Errorf("failed to retrieve adjusted inventory: %w", err)
	}

	// Map entity to response model
	response := mappers.InventoryEntityToResponse(inventoryWithRelations)
	return response, nil
}

func (p *InventoryProcessorImpl) GetLowStockItems(ctx context.Context, outletID uuid.UUID) ([]models.InventoryResponse, error) {
	// Validate outlet exists
	_, err := p.outletRepo.GetByID(ctx, outletID)
	if err != nil {
		return nil, fmt.Errorf("invalid outlet: %w", err)
	}

	inventoryEntities, err := p.inventoryRepo.GetLowStock(ctx, outletID)
	if err != nil {
		return nil, fmt.Errorf("failed to get low stock items: %w", err)
	}

	responses := make([]models.InventoryResponse, len(inventoryEntities))
	for i, entity := range inventoryEntities {
		response := mappers.InventoryEntityToResponse(entity)
		if response != nil {
			responses[i] = *response
		}
	}

	return responses, nil
}

func (p *InventoryProcessorImpl) GetZeroStockItems(ctx context.Context, outletID uuid.UUID) ([]models.InventoryResponse, error) {
	// Validate outlet exists
	_, err := p.outletRepo.GetByID(ctx, outletID)
	if err != nil {
		return nil, fmt.Errorf("invalid outlet: %w", err)
	}

	inventoryEntities, err := p.inventoryRepo.GetZeroStock(ctx, outletID)
	if err != nil {
		return nil, fmt.Errorf("failed to get zero stock items: %w", err)
	}

	responses := make([]models.InventoryResponse, len(inventoryEntities))
	for i, entity := range inventoryEntities {
		response := mappers.InventoryEntityToResponse(entity)
		if response != nil {
			responses[i] = *response
		}
	}

	return responses, nil
}

func (p *InventoryProcessorImpl) RestockInventory(ctx context.Context, outletID uuid.UUID, items []contract.RestockItem, reason string) (*contract.RestockInventoryResponse, error) {
	outlet, err := p.outletRepo.GetByID(ctx, outletID)
	if err != nil {
		return nil, fmt.Errorf("invalid outlet: %w", err)
	}

	if len(items) == 0 {
		return nil, fmt.Errorf("no items provided for restocking")
	}

	restockResults := make([]contract.RestockItemResult, 0, len(items))
	restockedAt := time.Now()

	for _, item := range items {
		result := contract.RestockItemResult{
			ItemID:   item.ItemID,
			ItemType: item.ItemType,
			AddedQty: item.Quantity,
			Success:  false,
		}

		switch item.ItemType {
		case "PRODUCT":
			if err := p.restockProduct(ctx, outletID, item.ItemID, item.Quantity, reason, outlet.OrganizationID); err != nil {
				result.Error = err.Error()
			}
		case "INGREDIENT":
			if err := p.restockIngredient(ctx, outletID, item.ItemID, item.Quantity, reason, outlet.OrganizationID); err != nil {
				result.Error = err.Error()
			}
		default:
			result.Error = fmt.Sprintf("unsupported item type: %s", item.ItemType)
		}

		restockResults = append(restockResults, result)
	}

	return &contract.RestockInventoryResponse{
		OutletID:    outletID,
		Items:       restockResults,
		Reason:      reason,
		RestockedAt: restockedAt,
	}, nil
}

func (p *InventoryProcessorImpl) restockProduct(ctx context.Context, outletID, productID uuid.UUID, quantity int, reason string, organizationID uuid.UUID) error {
	contextInfo := appcontext.FromGinContext(ctx)

	var previousQuantity int
	var newQuantity int

	inventory, err := p.inventoryRepo.GetByProductAndOutlet(ctx, productID, outletID)
	if err != nil {
		createReq := &models.CreateInventoryRequest{
			OutletID:     outletID,
			ProductID:    productID,
			Quantity:     quantity,
			ReorderLevel: 0,
		}
		inventoryEntity := mappers.CreateInventoryRequestToEntity(createReq)
		if err := p.inventoryRepo.Create(ctx, inventoryEntity); err != nil {
			return fmt.Errorf("failed to create inventory for product %s: %w", productID, err)
		}
		previousQuantity = 0
		newQuantity = quantity
	} else {
		previousQuantity = inventory.Quantity
		newQuantity = inventory.Quantity + quantity
		updateReq := &models.UpdateInventoryRequest{
			Quantity: &newQuantity,
		}
		mappers.UpdateInventoryEntityFromRequest(inventory, updateReq)

		if err := p.inventoryRepo.Update(ctx, inventory); err != nil {
			return fmt.Errorf("failed to update inventory for product %s: %w", productID, err)
		}
	}

	referenceType := entities.InventoryMovementReferenceTypeManual
	movement := &entities.InventoryMovement{
		OrganizationID:   organizationID,
		OutletID:         outletID,
		ItemID:           productID,
		ItemType:         "PRODUCT",
		MovementType:     entities.InventoryMovementTypePurchase,
		Quantity:         float64(quantity),
		PreviousQuantity: float64(previousQuantity),
		NewQuantity:      float64(newQuantity),
		ReferenceType:    &referenceType,
		Reason:           &reason,
		UserID:           contextInfo.UserID,
	}

	if err := p.inventoryMovementRepo.Create(ctx, movement); err != nil {
		return fmt.Errorf("failed to create inventory movement record: %w", err)
	}

	return nil
}

func (p *InventoryProcessorImpl) restockIngredient(ctx context.Context, outletID, ingredientID uuid.UUID, quantity int, reason string, organizationID uuid.UUID) error {
	contextInfo := appcontext.FromGinContext(ctx)
	ingredient, err := p.getIngredientByID(ctx, ingredientID, organizationID)
	if err != nil {
		return fmt.Errorf("failed to get ingredient %s: %w", ingredientID, err)
	}

	previousStock := ingredient.Stock
	newStock := ingredient.Stock + float64(quantity)

	if err := p.updateIngredientStock(ctx, ingredientID, float64(quantity), organizationID); err != nil {
		return fmt.Errorf("failed to update ingredient stock: %w", err)
	}

	referenceType := entities.InventoryMovementReferenceTypeManual
	movement := &entities.InventoryMovement{
		OrganizationID:   organizationID,
		OutletID:         outletID,
		ItemID:           ingredientID,
		ItemType:         "INGREDIENT",
		MovementType:     entities.InventoryMovementTypePurchase,
		Quantity:         float64(quantity),
		PreviousQuantity: previousStock,
		NewQuantity:      newStock,
		ReferenceType:    &referenceType,
		Reason:           &reason,
		UserID:           contextInfo.UserID,
	}

	if err := p.inventoryMovementRepo.Create(ctx, movement); err != nil {
		return fmt.Errorf("failed to create inventory movement record: %w", err)
	}

	return nil
}

func (p *InventoryProcessorImpl) getIngredientByID(ctx context.Context, ingredientID, organizationID uuid.UUID) (*models.Ingredient, error) {
	ingredient, err := p.ingredientRepo.GetByID(ctx, ingredientID, organizationID)
	if err != nil {
		return nil, fmt.Errorf("failed to get ingredient %s: %w", ingredientID, err)
	}

	return &models.Ingredient{
		ID:             ingredient.ID,
		Name:           ingredient.Name,
		Stock:          ingredient.Stock,
		UnitID:         ingredient.UnitID,
		OrganizationID: ingredient.OrganizationID,
		OutletID:       ingredient.OutletID,
		IsActive:       ingredient.IsActive,
		CreatedAt:      ingredient.CreatedAt,
		UpdatedAt:      ingredient.UpdatedAt,
	}, nil
}

func (p *InventoryProcessorImpl) updateIngredientStock(ctx context.Context, ingredientID uuid.UUID, quantityToAdd float64, organizationID uuid.UUID) error {
	if err := p.ingredientRepo.UpdateStock(ctx, ingredientID, quantityToAdd, organizationID); err != nil {
		return fmt.Errorf("failed to update ingredient stock: %w", err)
	}
	return nil
}
