package auth

import (
	"context"
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"crypto/subtle"
	"encoding/base64"
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/nexuscrm/backend/internal/domain/ports"
	"github.com/nexuscrm/backend/pkg/errors"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// ComputeEffectiveScope returns the intersection of app-declared and client-requested scopes.
// Three states:
//   - App has no scopes configured (nil/empty): returns "" (legacy, full access)
//   - Client didn't request specific scopes: returns all app scopes
//   - Intersection computed: returns intersection; errors if empty on a configured app
func ComputeEffectiveScope(appScopes *string, requestedScopes string) (string, error) {
	if appScopes == nil || strings.TrimSpace(*appScopes) == "" {
		return "", nil // Legacy app, full access
	}
	if strings.TrimSpace(requestedScopes) == "" {
		return *appScopes, nil // Grant all app scopes
	}
	appSet := make(map[string]bool)
	for _, s := range strings.Fields(*appScopes) {
		appSet[s] = true
	}
	var result []string
	for _, s := range strings.Fields(requestedScopes) {
		if appSet[s] {
			result = append(result, s)
		}
	}
	if len(result) == 0 {
		return "", fmt.Errorf("requested scope is not permitted by this Connected App")
	}
	return strings.Join(result, " "), nil
}

// getInstanceURL returns the API base URL from environment or default.
func getInstanceURL() string {
	if url := os.Getenv("API_BASE_URL"); url != "" {
		return url
	}
	return "http://127.0.0.1:3001"
}

// computeTokenSignature computes HMAC-SHA256 signature per Salesforce convention.
// Signature = Base64(HMAC-SHA256(consumerSecret, tokenID + issuedAt))
func computeTokenSignature(consumerSecret, tokenID, issuedAt string) string {
	mac := hmac.New(sha256.New, []byte(consumerSecret))
	mac.Write([]byte(tokenID + issuedAt))
	return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}

// OAuthService handles OAuth 2.0 flows
type OAuthService struct {
	userRepo    ports.UserRepository
	systemRepo  ports.SystemRepository
	sessionRepo ports.SessionRepository
	authService *AuthService
	oauthRepo   ports.OAuthRepository
}

func NewOAuthService(
	userRepo ports.UserRepository,
	systemRepo ports.SystemRepository,
	sessionRepo ports.SessionRepository,
	authService *AuthService,
	oauthRepo ports.OAuthRepository,
) *OAuthService {
	return &OAuthService{
		userRepo:    userRepo,
		systemRepo:  systemRepo,
		sessionRepo: sessionRepo,
		authService: authService,
		oauthRepo:   oauthRepo,
	}
}

// GenerateAuthCode creates a temporary authorization code
func (s *OAuthService) GenerateAuthCode(ctx context.Context, clientID, redirectURI, scope, userID string) (string, error) {
	// Validate Client
	app, err := s.GetConnectedApp(ctx, clientID)
	if err != nil {
		return "", err
	}
	if app.CallbackURL == nil || *app.CallbackURL != redirectURI {
		return "", errors.NewValidationError("redirect_uri", "Mismatch with registered callback URL")
	}

	code, err := generateRandomString(32)
	if err != nil {
		return "", errors.NewInternalError("failed to generate authorization code", err)
	}
	expiresAt := time.Now().Add(10 * time.Minute)

	// Store in database (replaces in-memory storage)
	err = s.oauthRepo.StoreAuthCode(ctx, code, clientID, redirectURI, scope, userID, "", expiresAt)
	if err != nil {
		return "", err
	}

	return code, nil
}

// ExchangeToken swaps an auth code for access/refresh tokens
func (s *OAuthService) ExchangeToken(ctx context.Context, clientID, clientSecret, code, redirectURI string) (*models.SessionToken, error) {
	// Validate Client
	app, err := s.GetConnectedApp(ctx, clientID)
	if err != nil {
		return nil, err
	}
	// Timing-safe comparison to prevent timing attacks
	if subtle.ConstantTimeCompare([]byte(app.ConsumerSecret), []byte(clientSecret)) != 1 {
		return nil, errors.NewUnauthorizedError("Invalid client secret")
	}

	// Consume code from database (atomic one-time use)
	req, err := s.oauthRepo.ConsumeAuthCode(ctx, code)
	if err != nil {
		return nil, err
	}

	if req == nil {
		return nil, errors.NewValidationError("code", "Invalid or expired authorization code")
	}

	// Validate client and redirect URI match the stored values
	if req.ClientID != clientID {
		return nil, errors.NewValidationError("client_id", "Code was issued to a different client")
	}
	if req.RedirectURI != redirectURI {
		return nil, errors.NewValidationError("redirect_uri", "Redirect URI mismatch")
	}

	// Check AdminApproved access policy before issuing token
	permitted, err := s.IsUserPermittedForApp(ctx, app, req.UserID)
	if err != nil {
		return nil, errors.NewInternalError("failed to check app access policy", err)
	}
	if !permitted {
		return nil, errors.NewForbiddenError("User is not authorized to use this Connected App")
	}

	// Compute effective scope (intersection of app-declared and code-requested scopes)
	effectiveScope, err := ComputeEffectiveScope(app.Scopes, req.Scope)
	if err != nil {
		return nil, errors.NewValidationError("scope", err.Error())
	}

	// Issue Session (JWT) calling existing AuthService
	token, err := s.authService.CreateSessionForUser(ctx, req.UserID, "OAuth2", "API", &SessionOptions{
		ConnectedAppId: app.ID,
		Scope:          effectiveScope,
	})
	if err != nil {
		return nil, err
	}

	// Stamp scope on response so callers can inspect granted scopes
	token.Scope = effectiveScope

	// Populate Salesforce Compatibility Fields
	instanceURL := getInstanceURL()
	token.ID = fmt.Sprintf("%s/id/%s/%s", instanceURL, constants.DefaultOrgID, req.UserID)
	token.InstanceURL = instanceURL
	token.IssuedAt = fmt.Sprintf("%d", time.Now().UnixMilli())
	token.Signature = computeTokenSignature(app.ConsumerSecret, token.ID, token.IssuedAt)

	return token, nil
}

// GetConnectedApp retrieves app by Client ID.
func (s *OAuthService) GetConnectedApp(ctx context.Context, clientID string) (*models.SystemConnectedApp, error) {
	return s.systemRepo.GetConnectedAppByClientID(ctx, clientID)
}

// IsUserPermittedForApp checks if the user is authorized to use the app
// when PermittedUsers = 'AdminApproved'. Returns true if:
// - App.PermittedUsers != "AdminApproved" (all users allowed)
// - User's profile is assigned to the app
// - User has a permissionset that is assigned to the app
func (s *OAuthService) IsUserPermittedForApp(ctx context.Context, app *models.SystemConnectedApp, userID string) (bool, error) {
	if app.PermittedUsers == nil || *app.PermittedUsers != "AdminApproved" {
		return true, nil
	}
	return s.systemRepo.IsUserPermittedForApp(ctx, app.ID, userID)
}

func generateRandomString(n int) (string, error) {
	b := make([]byte, n)
	if _, err := rand.Read(b); err != nil {
		return "", errors.NewInternalError("failed to generate random bytes", err)
	}
	return base64.URLEncoding.EncodeToString(b), nil
}
