package kfa

import (
	"context"
	"encoding/json"
	"reflect"
	"strings"
	"unicode"

	"service/internal/infrastructure/database"
	"service/pkg/utils/query"

	"github.com/jmoiron/sqlx"
)

type CommandRepository interface {
	UpsertProductBundle(ctx context.Context, product *KfaProduct, ingredients []KfaProductActiveIngredient, packagings []KfaProductPackaging) error
}

type commandRepository struct {
	dbManager database.Service
	connName  string
}

func NewCommandRepository(dbManager database.Service, connName string) CommandRepository {
	return &commandRepository{
		dbManager: dbManager,
		connName:  connName,
	}
}

func (r *commandRepository) UpsertProductBundle(ctx context.Context, product *KfaProduct, ingredients []KfaProductActiveIngredient, packagings []KfaProductPackaging) (err error) {
	dbGorm, err := r.dbManager.GetGormDB(r.connName)
	if err != nil {
		return err
	}
	sqldb, err := dbGorm.DB()
	if err != nil {
		return err
	}
	dbx := sqlx.NewDb(sqldb, "postgres")

	// Inisialisasi Query Builder (Nonaktifkan limit sekuritas untuk internal worker)
	sqlBuilder := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).
		SetQueryLogging(false).
		SetSecurityOptions(false, 0)

	// Ekstrak semua kolom yang akan digunakan untuk mendaftarkannya ke whitelist Query Builder
	productData, productUpdateCols := extractInsertData(product, "kfa_code")
	var allowedCols []string
	allowedCols = append(allowedCols, productData.Columns...)
	if len(ingredients) > 0 {
		ingData, _ := extractInsertData(ingredients[0], "")
		allowedCols = append(allowedCols, ingData.Columns...)
	}
	if len(packagings) > 0 {
		pkgData, _ := extractInsertData(packagings[0], "")
		allowedCols = append(allowedCols, pkgData.Columns...)
	}
	allowedCols = append(allowedCols, "product_kfa_code", "kfa_code") // pastikan key relasi / conflict dimasukkan

	// Daftarkan kolom agar terhindar dari error 'disallowed column'
	sqlBuilder.SetAllowedColumns(allowedCols)

	tx, err := dbx.BeginTxx(ctx, nil)
	if err != nil {
		return err
	}

	// Tangani Rollback jika terjadi kegagalan atau panic
	defer func() {
		if p := recover(); p != nil {
			tx.Rollback()
			panic(p)
		} else if err != nil {
			tx.Rollback()
		}
	}()

	// 1. Upsert Induk Product (Manual Check & Update/Insert to avoid PG Constraint Error 42P10)
	var count int
	err = tx.GetContext(ctx, &count, "SELECT count(*) FROM master.kfa_products WHERE kfa_code = $1", product.KfaCode)
	if err != nil {
		return err
	}

	if count > 0 {
		updateData := query.UpdateData{
			Columns: productUpdateCols,
			Values:  make([]interface{}, len(productUpdateCols)),
		}
		for i, col := range productUpdateCols {
			for j, insertCol := range productData.Columns {
				if col == insertCol {
					updateData.Values[i] = productData.Values[j]
					break
				}
			}
		}
		_, err = sqlBuilder.ExecuteUpdate(ctx, tx, "master.kfa_products", updateData, []query.FilterGroup{{
			Filters: []query.DynamicFilter{query.CreateEqualFilter("kfa_code", product.KfaCode)},
		}})
	} else {
		_, err = sqlBuilder.ExecuteInsert(ctx, tx, "master.kfa_products", productData)
	}
	if err != nil {
		return err
	}

	// 2. Hapus Active Ingredients Lama
	delFilters := []query.FilterGroup{{
		Filters: []query.DynamicFilter{
			query.CreateEqualFilter("product_kfa_code", product.KfaCode),
		},
	}}
	_, err = sqlBuilder.ExecuteDelete(ctx, tx, "master.kfa_product_active_ingredients", delFilters)
	if err != nil {
		return err
	}

	// 3. Insert Active Ingredients Baru
	for _, ing := range ingredients {
		ingData, _ := extractInsertData(ing, "")
		_, err = sqlBuilder.ExecuteInsert(ctx, tx, "master.kfa_product_active_ingredients", ingData)
		if err != nil {
			return err
		}
	}

	// 4. Hapus Packagings Lama
	_, err = sqlBuilder.ExecuteDelete(ctx, tx, "master.kfa_product_packagings", delFilters)
	if err != nil {
		return err
	}

	// 5. Insert Packagings Baru
	for _, pkg := range packagings {
		pkgData, _ := extractInsertData(pkg, "")
		_, err = sqlBuilder.ExecuteInsert(ctx, tx, "master.kfa_product_packagings", pkgData)
		if err != nil {
			return err
		}
	}

	err = tx.Commit()
	return err
}

// Helper: Ekstrak data model struct menjadi InsertData format untuk QueryBuilder
func extractInsertData(obj interface{}, conflictCol string) (query.InsertData, []string) {
	val := reflect.ValueOf(obj)
	if val.Kind() == reflect.Ptr {
		val = val.Elem()
	}
	typ := val.Type()

	var cols []string
	var vals []interface{}
	var updateCols []string

	for i := 0; i < typ.NumField(); i++ {
		field := typ.Field(i)
		if field.PkgPath != "" { // Skip properti yang tidak diekspor
			continue
		}

		colName := toSnakeCase(field.Name)
		if colName == "id" { // ID selalu di-skip pada operasi insert karena dia auto increment
			continue
		}

		fieldVal := val.Field(i).Interface()
		cols = append(cols, colName)

		if b, ok := fieldVal.(json.RawMessage); ok {
			if len(b) == 0 || string(b) == "null" {
				vals = append(vals, nil)
			} else {
				vals = append(vals, string(b))
			}
		} else {
			vals = append(vals, fieldVal)
		}

		// Cek apa saja kolom yang valid di-update saat konflik (mengecualikan relasi/kode referensi)
		if colName != conflictCol && colName != "created_at" && colName != "product_kfa_code" {
			updateCols = append(updateCols, colName)
		}
	}

	return query.InsertData{
		Columns: cols,
		Values:  vals,
	}, updateCols
}

// Helper: Ubah PascalCase field Go ke mode snake_case untuk tabel database
func toSnakeCase(s string) string {
	if s == "ID" {
		return "id"
	}
	var result strings.Builder
	var prevUpper bool
	for i, r := range s {
		if unicode.IsUpper(r) {
			if i > 0 && !prevUpper {
				result.WriteByte('_')
			}
			result.WriteRune(unicode.ToLower(r))
			prevUpper = true
		} else {
			result.WriteRune(r)
			prevUpper = false
		}
	}
	return result.String()
}
