package services

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"strings"

	"github.com/nexuscrm/auth"
	pkgerrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/records"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"golang.org/x/crypto/bcrypt"
)

// SystemManager handles system-level operations
type SystemManager struct {
	persistence  *records.PersistenceService
	repo         ports.SystemRepository
	setupAuditor *auth.SetupAuditor
}

type dynamicOAuthClientRegistrationRecorder interface {
	RecordAdminDynamicOAuthClient(context.Context, string, string, string) error
}

// NewSystemManager creates a new SystemManager
func NewSystemManager(persistence *records.PersistenceService, repo ports.SystemRepository) *SystemManager {
	return &SystemManager{
		persistence: persistence,
		repo:        repo,
	}
}

func (sm *SystemManager) SetSetupAuditor(auditor *auth.SetupAuditor) {
	if sm != nil {
		sm.setupAuditor = auditor
	}
}

// GetConfig retrieves a system configuration value
func (sm *SystemManager) GetConfig(ctx context.Context, key string) (*string, error) {
	return sm.repo.GetConfig(ctx, key)
}

// SetConfig sets a system configuration value
func (sm *SystemManager) SetConfig(ctx context.Context, key string, value string, isSecret bool, description string) error {
	config := models.SystemConfig{
		KeyName:     key,
		Value:       value,
		IsSecret:    isSecret,
		Description: &description,
	}

	if err := sm.repo.UpsertConfig(ctx, &config); err != nil {
		return pkgerrors.NewInternalError(fmt.Sprintf("failed to upsert config %s", key), err)
	}
	return nil
}

// DeleteConfig deletes a system configuration if it exists
func (sm *SystemManager) DeleteConfig(ctx context.Context, key string) error {
	if err := sm.repo.DeleteConfig(ctx, key); err != nil {
		return pkgerrors.NewInternalError(fmt.Sprintf("failed to delete config %s", key), err)
	}
	return nil
}

// UpsertProfile creates or updates a system profile
func (sm *SystemManager) UpsertProfile(ctx context.Context, id, name, description string, isSystem bool) error {
	ctx = constants.WithSecurityContext(ctx, constants.SystemContext, "SystemManager.UpsertProfile")

	profile := models.Profile{
		ID:          id,
		Name:        name,
		Description: &description,
		IsSystem:    isSystem,
		// SF-faithful license; must be set explicitly because ToSObject marshals
		// the zero-value "" and would overwrite the DB default 'Standard' on the
		// UPDATE branch below. All bootstrap profiles are Standard-license in SF.
		UserType: "Standard",
	}

	// Check existence by Name (Unique Key) or ID
	existingID, err := sm.repo.GetProfileIDByName(ctx, name)
	if err != nil {
		return pkgerrors.NewInternalError("failed to check profile existence", err)
	}

	if existingID != "" {
		// Update
		profile.ID = existingID
		if err := sm.persistence.Update(ctx, constants.TableProfile, existingID, profile.ToSObject(), nil); err != nil {
			return pkgerrors.NewInternalError(fmt.Sprintf("failed to update profile %s", name), err)
		}
	} else {
		// Insert
		if _, err := sm.persistence.Insert(ctx, constants.TableProfile, profile.ToSObject(), nil); err != nil {
			return pkgerrors.NewInternalError(fmt.Sprintf("failed to insert profile %s", name), err)
		}
	}
	return nil
}

// UpsertUser creates or updates a system user
func (sm *SystemManager) UpsertUser(ctx context.Context, id, name, email, password, profileID string) error {
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		return pkgerrors.NewInternalError(fmt.Sprintf("failed to hash password for user %s", email), err)
	}

	var firstName, lastName string
	// Simple Name Split - use full name as first name if single word
	parts := strings.SplitN(name, " ", 2)
	firstName = parts[0]
	if len(parts) > 1 {
		lastName = parts[1]
	}

	username := email // Default username to email

	ctx = constants.WithSecurityContext(ctx, constants.SystemContext, "SystemManager.UpsertUser")

	// Prepare data struct
	user := models.User{
		ID:        id,
		Username:  username,
		Email:     email,
		Password:  string(hashedPassword),
		FirstName: firstName,
		LastName:  lastName,
		ProfileID: profileID,
		IsActive:  true,
		// CreatedDate handles by Persistence default or set here?
		// Persistence handles created_date if missing? It populates system fields.
	}

	// Check existence by Email (Unique Key)
	existingID, err := sm.repo.GetUserIDByEmail(ctx, email)
	if err != nil {
		return pkgerrors.NewInternalError("failed to check user existence", err)
	}

	if existingID != "" {
		// Update
		user.ID = existingID // Force ID to match existing
		if err := sm.persistence.Update(ctx, constants.TableUser, existingID, user.ToSObject(), nil); err != nil {
			return pkgerrors.NewInternalError(fmt.Sprintf("failed to update user %s", email), err)
		}
	} else {
		// Insert
		if _, err := sm.persistence.Insert(ctx, constants.TableUser, user.ToSObject(), nil); err != nil {
			return pkgerrors.NewInternalError(fmt.Sprintf("failed to insert user %s", email), err)
		}
	}
	return nil
}

// ForceSetUserPassword hashes and force-updates a user's password.
// Used after batch upsert to ensure explicitly configured passwords take effect.
func (sm *SystemManager) ForceSetUserPassword(ctx context.Context, email, password string) error {
	h, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		return pkgerrors.NewInternalError("failed to hash password for force update", err)
	}
	return sm.repo.ForceUpdateUserPassword(ctx, email, string(h))
}

// BatchUpsertProfiles inserts multiple profiles using direct SQL for performance during bootstrap
func (sm *SystemManager) BatchUpsertProfiles(ctx context.Context, profiles []models.Profile) error {
	return sm.repo.BatchUpsertProfiles(ctx, profiles)
}

// BatchUpsertUsers inserts multiple users using direct SQL for performance during bootstrap
// Uses a dynamic cache for password hashing to avoid redundant BCrypt calls for identical passwords
func (sm *SystemManager) BatchUpsertUsers(ctx context.Context, users []models.User) error {
	if len(users) == 0 {
		return nil
	}

	hashCache := make(map[string]string)

	preparedUsers := make([]models.User, len(users))

	for i, u := range users {
		hashedPwd, ok := hashCache[u.Password]
		if !ok {
			h, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
			if err != nil {
				return pkgerrors.NewInternalError("failed to hash password", err)
			}
			hashedPwd = string(h)
			hashCache[u.Password] = hashedPwd
		}

		// Calculate Names
		// Caller (ProvisionBootstrapIdentity) is responsible for splitting Name into FirstName/LastName
		// stored in u.FirstName and u.LastName

		// Create a copy with hashed password
		preparedUser := u
		preparedUser.Password = hashedPwd
		preparedUsers[i] = preparedUser
	}

	return sm.repo.BatchUpsertUsers(ctx, preparedUsers)
}
func (sm *SystemManager) UpsertConnectedApp(ctx context.Context, id, name, consumerKey, consumerSecret string, sessionIssuerClass ...string) error {
	ctx = constants.WithSecurityContext(ctx, constants.SystemContext, "SystemManager.UpsertConnectedApp")

	app := models.SObject{
		constants.FieldID:             id,
		constants.FieldName:           name,
		constants.FieldConsumerKey:    consumerKey,
		constants.FieldConsumerSecret: consumerSecret,
		constants.FieldIsActive:       true,
		constants.FieldCallbackURL:    bootstrapConnectedAppCallbackURL(),
	}
	if len(sessionIssuerClass) > 0 && strings.TrimSpace(sessionIssuerClass[0]) != "" {
		app[constants.ColSysConnectedApp_SessionIssuerClass] = strings.TrimSpace(sessionIssuerClass[0])
	}

	// Upsert: try Update first, then Insert if not found
	err := sm.persistence.Update(ctx, constants.TableConnectedApp, id, app, nil)
	if err == nil {
		return nil
	}

	// If update failed (likely not found), Insert
	_, err = sm.persistence.Insert(ctx, constants.TableConnectedApp, app, nil)
	return err
}

// RegisterAdminDynamicOAuthClient persists an administrator-approved RFC 7591
// public client through the ordinary Connected App write path. The HTTP route
// performs the administrator admission; this method uses SystemContext only to
// materialize its normalized, security-constrained record. It deliberately
// writes admin_preapproved rather than trusted: a dynamically shaped client is
// never granted user authority merely because it registered itself.
func (sm *SystemManager) RegisterAdminDynamicOAuthClient(ctx context.Context, actorID string, registration *auth.DynamicClientRegistration) error {
	if registration == nil || strings.TrimSpace(registration.ClientID) == "" || strings.TrimSpace(registration.Scope) == "" || len(registration.RedirectURIs) == 0 || strings.TrimSpace(actorID) == "" {
		return pkgerrors.NewValidationError("registration", "validated client id, redirect uris, and scope are required")
	}
	if sm.setupAuditor == nil {
		return pkgerrors.NewInternalError("setup audit is required for dynamic client registration", nil)
	}
	redirectURIs, err := json.Marshal(registration.RedirectURIs)
	if err != nil {
		return pkgerrors.NewInternalError("failed to encode redirect uris", err)
	}
	name := registration.ClientID
	if strings.TrimSpace(registration.ClientName) != "" {
		name = fmt.Sprintf("%s (%s)", strings.TrimSpace(registration.ClientName), registration.ClientID)
	}
	ctx = constants.WithSecurityContext(ctx, constants.SystemContext, "SystemManager.RegisterAdminDynamicOAuthClient")
	app := models.SObject{
		constants.FieldName:                                  name,
		constants.ColSysConnectedApp_ConsumerKey:             registration.ClientID,
		constants.ColSysConnectedApp_ClientType:              "public",
		constants.ColSysConnectedApp_TokenEndpointAuthMethod: "none",
		constants.ColSysConnectedApp_RequirePkce:             true,
		constants.ColSysConnectedApp_IssueRefreshTokens:      issuesRefreshTokens(registration.GrantTypes),
		constants.ColSysConnectedApp_AllowedGrantTypes:       strings.Join(registration.GrantTypes, " "),
		constants.ColSysConnectedApp_ConsentPolicy:           "admin_preapproved",
		constants.ColSysConnectedApp_RedirectURIs:            string(redirectURIs),
		constants.ColSysConnectedApp_Scopes:                  registration.Scope,
		constants.ColSysConnectedApp_PermittedUsers:          "AllUsers",
		constants.ColSysConnectedApp_IsActive:                true,
	}
	return sm.persistence.RunInTransaction(ctx, func(txCtx context.Context) error {
		sourcePackName := strings.TrimSpace(registration.SourcePackName)
		insertCtx := txCtx
		if sourcePackName != "" {
			// The generic Connected App write validator must see the already
			// authorized scope owner before it validates source-backed scopes.
			// The L2 registration binding is written below in this transaction.
			insertCtx = records.WithConnectedAppScopeOwnerPack(insertCtx, sourcePackName)
		}
		created, insertErr := sm.persistence.Insert(insertCtx, constants.TableConnectedApp, app, nil)
		if insertErr != nil {
			return insertErr
		}
		{
			registrar, ok := sm.repo.(dynamicOAuthClientRegistrationRecorder)
			if !ok {
				return pkgerrors.NewInternalError("dynamic OAuth client registration storage is not configured", nil)
			}
			appID, _ := created[constants.FieldID].(string)
			if strings.TrimSpace(appID) == "" {
				return pkgerrors.NewInternalError("created connected app is missing an id", nil)
			}
			if err := registrar.RecordAdminDynamicOAuthClient(txCtx, appID, sourcePackName, actorID); err != nil {
				return pkgerrors.NewInternalError("failed to record dynamic OAuth client registration", err)
			}
			// The L2 binding participates in the same scope-policy read path as
			// source declarations. Invalidate only after this transaction commits.
			sm.persistence.InvalidateOAuthScopePolicy(txCtx)
		}
		sm.setupAuditor.Record(txCtx, "Register OAuth client", "Connected App", "Registered OAuth client "+registration.ClientID, strings.TrimSpace(actorID))
		return nil
	})
}

func issuesRefreshTokens(grantTypes []string) bool {
	for _, grantType := range grantTypes {
		if grantType == auth.OAuthGrantRefreshToken {
			return true
		}
	}
	return false
}

func bootstrapConnectedAppCallbackURL() string {
	baseURL := strings.TrimRight(strings.TrimSpace(os.Getenv("API_BASE_URL")), "/")
	if baseURL == "" {
		return "http://127.0.0.1:8080/callback"
	}
	return baseURL + "/callback"
}
