/*
Copyright © The ESO Authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package onepassword

import (
	"strings"
	"time"

	"github.com/1Password/connect-sdk-go/connect"
	"github.com/1Password/connect-sdk-go/onepassword"
	"k8s.io/client-go/util/retry"
)

func is403AuthError(err error) bool {
	if err == nil {
		return false
	}

	// sadly, we don't have an error's body from the onepassword http call to match on, so all we do is string match.
	return strings.Contains(err.Error(), "status 403: Authorization")
}

// retryClient wraps a connect.Client with retry logic for 403 authorization errors.
type retryClient struct {
	client connect.Client
}

// newRetryClient creates a new retryClient that wraps the given connect.Client.
func newRetryClient(client connect.Client) connect.Client {
	return &retryClient{client: client}
}

// retryOn403 will retry the operation if it returns a 403 error.
// For the reason, see: https://github.com/external-secrets/external-secrets/issues/4205
func retryOn403(operation func() error) error {
	backoff := retry.DefaultBackoff
	backoff.Duration = 100 * time.Millisecond
	backoff.Steps = 3

	return retry.OnError(backoff, is403AuthError, operation)
}

// retryWithResult is a helper to wrap function calls that return a result and error.
func retryWithResult[T any](fn func() (T, error)) (T, error) {
	var result T
	err := retryOn403(func() error {
		var retryErr error
		result, retryErr = fn()
		return retryErr
	})
	return result, err
}

func (r *retryClient) GetVaults() ([]onepassword.Vault, error) {
	return retryWithResult(r.client.GetVaults)
}

func (r *retryClient) GetVault(uuid string) (*onepassword.Vault, error) {
	return retryWithResult(func() (*onepassword.Vault, error) {
		return r.client.GetVault(uuid)
	})
}

func (r *retryClient) GetVaultByUUID(uuid string) (*onepassword.Vault, error) {
	return retryWithResult(func() (*onepassword.Vault, error) {
		return r.client.GetVaultByUUID(uuid)
	})
}

func (r *retryClient) GetVaultByTitle(title string) (*onepassword.Vault, error) {
	return retryWithResult(func() (*onepassword.Vault, error) {
		return r.client.GetVaultByTitle(title)
	})
}

func (r *retryClient) GetVaultsByTitle(uuid string) ([]onepassword.Vault, error) {
	return retryWithResult(func() ([]onepassword.Vault, error) {
		return r.client.GetVaultsByTitle(uuid)
	})
}

func (r *retryClient) GetItems(vaultQuery string) ([]onepassword.Item, error) {
	return retryWithResult(func() ([]onepassword.Item, error) {
		return r.client.GetItems(vaultQuery)
	})
}

func (r *retryClient) GetItem(itemQuery, vaultQuery string) (*onepassword.Item, error) {
	return retryWithResult(func() (*onepassword.Item, error) {
		return r.client.GetItem(itemQuery, vaultQuery)
	})
}

func (r *retryClient) GetItemByUUID(uuid, vaultQuery string) (*onepassword.Item, error) {
	return retryWithResult(func() (*onepassword.Item, error) {
		return r.client.GetItemByUUID(uuid, vaultQuery)
	})
}

func (r *retryClient) GetItemByTitle(title, vaultQuery string) (*onepassword.Item, error) {
	return retryWithResult(func() (*onepassword.Item, error) {
		return r.client.GetItemByTitle(title, vaultQuery)
	})
}

func (r *retryClient) GetItemsByTitle(title, vaultQuery string) ([]onepassword.Item, error) {
	return retryWithResult(func() ([]onepassword.Item, error) {
		return r.client.GetItemsByTitle(title, vaultQuery)
	})
}

func (r *retryClient) CreateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) {
	return retryWithResult(func() (*onepassword.Item, error) {
		return r.client.CreateItem(item, vaultQuery)
	})
}

func (r *retryClient) UpdateItem(item *onepassword.Item, vaultQuery string) (*onepassword.Item, error) {
	return retryWithResult(func() (*onepassword.Item, error) {
		return r.client.UpdateItem(item, vaultQuery)
	})
}

func (r *retryClient) DeleteItem(item *onepassword.Item, vaultQuery string) error {
	return retryOn403(func() error {
		return r.client.DeleteItem(item, vaultQuery)
	})
}

func (r *retryClient) DeleteItemByID(itemUUID, vaultQuery string) error {
	return retryOn403(func() error {
		return r.client.DeleteItemByID(itemUUID, vaultQuery)
	})
}

func (r *retryClient) DeleteItemByTitle(title, vaultQuery string) error {
	return retryOn403(func() error {
		return r.client.DeleteItemByTitle(title, vaultQuery)
	})
}

func (r *retryClient) GetFiles(itemQuery, vaultQuery string) ([]onepassword.File, error) {
	return retryWithResult(func() ([]onepassword.File, error) {
		return r.client.GetFiles(itemQuery, vaultQuery)
	})
}

func (r *retryClient) GetFile(uuid, itemQuery, vaultQuery string) (*onepassword.File, error) {
	return retryWithResult(func() (*onepassword.File, error) {
		return r.client.GetFile(uuid, itemQuery, vaultQuery)
	})
}

func (r *retryClient) GetFileContent(file *onepassword.File) ([]byte, error) {
	return retryWithResult(func() ([]byte, error) {
		return r.client.GetFileContent(file)
	})
}

func (r *retryClient) DownloadFile(file *onepassword.File, targetDirectory string, overwrite bool) (string, error) {
	return retryWithResult(func() (string, error) {
		return r.client.DownloadFile(file, targetDirectory, overwrite)
	})
}

func (r *retryClient) LoadStructFromItemByUUID(config any, itemUUID, vaultQuery string) error {
	return retryOn403(func() error {
		return r.client.LoadStructFromItemByUUID(config, itemUUID, vaultQuery)
	})
}

func (r *retryClient) LoadStructFromItemByTitle(config any, itemTitle, vaultQuery string) error {
	return retryOn403(func() error {
		return r.client.LoadStructFromItemByTitle(config, itemTitle, vaultQuery)
	})
}

func (r *retryClient) LoadStructFromItem(config any, itemQuery, vaultQuery string) error {
	return retryOn403(func() error {
		return r.client.LoadStructFromItem(config, itemQuery, vaultQuery)
	})
}

func (r *retryClient) LoadStruct(config any) error {
	return retryOn403(func() error {
		return r.client.LoadStruct(config)
	})
}
