package service

import (
	"errors"
	"fmt"
	"strings"

	common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
	entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
	repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
	validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/validations"
	"gitlab.com/mbugroup/lti-api.git/internal/utils"

	"github.com/go-playground/validator/v10"
	"github.com/gofiber/fiber/v2"
	"github.com/sirupsen/logrus"
	"gorm.io/gorm"
)

type ProductService interface {
	GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Product, int64, error)
	GetOne(ctx *fiber.Ctx, id uint) (*entity.Product, error)
	CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Product, error)
	UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Product, error)
	DeleteOne(ctx *fiber.Ctx, id uint) error
}

type productService struct {
	Log        *logrus.Logger
	Validate   *validator.Validate
	Repository repository.ProductRepository
}

var depletionProductFlags = []string{
	string(utils.FlagAyamAfkir),
	string(utils.FlagAyamCulling),
	string(utils.FlagAyamMati),
}

func normalizeProductFlags(raw []string) ([]string, error) {
	normalized, invalid := utils.NormalizeFlagsForGroup(raw, utils.FlagGroupProduct)
	if len(invalid) > 0 {
		invalidStr := strings.Join(utils.FlagTypesToStrings(invalid), ", ")
		allowedStr := strings.Join(utils.FlagTypesToStrings(utils.AllowedFlagTypes(utils.FlagGroupProduct)), ", ")
		return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid product flags: %s. Allowed flags: %s", invalidStr, allowedStr))
	}
	return utils.FlagTypesToStrings(normalized), nil
}

func productMainFlagOptionsString() []string {
	mainFlags := utils.ProductMainFlags()
	result := make([]string, len(mainFlags))
	for i, flag := range mainFlags {
		result[i] = string(flag)
	}
	return result
}

func productSubFlagOptionsString(flag utils.FlagType) []string {
	subFlagsByFlag := utils.ProductSubFlagsByFlag()
	subFlags := subFlagsByFlag[flag]
	result := make([]string, len(subFlags))
	for i, subFlag := range subFlags {
		result[i] = string(subFlag)
	}
	return result
}

func normalizeStructuredSubFlagsInput(subFlagRaw *string, subFlagsRaw []string, hasSubFlagsField bool) ([]utils.FlagType, error) {
	values := make([]string, 0, len(subFlagsRaw)+1)

	if subFlagRaw != nil {
		single := strings.TrimSpace(*subFlagRaw)
		if single == "" {
			return nil, fiber.NewError(fiber.StatusBadRequest, "sub_flag cannot be empty")
		}
		values = append(values, single)
	}

	if hasSubFlagsField {
		for _, raw := range subFlagsRaw {
			item := strings.TrimSpace(raw)
			if item == "" {
				return nil, fiber.NewError(fiber.StatusBadRequest, "sub_flags cannot contain empty value")
			}
			values = append(values, item)
		}
	}

	if len(values) == 0 {
		return nil, nil
	}

	return utils.NormalizeFlagTypes(values), nil
}

func resolveProductFlagsFromFlagInput(flagRaw *string, subFlagRaw *string, subFlagsRaw []string, hasSubFlagsField bool) ([]string, bool, error) {
	if flagRaw == nil && subFlagRaw == nil && !hasSubFlagsField {
		return nil, false, nil
	}

	if flagRaw == nil && (subFlagRaw != nil || hasSubFlagsField) {
		return nil, false, fiber.NewError(fiber.StatusBadRequest, "flag is required when sub_flag/sub_flags is provided")
	}

	flagText := strings.TrimSpace(*flagRaw)
	if flagText == "" {
		return nil, false, fiber.NewError(fiber.StatusBadRequest, "flag cannot be empty")
	}

	flag := utils.CanonicalFlagType(flagText)
	if !utils.IsProductMainFlag(flag) {
		return nil, false, fiber.NewError(
			fiber.StatusBadRequest,
			fmt.Sprintf("Invalid product flag: %s. Allowed flags: %s", flagText, strings.Join(productMainFlagOptionsString(), ", ")),
		)
	}

	out := []string{string(flag)}

	normalizedSubFlags, err := normalizeStructuredSubFlagsInput(subFlagRaw, subFlagsRaw, hasSubFlagsField)
	if err != nil {
		return nil, false, err
	}

	if len(normalizedSubFlags) == 0 {
		if !utils.ProductFlagAllowWithoutSubFlag(flag) {
			return nil, false, fiber.NewError(
				fiber.StatusBadRequest,
				fmt.Sprintf("sub_flag/sub_flags is required for flag %s", string(flag)),
			)
		}
		normalizedOut, normalizeErr := normalizeProductFlags(out)
		if normalizeErr != nil {
			return nil, false, normalizeErr
		}
		return normalizedOut, true, nil
	}

	invalidSubFlags := make([]string, 0)
	for _, subFlag := range normalizedSubFlags {
		if !utils.IsValidProductSubFlag(flag, subFlag) {
			invalidSubFlags = append(invalidSubFlags, string(subFlag))
		}
	}
	if len(invalidSubFlags) > 0 {
		return nil, false, fiber.NewError(
			fiber.StatusBadRequest,
			fmt.Sprintf("Invalid sub_flags %s for flag %s. Allowed sub_flags: %s", strings.Join(invalidSubFlags, ", "), string(flag), strings.Join(productSubFlagOptionsString(flag), ", ")),
		)
	}

	out = append(out, utils.FlagTypesToStrings(normalizedSubFlags)...)
	normalizedOut, normalizeErr := normalizeProductFlags(out)
	if normalizeErr != nil {
		return nil, false, normalizeErr
	}
	return normalizedOut, true, nil
}

func resolveCreateProductFlags(req *validation.Create) ([]string, error) {
	hasStructuredInput := req.Flag != nil || req.SubFlag != nil || req.SubFlags != nil
	if len(req.Flags) > 0 && hasStructuredInput {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Use either flags or flag/sub_flag/sub_flags, not both")
	}

	if len(req.Flags) > 0 {
		return normalizeProductFlags(req.Flags)
	}

	flags, _, err := resolveProductFlagsFromFlagInput(req.Flag, req.SubFlag, req.SubFlags, req.SubFlags != nil)
	return flags, err
}

func resolveUpdateProductFlags(req *validation.Update) (bool, []string, error) {
	hasStructuredInput := req.Flag != nil || req.SubFlag != nil || req.SubFlags != nil

	if req.Flags != nil {
		if hasStructuredInput {
			if len(*req.Flags) > 0 {
				return false, nil, fiber.NewError(fiber.StatusBadRequest, "Use either flags or flag/sub_flag/sub_flags, not both")
			}
		} else {
			flags, err := normalizeProductFlags(*req.Flags)
			if err != nil {
				return false, nil, err
			}
			return true, flags, nil
		}
	}

	subFlagsRaw := make([]string, 0)
	if req.SubFlags != nil {
		subFlagsRaw = *req.SubFlags
	}
	flags, provided, err := resolveProductFlagsFromFlagInput(req.Flag, req.SubFlag, subFlagsRaw, req.SubFlags != nil)
	if err != nil {
		return false, nil, err
	}
	return provided, flags, nil
}

func NewProductService(repo repository.ProductRepository, validate *validator.Validate) ProductService {
	return &productService{
		Log:        utils.Log,
		Validate:   validate,
		Repository: repo,
	}
}

func (s productService) withRelations(db *gorm.DB) *gorm.DB {
	return db.
		Preload("CreatedUser").
		Preload("Uom").
		Preload("ProductCategory").
		Preload("Flags").
		Preload("ProductSuppliers").
		Preload("ProductSuppliers.Supplier", func(db *gorm.DB) *gorm.DB {
			return db.Order("suppliers.name ASC")
		})
}

func (s productService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Product, int64, error) {
	if err := s.Validate.Struct(params); err != nil {
		return nil, 0, err
	}

	offset := (params.Page - 1) * params.Limit

	products, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
		db = s.withRelations(db)

		includeAll := params.IncludeAll != nil && *params.IncludeAll
		if params.IsDepletion != nil && *params.IsDepletion {
			// Auto-expand visibility for depletion catalog so FE doesn't need include_all=true.
			includeAll = true
		}

		// Default: show only visible products.
		// include_all=true can be used to fetch all records (including hidden/system products).
		// is_depletion, when provided, is composed as an additional flag filter.
		if !includeAll {
			db = db.Where("is_visible = ?", true)
		}
		if params.Search != "" {
			db = db.Where("name ILIKE ?", "%"+params.Search+"%")
		}
		if params.ProductCategoryID != 0 {
			db = db.Where("product_category_id = ?", params.ProductCategoryID)
		}
		if params.IsDepletion != nil {
			existsQuery := `
				EXISTS (
					SELECT 1
					FROM flags f
					WHERE f.flagable_type = ?
					  AND f.flagable_id = products.id
					  AND UPPER(f.name) IN ?
				)
			`
			if *params.IsDepletion {
				db = db.Where(existsQuery, entity.FlagableTypeProduct, depletionProductFlags)
			} else {
				db = db.Where("NOT "+existsQuery, entity.FlagableTypeProduct, depletionProductFlags)
			}
		}
		return db.Order("created_at DESC").Order("updated_at DESC")
	})

	if err != nil {
		s.Log.Errorf("Failed to get products: %+v", err)
		return nil, 0, err
	}
	return products, total, nil
}

func (s productService) GetOne(c *fiber.Ctx, id uint) (*entity.Product, error) {
	product, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, fiber.NewError(fiber.StatusNotFound, "Product not found")
	}
	if err != nil {
		s.Log.Errorf("Failed get product by id: %+v", err)
		return nil, err
	}
	return product, nil
}

func (s *productService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Product, error) {
	if err := s.Validate.Struct(req); err != nil {
		return nil, err
	}

	ctx := c.Context()
	name := strings.TrimSpace(req.Name)
	brand := strings.TrimSpace(req.Brand)
	var sku *string
	if req.Sku != nil {
		trimmed := strings.ToUpper(strings.TrimSpace(*req.Sku))
		if trimmed != "" {
			sku = &trimmed
		}
	}

	if exists, err := s.Repository.NameExists(ctx, name, nil); err != nil {
		s.Log.Errorf("Failed to check product name: %+v", err)
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check product name")
	} else if exists {
		return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Product with name %s already exists", name))
	}

	if sku != nil {
		if exists, err := s.Repository.SkuExists(ctx, *sku, nil); err != nil {
			s.Log.Errorf("Failed to check product sku: %+v", err)
			return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check product sku")
		} else if exists {
			return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Product with sku %s already exists", *sku))
		}
	}

	if err := common.EnsureRelations(ctx,
		common.RelationCheck{Name: "Uom", ID: &req.UomID, Exists: s.Repository.UomExists},
		common.RelationCheck{Name: "Product category", ID: &req.ProductCategoryID, Exists: s.Repository.CategoryExists},
	); err != nil {
		return nil, err
	}

	var (
		supplierLinks []entity.ProductSupplier
		supplierIDs   []uint
	)
	if len(req.Suppliers) > 0 {
		seen := make(map[uint]struct{}, len(req.Suppliers))
		supplierLinks = make([]entity.ProductSupplier, 0, len(req.Suppliers))
		supplierIDs = make([]uint, 0, len(req.Suppliers))
		for _, supplier := range req.Suppliers {
			if _, exists := seen[supplier.SupplierID]; exists {
				return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate supplier_id %d", supplier.SupplierID))
			}
			seen[supplier.SupplierID] = struct{}{}
			supplierIDs = append(supplierIDs, supplier.SupplierID)
			supplierLinks = append(supplierLinks, entity.ProductSupplier{
				SupplierId: supplier.SupplierID,
				Price:      supplier.Price,
			})
		}
		suppliers, err := s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
		if err != nil {
			s.Log.Errorf("Failed to validate suppliers: %+v", err)
			return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate suppliers")
		}
		if len(suppliers) != len(supplierIDs) {
			actual := make([]uint, len(suppliers))
			for i, supplier := range suppliers {
				actual[i] = supplier.Id
			}
			missing := utils.MissingUintIDs(supplierIDs, actual)
			return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Suppliers with ids %v not found", missing))
		}
		for _, sup := range suppliers {
			if strings.ToUpper(sup.Category) != string(utils.SupplierCategorySapronak) {
				return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier with id %d is not category SAPRONAK", sup.Id))
			}
		}
	}

	productFlags, flagErr := resolveCreateProductFlags(req)
	if flagErr != nil {
		return nil, flagErr
	}

	createBody := &entity.Product{
		Name:              name,
		Brand:             brand,
		Sku:               sku,
		UomId:             req.UomID,
		ProductCategoryId: req.ProductCategoryID,
		ProductPrice:      req.ProductPrice,
		SellingPrice:      req.SellingPrice,
		Tax:               req.Tax,
		ExpiryPeriod:      req.ExpiryPeriod,
		IsVisible:         true,
		CreatedBy:         1,
	}

	err := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		repoTx := s.Repository.WithTx(tx)

		if err := repoTx.CreateOne(ctx, createBody, nil); err != nil {
			return err
		}

		if err := s.Repository.SyncFlags(ctx, tx, createBody.Id, productFlags); err != nil {
			return err
		}

		return s.Repository.SyncSuppliersDiff(ctx, tx, createBody.Id, supplierLinks)
	})

	if err != nil {
		s.Log.Errorf("Failed to create product: %+v", err)
		return nil, err
	}

	return s.GetOne(c, createBody.Id)
}

func (s productService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Product, error) {
	if err := s.Validate.Struct(req); err != nil {
		return nil, err
	}

	updateBody := make(map[string]any)

	if req.Name != nil {
		name := strings.TrimSpace(*req.Name)
		if name == "" {
			return nil, fiber.NewError(fiber.StatusBadRequest, "Name cannot be empty")
		}
		if exists, err := s.Repository.NameExists(c.Context(), name, &id); err != nil {
			s.Log.Errorf("Failed to check product name: %+v", err)
			return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check product name")
		} else if exists {
			return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Product with name %s already exists", name))
		}
		updateBody["name"] = name
	}

	if req.Brand != nil {
		brand := strings.TrimSpace(*req.Brand)
		if brand == "" {
			return nil, fiber.NewError(fiber.StatusBadRequest, "Brand cannot be empty")
		}
		updateBody["brand"] = brand
	}

	if req.Sku != nil {
		sku := strings.ToUpper(strings.TrimSpace(*req.Sku))
		if sku == "" {
			updateBody["sku"] = nil
		} else {
			if exists, err := s.Repository.SkuExists(c.Context(), sku, &id); err != nil {
				s.Log.Errorf("Failed to check product sku: %+v", err)
				return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check product sku")
			} else if exists {
				return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Product with sku %s already exists", sku))
			}
			updateBody["sku"] = sku
		}
	}

	if err := common.EnsureRelations(c.Context(),
		common.RelationCheck{Name: "Uom", ID: req.UomID, Exists: s.Repository.UomExists},
		common.RelationCheck{Name: "Product category", ID: req.ProductCategoryID, Exists: s.Repository.CategoryExists},
	); err != nil {
		return nil, err
	}

	if req.UomID != nil {
		updateBody["uom_id"] = *req.UomID
	}

	if req.ProductCategoryID != nil {
		updateBody["product_category_id"] = *req.ProductCategoryID
	}

	if req.ProductPrice != nil {
		updateBody["product_price"] = *req.ProductPrice
	}
	if req.SellingPrice != nil {
		updateBody["selling_price"] = req.SellingPrice
	}
	if req.Tax != nil {
		updateBody["tax"] = req.Tax
	}
	if req.ExpiryPeriod != nil {
		updateBody["expiry_period"] = req.ExpiryPeriod
	}

	ctx := c.Context()

	var supplierLinks []entity.ProductSupplier
	var supplierUpdate bool
	if req.Suppliers != nil {
		supplierUpdate = true
		if len(*req.Suppliers) > 0 {
			seen := make(map[uint]struct{}, len(*req.Suppliers))
			supplierLinks = make([]entity.ProductSupplier, 0, len(*req.Suppliers))
			supplierIDs := make([]uint, 0, len(*req.Suppliers))
			for _, supplier := range *req.Suppliers {
				if _, exists := seen[supplier.SupplierID]; exists {
					return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate supplier_id %d", supplier.SupplierID))
				}
				seen[supplier.SupplierID] = struct{}{}
				supplierIDs = append(supplierIDs, supplier.SupplierID)
				supplierLinks = append(supplierLinks, entity.ProductSupplier{
					SupplierId: supplier.SupplierID,
					Price:      supplier.Price,
				})
			}

			suppliers, err := s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
			if err != nil {
				s.Log.Errorf("Failed to validate suppliers: %+v", err)
				return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate suppliers")
			}
			if len(suppliers) != len(supplierIDs) {
				actual := make([]uint, len(suppliers))
				for i, supplier := range suppliers {
					actual[i] = supplier.Id
				}
				missing := utils.MissingUintIDs(supplierIDs, actual)
				return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Suppliers with ids %v not found", missing))
			}
			for _, sup := range suppliers {
				if strings.ToUpper(sup.Category) != string(utils.SupplierCategorySapronak) {
					return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier with id %d is not category SAPRONAK", sup.Id))
				}
			}
		}
	}

	var (
		flagUpdate bool
		flagValues []string
	)
	var flagErr error
	flagUpdate, flagValues, flagErr = resolveUpdateProductFlags(req)
	if flagErr != nil {
		return nil, flagErr
	}

	if len(updateBody) == 0 && !supplierUpdate && !flagUpdate {
		return s.GetOne(c, id)
	}

	err := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		repoTx := s.Repository.WithTx(tx)

		if len(updateBody) > 0 {
			if err := repoTx.PatchOne(ctx, id, updateBody, nil); err != nil {
				return err
			}
		} else {
			if _, err := repoTx.GetByID(ctx, id, nil); err != nil {
				return err
			}
		}

		if supplierUpdate {
			if err := s.Repository.SyncSuppliersDiff(ctx, tx, id, supplierLinks); err != nil {
				return err
			}
		}

		if flagUpdate {
			if err := s.Repository.SyncFlags(ctx, tx, id, flagValues); err != nil {
				return err
			}
		}

		return nil
	})

	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, fiber.NewError(fiber.StatusNotFound, "Product not found")
		}
		s.Log.Errorf("Failed to update product: %+v", err)
		return nil, err
	}
	return s.GetOne(c, id)
}

func (s productService) DeleteOne(c *fiber.Ctx, id uint) error {
	err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
		repoTx := s.Repository.WithTx(tx)

		if err := s.Repository.SyncSuppliersDiff(c.Context(), tx, id, nil); err != nil {
			return err
		}

		if err := s.Repository.DeleteFlags(c.Context(), tx, id); err != nil {
			return err
		}

		return repoTx.DeleteOne(c.Context(), id)
	})

	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return fiber.NewError(fiber.StatusNotFound, "Product not found")
		}
		s.Log.Errorf("Failed to delete product: %+v", err)
		return err
	}
	return nil
}
