package middleware

import (
	"errors"

	"github.com/gofiber/fiber/v2"
	"gorm.io/gorm"

	entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
)

type ScopeFilter struct {
	IDs      []uint
	Restrict bool
}

type roleScope struct {
	allArea      bool
	allLocation  bool
	areaIDs      []uint
	locationIDs  []uint
	hasAnyScopes bool
}

func ResolveAreaScope(c *fiber.Ctx, db *gorm.DB) (ScopeFilter, error) {
	scope, err := collectRoleScope(c)
	if err != nil || !scope.hasAnyScopes {
		return ScopeFilter{}, err
	}

	if scope.allArea || scope.allLocation {
		return ScopeFilter{}, nil
	}

	allowed := uniqueUint(scope.areaIDs)
	if len(scope.locationIDs) > 0 {
		derived, err := areaIDsByLocationIDs(db, scope.locationIDs)
		if err != nil {
			return ScopeFilter{}, err
		}
		allowed = uniqueUint(append(allowed, derived...))
	}

	if len(allowed) == 0 {
		return ScopeFilter{Restrict: true}, nil
	}
	return ScopeFilter{IDs: allowed, Restrict: true}, nil
}

func ResolveLocationScope(c *fiber.Ctx, db *gorm.DB) (ScopeFilter, error) {
	scope, err := collectRoleScope(c)
	if err != nil || !scope.hasAnyScopes {
		return ScopeFilter{}, err
	}

	if scope.allLocation || scope.allArea {
		return ScopeFilter{}, nil
	}

	areaIDs := uniqueUint(scope.areaIDs)
	locationIDs := uniqueUint(scope.locationIDs)

	switch {
	case len(locationIDs) > 0 && len(areaIDs) > 0:
		filtered, err := filterLocationIDsByAreaIDs(db, locationIDs, areaIDs)
		if err != nil {
			return ScopeFilter{}, err
		}
		locationIDs = filtered
	case len(locationIDs) == 0 && len(areaIDs) > 0:
		derived, err := locationIDsByAreaIDs(db, areaIDs)
		if err != nil {
			return ScopeFilter{}, err
		}
		locationIDs = derived
	}

	locationIDs = uniqueUint(locationIDs)
	if len(locationIDs) == 0 {
		return ScopeFilter{Restrict: true}, nil
	}
	return ScopeFilter{IDs: locationIDs, Restrict: true}, nil
}

func ResolveLocationAreaScopes(c *fiber.Ctx, db *gorm.DB) (ScopeFilter, ScopeFilter, error) {
	locationScope, err := ResolveLocationScope(c, db)
	if err != nil {
		return ScopeFilter{}, ScopeFilter{}, err
	}
	areaScope, err := ResolveAreaScope(c, db)
	if err != nil {
		return ScopeFilter{}, ScopeFilter{}, err
	}
	return locationScope, areaScope, nil
}

func collectRoleScope(c *fiber.Ctx) (roleScope, error) {
	ctx, ok := AuthDetails(c)
	if !ok || ctx == nil {
		return roleScope{}, nil
	}

	userAreaIDs := uniqueUint(ctx.UserAreaIDs)
	userLocationIDs := uniqueUint(ctx.UserLocationIDs)
	userScope := roleScope{
		allArea:      ctx.UserAllArea,
		allLocation:  ctx.UserAllLocation,
		areaIDs:      userAreaIDs,
		locationIDs:  userLocationIDs,
		hasAnyScopes: ctx.UserAllArea || ctx.UserAllLocation || len(userAreaIDs) > 0 || len(userLocationIDs) > 0,
	}
	if userScope.hasAnyScopes {
		return userScope, nil
	}

	return roleScope{}, nil
}

func areaIDsByLocationIDs(db *gorm.DB, locationIDs []uint) ([]uint, error) {
	if db == nil {
		return nil, errors.New("database not configured")
	}
	if len(locationIDs) == 0 {
		return nil, nil
	}
	var areaIDs []uint
	if err := db.Model(&entity.Location{}).
		Where("deleted_at IS NULL").
		Where("id IN ?", locationIDs).
		Distinct("area_id").
		Pluck("area_id", &areaIDs).Error; err != nil {
		return nil, err
	}
	return areaIDs, nil
}

func locationIDsByAreaIDs(db *gorm.DB, areaIDs []uint) ([]uint, error) {
	if db == nil {
		return nil, errors.New("database not configured")
	}
	if len(areaIDs) == 0 {
		return nil, nil
	}
	var locationIDs []uint
	if err := db.Model(&entity.Location{}).
		Where("deleted_at IS NULL").
		Where("area_id IN ?", areaIDs).
		Distinct("id").
		Pluck("id", &locationIDs).Error; err != nil {
		return nil, err
	}
	return locationIDs, nil
}

func filterLocationIDsByAreaIDs(db *gorm.DB, locationIDs, areaIDs []uint) ([]uint, error) {
	if db == nil {
		return nil, errors.New("database not configured")
	}
	if len(locationIDs) == 0 || len(areaIDs) == 0 {
		return nil, nil
	}
	var filtered []uint
	if err := db.Model(&entity.Location{}).
		Where("deleted_at IS NULL").
		Where("id IN ?", locationIDs).
		Where("area_id IN ?", areaIDs).
		Distinct("id").
		Pluck("id", &filtered).Error; err != nil {
		return nil, err
	}
	return filtered, nil
}

func uniqueUint(ids []uint) []uint {
	if len(ids) == 0 {
		return nil
	}
	seen := make(map[uint]struct{}, len(ids))
	result := make([]uint, 0, len(ids))
	for _, id := range ids {
		if id == 0 {
			continue
		}
		if _, ok := seen[id]; ok {
			continue
		}
		seen[id] = struct{}{}
		result = append(result, id)
	}
	return result
}

func ApplyScopeFilter(db *gorm.DB, scope ScopeFilter, column string) *gorm.DB {
	if db == nil || !scope.Restrict {
		return db
	}
	if len(scope.IDs) == 0 {
		return db.Where("1 = 0")
	}
	return db.Where(column+" IN ?", scope.IDs)
}

func ApplyLocationScope(c *fiber.Ctx, db *gorm.DB, column string) (*gorm.DB, error) {
	scopeDB := db
	if db != nil {
		scopeDB = db.Session(&gorm.Session{NewDB: true})
	}
	scope, err := ResolveLocationScope(c, scopeDB)
	if err != nil {
		return db, err
	}
	return ApplyScopeFilter(db, scope, column), nil
}

func ApplyAreaScope(c *fiber.Ctx, db *gorm.DB, column string) (*gorm.DB, error) {
	scopeDB := db
	if db != nil {
		scopeDB = db.Session(&gorm.Session{NewDB: true})
	}
	scope, err := ResolveAreaScope(c, scopeDB)
	if err != nil {
		return db, err
	}
	return ApplyScopeFilter(db, scope, column), nil
}

func ApplyLocationAreaScope(c *fiber.Ctx, db *gorm.DB, locationColumn, areaColumn string) (*gorm.DB, error) {
	scopeDB := db
	if db != nil {
		scopeDB = db.Session(&gorm.Session{NewDB: true})
	}

	if locationColumn != "" {
		locationScope, err := ResolveLocationScope(c, scopeDB)
		if err != nil {
			return db, err
		}
		db = ApplyScopeFilter(db, locationScope, locationColumn)
	}

	if areaColumn != "" {
		areaScope, err := ResolveAreaScope(c, scopeDB)
		if err != nil {
			return db, err
		}
		db = ApplyScopeFilter(db, areaScope, areaColumn)
	}

	return db, nil
}

func EnsureWarehouseAccess(c *fiber.Ctx, db *gorm.DB, warehouseID uint) error {
	if warehouseID == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Invalid warehouse id")
	}
	if db == nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Database not configured")
	}

	scope, err := ResolveLocationScope(c, db)
	if err != nil || !scope.Restrict {
		return err
	}
	if len(scope.IDs) == 0 {
		return fiber.NewError(fiber.StatusNotFound, "Warehouse not found")
	}

	var count int64
	if err := ApplyScopeFilter(
		db.WithContext(c.Context()).
			Model(&entity.Warehouse{}).
			Where("id = ?", warehouseID),
		scope,
		"warehouses.location_id",
	).Count(&count).Error; err != nil {
		return err
	}
	if count == 0 {
		return fiber.NewError(fiber.StatusNotFound, "Warehouse not found")
	}
	return nil
}

func EnsureAreaAccess(c *fiber.Ctx, db *gorm.DB, areaID uint) error {
	if areaID == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Invalid area id")
	}
	if db == nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Database not configured")
	}

	scope, err := ResolveAreaScope(c, db)
	if err != nil || !scope.Restrict {
		return err
	}
	if len(scope.IDs) == 0 {
		return fiber.NewError(fiber.StatusNotFound, "Area not found")
	}

	var count int64
	if err := ApplyScopeFilter(
		db.WithContext(c.Context()).
			Model(&entity.Area{}).
			Where("id = ?", areaID),
		scope,
		"areas.id",
	).Count(&count).Error; err != nil {
		return err
	}
	if count == 0 {
		return fiber.NewError(fiber.StatusNotFound, "Area not found")
	}
	return nil
}

func EnsureLocationAccess(c *fiber.Ctx, db *gorm.DB, locationID uint) error {
	if locationID == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Invalid location id")
	}
	if db == nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Database not configured")
	}

	scope, err := ResolveLocationScope(c, db)
	if err != nil || !scope.Restrict {
		return err
	}
	if len(scope.IDs) == 0 {
		return fiber.NewError(fiber.StatusNotFound, "Location not found")
	}

	var count int64
	if err := ApplyScopeFilter(
		db.WithContext(c.Context()).
			Model(&entity.Location{}).
			Where("id = ?", locationID),
		scope,
		"locations.id",
	).Count(&count).Error; err != nil {
		return err
	}
	if count == 0 {
		return fiber.NewError(fiber.StatusNotFound, "Location not found")
	}
	return nil
}

func EnsureKandangAccess(c *fiber.Ctx, db *gorm.DB, kandangID uint) error {
	if kandangID == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang id")
	}
	if db == nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Database not configured")
	}

	scope, err := ResolveLocationScope(c, db)
	if err != nil || !scope.Restrict {
		return err
	}
	if len(scope.IDs) == 0 {
		return fiber.NewError(fiber.StatusNotFound, "Kandang not found")
	}

	var count int64
	if err := ApplyScopeFilter(
		db.WithContext(c.Context()).
			Model(&entity.Kandang{}).
			Where("id = ?", kandangID),
		scope,
		"kandangs.location_id",
	).Count(&count).Error; err != nil {
		return err
	}
	if count == 0 {
		return fiber.NewError(fiber.StatusNotFound, "Kandang not found")
	}
	return nil
}

func EnsureProductWarehouseAccess(c *fiber.Ctx, db *gorm.DB, productWarehouseID uint) error {
	if productWarehouseID == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Invalid product warehouse id")
	}
	if db == nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Database not configured")
	}

	scope, err := ResolveLocationScope(c, db)
	if err != nil || !scope.Restrict {
		return err
	}
	if len(scope.IDs) == 0 {
		return fiber.NewError(fiber.StatusNotFound, "Product warehouse not found")
	}

	var count int64
	q := db.WithContext(c.Context()).
		Table("product_warehouses pw").
		Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
		Where("pw.id = ?", productWarehouseID)
	q = ApplyScopeFilter(q, scope, "w.location_id")
	if err := q.Count(&count).Error; err != nil {
		return err
	}
	if count == 0 {
		return fiber.NewError(fiber.StatusNotFound, "Product warehouse not found")
	}
	return nil
}

func EnsureStockLogAccess(c *fiber.Ctx, db *gorm.DB, stockLogID uint) error {
	if stockLogID == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Invalid stock log id")
	}
	if db == nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Database not configured")
	}

	scope, err := ResolveLocationScope(c, db)
	if err != nil || !scope.Restrict {
		return err
	}
	if len(scope.IDs) == 0 {
		return fiber.NewError(fiber.StatusNotFound, "Stock log not found")
	}

	var count int64
	q := db.WithContext(c.Context()).
		Table("stock_logs sl").
		Joins("JOIN product_warehouses pw ON pw.id = sl.product_warehouse_id").
		Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
		Where("sl.id = ?", stockLogID)
	q = ApplyScopeFilter(q, scope, "w.location_id")
	if err := q.Count(&count).Error; err != nil {
		return err
	}
	if count == 0 {
		return fiber.NewError(fiber.StatusNotFound, "Stock log not found")
	}
	return nil
}

func EnsureMarketingAccess(c *fiber.Ctx, db *gorm.DB, marketingID uint) error {
	if marketingID == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Invalid marketing id")
	}
	if db == nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Database not configured")
	}

	scope, err := ResolveLocationScope(c, db)
	if err != nil || !scope.Restrict {
		return err
	}
	if len(scope.IDs) == 0 {
		return fiber.NewError(fiber.StatusNotFound, "Marketing not found")
	}

	var count int64
	q := db.WithContext(c.Context()).
		Table("marketings m").
		Joins("JOIN marketing_products mp ON mp.marketing_id = m.id").
		Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
		Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
		Where("m.id = ?", marketingID)
	q = ApplyScopeFilter(q, scope, "w.location_id")
	if err := q.Count(&count).Error; err != nil {
		return err
	}
	if count == 0 {
		return fiber.NewError(fiber.StatusNotFound, "Marketing not found")
	}
	return nil
}

func EnsureRecordingAccess(c *fiber.Ctx, db *gorm.DB, recordingID uint) error {
	if recordingID == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Invalid recording id")
	}
	if db == nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Database not configured")
	}

	scope, err := ResolveLocationScope(c, db)
	if err != nil || !scope.Restrict {
		return err
	}
	if len(scope.IDs) == 0 {
		return fiber.NewError(fiber.StatusNotFound, "Recording not found")
	}

	var count int64
	q := db.WithContext(c.Context()).
		Table("recordings r").
		Joins("JOIN project_flock_kandangs pfk ON pfk.id = r.project_flock_kandangs_id").
		Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id").
		Where("r.id = ?", recordingID)
	q = ApplyScopeFilter(q, scope, "pf.location_id")
	if err := q.Count(&count).Error; err != nil {
		return err
	}
	if count == 0 {
		return fiber.NewError(fiber.StatusNotFound, "Recording not found")
	}
	return nil
}

func EnsureUniformityAccess(c *fiber.Ctx, db *gorm.DB, uniformityID uint) error {
	if uniformityID == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Invalid uniformity id")
	}
	if db == nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Database not configured")
	}

	scope, err := ResolveLocationScope(c, db)
	if err != nil || !scope.Restrict {
		return err
	}
	if len(scope.IDs) == 0 {
		return fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
	}

	var count int64
	q := db.WithContext(c.Context()).
		Table("project_flock_kandang_uniformity u").
		Joins("JOIN project_flock_kandangs pfk ON pfk.id = u.project_flock_kandang_id").
		Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id").
		Where("u.id = ?", uniformityID)
	q = ApplyScopeFilter(q, scope, "pf.location_id")
	if err := q.Count(&count).Error; err != nil {
		return err
	}
	if count == 0 {
		return fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
	}
	return nil
}

func EnsureLayingTransferAccess(c *fiber.Ctx, db *gorm.DB, transferID uint) error {
	if transferID == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Invalid transfer id")
	}
	if db == nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Database not configured")
	}

	scope, err := ResolveLocationScope(c, db)
	if err != nil || !scope.Restrict {
		return err
	}
	if len(scope.IDs) == 0 {
		return fiber.NewError(fiber.StatusNotFound, "Transfer not found")
	}

	var count int64
	q := db.WithContext(c.Context()).
		Table("laying_transfers lt").
		Joins("JOIN project_flocks pf_from ON pf_from.id = lt.from_project_flock_id").
		Joins("JOIN project_flocks pf_to ON pf_to.id = lt.to_project_flock_id").
		Where("lt.id = ?", transferID).
		Where("(pf_from.location_id IN ? OR pf_to.location_id IN ?)", scope.IDs, scope.IDs)
	if err := q.Count(&count).Error; err != nil {
		return err
	}
	if count == 0 {
		return fiber.NewError(fiber.StatusNotFound, "Transfer not found")
	}
	return nil
}

func EnsureProjectFlockAccess(c *fiber.Ctx, db *gorm.DB, projectFlockID uint) error {
	if projectFlockID == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
	}
	if db == nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Database not configured")
	}

	scope, err := ResolveLocationScope(c, db)
	if err != nil || !scope.Restrict {
		return err
	}
	if len(scope.IDs) == 0 {
		return fiber.NewError(fiber.StatusNotFound, "Project Flock not found")
	}

	var count int64
	if err := ApplyScopeFilter(
		db.WithContext(c.Context()).
			Model(&entity.ProjectFlock{}).
			Where("id = ?", projectFlockID),
		scope,
		"project_flocks.location_id",
	).Count(&count).Error; err != nil {
		return err
	}
	if count == 0 {
		return fiber.NewError(fiber.StatusNotFound, "Project Flock not found")
	}
	return nil
}

func EnsureProjectFlockKandangAccess(c *fiber.Ctx, db *gorm.DB, projectFlockID, projectFlockKandangID uint) error {
	if projectFlockKandangID == 0 {
		return fiber.NewError(fiber.StatusBadRequest, "Invalid project flock kandang id")
	}
	if db == nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Database not configured")
	}

	scope, err := ResolveLocationScope(c, db)
	if err != nil || !scope.Restrict {
		return err
	}
	if len(scope.IDs) == 0 {
		return fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found")
	}

	var count int64
	q := db.WithContext(c.Context()).
		Table("project_flock_kandangs").
		Joins("JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id").
		Where("project_flock_kandangs.id = ?", projectFlockKandangID)
	if projectFlockID > 0 {
		q = q.Where("project_flock_kandangs.project_flock_id = ?", projectFlockID)
	}
	q = ApplyScopeFilter(q, scope, "project_flocks.location_id")
	if err := q.Count(&count).Error; err != nil {
		return err
	}
	if count == 0 {
		return fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found")
	}
	return nil
}
