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/nonstocks/repositories"
	validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/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 NonstockService interface {
	GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Nonstock, int64, error)
	GetOne(ctx *fiber.Ctx, id uint) (*entity.Nonstock, error)
	CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Nonstock, error)
	UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Nonstock, error)
	DeleteOne(ctx *fiber.Ctx, id uint) error
}

type nonstockService struct {
	Log        *logrus.Logger
	Validate   *validator.Validate
	Repository repository.NonstockRepository
}

func NewNonstockService(repo repository.NonstockRepository, validate *validator.Validate) NonstockService {
	return &nonstockService{
		Log:        utils.Log,
		Validate:   validate,
		Repository: repo,
	}
}

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

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

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

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

		if params.SupplierID > 0 {
			db = db.Joins("INNER JOIN nonstock_suppliers ON nonstock_suppliers.nonstock_id = nonstocks.id").
				Where("nonstock_suppliers.supplier_id = ?", params.SupplierID).
				Distinct()
		}

		db = s.withRelations(db)

		if params.Search != "" {
			return db.Where("name ILIKE ?", "%"+params.Search+"%")
		}
		return db.Order("created_at DESC").Order("updated_at DESC")
	})

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

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

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

	ctx := c.Context()
	name := strings.TrimSpace(req.Name)

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

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

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

	nonstockFlags, flagErr := normalizeNonstockFlags(req.Flags)
	if flagErr != nil {
		return nil, flagErr
	}

	createBody := &entity.Nonstock{
		Name:      req.Name,
		UomId:     req.UomID,
		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, nonstockFlags); err != nil {
			return err
		}

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

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

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

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

	updateBody := make(map[string]any)

	ctx := c.Context()

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

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

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

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

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

	var (
		flagUpdate bool
		flagValues []string
	)
	if req.Flags != nil {
		flagUpdate = true
		var flagErr error
		flagValues, flagErr = normalizeNonstockFlags(*req.Flags)
		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, "Nonstock not found")
		}
		s.Log.Errorf("Failed to update nonstock: %+v", err)
		return nil, err
	}
	return s.GetOne(c, id)
}

func (s nonstockService) 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, "Nonstock not found")
		}
		s.Log.Errorf("Failed to delete nonstock: %+v", err)
		return err
	}
	return nil
}

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