package auth

import (
	"crypto/rand"
	"encoding/hex"
	"os"
	"strings"
	"time"

	"github.com/golang-jwt/jwt/v5"
	apperrors "github.com/nexuscrm/backend/pkg/errors"
	"github.com/nexuscrm/backend/pkg/utils"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/rs/zerolog/log"
)

// UserSession represents the user session data stored in JWT
type UserSession struct {
	ID          string  `json:"id"`
	Name        string  `json:"name"`
	Email       string  `json:"email"`
	ProfileId   string  `json:"profile_id"`             // Required: User's permissions profile
	ProfileName string  `json:"profile_name,omitempty"` // Display name of the profile
	RoleId         *string `json:"role_id,omitempty"`      // Optional: Role for hierarchy-based data sharing (Salesforce pattern)
	RoleName       *string `json:"role_name,omitempty"`    // Display name of the role
	ConnectedAppId string  `json:"connected_app_id,omitempty"` // Which Connected App created this session
	Scope          string  `json:"scope,omitempty"`            // Granted OAuth scope (empty = full access)
}

// IsSuperUser checks if the user has super user privileges
func (u UserSession) IsSuperUser() bool {
	return constants.IsSuperUser(u.ProfileId)
}

// DisplayName constructs the user's display name from available fields
// Prioritizes First+Last name, falls back to Username
func (u UserSession) DisplayName() string {
	// If First or Last name is present, construct full name
	// This assumes the source struct (SystemUser) had them populated.
	// However, UserSession stores "Name" which is usually pre-calculated
	// or we need to access First/Last from somewhere else.
	// Looking at UserSession struct in this file (lines 14-20):
	// Name string `json:"name"`
	// It doesn't store First/Last name separately in the JWT claims?
	// Wait, line 16 has `Name string`.
	// The problem described in the plan was "Duplicate Business Logic: User display name construction... is repeated in 3 separate places in auth_service.go".
	// In auth_service.go, it builds the name from `user.FirstName` and `user.LastName` (from DB model) and then puts it into `UserSession.Name`.
	// So `UserSession` only holds the result.
	// The logic duplication is converting `models.User` -> `auth.UserSession` (or just display name string).
	// So we should add a helper that takes first/last/username and returns the string.
	// Or add First/Last to UserSession?
	// The plan said: "Add func (u *UserSession) DisplayName() string".
	// But `auth_service.go` logic is building it BEFORE creating UserSession mostly?
	// Line 57 in auth_service: `displayName := user.Username ... if user.FirstName ...`
	// Then `userSession := auth.UserSession{ ... Name: displayName ... }`.
	// So the duplication is in the *generation* of that string.
	// AND in GetUserByID (line 289) it does the same.
	// AND on login/create session.
	// So we need a helper: `func GenerateDisplayName(username, first, last string) string`.
	// OR `func (u *SystemUser) DisplayName() string`? But SystemUser is in `shared/pkg/models`.
	// If I can't touch shared models easily (or don't want to), I can put a helper in this package.
	// `func ConstructDisplayName(username, firstName, lastName string) string`
	return u.Name
}

// ConstructDisplayName helper to standardize name generation
func ConstructDisplayName(username, firstName, lastName string) string {
	parts := []string{}
	if firstName != "" {
		parts = append(parts, firstName)
	}
	if lastName != "" {
		parts = append(parts, lastName)
	}
	if len(parts) > 0 {
		return strings.Join(parts, " ")
	}
	return username
}

// ResolveLookupDisplayName returns the display label for a referenced record.
// User references follow the same naming rule as auth/session display names.
func ResolveLookupDisplayName(objectAPIName string, record models.SObject) string {
	if record == nil {
		return ""
	}

	if strings.EqualFold(objectAPIName, constants.TableUser) {
		displayName := ConstructDisplayName(
			recordString(record, constants.FieldUsername),
			recordString(record, constants.FieldFirstName),
			recordString(record, constants.FieldLastName),
		)
		if displayName != "" {
			return displayName
		}
	}

	for _, fieldName := range []string{constants.FieldName, "DeveloperName", constants.FieldUsername} {
		if value := recordString(record, fieldName); value != "" {
			return value
		}
	}

	return ""
}

func recordString(record models.SObject, fieldName string) string {
	raw, ok := record[fieldName]
	if !ok || raw == nil {
		return ""
	}

	text, ok := raw.(string)
	if !ok {
		return ""
	}

	return strings.TrimSpace(text)
}

// Claims represents JWT claims
type Claims struct {
	User UserSession `json:"user"`
	jwt.RegisteredClaims
}

var jwtSecret = []byte(getJWTSecret())

func getJWTSecret() string {
	secret := os.Getenv("JWT_SECRET")
	if secret == "" {
		env := os.Getenv("NEXUS_ENV")
		if env == "production" || env == "staging" {
			log.Fatal().Msg("SECURITY: JWT_SECRET environment variable is required in production/staging. " +
				"Generate one with: openssl rand -base64 32")
		}
		// Development/test: auto-generate an ephemeral secret and warn.
		// Tokens will not survive restarts, which is acceptable for local dev.
		b := make([]byte, 32)
		if _, err := rand.Read(b); err != nil {
			log.Fatal().Err(err).Msg("Failed to generate ephemeral JWT secret")
		}
		secret = hex.EncodeToString(b)
		log.Warn().Msg("SECURITY: JWT_SECRET not set — using ephemeral random secret (tokens will not survive restarts). " +
			"Set JWT_SECRET for persistent sessions.")
	}
	return secret
}

// GenerateToken creates a JWT token for a user session
func GenerateToken(session UserSession) (string, error) {
	expirationTime := time.Now().Add(24 * time.Hour)
	jti := utils.GenerateID()

	claims := &Claims{
		User: session,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(expirationTime),
			IssuedAt:  jwt.NewNumericDate(time.Now()),
			ID:        jti,
			Subject:   session.ID,
		},
	}

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString(jwtSecret)
}

// ValidateToken validates and parses a JWT token
func ValidateToken(tokenString string) (*Claims, error) {
	token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
		// Verify signing method
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, apperrors.NewUnauthorizedError("invalid signing method")
		}
		return jwtSecret, nil
	})

	if err != nil {
		return nil, err
	}

	if claims, ok := token.Claims.(*Claims); ok && token.Valid {
		return claims, nil
	}

	return nil, apperrors.NewUnauthorizedError("invalid token")
}

// DecodeToken decodes a token without validation (for extracting JTI)
func DecodeToken(tokenString string) (*Claims, error) {
	token, _, err := new(jwt.Parser).ParseUnverified(tokenString, &Claims{})
	if err != nil {
		return nil, err
	}

	if claims, ok := token.Claims.(*Claims); ok {
		return claims, nil
	}

	return nil, apperrors.NewUnauthorizedError("invalid token claims")
}

// ToMap converts UserSession to a map for formula context
func (u UserSession) ToMap() map[string]interface{} {
	return map[string]interface{}{
		constants.FieldID:   u.ID,
		constants.FieldName: u.Name,
		"email":             u.Email,
		"profile_id":        u.ProfileId,
		// Handle nil pointer safely for optional fields
		"role_id": func() string {
			if u.RoleId != nil {
				return *u.RoleId
			}
			return ""
		}(),
	}
}
