package api

import (
	"context"
	"database/sql"
	"encoding/json"
	"net/http"
	"strconv"
	"strings"

	"github.com/gin-gonic/gin"
	"golang.org/x/crypto/bcrypt"

	"github.com/rycroftapparel/workpulse-api/internal/httpapi"
	"github.com/rycroftapparel/workpulse-api/internal/permissions"
)

func adminUserResponse(id uint64, email, name, role string, isActive bool, createdAt interface{}, permRaw []byte, divisionAccessAll bool, visibleDivisionCodes []string) gin.H {
	return gin.H{
		"id":                   id,
		"email":                email,
		"name":                 name,
		"role":                 role,
		"isActive":             isActive,
		"createdAt":            createdAt,
		"permissions":          permissionsForResponse(permissions.Normalize(role, permRaw)),
		"divisionAccessAll":    divisionAccessAll,
		"visibleDivisionCodes": visibleDivisionCodes,
	}
}

var adminAllowedRoles = map[string]struct{}{
	"superadmin": {},
	"user":       {},
}

func adminNormalizeRole(r string) (string, bool) {
	r = strings.TrimSpace(strings.ToLower(r))
	_, ok := adminAllowedRoles[r]
	return r, ok
}

func (s *Server) adminListUsers(c *gin.Context) {
	ctx, cancel := s.ctx(c)
	defer cancel()

	limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
	if limit < 1 {
		limit = 1
	}
	if limit > 100 {
		limit = 100
	}
	offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
	if offset < 0 {
		offset = 0
	}
	q := strings.TrimSpace(c.Query("q"))

	var total int
	countSQL := `SELECT COUNT(*) FROM users`
	var rows *sql.Rows
	var err error

	if q == "" {
		err = s.DB.QueryRowContext(ctx, countSQL).Scan(&total)
		if err != nil {
			c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
			return
		}
		rows, err = s.DB.QueryContext(ctx,
			`SELECT id, email, name, role, is_active, created_at, permissions FROM users ORDER BY id ASC LIMIT $1 OFFSET $2`,
			limit, offset)
	} else {
		pat := "%" + q + "%"
		err = s.DB.QueryRowContext(ctx, countSQL+` WHERE email ILIKE $1 OR name ILIKE $1`, pat).Scan(&total)
		if err != nil {
			c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
			return
		}
		rows, err = s.DB.QueryContext(ctx,
			`SELECT id, email, name, role, is_active, created_at, permissions FROM users WHERE email ILIKE $1 OR name ILIKE $1 ORDER BY id ASC LIMIT $2 OFFSET $3`,
			pat, limit, offset)
	}
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	defer rows.Close()

	var list []gin.H
	for rows.Next() {
		var id uint64
		var email, name, role string
		var isActive bool
		var createdAt interface{}
		var permRaw []byte
		if err := rows.Scan(&id, &email, &name, &role, &isActive, &createdAt, &permRaw); err != nil {
			c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
			return
		}
		all, codes := s.adminUserDivisionVisibility(ctx, id, role)
		list = append(list, adminUserResponse(id, email, name, role, isActive, createdAt, permRaw, all, codes))
	}
	if err := rows.Err(); err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	c.JSON(http.StatusOK, httpapi.OK(gin.H{"users": list, "total": total}))
}

type adminCreateUserBody struct {
	Email                string          `json:"email" binding:"required,email"`
	Password             string          `json:"password" binding:"required,min=8"`
	Name                 string          `json:"name" binding:"required"`
	Role                 string          `json:"role" binding:"required"`
	Permissions          json.RawMessage `json:"permissions"`
	DivisionAccessAll    bool            `json:"divisionAccessAll"`
	VisibleDivisionCodes []string        `json:"visibleDivisionCodes"`
}

func (s *Server) adminCreateUser(c *gin.Context) {
	var body adminCreateUserBody
	if err := c.ShouldBindJSON(&body); err != nil {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", err.Error()))
		return
	}
	role, ok := adminNormalizeRole(body.Role)
	if !ok {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", "role must be superadmin or user"))
		return
	}
	hash, err := bcrypt.GenerateFromPassword([]byte(body.Password), bcrypt.DefaultCost)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("crypto", err.Error()))
		return
	}
	_, permBytes, err := parsePermissionsBody(role, body.Permissions)
	if err != nil {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", "permissions tidak valid"))
		return
	}
	ctx, cancel := s.ctx(c)
	defer cancel()
	var id uint64
	tx, err := s.DB.BeginTx(ctx, nil)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	defer tx.Rollback()
	err = tx.QueryRowContext(ctx,
		`INSERT INTO users (email, password_hash, name, role, is_active, permissions) VALUES ($1,$2,$3,$4,true,$5) RETURNING id`,
		strings.ToLower(strings.TrimSpace(body.Email)), string(hash), strings.TrimSpace(body.Name), role, permBytes,
	).Scan(&id)
	if err != nil {
		if strings.Contains(err.Error(), "23505") || strings.Contains(strings.ToLower(err.Error()), "duplicate") {
			c.JSON(http.StatusConflict, httpapi.Fail("duplicate", "email already exists"))
			return
		}
		if respondAuthDBErr(c, err) {
			return
		}
	}
	if err := s.replaceUserDivisionVisibility(ctx, tx, id, body.DivisionAccessAll, body.VisibleDivisionCodes); err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	if err := tx.Commit(); err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	all, codes := s.adminUserDivisionVisibility(ctx, id, role)
	adminUID := userID(c)
	s.recordUserActivity(c, ctx, adminUID, "admin.user_create", "Workspace user created", strings.TrimSpace(body.Name)+" · "+strings.ToLower(strings.TrimSpace(body.Email)), gin.H{"targetUserId": id, "role": role})
	c.JSON(http.StatusOK, httpapi.OK(adminUserResponse(id, strings.ToLower(strings.TrimSpace(body.Email)), strings.TrimSpace(body.Name), role, true, nil, permBytes, all, codes)))
}

func (s *Server) adminGetUser(c *gin.Context) {
	id, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil || id == 0 {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", "invalid user id"))
		return
	}
	ctx, cancel := s.ctx(c)
	defer cancel()
	var email, name, role string
	var isActive bool
	var createdAt interface{}
	var permRaw []byte
	err = s.DB.QueryRowContext(ctx,
		`SELECT email, name, role, is_active, created_at, permissions FROM users WHERE id = $1`, id,
	).Scan(&email, &name, &role, &isActive, &createdAt, &permRaw)
	if err == sql.ErrNoRows {
		c.JSON(http.StatusNotFound, httpapi.Fail("not_found", "user not found"))
		return
	}
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	all, codes := s.adminUserDivisionVisibility(ctx, id, role)
	c.JSON(http.StatusOK, httpapi.OK(adminUserResponse(id, email, name, role, isActive, createdAt, permRaw, all, codes)))
}

type adminPatchUserBody struct {
	Name                 *string          `json:"name"`
	Role                 *string          `json:"role"`
	IsActive             *bool            `json:"isActive"`
	Permissions          *json.RawMessage `json:"permissions"`
	DivisionAccessAll    *bool            `json:"divisionAccessAll"`
	VisibleDivisionCodes *[]string        `json:"visibleDivisionCodes"`
}

func (s *Server) adminCountActiveSuperadmins(ctx context.Context) (int, error) {
	var n int
	err := s.DB.QueryRowContext(ctx, `SELECT COUNT(*) FROM users WHERE role = 'superadmin' AND is_active = true`).Scan(&n)
	return n, err
}

func (s *Server) adminPatchUser(c *gin.Context) {
	id, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil || id == 0 {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", "invalid user id"))
		return
	}
	var body adminPatchUserBody
	if err := c.ShouldBindJSON(&body); err != nil {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", err.Error()))
		return
	}
	if body.Name == nil && body.Role == nil && body.IsActive == nil && body.Permissions == nil && body.DivisionAccessAll == nil && body.VisibleDivisionCodes == nil {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", "no fields to update"))
		return
	}

	var newRole string
	var newIsActive bool
	var curEmail, curName, curRole string
	var curIsActive bool

	ctx, cancel := s.ctx(c)
	defer cancel()

	err = s.DB.QueryRowContext(ctx, `SELECT email, name, role, is_active FROM users WHERE id = $1`, id).
		Scan(&curEmail, &curName, &curRole, &curIsActive)
	if err == sql.ErrNoRows {
		c.JSON(http.StatusNotFound, httpapi.Fail("not_found", "user not found"))
		return
	}
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}

	newRole = curRole
	newIsActive = curIsActive
	if body.Role != nil {
		r, ok := adminNormalizeRole(*body.Role)
		if !ok {
			c.JSON(http.StatusBadRequest, httpapi.Fail("validation", "role must be superadmin or user"))
			return
		}
		newRole = r
	}
	if body.IsActive != nil {
		newIsActive = *body.IsActive
	}

	wasSuper := curRole == "superadmin" && curIsActive
	losesSuper := wasSuper && (newRole != "superadmin" || !newIsActive)
	if losesSuper {
		cnt, err := s.adminCountActiveSuperadmins(ctx)
		if err != nil {
			c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
			return
		}
		if cnt <= 1 {
			c.JSON(http.StatusBadRequest, httpapi.Fail("validation", "cannot remove or deactivate the last active superadmin"))
			return
		}
	}

	var sets []string
	var args []interface{}
	n := 1
	if body.Name != nil {
		sets = append(sets, "name = $"+strconv.Itoa(n))
		args = append(args, strings.TrimSpace(*body.Name))
		n++
	}
	if body.Role != nil {
		sets = append(sets, "role = $"+strconv.Itoa(n))
		args = append(args, newRole)
		n++
		if body.Permissions == nil && newRole == "superadmin" {
			if b, err := permissions.ToJSON(permissions.AllEnabled()); err == nil {
				sets = append(sets, "permissions = $"+strconv.Itoa(n))
				args = append(args, b)
				n++
			}
		}
	}
	if body.IsActive != nil {
		sets = append(sets, "is_active = $"+strconv.Itoa(n))
		args = append(args, newIsActive)
		n++
	}
	if body.Permissions != nil {
		_, permBytes, err := parsePermissionsBody(newRole, *body.Permissions)
		if err != nil {
			c.JSON(http.StatusBadRequest, httpapi.Fail("validation", "permissions tidak valid"))
			return
		}
		sets = append(sets, "permissions = $"+strconv.Itoa(n))
		args = append(args, permBytes)
		n++
	}
	tx, err := s.DB.BeginTx(ctx, nil)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	defer tx.Rollback()
	if len(sets) > 0 {
		args = append(args, id)
		q := `UPDATE users SET ` + strings.Join(sets, ", ") + ` WHERE id = $` + strconv.Itoa(n)
		res, err := tx.ExecContext(ctx, q, args...)
		if err != nil {
			c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
			return
		}
		ra, _ := res.RowsAffected()
		if ra == 0 {
			c.JSON(http.StatusNotFound, httpapi.Fail("not_found", "user not found"))
			return
		}
	}
	if body.DivisionAccessAll != nil || body.VisibleDivisionCodes != nil {
		all := false
		if body.DivisionAccessAll != nil {
			all = *body.DivisionAccessAll
		}
		codes := []string{}
		if body.VisibleDivisionCodes != nil {
			codes = *body.VisibleDivisionCodes
		}
		if err := s.replaceUserDivisionVisibility(ctx, tx, id, all, codes); err != nil {
			c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
			return
		}
	}
	if err := tx.Commit(); err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}

	var email, name, role string
	var isActive bool
	var createdAt interface{}
	var permRaw []byte
	_ = s.DB.QueryRowContext(ctx, `SELECT email, name, role, is_active, created_at, permissions FROM users WHERE id = $1`, id).
		Scan(&email, &name, &role, &isActive, &createdAt, &permRaw)
	all, codes := s.adminUserDivisionVisibility(ctx, id, role)
	adminUID := userID(c)
	s.recordUserActivity(c, ctx, adminUID, "admin.user_update", "Workspace user updated", email+" · "+name, gin.H{"targetUserId": id})
	c.JSON(http.StatusOK, httpapi.OK(adminUserResponse(id, email, name, role, isActive, createdAt, permRaw, all, codes)))
}

type adminResetPasswordBody struct {
	NewPassword string `json:"newPassword" binding:"required,min=8"`
}

// adminPostUserPassword sets a new password for any user (superadmin only).
func (s *Server) adminPostUserPassword(c *gin.Context) {
	id, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil || id == 0 {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", "invalid user id"))
		return
	}
	var body adminResetPasswordBody
	if err := c.ShouldBindJSON(&body); err != nil {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", httpapi.BindErrorMessage(err)))
		return
	}
	newPass := strings.TrimSpace(body.NewPassword)
	if len(newPass) < 8 {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", "Password baru minimal 8 karakter."))
		return
	}

	ctx, cancel := s.ctx(c)
	defer cancel()

	var exists bool
	err = s.DB.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM users WHERE id = $1)`, id).Scan(&exists)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	if !exists {
		c.JSON(http.StatusNotFound, httpapi.Fail("not_found", "user not found"))
		return
	}

	hash, err := bcrypt.GenerateFromPassword([]byte(newPass), bcrypt.DefaultCost)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("crypto", err.Error()))
		return
	}
	if _, err := s.DB.ExecContext(ctx, `UPDATE users SET password_hash = $1 WHERE id = $2`, string(hash), id); err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	_ = s.revokeAllRefreshSessions(ctx, id)

	var email, name string
	_ = s.DB.QueryRowContext(ctx, `SELECT email, COALESCE(NULLIF(TRIM(name), ''), email) FROM users WHERE id = $1`, id).Scan(&email, &name)
	s.recordUserActivity(c, ctx, userID(c), "admin.user_password_reset", "Reset user password", name+" · "+email, gin.H{"targetUserId": id})

	c.JSON(http.StatusOK, httpapi.OK(gin.H{"ok": true}))
}
