/*
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 beyondtrustworkloadcredentialsdynamic provides a generator for BeyondTrust Workload Credentials dynamic credentials.
package beyondtrustworkloadcredentialsdynamic

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

	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/yaml"

	genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
	beyondtrustworkloadcredentialsprovider "github.com/external-secrets/external-secrets/providers/v1/beyondtrustworkloadcredentials"
	"github.com/external-secrets/external-secrets/providers/v1/beyondtrustworkloadcredentials/httpclient"
	btwcutil "github.com/external-secrets/external-secrets/providers/v1/beyondtrustworkloadcredentials/util"
)

// Generator implements BeyondtrustWorkloadCredentials dynamic generator.
type Generator struct {
	// NewBeyondtrustWorkloadCredentialsClient is a factory function to create a BeyondtrustWorkloadCredentials client.
	// If nil, defaults to httpclient.NewBeyondtrustWorkloadCredentialsClient.
	NewBeyondtrustWorkloadCredentialsClient func(server, token string) (btwcutil.Client, error)
}

const (
	errNoSpec        = "no config spec provided"
	errParseSpec     = "unable to parse spec: %w"
	errMissingConfig = "no beyondtrustworkloadcredentials provider config in spec"
	errNoPath        = "path is required in spec"
	errGetSecret     = "unable to generate dynamic secret: %w"
)

// Generate creates the dynamic credentials by calling BeyondtrustWorkloadCredentials generate endpoint.
func (g *Generator) Generate(ctx context.Context, jsonSpec *apiextensions.JSON, kube client.Client, namespace string) (map[string][]byte, genv1alpha1.GeneratorProviderState, error) {
	spec, err := getDynamicSecretSpec(jsonSpec)
	if err != nil {
		return nil, nil, err
	}
	provider := spec.Spec.Provider

	// parse and validate folder path and secret name early
	fullPath := spec.Spec.Provider.FolderPath
	folderPath, secretName := parsePath(fullPath)
	if secretName == "" {
		return nil, nil, fmt.Errorf("invalid path: missing secret name in %q", fullPath)
	}

	// create BeyondtrustWorkloadCredentials provider and initialize a client for generator controller
	clientFactory := g.NewBeyondtrustWorkloadCredentialsClient
	if clientFactory == nil {
		clientFactory = httpclient.NewBeyondtrustWorkloadCredentialsClient
	}
	prov := beyondtrustworkloadcredentialsprovider.Provider{
		NewBeyondtrustWorkloadCredentialsClient: clientFactory,
	}
	cl, err := prov.NewGeneratorClient(ctx, kube, provider, namespace)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to create BeyondtrustWorkloadCredentials client: %w", err)
	}

	// call generate
	generatedSecret, err := cl.GenerateDynamicSecret(ctx, secretName, folderPath)
	if err != nil {
		return nil, nil, fmt.Errorf(errGetSecret, err)
	}
	if generatedSecret == nil {
		return nil, nil, fmt.Errorf("generated secret is nil")
	}

	out := convertToByteMap(generatedSecret)

	// prepare provider state
	state := &struct {
		Path string `json:"path,omitempty"`
	}{Path: spec.Spec.Provider.FolderPath}

	stateJSON, _ := json.Marshal(state)
	gpState := genv1alpha1.GeneratorProviderState(&apiextensions.JSON{Raw: stateJSON})

	return out, gpState, nil
}

// Cleanup is a no-op for BeyondtrustWorkloadCredentials dynamic generator.
func (g *Generator) Cleanup(_ context.Context, _ *apiextensions.JSON, _ genv1alpha1.GeneratorProviderState, _ client.Client, _ string) error {
	return nil
}

// getDynamicSecretSpec checks if the provided spec is valid.
func getDynamicSecretSpec(jsonSpec *apiextensions.JSON) (*genv1alpha1.BeyondtrustWorkloadCredentialsDynamicSecret, error) {
	if jsonSpec == nil {
		return nil, errors.New(errNoSpec)
	}

	spec, err := parseSpec(jsonSpec.Raw)
	if err != nil {
		return nil, fmt.Errorf(errParseSpec, err)
	}
	if spec == nil || spec.Spec.Provider == nil {
		return nil, errors.New(errMissingConfig)
	}
	if spec.Spec.Provider.FolderPath == "" {
		return nil, errors.New(errNoPath)
	}

	return spec, nil
}

// parseSpec unmarshals the JSON spec into a BeyondtrustWorkloadCredentialsDynamicSecret struct.
func parseSpec(data []byte) (*genv1alpha1.BeyondtrustWorkloadCredentialsDynamicSecret, error) {
	var spec genv1alpha1.BeyondtrustWorkloadCredentialsDynamicSecret
	if err := yaml.Unmarshal(data, &spec); err != nil {
		return nil, err
	}
	return &spec, nil
}

// Parse the path to extract folder and secret name.
// Path format: "folder/subfolder/secretname" or just "secretname".
func parsePath(fullPath string) (*string, string) {
	var folderPath *string
	var secretName string

	lastSlash := strings.LastIndex(fullPath, "/")
	if lastSlash >= 0 {
		folder := fullPath[:lastSlash]
		folderPath = &folder
		secretName = fullPath[lastSlash+1:]
	} else {
		secretName = fullPath
	}
	return folderPath, secretName
}

// Convert generatedSecret to map[string][]byte.
func convertToByteMap(generatedSecret *btwcutil.GeneratedSecret) map[string][]byte {
	out := make(map[string][]byte)

	// Defensive nil check
	if generatedSecret == nil {
		return out
	}

	out["accessKeyId"] = []byte(generatedSecret.AccessKeyID)
	out["secretAccessKey"] = []byte(generatedSecret.SecretAccessKey)
	out["leaseId"] = []byte(generatedSecret.LeaseID)
	out["expiration"] = []byte(generatedSecret.Expiration)

	if generatedSecret.SessionToken != "" {
		out["sessionToken"] = []byte(generatedSecret.SessionToken)
	}

	return out
}

// NewGenerator creates a new BeyondtrustWorkloadCredentials generator instance.
func NewGenerator() genv1alpha1.Generator {
	return &Generator{}
}

// Kind returns the generator kind string.
func Kind() string {
	return string(genv1alpha1.GeneratorKindBeyondtrustWorkloadCredentialsDynamicSecret)
}
