package common

import (
	"context"
	"fmt"
	"reflect"
	"strings"

	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/dao"
	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/log"

	"go.uber.org/zap"
	"gorm.io/gorm"
)

type commonDAOImpl struct {
	DB *gorm.DB
}

func NewCommonDAO() CommonDAO {
	db := dao.InitMySqlDB()
	return &commonDAOImpl{DB: db}
}
func NewCommonDAOWithDB(db *gorm.DB) CommonDAO {
	return &commonDAOImpl{DB: db}
}

// GetDB returns the underlying *gorm.DB
func (d *commonDAOImpl) GetDB() *gorm.DB {
	return d.DB
}

// DeleteAll deletes all rows from the table corresponding to the provided model.
// The model can be a struct or a pointer to struct. This uses AllowGlobalUpdate.
func (d *commonDAOImpl) DeleteAll(ctx context.Context, model any, tx ...*gorm.DB) error {
	if model == nil {
		return fmt.Errorf("model must not be nil")
	}

	// Ensure we have a pointer to struct for Model/Delete
	var modelPtr any
	t := reflect.TypeOf(model)
	if t.Kind() == reflect.Ptr {
		if t.Elem().Kind() != reflect.Struct {
			return fmt.Errorf("model must be a struct or pointer to struct")
		}
		modelPtr = model
	} else {
		if t.Kind() != reflect.Struct {
			return fmt.Errorf("model must be a struct or pointer to struct")
		}
		modelPtr = reflect.New(t).Interface()
	}

	db := getDB(d.DB, tx...)
	// Allow global delete without WHERE
	if err := db.Session(&gorm.Session{AllowGlobalUpdate: true}).Model(modelPtr).Delete(modelPtr).Error; err != nil {
		log.Error(ctx, "DeleteAll failed", zap.Error(err), zap.String("model", getModelPointerName(modelPtr)))
		return err
	}
	log.Info(ctx, "DeleteAll succeeded", zap.String("model", getModelPointerName(modelPtr)))
	return nil
}

// CreateEntity inserts a new entity or entity array into the database
func (d *commonDAOImpl) CreateEntity(ctx context.Context, modelPointer any, tx ...*gorm.DB) error {
	if !isPointer(modelPointer) {
		return fmt.Errorf("modelPointer must be a pointer type")
	}
	db := getDB(d.DB, tx...)
	if err := db.Create(modelPointer).Error; err != nil {
		log.Error(ctx, "CreateEntity failed", zap.Error(err))
		sql := db.Statement.SQL.String()
		log.Error(ctx, "llt debug", zap.String("sql", sql))
		return err
	}
	return nil
}

// SaveOrUpdateEntity saves a new entity or updates an existing one
func (d *commonDAOImpl) SaveOrUpdateEntity(ctx context.Context, modelPointer any, tx ...*gorm.DB) error {
	if !isPointer(modelPointer) {
		return fmt.Errorf("modelPointer must be a pointer type")
	}
	db := getDB(d.DB, tx...)
	if err := db.Save(modelPointer).Error; err != nil {
		log.Error(ctx, "SaveOrUpdateEntity failed", zap.Error(err))
		return err
	}
	return nil
}

// ListEntitiesWithPagination queries entities with pagination
func (d *commonDAOImpl) ListEntitiesWithPagination(ctx context.Context, pageParams PageParams, modelArrayPointer any, tx ...*gorm.DB) (*Paginator, error) {
	if !isPointerToSlice(modelArrayPointer) {
		return nil, fmt.Errorf("modelArrayPointer must be a pointer to a slice")
	}
	// 1. Build the query
	db := getDB(d.DB, tx...)
	db = applyFiltersAndSorting(db, pageParams)
	offset, pageSize := calculatePagination(pageParams)

	// 2. Count total records
	var total int64
	if err := db.Model(modelArrayPointer).Count(&total).Error; err != nil {
		log.Error(ctx, "CountTotal failed", zap.Error(err))
		return nil, err
	}
	db = db.Debug()

	// 3. query records
	if err := db.Offset(offset).Limit(int(pageSize)).Find(modelArrayPointer).Error; err != nil {
		log.Error(ctx, "Fetch paginated data failed", zap.Error(err))
		return nil, err
	}

	// 4. Generate the next page token and return the paginator
	nextPageToken := generateNextPageToken(offset, int(pageSize), int(total))
	return NewPaginator(pageSize, uint64(total), nextPageToken, modelArrayPointer), nil
}

// CountEntities queries entities with pagination
func (d *commonDAOImpl) CountEntities(ctx context.Context, countParams CountParams, modelArrayPointer any, tx ...*gorm.DB) (int64, error) {
	var total int64
	if !isPointerToSlice(modelArrayPointer) {
		return total, fmt.Errorf("modelArrayPointer must be a pointer to a slice")
	}
	// 1. Build the query
	db := getDB(d.DB, tx...)
	db = buildWhereClause(db, countParams.EqualFilters, countParams.AdvancedFilters, countParams.ModelFilters, countParams.OrFilters)
	// 2. Count total records

	if err := db.Model(modelArrayPointer).Count(&total).Error; err != nil {
		log.Error(ctx, "CountTotal failed", zap.Error(err))
		return total, err
	}
	return total, nil
}

// ListEntitiesWithPaginationInMemory retrieves all entities at once and simulates pagination.
// False paging usage scenario: using multi-table joint queries and distinct deduplicate functionality
func (d *commonDAOImpl) ListEntitiesWithPaginationInMemory(ctx context.Context, pageParams PageParams, modelArrayPointer any, db *gorm.DB) (*Paginator, error) {
	if !isPointerToSlice(modelArrayPointer) {
		return nil, fmt.Errorf("modelArrayPointer must be a pointer to a slice")
	}
	// Apply filters to the DB query
	db = applyFiltersAndSorting(db, pageParams)

	// 1. Query all data
	if err := db.Find(modelArrayPointer).Error; err != nil {
		log.Error(ctx, "Failed to fetch all data for in-memory pagination", zap.Error(err))
		return nil, err
	}

	// 2. Get the slice value from modelArrayPointer
	sliceValue := reflect.ValueOf(modelArrayPointer).Elem()
	total := sliceValue.Len()

	// 3. Calculate paginationCalculate the start and end index
	offset, pageSize := calculatePagination(pageParams)
	if total == 0 || offset >= total {
		sliceValue.Set(reflect.MakeSlice(sliceValue.Type(), 0, 0))
		return NewPaginator(pageSize, 0, "", modelArrayPointer), nil
	}
	end := offset + int(pageSize)
	if end > total {
		end = total
	}

	// 4.  Paginated slice
	paginatedData := sliceValue.Slice(offset, end)
	sliceValue.Set(paginatedData)

	// 5. Generate the next page token
	nextPageToken := generateNextPageToken(offset, int(pageSize), total)

	// Return pagination result
	pagination := NewPaginator(pageSize, uint64(total), nextPageToken, modelArrayPointer)

	log.Info(ctx, "ListEntitiesWithPaginationInMemory", zap.Any("TotalPages", pagination.TotalPages), zap.Any("Total", pagination.Total))
	return pagination, nil
}

// ListEntitiesWithPaginationGivenAllEntities simulates pagination by providing all entities
func (d *commonDAOImpl) ListEntitiesWithPaginationGivenAllEntities(ctx context.Context, pageParams PageParams, modelArrayPointer any) (*Paginator, error) {
	// 1. Get the slice value from modelArrayPointer
	sliceValue := reflect.ValueOf(modelArrayPointer).Elem()
	total := sliceValue.Len()

	// 2. Calculate pagination: calculate the start and end index
	offset, pageSize := calculatePagination(pageParams)
	if total == 0 || offset >= total {
		sliceValue.Set(reflect.MakeSlice(sliceValue.Type(), 0, 0))
		return NewPaginator(pageSize, 0, "", modelArrayPointer), nil
	}
	end := offset + int(pageSize)
	if end > total {
		end = total
	}

	// 3. Paginated slice
	paginatedData := sliceValue.Slice(offset, end)
	sliceValue.Set(paginatedData)

	// 4. Generate the next page token
	nextPageToken := generateNextPageToken(offset, int(pageSize), total)

	// Return pagination result
	pagination := NewPaginator(pageSize, uint64(total), nextPageToken, modelArrayPointer)

	log.Info(ctx, "ListEntitiesWithPaginationGivenAllEntities", zap.Any("TotalPages", pagination.TotalPages), zap.Any("Total", pagination.Total))
	return pagination, nil
}

// ListEntities queries entities based on filters and pagination.
func (d *commonDAOImpl) ListEntities(ctx context.Context, params ListParams, modelArrayPointer any, tx ...*gorm.DB) error {
	if !isPointerToSlice(modelArrayPointer) {
		return fmt.Errorf("modelArrayPointer must be a pointer to a slice")
	}

	db := getDB(d.DB, tx...)
	listParams := PageParams{
		EqualFilters:    params.EqualFilters,
		AdvancedFilters: params.AdvancedFilters,
		ModelFilters:    params.ModelFilters,
		OrFilters:       params.OrFilters,
		// OrderBy:         params.OrderBy,
		// Order:           params.Order,
		Orders: params.Orders,
	}
	// Apply filters and sorting
	db = applyFiltersAndSorting(db, listParams)

	// Execute the query
	if err := db.Find(modelArrayPointer).Error; err != nil {
		log.Error(ctx, "Error executing ListEntities query", zap.Error(err))
		return err
	}

	return nil
}

// GetEntity retrieves a single entity based on filter parameters.
func (d *commonDAOImpl) GetEntity(ctx context.Context, params ComFilter, modelPointer any, tx ...*gorm.DB) error {
	if !isPointer(modelPointer) {
		return fmt.Errorf("modelPointer must be a pointer type")
	}
	db := getDB(d.DB, tx...)
	db = applyFilters(db, params)

	if err := db.First(modelPointer).Error; err != nil {
		if err == gorm.ErrRecordNotFound {
			modeName := getModelPointerName(modelPointer)
			log.Info(ctx, fmt.Sprintf("GetEntity [%s] Record not found", modeName), zap.Any("params", params))
			return nil
		}
		log.Error(ctx, "GetEntity failed", zap.Error(err))
		return err
	}
	return nil
}

// GetEntityById retrieves a single entity.
func (d *commonDAOImpl) GetEntityById(ctx context.Context, id any, modelPointer any, tx ...*gorm.DB) error {
	comFilter := ComFilter{
		EqualFilters: map[string]any{
			"id": id,
		},
	}

	return d.GetEntity(ctx, comFilter, modelPointer, tx...)
}

// UpdateEntities updates an entity based on filter and updates provided. updates is map[string]any or struct
func (d *commonDAOImpl) UpdateEntities(ctx context.Context, comFilter ComFilter, updates any, model any, tx ...*gorm.DB) error {
	db := getDB(d.DB, tx...)
	db = applyFilters(db, comFilter)
	if err := db.Model(model).Updates(updates).Error; err != nil {
		log.Error(ctx, "Update entity error", zap.Error(err))
		return err
	}
	return nil
}

// UpdateEntityById update one entity. updates is map[string]any or struct
func (d *commonDAOImpl) UpdateEntityById(ctx context.Context, id any, updates any, model any, tx ...*gorm.DB) error {
	db := getDB(d.DB, tx...)

	comFilter := ComFilter{
		EqualFilters: map[string]any{
			"id": id,
		},
	}

	return d.UpdateAndQueryFirstEntity(ctx, comFilter, updates, model, db)
}

func (d *commonDAOImpl) UpdateAndQueryFirstEntity(ctx context.Context, comFilter ComFilter, updates any, model any, tx ...*gorm.DB) error {
	db := getDB(d.DB, tx...)
	db = applyFilters(db, comFilter)
	if err := db.Model(model).Updates(updates).Error; err != nil {
		return err
	}

	if err := db.First(model).Error; err != nil {
		log.Error(ctx, "Error fetching updated entity", zap.Error(err))
		return err
	}
	return nil
}

// DeleteEntity deletes an entity based on filter parameters.
func (d *commonDAOImpl) DeleteEntity(ctx context.Context, comFilter ComFilter, modelPointer any, tx ...*gorm.DB) error {
	if !isPointer(modelPointer) {
		return fmt.Errorf("modelPointer must be a pointer type")
	}
	db := getDB(d.DB, tx...)
	db = applyFilters(db, comFilter)

	if err := db.Delete(modelPointer).Error; err != nil {
		log.Error(ctx, "Delete entity error", zap.Error(err))
		return err
	}
	return nil
}

// DeleteEntity deletes an entity by id.
func (d *commonDAOImpl) DeleteEntityById(ctx context.Context, id any, modelPointer any, tx ...*gorm.DB) error {
	comFilter := ComFilter{
		EqualFilters: map[string]any{
			"id": id,
		},
	}

	return d.DeleteEntity(ctx, comFilter, modelPointer, tx...)
}

// Generate next page token
func generateNextPageToken(offset, pageSize, total int) string {
	if offset+pageSize < total {
		return encodePageToken(offset + pageSize)
	}
	return ""

}

// Decode page token or return 0 if decoding fails
func decodePageTokenOrDefault(pageToken string) int {
	if pageToken == "" {
		return 0
	}
	offset, err := decodePageToken(pageToken)
	if err != nil {
		return 0
	}
	return offset
}

// Calculate pagination
func calculatePagination(pageParams PageParams) (int, int32) {
	offset := decodePageTokenOrDefault(pageParams.PageToken)
	offset += int(pageParams.Skip)

	pageSize := pageParams.PageSize
	if pageSize <= 0 {
		pageSize = DEFAULTPAGESIZE
	}
	if offset < 0 {
		offset = 0
	}
	return offset, pageSize
}

// applyFilters applies filter conditions to the Gorm query.
func applyFilters(db *gorm.DB, comFilter ComFilter) *gorm.DB {
	return buildWhereClause(db, comFilter.EqualFilters, comFilter.AdvancedFilters, comFilter.ModelFilters, comFilter.OrFilters)
}

// Apply filters and sorting
func applyFiltersAndSorting(db *gorm.DB, params PageParams) *gorm.DB {
	db = buildWhereClause(db, params.EqualFilters, params.AdvancedFilters, params.ModelFilters, params.OrFilters)

	if params.OrderBy != "" && params.Order != "" {
		db = db.Order(params.OrderBy + " " + params.Order)
	}
	if len(params.Orders) > 0 {
		for _, order := range params.Orders {
			db = db.Order(order)
		}

	}
	return db
}

func buildWhereClause(db *gorm.DB, equalFilters map[string]any, advancedFilters map[string]any, modelFilter any, orFilters map[string]any) *gorm.DB {
	if modelFilter != nil {
		db = db.Where(modelFilter)
	}

	for key, value := range equalFilters {
		db = db.Where(key+" = ?", value)
	}

	for key, value := range advancedFilters {
		db = db.Where(key, value)
	}

	if len(orFilters) == 1 {
		for key, value := range orFilters {
			db = db.Or(key, value)
		}
	} else if len(orFilters) > 1 {
		// orFilters["duration in (?) "] = 20
		var orConditions []string
		var orValues []any

		for key, value := range orFilters {
			orConditions = append(orConditions, key)
			orValues = append(orValues, value)
		}
		//  case： AND ((name like ('%03%')  OR expr like ('%03%') ))
		db = db.Where("("+strings.Join(orConditions, " OR ")+")", orValues...)
	}
	return db
}

// Helper function: Get the appropriate DB
func getDB(defaultDB *gorm.DB, db ...*gorm.DB) *gorm.DB {
	if len(db) > 0 {
		return db[0]
	}
	return defaultDB
}

// Helper function: Checks if the input is a pointer
func isPointer(input any) bool {
	return reflect.TypeOf(input).Kind() == reflect.Ptr
}

// Helper function: Checks if the input is a pointer to a slice
func isPointerToSlice(input any) bool {
	t := reflect.TypeOf(input)
	return t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Slice
}

func getModelPointerName(modelPointer any) string {
	t := reflect.TypeOf(modelPointer)
	if t.Kind() == reflect.Ptr {
		t = t.Elem()
	}
	return t.Name()
}

//// Helper function: Create a slice of the model type
//func createSliceOfModel(model any) any {
//	modelType := reflect.TypeOf(model)
//	return reflect.MakeSlice(reflect.SliceOf(modelType), 0, 0).Interface()
//}
//
//// countTotal counts the total number of records in the database based on the model.
//func countTotal(ctx context.Context, db *gorm.DB, modelArrayPointer any) (*int64, error) {
//	elemType := reflect.TypeOf(modelArrayPointer).Elem().Elem()
//	elemInstance := reflect.New(elemType).Interface()
//
//	// Perform the count operation on the element type (the table corresponding to this type)
//	var total int64
//	if err := db.Model(elemInstance).Count(&total).Error; err != nil {
//		log.Error(ctx, "CountTotal failed", zap.Error(err))
//		return nil, err
//	}
//
//	return &total, nil
//}
//
//// Fetch paginated data
//func paginateData(ctx context.Context, db *gorm.DB, offset, limit int, model any) (any, error) {
//	//data := model
//	data := createSliceOfModel(model)
//	if err := db.Offset(offset).Limit(limit).Find(&data).Error; err != nil {
//		log.Error(ctx, "PaginateData failed", zap.Error(err))
//		return nil, err
//	}
//	return data, nil
//}
