package service

import (
	"context"
	"errors"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"net/url"
	"path"
	"strings"
	"time"

	"golang.org/x/crypto/bcrypt"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"

	"github.com/pocket-id/pocket-id/backend/internal/common"
	"github.com/pocket-id/pocket-id/backend/internal/dto"
	"github.com/pocket-id/pocket-id/backend/internal/model"
	datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
	"github.com/pocket-id/pocket-id/backend/internal/oidc"
	"github.com/pocket-id/pocket-id/backend/internal/storage"
	"github.com/pocket-id/pocket-id/backend/internal/utils"
	imageutil "github.com/pocket-id/pocket-id/backend/internal/utils/image"
)

const (
	GrantTypeAuthorizationCode = "authorization_code"
	GrantTypeRefreshToken      = "refresh_token"
	GrantTypeDeviceCode        = "urn:ietf:params:oauth:grant-type:device_code"
	GrantTypeClientCredentials = "client_credentials"

	AccessTokenDuration  = time.Hour
	RefreshTokenDuration = 30 * 24 * time.Hour // 30 days
)

type OidcService struct {
	db               *gorm.DB
	jwtService       *JwtService
	appConfigService *AppConfigService
	previewBuilder   oidcClientPreviewBuilder
	scimService      *ScimService

	httpClient  *http.Client
	fileStorage storage.FileStorage
}

type oidcClientPreviewBuilder interface {
	BuildClientPreview(ctx context.Context, client model.OidcClient, userID string, scopes []string, authenticationMethod string) (*oidc.ClientPreview, error)
}

func NewOidcService(
	db *gorm.DB,
	jwtService *JwtService,
	appConfigService *AppConfigService,
	previewBuilder oidcClientPreviewBuilder,
	scimService *ScimService,
	httpClient *http.Client,
	fileStorage storage.FileStorage,
) (s *OidcService, err error) {
	s = &OidcService{
		db:               db,
		jwtService:       jwtService,
		appConfigService: appConfigService,
		previewBuilder:   previewBuilder,
		scimService:      scimService,
		httpClient:       httpClient,
		fileStorage:      fileStorage,
	}

	return s, nil
}

func (s *OidcService) GetClient(ctx context.Context, clientID string) (model.OidcClient, error) {
	return s.getClientInternal(ctx, clientID, s.db, false)
}

func (s *OidcService) getClientInternal(ctx context.Context, clientID string, tx *gorm.DB, forUpdate bool) (model.OidcClient, error) {
	var client model.OidcClient
	q := tx.
		WithContext(ctx).
		Preload("CreatedBy").
		Preload("AllowedUserGroups")
	if forUpdate {
		q = q.Clauses(clause.Locking{Strength: "UPDATE"})
	}
	q = q.First(&client, "id = ?", clientID)
	if q.Error != nil {
		return model.OidcClient{}, q.Error
	}
	return client, nil
}

func (s *OidcService) ListClients(ctx context.Context, name string, listRequestOptions utils.ListRequestOptions) ([]model.OidcClient, utils.PaginationResponse, error) {
	var clients []model.OidcClient

	query := s.db.
		WithContext(ctx).
		Preload("CreatedBy").
		Model(&model.OidcClient{})

	if name != "" {
		query = query.Where("name LIKE ?", "%"+name+"%")
	}

	// As allowedUserGroupsCount is not a column, we need to manually sort it
	if listRequestOptions.Sort.Column == "allowedUserGroupsCount" && utils.IsValidSortDirection(listRequestOptions.Sort.Direction) {
		query = query.Select("oidc_clients.*, COUNT(oidc_clients_allowed_user_groups.oidc_client_id)").
			Joins("LEFT JOIN oidc_clients_allowed_user_groups ON oidc_clients.id = oidc_clients_allowed_user_groups.oidc_client_id").
			Group("oidc_clients.id").
			Order("COUNT(oidc_clients_allowed_user_groups.oidc_client_id) " + listRequestOptions.Sort.Direction)

		response, err := utils.Paginate(listRequestOptions.Pagination.Page, listRequestOptions.Pagination.Limit, query, &clients)
		return clients, response, err
	}

	response, err := utils.PaginateFilterAndSort(listRequestOptions, query, &clients)
	return clients, response, err
}

func (s *OidcService) CreateClient(ctx context.Context, input dto.OidcClientCreateDto, userID string) (model.OidcClient, error) {
	client := model.OidcClient{
		Base: model.Base{
			ID: input.ID,
		},
		CreatedByID: new(userID),
	}
	updateOIDCClientModelFromDto(&client, &input.OidcClientUpdateDto)

	err := s.db.
		WithContext(ctx).
		Create(&client).
		Error
	if err != nil {
		if errors.Is(err, gorm.ErrDuplicatedKey) {
			return model.OidcClient{}, &common.ClientIdAlreadyExistsError{}
		}
		return model.OidcClient{}, err
	}

	// All storage operations must be executed outside of a transaction
	if input.LogoURL != nil {
		err = s.downloadAndSaveLogoFromURL(ctx, client.ID, *input.LogoURL, true)
		if err != nil {
			return model.OidcClient{}, fmt.Errorf("failed to download logo: %w", err)
		}
	}

	if input.DarkLogoURL != nil {
		err = s.downloadAndSaveLogoFromURL(ctx, client.ID, *input.DarkLogoURL, false)
		if err != nil {
			return model.OidcClient{}, fmt.Errorf("failed to download dark logo: %w", err)
		}
	}

	return client, nil
}

func (s *OidcService) UpdateClient(ctx context.Context, clientID string, input dto.OidcClientUpdateDto) (model.OidcClient, error) {
	tx := s.db.Begin()
	defer func() {
		tx.Rollback()
	}()

	var client model.OidcClient
	err := tx.WithContext(ctx).
		Preload("CreatedBy").
		First(&client, "id = ?", clientID).Error
	if err != nil {
		return model.OidcClient{}, err
	}

	updateOIDCClientModelFromDto(&client, &input)

	if !input.IsGroupRestricted {
		// Clear allowed user groups if the restriction is removed
		err = tx.Model(&client).Association("AllowedUserGroups").Clear()
		if err != nil {
			return model.OidcClient{}, err
		}
	}

	err = tx.WithContext(ctx).Save(&client).Error
	if err != nil {
		return model.OidcClient{}, err
	}

	err = tx.Commit().Error
	if err != nil {
		return model.OidcClient{}, err
	}

	// All storage operations must be executed outside of a transaction
	if input.LogoURL != nil {
		err = s.downloadAndSaveLogoFromURL(ctx, client.ID, *input.LogoURL, true)
		if err != nil {
			return model.OidcClient{}, fmt.Errorf("failed to download logo: %w", err)
		}
	}

	if input.DarkLogoURL != nil {
		err = s.downloadAndSaveLogoFromURL(ctx, client.ID, *input.DarkLogoURL, false)
		if err != nil {
			return model.OidcClient{}, fmt.Errorf("failed to download dark logo: %w", err)
		}
	}

	return client, nil
}

func updateOIDCClientModelFromDto(client *model.OidcClient, input *dto.OidcClientUpdateDto) {
	// Base fields
	client.Name = input.Name
	client.Description = input.Description
	client.CallbackURLs = input.CallbackURLs
	client.LogoutCallbackURLs = input.LogoutCallbackURLs
	client.IsPublic = input.IsPublic
	// PKCE is required for public clients
	client.PkceEnabled = input.IsPublic || input.PkceEnabled
	// Reset any pkce support prompt if previously flagged
	if !input.PkceEnabled {
		client.PkceSupported = false
	}
	client.RequiresReauthentication = input.RequiresReauthentication
	client.RequiresPushedAuthorizationRequests = input.RequiresPushedAuthorizationRequests
	client.SkipConsent = input.SkipConsent
	client.LaunchURL = input.LaunchURL
	client.IsGroupRestricted = input.IsGroupRestricted

	// Credentials
	client.Credentials.FederatedIdentities = make([]model.OidcClientFederatedIdentity, len(input.Credentials.FederatedIdentities))
	for i, fi := range input.Credentials.FederatedIdentities {
		client.Credentials.FederatedIdentities[i] = model.OidcClientFederatedIdentity{
			Issuer:           fi.Issuer,
			Audience:         fi.Audience,
			Subject:          fi.Subject,
			JWKS:             fi.JWKS,
			ReplayProtection: fi.ReplayProtection,
		}
	}

}

func (s *OidcService) DeleteClient(ctx context.Context, clientID string) error {
	var client model.OidcClient
	err := s.db.
		WithContext(ctx).
		Where("id = ?", clientID).
		Clauses(clause.Returning{}).
		Delete(&client).
		Error
	if err != nil {
		return err
	}

	// Delete images if present
	// Note that storage operations must be done outside of a transaction
	if client.ImageType != nil && *client.ImageType != "" {
		old := path.Join("oidc-client-images", client.ID+"."+*client.ImageType)
		_ = s.fileStorage.Delete(ctx, old)
	}
	if client.DarkImageType != nil && *client.DarkImageType != "" {
		old := path.Join("oidc-client-images", client.ID+"-dark."+*client.DarkImageType)
		_ = s.fileStorage.Delete(ctx, old)
	}

	return nil
}

func (s *OidcService) CreateClientSecret(ctx context.Context, clientID string) (string, error) {
	tx := s.db.Begin()
	defer func() {
		tx.Rollback()
	}()

	var client model.OidcClient
	err := tx.
		WithContext(ctx).
		First(&client, "id = ?", clientID).
		Error
	if err != nil {
		return "", err
	}

	clientSecret, err := utils.GenerateRandomAlphanumericString(32)
	if err != nil {
		return "", err
	}

	hashedSecret, err := bcrypt.GenerateFromPassword([]byte(clientSecret), bcrypt.DefaultCost)
	if err != nil {
		return "", err
	}

	client.Secret = string(hashedSecret)
	err = tx.
		WithContext(ctx).
		Save(&client).
		Error
	if err != nil {
		return "", err
	}

	err = tx.Commit().Error
	if err != nil {
		return "", err
	}

	return clientSecret, nil
}

func (s *OidcService) GetClientLogo(ctx context.Context, clientID string, light bool) (io.ReadCloser, int64, string, error) {
	var client model.OidcClient
	err := s.db.
		WithContext(ctx).
		First(&client, "id = ?", clientID).
		Error
	if err != nil {
		return nil, 0, "", err
	}

	var suffix string
	var ext string
	switch {
	case !light && client.DarkImageType != nil:
		// Dark logo if requested and exists
		suffix = "-dark"
		ext = *client.DarkImageType
	case client.ImageType != nil:
		// Light logo if requested or no dark logo is available
		ext = *client.ImageType
	default:
		return nil, 0, "", errors.New("image not found")
	}

	mimeType := utils.GetImageMimeType(ext)
	if mimeType == "" {
		return nil, 0, "", fmt.Errorf("unsupported image type '%s'", ext)
	}
	key := path.Join("oidc-client-images", client.ID+suffix+"."+ext)
	reader, size, err := s.fileStorage.Open(ctx, key)
	if err != nil {
		return nil, 0, "", err
	}

	return reader, size, mimeType, nil
}

func (s *OidcService) UpdateClientLogo(ctx context.Context, clientID string, file *multipart.FileHeader, light bool) error {
	fileType := strings.ToLower(utils.GetFileExtension(file.Filename))
	if mimeType := utils.GetImageMimeType(fileType); mimeType == "" {
		return &common.FileTypeNotSupportedError{}
	}

	var darkSuffix string
	if !light {
		darkSuffix = "-dark"
	}

	imagePath := path.Join("oidc-client-images", clientID+darkSuffix+"."+fileType)
	reader, err := file.Open()
	if err != nil {
		return err
	}
	defer reader.Close()
	strippedReader, err := imageutil.StripMetadata(reader, fileType)
	if err != nil {
		return err
	}

	err = s.fileStorage.Save(ctx, imagePath, strippedReader)
	if err != nil {
		return err
	}

	err = s.updateClientLogoType(ctx, clientID, fileType, light)
	if err != nil {
		return err
	}

	return nil
}

func (s *OidcService) DeleteClientLogo(ctx context.Context, clientID string) error {
	return s.deleteClientLogoInternal(ctx, clientID, "", func(client *model.OidcClient) (string, error) {
		if client.ImageType == nil {
			return "", errors.New("image not found")
		}

		oldImageType := *client.ImageType
		client.ImageType = nil
		return oldImageType, nil
	})
}

func (s *OidcService) DeleteClientDarkLogo(ctx context.Context, clientID string) error {
	return s.deleteClientLogoInternal(ctx, clientID, "-dark", func(client *model.OidcClient) (string, error) {
		if client.DarkImageType == nil {
			return "", errors.New("image not found")
		}

		oldImageType := *client.DarkImageType
		client.DarkImageType = nil
		return oldImageType, nil
	})
}

func (s *OidcService) deleteClientLogoInternal(ctx context.Context, clientID string, imagePathSuffix string, setClientImage func(*model.OidcClient) (string, error)) error {
	tx := s.db.Begin()
	defer func() {
		tx.Rollback()
	}()

	var client model.OidcClient
	err := tx.
		WithContext(ctx).
		First(&client, "id = ?", clientID).
		Error
	if err != nil {
		return err
	}

	oldImageType, err := setClientImage(&client)
	if err != nil {
		return err
	}

	err = tx.
		WithContext(ctx).
		Save(&client).
		Error
	if err != nil {
		return err
	}

	err = tx.Commit().Error
	if err != nil {
		return err
	}

	// All storage operations must be performed outside of a database transaction
	imagePath := path.Join("oidc-client-images", client.ID+imagePathSuffix+"."+oldImageType)
	err = s.fileStorage.Delete(ctx, imagePath)
	if err != nil {
		return err
	}

	return nil
}

func (s *OidcService) UpdateAllowedUserGroups(ctx context.Context, id string, input dto.OidcUpdateAllowedUserGroupsDto) (client model.OidcClient, err error) {
	tx := s.db.Begin()
	defer func() {
		tx.Rollback()
	}()

	client, err = s.getClientInternal(ctx, id, tx, true)
	if err != nil {
		return model.OidcClient{}, err
	}

	// Fetch the user groups based on UserGroupIDs in input
	var groups []model.UserGroup
	if len(input.UserGroupIDs) > 0 {
		err = tx.
			WithContext(ctx).
			Where("id IN (?)", input.UserGroupIDs).
			Find(&groups).
			Error
		if err != nil {
			return model.OidcClient{}, err
		}
	}

	// Replace the current user groups with the new set of user groups
	err = tx.
		WithContext(ctx).
		Model(&client).
		Association("AllowedUserGroups").
		Replace(groups)
	if err != nil {
		return model.OidcClient{}, err
	}

	// Save the updated client
	err = tx.
		WithContext(ctx).
		Save(&client).
		Error
	if err != nil {
		return model.OidcClient{}, err
	}

	err = tx.Commit().Error
	if err != nil {
		return model.OidcClient{}, err
	}

	s.scimService.ScheduleSync()
	return client, nil
}

func (s *OidcService) GetAllowedGroupsCountOfClient(ctx context.Context, id string) (int64, error) {
	// We only perform select queries here, so we can rollback in all cases
	tx := s.db.Begin()
	defer func() {
		tx.Rollback()
	}()

	var client model.OidcClient
	err := tx.WithContext(ctx).Where("id = ?", id).First(&client).Error
	if err != nil {
		return 0, err
	}

	count := tx.WithContext(ctx).Model(&client).Association("AllowedUserGroups").Count()
	return count, nil
}

func (s *OidcService) ListAuthorizedClients(ctx context.Context, userID string, listRequestOptions utils.ListRequestOptions) ([]model.UserAuthorizedOidcClient, utils.PaginationResponse, error) {

	query := s.db.
		WithContext(ctx).
		Model(&model.UserAuthorizedOidcClient{}).
		Preload("Client").
		Where("user_id = ?", userID)

	var authorizedClients []model.UserAuthorizedOidcClient
	response, err := utils.PaginateFilterAndSort(listRequestOptions, query, &authorizedClients)

	return authorizedClients, response, err
}

func (s *OidcService) RevokeAuthorizedClient(ctx context.Context, userID string, clientID string) error {
	tx := s.db.Begin()
	defer func() {
		tx.Rollback()
	}()

	var authorizedClient model.UserAuthorizedOidcClient
	err := tx.
		WithContext(ctx).
		Where("user_id = ? AND client_id = ?", userID, clientID).
		First(&authorizedClient).Error
	if err != nil {
		return err
	}

	err = tx.WithContext(ctx).Delete(&authorizedClient).Error
	if err != nil {
		return err
	}

	if err = oidc.RevokeUserClientSessions(ctx, tx, userID, clientID); err != nil {
		return err
	}

	err = tx.Commit().Error
	if err != nil {
		return err
	}

	return nil
}

func (s *OidcService) ListAccessibleOidcClients(ctx context.Context, userID string, listRequestOptions utils.ListRequestOptions) ([]dto.AccessibleOidcClientDto, utils.PaginationResponse, error) {
	tx := s.db.Begin()
	defer func() {
		tx.Rollback()
	}()

	var user model.User
	err := tx.
		WithContext(ctx).
		Preload("UserGroups").
		First(&user, "id = ?", userID).
		Error
	if err != nil {
		return nil, utils.PaginationResponse{}, err
	}

	userGroupIDs := make([]string, len(user.UserGroups))
	for i, group := range user.UserGroups {
		userGroupIDs[i] = group.ID
	}

	// Build the query for accessible clients
	query := tx.
		WithContext(ctx).
		Model(&model.OidcClient{}).
		Preload("UserAuthorizedOidcClients", "user_id = ?", userID)

	// If user has no groups, only return clients with no allowed user groups
	if len(userGroupIDs) == 0 {
		query = query.Where(`NOT EXISTS (
        SELECT 1 FROM oidc_clients_allowed_user_groups
        WHERE oidc_clients_allowed_user_groups.oidc_client_id = oidc_clients.id)`)
	} else {
		query = query.Where(`
        NOT EXISTS (
            SELECT 1 FROM oidc_clients_allowed_user_groups
            WHERE oidc_clients_allowed_user_groups.oidc_client_id = oidc_clients.id
        ) OR EXISTS (
            SELECT 1 FROM oidc_clients_allowed_user_groups
            WHERE oidc_clients_allowed_user_groups.oidc_client_id = oidc_clients.id
            AND oidc_clients_allowed_user_groups.user_group_id IN (?))`, userGroupIDs)
	}

	var clients []model.OidcClient

	// Handle custom sorting for lastUsedAt column
	var response utils.PaginationResponse
	if listRequestOptions.Sort.Column == "lastUsedAt" && utils.IsValidSortDirection(listRequestOptions.Sort.Direction) {
		query = query.
			Joins("LEFT JOIN user_authorized_oidc_clients ON oidc_clients.id = user_authorized_oidc_clients.client_id AND user_authorized_oidc_clients.user_id = ?", userID).
			Order("user_authorized_oidc_clients.last_used_at " + listRequestOptions.Sort.Direction + " NULLS LAST")
	}

	response, err = utils.PaginateFilterAndSort(listRequestOptions, query, &clients)
	if err != nil {
		return nil, utils.PaginationResponse{}, err
	}

	dtos := make([]dto.AccessibleOidcClientDto, len(clients))
	for i, client := range clients {
		var lastUsedAt *datatype.DateTime
		if len(client.UserAuthorizedOidcClients) > 0 {
			lastUsedAt = &client.UserAuthorizedOidcClients[0].LastUsedAt
		}
		dtos[i] = dto.AccessibleOidcClientDto{
			OidcClientMetaDataDto: dto.OidcClientMetaDataDto{
				ID:          client.ID,
				Name:        client.Name,
				Description: client.Description,
				LaunchURL:   client.LaunchURL,
				HasLogo:     client.HasLogo(),
				HasDarkLogo: client.HasDarkLogo(),
			},
			LastUsedAt: lastUsedAt,
		}
	}

	return dtos, response, err
}

func (s *OidcService) GetClientPreview(ctx context.Context, clientID string, userID string, scopes []string, authenticationMethod string) (*dto.OidcClientPreviewDto, error) {
	client, err := s.getClientInternal(ctx, clientID, s.db, false)
	if err != nil {
		return nil, err
	}

	var user model.User
	err = s.db.
		WithContext(ctx).
		Preload("UserGroups").
		First(&user, "id = ?", userID).
		Error
	if err != nil {
		return nil, err
	}

	if !oidc.IsUserGroupAllowedToAuthorize(user, client) {
		return nil, &common.OidcAccessDeniedError{}
	}

	preview, err := s.previewBuilder.BuildClientPreview(ctx, client, userID, scopes, authenticationMethod)
	if err != nil {
		return nil, err
	}
	return &dto.OidcClientPreviewDto{
		IdToken:     preview.IDToken,
		AccessToken: preview.AccessToken,
		UserInfo:    preview.UserInfo,
	}, nil
}

var errLogoTooLarge = errors.New("logo is too large")

func httpClientWithCheckRedirect(source *http.Client, checkRedirect func(req *http.Request, via []*http.Request) error) *http.Client {
	if source == nil {
		source = http.DefaultClient
	}

	// Create a new client that clones the transport
	client := &http.Client{
		Transport: source.Transport,
	}

	// Assign the CheckRedirect function
	client.CheckRedirect = checkRedirect

	return client
}

func (s *OidcService) downloadAndSaveLogoFromURL(parentCtx context.Context, clientID string, raw string, light bool) error {
	u, err := url.Parse(raw)
	if err != nil {
		return err
	}

	ctx, cancel := context.WithTimeout(parentCtx, 15*time.Second)
	defer cancel()

	// Prevents SSRF by allowing only public IPs
	ok, err := utils.IsURLPrivate(ctx, u)
	if err != nil {
		return err
	} else if ok {
		return errors.New("private IP addresses are not allowed")
	}

	// We need to check this on redirects too
	client := httpClientWithCheckRedirect(s.httpClient, func(r *http.Request, via []*http.Request) error {
		if len(via) >= 10 {
			return errors.New("stopped after 10 redirects")
		}

		ok, err := utils.IsURLPrivate(r.Context(), r.URL)
		if err != nil {
			return err
		} else if ok {
			return errors.New("private IP addresses are not allowed")
		}

		return nil
	})

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, raw, nil)
	if err != nil {
		return err
	}
	req.Header.Set("User-Agent", "pocket-id/oidc-logo-fetcher")
	req.Header.Set("Accept", "image/*")

	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("failed to fetch logo: %s", resp.Status)
	}

	const maxLogoSize int64 = 2 * 1024 * 1024 // 2MB
	if resp.ContentLength > maxLogoSize {
		return errLogoTooLarge
	}

	// Prefer extension in path if supported
	ext := utils.GetFileExtension(u.Path)
	if ext == "" || utils.GetImageMimeType(ext) == "" {
		// Otherwise, try to detect from content type
		ext = utils.GetImageExtensionFromMimeType(resp.Header.Get("Content-Type"))
	}

	if ext == "" {
		return &common.FileTypeNotSupportedError{}
	}

	var darkSuffix string
	if !light {
		darkSuffix = "-dark"
	}

	limitReader := utils.NewLimitReader(resp.Body, maxLogoSize+1)
	strippedReader, err := imageutil.StripMetadata(limitReader, ext)
	if errors.Is(err, utils.ErrSizeExceeded) {
		return errLogoTooLarge
	} else if err != nil {
		return err
	}

	imagePath := path.Join("oidc-client-images", clientID+darkSuffix+"."+ext)
	err = s.fileStorage.Save(ctx, imagePath, strippedReader)
	if errors.Is(err, utils.ErrSizeExceeded) {
		return errLogoTooLarge
	} else if err != nil {
		return err
	}

	err = s.updateClientLogoType(ctx, clientID, ext, light)
	if err != nil {
		return err
	}

	return nil
}

func (s *OidcService) updateClientLogoType(ctx context.Context, clientID string, ext string, light bool) error {
	var darkSuffix string
	if !light {
		darkSuffix = "-dark"
	}

	tx := s.db.Begin()
	defer func() {
		tx.Rollback()
	}()

	// We need to acquire an update lock for the row to be locked, since we'll update it later
	var client model.OidcClient
	err := tx.
		WithContext(ctx).
		Clauses(clause.Locking{Strength: "UPDATE"}).
		First(&client, "id = ?", clientID).
		Error
	if err != nil {
		return fmt.Errorf("failed to look up client: %w", err)
	}

	var currentType *string
	if light {
		currentType = client.ImageType
		client.ImageType = &ext
	} else {
		currentType = client.DarkImageType
		client.DarkImageType = &ext
	}

	err = tx.
		WithContext(ctx).
		Save(&client).
		Error
	if err != nil {
		return fmt.Errorf("failed to save updated client: %w", err)
	}

	err = tx.Commit().Error
	if err != nil {
		return fmt.Errorf("failed to commit transaction: %w", err)
	}

	// Storage operations must be executed outside of a transaction
	if currentType != nil && *currentType != ext {
		old := path.Join("oidc-client-images", client.ID+darkSuffix+"."+*currentType)
		_ = s.fileStorage.Delete(ctx, old)
	}

	return nil
}

func (s *OidcService) GetClientScimServiceProvider(ctx context.Context, clientID string) (model.ScimServiceProvider, error) {
	var provider model.ScimServiceProvider
	err := s.db.
		WithContext(ctx).
		First(&provider, "oidc_client_id = ?", clientID).
		Error
	if err != nil {
		return model.ScimServiceProvider{}, err
	}

	return provider, nil
}
