package routes

import (
	"bssapp-backend/auth"
	"bssapp-backend/internal/auditlog"
	"bssapp-backend/internal/security"
	"bssapp-backend/models"
	"bssapp-backend/queries"
	"bssapp-backend/repository"
	"bssapp-backend/services"
	"database/sql"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/gorilla/mux"
	"github.com/lib/pq"
	"golang.org/x/crypto/bcrypt"
)

/* ======================================================
   🔐 LOGIN
====================================================== */

type LoginRequest struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

func looksLikeBcryptHash(value string) bool {
	return strings.HasPrefix(value, "$2a$") ||
		strings.HasPrefix(value, "$2b$") ||
		strings.HasPrefix(value, "$2y$")
}

func ensureLegacyUserReadyForSession(db *sql.DB, legacyUser *models.User) (int64, error) {
	desiredID := int64(legacyUser.ID)

	_, err := db.Exec(`
		INSERT INTO mk_dfusr (
			id,
			username,
			email,
			full_name,
			mobile,
			address,
			is_active,
			password_hash,
			force_password_change,
			created_at,
			updated_at
		)
		VALUES (
			$1,$2,$3,$4,$5,$6,$7,'',true,NOW(),NOW()
		)
		ON CONFLICT (id)
		DO UPDATE SET
			username = EXCLUDED.username,
			email = EXCLUDED.email,
			full_name = COALESCE(NULLIF(EXCLUDED.full_name, ''), mk_dfusr.full_name),
			mobile = COALESCE(NULLIF(EXCLUDED.mobile, ''), mk_dfusr.mobile),
			address = COALESCE(NULLIF(EXCLUDED.address, ''), mk_dfusr.address),
			is_active = EXCLUDED.is_active,
			force_password_change = true,
			updated_at = NOW()
	`,
		desiredID,
		strings.TrimSpace(legacyUser.Username),
		strings.TrimSpace(legacyUser.Email),
		strings.TrimSpace(legacyUser.FullName),
		strings.TrimSpace(legacyUser.Mobile),
		strings.TrimSpace(legacyUser.Address),
		legacyUser.IsActive,
	)
	if err == nil {
		return desiredID, nil
	}

	mkRepo := repository.NewMkUserRepository(db)
	existing, lookupErr := mkRepo.GetByUsername(legacyUser.Username)
	if lookupErr != nil {
		return 0, err
	}

	_, updErr := db.Exec(`
		UPDATE mk_dfusr
		SET
			is_active = $1,
			force_password_change = true,
			updated_at = NOW()
		WHERE id = $2
	`, legacyUser.IsActive, existing.ID)
	if updErr != nil {
		return 0, updErr
	}

	return existing.ID, nil
}

func LoginHandler(db *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {

		w.Header().Set("Content-Type", "application/json; charset=utf-8")

		// --------------------------------------------------
		// 0️⃣ REQUEST
		// --------------------------------------------------
		var req LoginRequest
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			http.Error(w, "Geçersiz JSON", http.StatusBadRequest)
			return
		}

		login := strings.TrimSpace(req.Username)
		pass := req.Password // ⚠️ TRIM YAPMA

		if login == "" || pass == "" {
			http.Error(w, "Kullanıcı adı veya parola hatalı", http.StatusUnauthorized)
			return
		}

		mkRepo := repository.NewMkUserRepository(db)

		// ==================================================
		// 1️⃣ mk_dfusr ÖNCELİKLİ
		// ==================================================
		mkUser, err := mkRepo.GetByUsername(login)
		if err == nil {
			log.Println("🧪 MK USER FROM DB")
			log.Printf("🧪 ID=%d role_id=%d role_code='%s' depts=%v",
				mkUser.ID,
				mkUser.RoleID,
				mkUser.RoleCode,
				mkUser.DepartmentCodes,
			)

		}

		log.Printf(
			"🔎 LOGIN DEBUG | mk_user_found=%v err=%v hash_len=%d",
			err == nil,
			err,
			func() int {
				if err == nil {
					return len(strings.TrimSpace(mkUser.PasswordHash))
				}
				return 0
			}(),
		)

		if err == nil {

			// mk_dfusr authoritative
			mkHash := strings.TrimSpace(mkUser.PasswordHash)
			if mkHash != "" {
				if looksLikeBcryptHash(mkHash) {
					cmpErr := bcrypt.CompareHashAndPassword(
						[]byte(mkHash),
						[]byte(pass),
					)
					if cmpErr == nil {
						_ = mkRepo.TouchLastLogin(mkUser.ID)
						writeLoginResponse(w, db, mkUser)
						return
					}

					if !mkUser.ForcePasswordChange {
						http.Error(w, "invalid credentials", http.StatusUnauthorized)
						return
					}

					log.Printf(
						"LOGIN FALLBACK legacy allowed (force_password_change=true) username=%s id=%d",
						mkUser.Username,
						mkUser.ID,
					)
				} else {
					log.Printf(
						"LOGIN FALLBACK legacy allowed (non-bcrypt mk hash) username=%s id=%d",
						mkUser.Username,
						mkUser.ID,
					)
				}
			}
			// password_hash boşsa legacy fallback
		} else if err != repository.ErrMkUserNotFound {
			log.Println("❌ mk_dfusr lookup error:", err)
			http.Error(w, "Giriş yapılamadı", http.StatusInternalServerError)
			return
		}

		// ==================================================
		// 2️⃣ dfusr FALLBACK (LEGACY)
		// ==================================================
		log.Println("🟡 LEGACY LOGIN PATH:", login)

		legacyRepo := repository.NewUserRepository(db)
		legacyUser, err := legacyRepo.GetLegacyUserForLogin(login)
		if err != nil || !legacyUser.IsActive {
			http.Error(w, "Kullanıcı adı veya parola hatalı", http.StatusUnauthorized)
			return
		}

		log.Printf(
			"🔎 LOGIN DEBUG | legacy_upass_len=%d prefix=%s",
			len(strings.TrimSpace(legacyUser.Upass)),
			func() string {
				if len(legacyUser.Upass) >= 4 {
					return legacyUser.Upass[:4]
				}
				return legacyUser.Upass
			}(),
		)

		if !services.CheckPasswordWithLegacy(legacyUser, pass) {
			http.Error(w, "Kullanıcı adı veya parola hatalı", http.StatusUnauthorized)
			return
		}

		// ==================================================
		// 3️⃣ LEGACY SESSION (PENDING MIGRATION)
		// - mk_dfusr migration is completed in /api/password/change
		// ==================================================
		mkID, err := ensureLegacyUserReadyForSession(db, legacyUser)
		if err != nil {
			log.Printf("LEGACY LOGIN MIGRATION BIND FAILED username=%s err=%v", login, err)
			http.Error(w, "Giriş yapılamadı", http.StatusInternalServerError)
			return
		}

		mkUser = &models.MkUser{
			ID:                  mkID,
			Username:            legacyUser.Username,
			Email:               legacyUser.Email,
			IsActive:            legacyUser.IsActive,
			RoleID:              int64(legacyUser.RoleID),
			RoleCode:            legacyUser.RoleCode,
			ForcePasswordChange: true,
		}

		auditlog.Write(auditlog.ActivityLog{
			ActionType:     "LEGACY_USER_LOGIN_PENDING_MIGRATION",
			ActionCategory: "security",
			Description:    "legacy giriş başarılı, ilk şifre değişikliği gerekli",
			IsSuccess:      true,
		})

		// ==================================================
		// 4️⃣ SUCCESS
		// ==================================================
		writeLoginResponse(w, db, mkUser)
	}
}

// ======================================================
// 🔑 LOGIN RESPONSE
// ======================================================

func writeLoginResponse(w http.ResponseWriter, db *sql.DB, user *models.MkUser) {
	// 🔥 ROLE GARANTİSİ
	if user.RoleID == 0 {

		_ = db.QueryRow(`
		SELECT dfrole_id
		FROM dfrole_usr
		WHERE dfusr_id = $1
		LIMIT 1
	`, user.ID).Scan(&user.RoleID)
	}

	if user.RoleCode == "" && user.RoleID > 0 {

		_ = db.QueryRow(`
		SELECT code
		FROM dfrole
		WHERE id = $1
	`, user.RoleID).Scan(&user.RoleCode)
	}

	log.Println("🧪 LOGIN RESPONSE USER DEBUG")
	log.Printf("🧪 user.ID        = %d", user.ID)
	log.Printf("🧪 user.Username  = %s", user.Username)
	log.Printf("🧪 user.RoleID    = %d", user.RoleID)
	log.Printf("🧪 user.RoleCode  = '%s'", user.RoleCode)
	log.Printf("🧪 user.IsActive  = %v", user.IsActive)

	permRepo := repository.NewPermissionRepository(db)
	perms, _ := permRepo.GetPermissionsByRoleID(user.RoleID)

	// ✅ CLAIMS BUILD
	claims := auth.BuildClaimsFromUser(user, 15*time.Minute)

	token, err := auth.GenerateToken(
		claims,
		user.Username,
		user.ForcePasswordChange,
	)
	if err != nil {
		http.Error(w, "Token üretilemedi", http.StatusInternalServerError)
		return
	}

	refreshPlain, refreshHash, err := security.GenerateRefreshToken()
	if err != nil {
		http.Error(w, "Refresh token üretilemedi", http.StatusInternalServerError)
		return
	}

	refreshExp := time.Now().Add(14 * 24 * time.Hour)
	rtRepo := repository.NewRefreshTokenRepository(db)
	if err := rtRepo.IssueRefreshToken(user.ID, refreshHash, refreshExp); err != nil {
		log.Printf("refresh token store failed user=%d err=%v", user.ID, err)
		http.Error(w, "Session başlatılamadı", http.StatusInternalServerError)
		return
	}

	setRefreshCookie(w, refreshPlain, refreshExp)

	_ = json.NewEncoder(w).Encode(map[string]any{
		"token": token,
		"user": map[string]any{
			"id":                    user.ID,
			"username":              user.Username,
			"email":                 user.Email,
			"is_active":             user.IsActive,
			"role_id":               user.RoleID,
			"role_code":             user.RoleCode,
			"force_password_change": user.ForcePasswordChange,
			"v3_username":           user.V3Username,
			"v3_usergroup":          user.V3UserGroup,
		},
		"permissions": perms,
	})
}

/* ======================================================
   🔎 LOOKUPS (aynen korunuyor)
====================================================== */

type LookupOption struct {
	Value string `json:"value"`
	Label string `json:"label"`
}

func GetRoleLookupRoute(db *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		rows, err := db.Query(queries.GetRoleLookup)
		if err != nil {
			http.Error(w, "role lookup error", http.StatusInternalServerError)
			return
		}
		defer rows.Close()

		var list []LookupOption
		for rows.Next() {
			var o LookupOption
			if err := rows.Scan(&o.Value, &o.Label); err == nil {
				list = append(list, o)
			}
		}
		_ = json.NewEncoder(w).Encode(list)
	}
}

func GetDepartmentLookupRoute(db *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		rows, err := db.Query(queries.GetDepartmentLookup)
		if err != nil {
			http.Error(w, "department lookup error", http.StatusInternalServerError)
			return
		}
		defer rows.Close()

		var list []LookupOption
		for rows.Next() {
			var o LookupOption
			if err := rows.Scan(&o.Value, &o.Label); err == nil {
				list = append(list, o)
			}
		}
		_ = json.NewEncoder(w).Encode(list)
	}
}

func GetPiyasaLookupRoute(db *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		rows, err := db.Query(queries.GetPiyasaLookup)
		if err != nil {
			http.Error(w, "piyasa lookup error", http.StatusInternalServerError)
			return
		}
		defer rows.Close()

		var list []LookupOption
		for rows.Next() {
			var o LookupOption
			if err := rows.Scan(&o.Value, &o.Label); err == nil {
				list = append(list, o)
			}
		}
		_ = json.NewEncoder(w).Encode(list)
	}
}

// ======================================================
// 🧾 NEBIM USER LOOKUP
// GET /api/lookups/nebim-users
// ======================================================
func GetNebimUserLookupRoute(db *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {

		rows, err := db.Query(queries.GetNebimUserLookup)
		if err != nil {
			http.Error(w, "nebim lookup error", http.StatusInternalServerError)
			return
		}
		defer rows.Close()

		var list []map[string]string
		for rows.Next() {
			var v, l, g string
			if err := rows.Scan(&v, &l, &g); err == nil {
				list = append(list, map[string]string{
					"value":           v,
					"label":           l,
					"user_group_code": g,
				})
			}
		}

		_ = json.NewEncoder(w).Encode(list)
	}
}
func UserCreateRoute(db *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {

		w.Header().Set("Content-Type", "application/json; charset=utf-8")

		var payload models.UserWrite
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			http.Error(w, "Geçersiz payload", http.StatusBadRequest)
			return
		}

		payload.Code = strings.TrimSpace(payload.Code)
		payload.FullName = strings.TrimSpace(payload.FullName)
		payload.Email = strings.TrimSpace(payload.Email)
		payload.Mobile = strings.TrimSpace(payload.Mobile)
		payload.Address = strings.TrimSpace(payload.Address)

		if payload.Code == "" {
			http.Error(w, "Kullanıcı kodu zorunludur", http.StatusUnprocessableEntity)
			return
		}

		tx, err := db.Begin()
		if err != nil {
			http.Error(w, "Transaction başlatılamadı", http.StatusInternalServerError)
			return
		}
		defer tx.Rollback()

		var newID int64
		log.Printf("DEBUG: UserCreateRoute payload=%+v", payload)
		err = tx.QueryRow(`
			INSERT INTO mk_dfusr (
				username,
				is_active,
				full_name,
				email,
				mobile,
				address,
				password_hash,
				force_password_change,
				created_at,
				updated_at
			)
			VALUES ($1,$2,$3,$4,$5,$6,'',true,NOW(),NOW())
			RETURNING id
		`,
			payload.Code,
			payload.IsActive,
			payload.FullName,
			payload.Email,
			payload.Mobile,
			payload.Address,
		).Scan(&newID)

		if err != nil {
			log.Printf("USER INSERT ERROR code=%q email=%q err=%v", payload.Code, payload.Email, err)
			http.Error(w, fmt.Sprintf("Kullanıcı oluşturulamadı: %v", err), http.StatusInternalServerError)
			return
		}

		// Keep mk_mail in sync for downstream mail mapping screens.
		if err := ensureMkMail(tx, payload.Email); err != nil {
			log.Printf("USER CREATE ensureMkMail ERROR user_id=%d email=%q err=%v", newID, payload.Email, err)
			http.Error(w, "Mail kaydi guncellenemedi", http.StatusInternalServerError)
			return
		}

		// ROLES
		for _, role := range payload.Roles {
			role = strings.TrimSpace(role)
			if role == "" {
				continue
			}
			if _, err := tx.Exec(queries.InsertUserRole, newID, role); err != nil {
				log.Printf("USER ROLE INSERT ERROR user_id=%d role=%q err=%v", newID, role, err)
				http.Error(w, "Rol eklenemedi", http.StatusInternalServerError)
				return
			}
		}

		// DEPARTMENTS
		for _, d := range payload.Departments {
			code := strings.TrimSpace(d.Code)
			if code == "" {
				continue
			}
			if _, err := tx.Exec(queries.InsertUserDepartment, newID, code); err != nil {
				log.Printf("USER DEPARTMENT INSERT ERROR user_id=%d department=%q err=%v", newID, code, err)
				http.Error(w, "Departman eklenemedi", http.StatusInternalServerError)
				return
			}
		}

		// PIYASALAR
		for _, p := range payload.Piyasalar {
			code := strings.TrimSpace(p.Code)
			if code == "" {
				continue
			}
			if _, err := tx.Exec(queries.InsertUserPiyasa, newID, code); err != nil {
				log.Printf("USER PIYASA INSERT ERROR user_id=%d piyasa=%q err=%v", newID, code, err)
				http.Error(w, "Piyasa eklenemedi", http.StatusInternalServerError)
				return
			}
		}

		// NEBIM
		for _, n := range payload.NebimUsers {
			username := strings.TrimSpace(n.Username)
			if username == "" {
				continue
			}
			if _, err := tx.Exec(queries.InsertUserNebim, newID, username); err != nil {
				log.Printf("USER NEBIM INSERT ERROR user_id=%d username=%q err=%v", newID, username, err)
				http.Error(w, "Nebim kullanıcısı eklenemedi", http.StatusInternalServerError)
				return
			}
		}

		if err := ensureOrderPriceListUserPriceGroupSchema(db); err != nil {
			log.Printf("USER CREATE PRICE GROUP SCHEMA ERROR user_id=%d err=%v", newID, err)
			http.Error(w, "Fiyat grubu tablosu hazirlanamadi", http.StatusInternalServerError)
			return
		}
		if err := saveOrderPriceListUserPriceGroupsTx(tx, newID, payload.OrderPriceListPriceGroups); err != nil {
			log.Printf("USER CREATE PRICE GROUP SAVE ERROR user_id=%d err=%v", newID, err)
			http.Error(w, "Fiyat gruplari eklenemedi", http.StatusInternalServerError)
			return
		}

		if err := tx.Commit(); err != nil {
			if pe, ok := err.(*pq.Error); ok {
				log.Printf(
					"USER CREATE COMMIT ERROR user_id=%d code=%s detail=%s constraint=%s table=%s err=%v",
					newID, pe.Code, pe.Detail, pe.Constraint, pe.Table, err,
				)
			} else {
				log.Printf("USER CREATE COMMIT ERROR user_id=%d err=%v", newID, err)
			}
			http.Error(w, "Commit başarısız", http.StatusInternalServerError)
			return
		}

		_ = json.NewEncoder(w).Encode(map[string]any{
			"success": true,
			"id":      newID,
		})
	}
}

// ======================================================
// 🔐 ROLES LIST
// GET /api/roles
// ======================================================

func GetRolesHandler(db *sql.DB) http.HandlerFunc {
	type RoleRow struct {
		ID    int64  `json:"id"`
		Code  string `json:"code"`
		Title string `json:"title"`
	}

	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json; charset=utf-8")

		rows, err := db.Query(queries.GetRoles)
		if err != nil {
			http.Error(w, "roles query error", http.StatusInternalServerError)
			return
		}
		defer rows.Close()

		var list []RoleRow
		for rows.Next() {
			var x RoleRow
			if err := rows.Scan(&x.ID, &x.Code, &x.Title); err == nil {
				list = append(list, x)
			}
		}

		_ = json.NewEncoder(w).Encode(list)
	}
}

// ======================================================
// 🏢 DEPARTMENTS LIST
// GET /api/departments
// ======================================================

func GetDepartmentsHandler(db *sql.DB) http.HandlerFunc {
	type DeptRow struct {
		ID    int64  `json:"id"`
		Code  string `json:"code"`
		Title string `json:"title"`
	}

	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json; charset=utf-8")

		rows, err := db.Query(queries.GetDepartments)
		if err != nil {
			http.Error(w, "departments query error", http.StatusInternalServerError)
			return
		}
		defer rows.Close()

		var list []DeptRow
		for rows.Next() {
			var x DeptRow
			if err := rows.Scan(&x.ID, &x.Code, &x.Title); err == nil {
				list = append(list, x)
			}
		}

		_ = json.NewEncoder(w).Encode(list)
	}
}

// ======================================================
// 🌍 PIYASALAR LIST
// GET /api/piyasalar
// ======================================================

func GetPiyasalarHandler(db *sql.DB) http.HandlerFunc {
	type PiyRow struct {
		Code  string `json:"code"`
		Title string `json:"title"`
	}

	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json; charset=utf-8")

		rows, err := db.Query(queries.GetPiyasalar)
		if err != nil {
			http.Error(w, "piyasalar query error", http.StatusInternalServerError)
			return
		}
		defer rows.Close()

		var list []PiyRow
		for rows.Next() {
			var x PiyRow
			if err := rows.Scan(&x.Code, &x.Title); err == nil {
				list = append(list, x)
			}
		}

		_ = json.NewEncoder(w).Encode(list)
	}
}

// ======================================================
// 🔗 ROLE → DEPARTMENTS UPDATE
// POST /api/roles/{id}/departments
// body: { "codes": ["D01","D02"] }
// ======================================================

type CodesPayload struct {
	Codes []string `json:"codes"`
}

func UpdateRoleDepartmentsHandler(db *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json; charset=utf-8")

		roleIDStr := mux.Vars(r)["id"]
		roleID, err := strconv.ParseInt(roleIDStr, 10, 64)
		if err != nil || roleID <= 0 {
			http.Error(w, "Geçersiz role id", http.StatusBadRequest)
			return
		}

		var payload CodesPayload
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			http.Error(w, "Geçersiz payload", http.StatusBadRequest)
			return
		}

		tx, err := db.Begin()
		if err != nil {
			http.Error(w, "Transaction başlatılamadı", http.StatusInternalServerError)
			return
		}
		defer tx.Rollback()

		// reset
		if _, err := tx.Exec(queries.DeleteRoleDepartments, roleID); err != nil {
			http.Error(w, "Role departments silinemedi", http.StatusInternalServerError)
			return
		}

		// insert new
		for _, code := range payload.Codes {
			code = strings.TrimSpace(code)
			if code == "" {
				continue
			}
			if _, err := tx.Exec(queries.InsertRoleDepartment, roleID, code); err != nil {
				http.Error(w, "Role department eklenemedi", http.StatusInternalServerError)
				return
			}
		}

		if err := tx.Commit(); err != nil {
			http.Error(w, "Commit başarısız", http.StatusInternalServerError)
			return
		}

		_ = json.NewEncoder(w).Encode(map[string]any{"success": true})
	}
}

// ======================================================
// 🔗 ROLE → PIYASALAR UPDATE
// POST /api/roles/{id}/piyasalar
// body: { "codes": ["TR","EU"] }  (piyasa_code list)
// ======================================================

func UpdateRolePiyasalarHandler(db *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json; charset=utf-8")

		roleIDStr := mux.Vars(r)["id"]
		roleID, err := strconv.ParseInt(roleIDStr, 10, 64)
		if err != nil || roleID <= 0 {
			http.Error(w, "Geçersiz role id", http.StatusBadRequest)
			return
		}

		var payload CodesPayload
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			http.Error(w, "Geçersiz payload", http.StatusBadRequest)
			return
		}

		tx, err := db.Begin()
		if err != nil {
			http.Error(w, "Transaction başlatılamadı", http.StatusInternalServerError)
			return
		}
		defer tx.Rollback()

		// reset
		if _, err := tx.Exec(queries.DeleteRolePiyasalar, roleID); err != nil {
			http.Error(w, "Role piyasalar silinemedi", http.StatusInternalServerError)
			return
		}

		// insert new
		for _, code := range payload.Codes {
			code = strings.TrimSpace(code)
			if code == "" {
				continue
			}
			if _, err := tx.Exec(queries.InsertRolePiyasa, roleID, code); err != nil {
				http.Error(w, "Role piyasa eklenemedi", http.StatusInternalServerError)
				return
			}
		}

		if err := tx.Commit(); err != nil {
			http.Error(w, "Commit başarısız", http.StatusInternalServerError)
			return
		}

		_ = json.NewEncoder(w).Encode(map[string]any{"success": true})
	}
}

// ======================================================
// 👤 USER → ROLES UPDATE
// POST /api/users/{id}/roles
// body: { "codes": ["admin","user"] }  (dfrole.code list)
// ======================================================

func UpdateUserRolesHandler(db *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json; charset=utf-8")

		// ---------- CLAIMS ----------
		claims, ok := auth.GetClaimsFromContext(r.Context())
		if !ok || !claims.IsAdmin() {
			http.Error(w, "forbidden", http.StatusForbidden)
			return
		}

		// ---------- TARGET USER ----------
		userIDStr := mux.Vars(r)["id"]
		userID, err := strconv.ParseInt(userIDStr, 10, 64)
		if err != nil || userID <= 0 {
			http.Error(w, "Geçersiz user id", http.StatusBadRequest)
			return
		}

		// ---------- PAYLOAD ----------
		var payload CodesPayload
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			http.Error(w, "Geçersiz payload", http.StatusBadRequest)
			return
		}

		// ---------- BEFORE (ROLE LIST) ----------
		oldRoles, _ := repository.GetUserRolesByUserID(db, userID)

		// ---------- TX ----------
		tx, err := db.Begin()
		if err != nil {
			http.Error(w, "Transaction başlatılamadı", http.StatusInternalServerError)
			return
		}
		defer tx.Rollback()

		// reset
		if _, err := tx.Exec(queries.DeleteUserRoles, userID); err != nil {
			http.Error(w, "User roles silinemedi", http.StatusInternalServerError)
			return
		}

		// insert new
		for _, roleCode := range payload.Codes {
			roleCode = strings.TrimSpace(roleCode)
			if roleCode == "" {
				continue
			}
			if _, err := tx.Exec(queries.InsertUserRole, userID, roleCode); err != nil {
				http.Error(w, "User role eklenemedi", http.StatusInternalServerError)
				return
			}
		}

		if err := tx.Commit(); err != nil {
			http.Error(w, "Commit başarısız", http.StatusInternalServerError)
			return
		}

		// ---------- AFTER (ROLE LIST) ----------
		newRoles, _ := repository.GetUserRolesByUserID(db, userID)

		// ---------- AUDIT ----------
		auditlog.Enqueue(r.Context(), auditlog.ActivityLog{
			ActionType:     "role_change",
			ActionCategory: "user_admin",
			ActionTarget:   fmt.Sprintf("/api/users/%d/roles", userID),
			Description:    "user roles updated",

			Username: claims.Username,
			RoleCode: claims.RoleCode,
			DfUsrID:  int64(claims.ID),

			TargetDfUsrID: userID,

			ChangeBefore: map[string]any{
				"roles": oldRoles,
			},
			ChangeAfter: map[string]any{
				"roles": newRoles,
			},

			IsSuccess: true,
		})

		_ = json.NewEncoder(w).Encode(map[string]any{
			"success": true,
		})
	}
}
