package records

import (
	"context"
	"reflect"
	"strings"

	appErrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

func (ps *PersistenceService) handlePostUpdateSessionInvalidations(txCtx context.Context, objectName, id string, oldRecord, effectiveUpdates models.SObject) error {
	if strings.EqualFold(objectName, constants.TableUser) && ps.sessionRepo != nil {
		wasActive, _ := oldRecord[constants.FieldIsActive].(bool)
		newActive, newActiveSet := effectiveUpdates[constants.FieldIsActive].(bool)
		if wasActive && newActiveSet && !newActive {
			if revokeErr := ps.sessionRepo.RevokeAllSessionsForUser(txCtx, id); revokeErr != nil {
				return appErrors.NewInternalError("failed to revoke sessions for deactivated user", revokeErr)
			}
			if ps.deactivationHook != nil {
				ps.deactivationHook.OnUserDeactivated(id)
			}
		}
	}
	if strings.EqualFold(objectName, constants.TableUser) && ps.sessionRepo != nil {
		profileChanged := false
		if newPID, ok := effectiveUpdates[constants.FieldProfileID].(string); ok {
			oldPID, _ := oldRecord[constants.FieldProfileID].(string)
			if newPID != oldPID {
				profileChanged = true
			}
		}
		roleChanged := false
		if _, ok := effectiveUpdates[constants.FieldUserRoleID]; ok {
			newRID, _ := effectiveUpdates[constants.FieldUserRoleID].(string)
			oldRID, _ := oldRecord[constants.FieldUserRoleID].(string)
			if newRID != oldRID {
				roleChanged = true
			}
		}
		if profileChanged || roleChanged {
			if revokeErr := ps.sessionRepo.RevokeAllSessionsForUser(txCtx, id); revokeErr != nil {
				return appErrors.NewInternalError("failed to revoke sessions for User Profile/Role reassignment", revokeErr)
			}
		}
	}
	if strings.EqualFold(objectName, constants.TableProfile) && ps.sessionRepo != nil {
		if newName, ok := effectiveUpdates[constants.FieldName].(string); ok {
			oldName, _ := oldRecord[constants.FieldName].(string)
			if newName != oldName {
				if _, revokeErr := ps.sessionRepo.RevokeAllSessionsByProfileID(txCtx, id); revokeErr != nil {
					return appErrors.NewInternalError("failed to revoke sessions for Profile rename", revokeErr)
				}
			}
		}
	}
	if strings.EqualFold(objectName, constants.TableUserRole) && ps.sessionRepo != nil {
		if newName, ok := effectiveUpdates[constants.FieldName].(string); ok {
			oldName, _ := oldRecord[constants.FieldName].(string)
			if newName != oldName {
				if _, revokeErr := ps.sessionRepo.RevokeAllSessionsByUserRoleID(txCtx, id); revokeErr != nil {
					return appErrors.NewInternalError("failed to revoke sessions for UserRole rename", revokeErr)
				}
			}
		}
	}
	if strings.EqualFold(objectName, constants.TableConnectedApp) {
		changed := connectedAppSessionInvalidationRequired(oldRecord, effectiveUpdates)
		if changed {
			if ps.sessionRepo != nil {
				if _, revokeErr := ps.sessionRepo.RevokeAllSessionsForConnectedApp(txCtx, id); revokeErr != nil {
					return appErrors.NewInternalError("failed to revoke sessions for ConnectedApp config change", revokeErr)
				}
			}
			// A security-policy change must also revoke the app's OAuth refresh
			// tokens, or a token issued under the old policy (e.g. before switching
			// the app to public) could mint a fresh session without the former
			// secret. Same transaction as the session revocation.
			if err := ps.revokeConnectedAppRefreshTokens(txCtx, id); err != nil {
				return err
			}
			ps.invalidateConnectedAppCache(txCtx, id)
		}
	}
	if strings.EqualFold(objectName, constants.TableConnectedAppAccess) && ps.sessionRepo != nil {
		if appID, ok := oldRecord[constants.ColSysConnectedAppAccess_ConnectedAppID].(string); ok && appID != "" {
			if _, revokeErr := ps.sessionRepo.RevokeAllSessionsForConnectedApp(txCtx, appID); revokeErr != nil {
				return appErrors.NewInternalError("failed to revoke sessions for ConnectedAppAccess change", revokeErr)
			}
			if err := ps.revokeConnectedAppRefreshTokens(txCtx, appID); err != nil {
				return err
			}
		}
	}
	return nil
}

// connectedAppRefreshTokenRevoker revokes OAuth refresh tokens for a Connected
// App atomically within the caller's transaction.
type connectedAppRefreshTokenRevoker interface {
	RevokeRefreshTokensForConnectedApp(ctx context.Context, appID string) (int, error)
}

// SetConnectedAppRefreshTokenRevoker wires the optional revoker so Connected App
// security changes/deletions also revoke the app's OAuth refresh tokens.
func (ps *PersistenceService) SetConnectedAppRefreshTokenRevoker(r connectedAppRefreshTokenRevoker) {
	ps.refreshTokenRevoker = r
}

// revokeConnectedAppRefreshTokens revokes the app's refresh tokens in the current
// transaction when a revoker is wired; errors propagate so the change fails
// closed alongside the session revocation.
func (ps *PersistenceService) revokeConnectedAppRefreshTokens(txCtx context.Context, appID string) error {
	if ps.refreshTokenRevoker == nil || strings.TrimSpace(appID) == "" {
		return nil
	}
	if _, err := ps.refreshTokenRevoker.RevokeRefreshTokensForConnectedApp(txCtx, appID); err != nil {
		return appErrors.NewInternalError("failed to revoke refresh tokens for ConnectedApp change", err)
	}
	return nil
}

var connectedAppSessionInvalidatingFields = []string{
	constants.ColSysConnectedApp_IsActive,
	constants.ColSysConnectedApp_RunAsUserID,
	constants.ColSysConnectedApp_PermittedUsers,
	constants.ColSysConnectedApp_ConsumerSecret,
	constants.ColSysConnectedApp_ConsumerSecretHash,
	constants.ColSysConnectedApp_CallbackURL,
	constants.ColSysConnectedApp_RedirectURIs,
	constants.ColSysConnectedApp_Scopes,
	constants.ColSysConnectedApp_ClientType,
	constants.ColSysConnectedApp_RequirePkce,
	constants.ColSysConnectedApp_TokenEndpointAuthMethod,
	constants.ColSysConnectedApp_AllowedGrantTypes,
	constants.ColSysConnectedApp_ConsentPolicy,
	constants.ColSysConnectedApp_ConsentVersion,
	constants.ColSysConnectedApp_IssueRefreshTokens,
	constants.ColSysConnectedApp_SessionIssuerClass,
}

// ConnectedAppSessionInvalidationRequired reports whether a policy change
// invalidates sessions issued for a Connected App. It is the single owner of
// the Connected App security-policy field set: callers with complete before
// and after record images use the exported helper, while the update pipeline
// uses the same predicate with its sparse effective-update image.
func ConnectedAppSessionInvalidationRequired(before, after map[string]any) bool {
	for _, field := range connectedAppSessionInvalidatingFields {
		if !reflect.DeepEqual(before[field], after[field]) {
			return true
		}
	}
	return false
}

func connectedAppSessionInvalidationRequired(before, effectiveUpdates models.SObject) bool {
	for _, field := range connectedAppSessionInvalidatingFields {
		if after, changed := effectiveUpdates[field]; changed && !reflect.DeepEqual(before[field], after) {
			return true
		}
	}
	return false
}
