package main

import (
	"context"
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"log"
	"net/http"
	"strings"
	"time"

	"github.com/golang-jwt/jwt/v5"
	"golang.org/x/crypto/bcrypt"
)

// IC3UserProfile is the authenticated user's full profile from ic3_user_master.
type IC3UserProfile struct {
	UserID      int        `json:"user_id"`
	Username    string     `json:"username"`
	FullName    string     `json:"full_name"`
	Email       string     `json:"email"`
	PhoneNumber string     `json:"phone_number"`
	RoleID      int        `json:"role_id"`
	RoleName    string     `json:"role_name"`
	IsActive    bool       `json:"is_active"`
	LastLoginAt *time.Time `json:"last_login_at"`
	CreatedAt   time.Time  `json:"created_at"`
}

// LoginHandler authenticates against ic3_user_master with bcrypt password verification.
// Checks is_active and locked_until before granting access.
// Issues a JWT containing username, role_name, and user_id.
func (db *DB) LoginHandler(w http.ResponseWriter, r *http.Request) {
	if db == nil {
		w.WriteHeader(http.StatusServiceUnavailable)
		json.NewEncoder(w).Encode(map[string]string{"error": "database unavailable"})
		return
	}
	if r.Method != http.MethodPost {
		w.WriteHeader(http.StatusMethodNotAllowed)
		json.NewEncoder(w).Encode(map[string]string{"error": "method not allowed"})
		return
	}

	var req LoginRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		w.WriteHeader(http.StatusBadRequest)
		json.NewEncoder(w).Encode(map[string]string{"error": "invalid request body"})
		return
	}
	req.Username = strings.TrimSpace(req.Username)
	if req.Username == "" || req.Password == "" {
		w.WriteHeader(http.StatusBadRequest)
		json.NewEncoder(w).Encode(map[string]string{"error": "username and password are required"})
		return
	}

	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()

	var u IC3UserProfile
	var passwordHash string
	var lockedUntil *time.Time

	err := db.pool.QueryRow(ctx, `
		SELECT
			u.user_id, u.username,
			COALESCE(u.full_name, ''), COALESCE(u.email, ''), COALESCE(u.phone_number, ''),
			COALESCE(u.role_id, 0), COALESCE(r.role_name, ''),
			u.password_hash, u.is_active, u.locked_until, u.last_login_at, u.created_at
		FROM ic3_user_master u
		LEFT JOIN ic3_role_master r ON r.role_id = u.role_id
		WHERE u.username = $1
	`, req.Username).Scan(
		&u.UserID, &u.Username,
		&u.FullName, &u.Email, &u.PhoneNumber,
		&u.RoleID, &u.RoleName,
		&passwordHash, &u.IsActive, &lockedUntil, &u.LastLoginAt, &u.CreatedAt,
	)
	if err != nil {
		w.WriteHeader(http.StatusUnauthorized)
		json.NewEncoder(w).Encode(map[string]string{"error": "invalid credentials"})
		return
	}

	if !u.IsActive {
		w.WriteHeader(http.StatusUnauthorized)
		json.NewEncoder(w).Encode(map[string]string{"error": "account is inactive"})
		return
	}

	if lockedUntil != nil && time.Now().Before(*lockedUntil) {
		w.WriteHeader(http.StatusUnauthorized)
		json.NewEncoder(w).Encode(map[string]string{"error": "account is temporarily locked, please try again later"})
		return
	}

	if err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(req.Password)); err != nil {
		// Increment failed attempts; lock account after 5 consecutive failures for 15 minutes
		go db.pool.Exec(context.Background(), `
			UPDATE ic3_user_master
			SET failed_login_attempts = failed_login_attempts + 1,
			    locked_until = CASE
			        WHEN failed_login_attempts + 1 >= 5
			        THEN NOW() + INTERVAL '15 minutes'
			        ELSE locked_until
			    END
			WHERE username = $1
		`, req.Username)
		w.WriteHeader(http.StatusUnauthorized)
		json.NewEncoder(w).Encode(map[string]string{"error": "invalid credentials"})
		return
	}

	// Successful login — reset failure counters and stamp last_login_at
	go db.pool.Exec(context.Background(), `
		UPDATE ic3_user_master
		SET last_login_at = NOW(), failed_login_attempts = 0, locked_until = NULL
		WHERE user_id = $1
	`, u.UserID)

	claims := Claims{
		Username: u.Username,
		Role:     u.RoleName,
		UserID:   int64(u.UserID),
		RegisteredClaims: jwt.RegisteredClaims{
			Subject:   u.Username,
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
			IssuedAt:  jwt.NewNumericDate(time.Now()),
		},
	}
	token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(jwtSecret)
	if err != nil {
		log.Printf("token signing error: %v", err)
		w.WriteHeader(http.StatusInternalServerError)
		json.NewEncoder(w).Encode(map[string]string{"error": "token error"})
		return
	}

	json.NewEncoder(w).Encode(map[string]any{
		"token":    token,
		"user":     u,
		"role":     u.RoleName,
		"username": u.Username,
		"user_id":  u.UserID,
	})
}

// GetCurrentUserHandler returns the authenticated user's profile from ic3_user_master.
// Reads the user_id from JWT claims set by authMiddleware.
func (db *DB) GetCurrentUserHandler(w http.ResponseWriter, r *http.Request) {
	claims := claimsFrom(r)
	if claims == nil {
		w.WriteHeader(http.StatusUnauthorized)
		json.NewEncoder(w).Encode(map[string]string{"error": "unauthorized"})
		return
	}

	ctx, cancel := context.WithTimeout(r.Context(), 4*time.Second)
	defer cancel()

	var u IC3UserProfile
	err := db.pool.QueryRow(ctx, `
		SELECT
			u.user_id, u.username,
			COALESCE(u.full_name, ''), COALESCE(u.email, ''), COALESCE(u.phone_number, ''),
			COALESCE(u.role_id, 0), COALESCE(r.role_name, ''),
			u.is_active, u.last_login_at, u.created_at
		FROM ic3_user_master u
		LEFT JOIN ic3_role_master r ON r.role_id = u.role_id
		WHERE u.user_id = $1
	`, claims.UserID).Scan(
		&u.UserID, &u.Username,
		&u.FullName, &u.Email, &u.PhoneNumber,
		&u.RoleID, &u.RoleName,
		&u.IsActive, &u.LastLoginAt, &u.CreatedAt,
	)
	if err != nil {
		w.WriteHeader(http.StatusNotFound)
		json.NewEncoder(w).Encode(map[string]string{"error": "user not found"})
		return
	}

	json.NewEncoder(w).Encode(APIResponse{Success: true, Data: u})
}

// LogoutHandler ends the session. JWT is stateless so client must clear the token.
func (db *DB) LogoutHandler(w http.ResponseWriter, r *http.Request) {
	json.NewEncoder(w).Encode(map[string]string{"message": "logged out successfully"})
}

// GetUsersHandler returns all users from ic3_user_master joined with their role.
func (db *DB) GetUsersHandler(w http.ResponseWriter, r *http.Request) {
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()

	rows, err := db.pool.Query(ctx, `
		SELECT
			u.user_id, u.username,
			COALESCE(u.full_name, ''), COALESCE(u.email, ''), COALESCE(u.phone_number, ''),
			COALESCE(u.role_id, 0), COALESCE(r.role_name, ''),
			u.is_active, u.last_login_at, u.created_at
		FROM ic3_user_master u
		LEFT JOIN ic3_role_master r ON r.role_id = u.role_id
		ORDER BY u.username
	`)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		json.NewEncoder(w).Encode(map[string]string{"error": "query failed"})
		return
	}
	defer rows.Close()

	var users []IC3UserProfile
	for rows.Next() {
		var u IC3UserProfile
		if err := rows.Scan(
			&u.UserID, &u.Username,
			&u.FullName, &u.Email, &u.PhoneNumber,
			&u.RoleID, &u.RoleName,
			&u.IsActive, &u.LastLoginAt, &u.CreatedAt,
		); err != nil {
			log.Printf("GetUsersHandler scan: %v", err)
			continue
		}
		users = append(users, u)
	}
	if users == nil {
		users = []IC3UserProfile{}
	}
	json.NewEncoder(w).Encode(APIResponse{Success: true, Data: users})
}

// generateToken creates a 32-byte cryptographically random token string.
func generateToken() (string, error) {
	b := make([]byte, 32)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	return base64.StdEncoding.EncodeToString(b), nil
}

// HashPassword hashes a password using bcrypt at the default cost.
func HashPassword(password string) (string, error) {
	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	return string(hash), err
}
