/*
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 client implements an HTTP client for interacting with the Onboardbase API,
// providing functionality to securely retrieve and manage secrets.
package client

import (
	"bytes"
	"context"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"

	aesdecrypt "github.com/Onboardbase/go-cryptojs-aes-decrypt/decrypt"
)

const (
	// HTTPTimeoutDuration defines the default timeout for HTTP requests.
	HTTPTimeoutDuration = 20 * time.Second

	// ObbSecretsEndpointPath defines the endpoint path for secrets API.
	ObbSecretsEndpointPath = "/secrets"

	errUnableToDecrtypt = "unable to decrypt secret payload"
)

// OnboardbaseClient defines the interface for interacting with Onboardbase API.
type OnboardbaseClient struct {
	baseURL             *url.URL
	OnboardbaseAPIKey   string
	VerifyTLS           bool
	UserAgent           string
	OnboardbasePassCode string
	httpClient          *http.Client
}

type queryParams map[string]string

type headers map[string]string

// DeleteSecretsRequest represents a request to delete secrets from Onboardbase.
type DeleteSecretsRequest struct {
	SecretID string `json:"secretId,omitempty"`
}

type httpRequestBody []byte

// Secrets represents a map of secret key-value pairs.
type Secrets map[string]string

// RawSecret represents a raw secret from Onboardbase.
type RawSecret struct {
	Key   string `json:"key,omitempty"`
	Value string `json:"value,omitempty"`
}

// RawSecrets represents a collection of raw secrets.
type RawSecrets []RawSecret

// APIError represents an error response from the Onboardbase API.
type APIError struct {
	Err     error
	Message string
	Data    string
}

type apiResponse struct {
	HTTPResponse *http.Response
	Body         []byte
}

type apiErrorResponse struct {
	Messages []string
	Success  bool
}

// SecretRequest represents a request for a single secret.
type SecretRequest struct {
	Environment string
	Project     string
	Name        string
}

// SecretsRequest represents a request for multiple secrets.
type SecretsRequest struct {
	Environment string
	Project     string
}

type secretResponseBodyObject struct {
	Title string `json:"title,omitempty"`
	ID    string `json:"id,omitempty"`
}

type secretResponseSecrets struct {
	ID    string `json:"id"`
	Key   string `json:"key"`
	Value string `json:"value"`
}

type secretResponseBodyData struct {
	Project     secretResponseBodyObject `json:"project,omitempty"`
	Environment secretResponseBodyObject `json:"environment,omitempty"`
	Team        secretResponseBodyObject `json:"team,omitempty"`
	Secrets     []secretResponseSecrets  `json:"secrets,omitempty"`
	Status      string                   `json:"status"`
	Message     string                   `json:"string"`
}

type secretResponseBody struct {
	Data    secretResponseBodyData `json:"data,omitempty"`
	Message string                 `json:"message,omitempty"`
	Status  string                 `json:"status,omitempty"`
}

// SecretResponse represents a single secret response from Onboardbase.
type SecretResponse struct {
	Name  string
	Value string
}

// SecretsResponse represents a collection of secrets from Onboardbase.
type SecretsResponse struct {
	Secrets Secrets
	Body    []byte
}

// NewOnboardbaseClient creates a new client for interacting with Onboardbase API.
// It requires an API key and passcode for authentication.
func NewOnboardbaseClient(onboardbaseAPIKey, onboardbasePasscode string) (*OnboardbaseClient, error) {
	tlsConfig := &tls.Config{
		MinVersion: tls.VersionTLS12,
	}
	httpTransport := &http.Transport{
		DisableKeepAlives: true,
		TLSClientConfig:   tlsConfig,
	}
	client := &OnboardbaseClient{
		OnboardbaseAPIKey:   onboardbaseAPIKey,
		OnboardbasePassCode: onboardbasePasscode,
		VerifyTLS:           true,
		UserAgent:           "onboardbase-external-secrets",
		httpClient: &http.Client{
			Timeout:   HTTPTimeoutDuration,
			Transport: httpTransport,
		},
	}

	if err := client.SetBaseURL("https://public.onboardbase.com/api/v1/"); err != nil {
		return nil, &APIError{Err: err, Message: "setting base URL failed"}
	}

	return client, nil
}

// BaseURL returns the base URL of the Onboardbase API.
func (c *OnboardbaseClient) BaseURL() *url.URL {
	u := *c.baseURL
	return &u
}

// SetBaseURL updates the base URL for the Onboardbase API client.
func (c *OnboardbaseClient) SetBaseURL(urlStr string) error {
	baseURL, err := url.Parse(strings.TrimSuffix(urlStr, "/"))

	if err != nil {
		return err
	}
	c.baseURL = baseURL
	return nil
}

// Authenticate verifies the API credentials with Onboardbase.
func (c *OnboardbaseClient) Authenticate() error {
	_, err := c.performRequest(
		&performRequestConfig{
			path:    "/team/members",
			method:  "GET",
			headers: headers{},
			params:  queryParams{},
			body:    httpRequestBody{},
		})

	if err != nil {
		return err
	}

	return nil
}

func (c *OnboardbaseClient) getSecretsFromPayload(data secretResponseBodyData) (map[string]string, error) {
	kv := make(map[string]string)
	for _, secret := range data.Secrets {
		passphrase := c.OnboardbasePassCode
		key, err := aesdecrypt.Run(secret.Key, passphrase)
		if err != nil {
			return nil, &APIError{Err: err, Message: errUnableToDecrtypt, Data: secret.Key}
		}
		value, err := aesdecrypt.Run(secret.Value, passphrase)
		if err != nil {
			return nil, &APIError{Err: err, Message: errUnableToDecrtypt, Data: secret.Value}
		}
		kv[key] = value
	}
	return kv, nil
}

func (c *OnboardbaseClient) mapSecretsByPlainKey(data secretResponseBodyData) (map[string]secretResponseSecrets, error) {
	kv := make(map[string]secretResponseSecrets)
	for _, secret := range data.Secrets {
		passphrase := c.OnboardbasePassCode
		key, err := aesdecrypt.Run(secret.Key, passphrase)
		if err != nil {
			return nil, &APIError{Err: err, Message: errUnableToDecrtypt, Data: secret.Key}
		}
		kv[key] = secret
	}
	return kv, nil
}

// GetSecret retrieves a specific secret from Onboardbase.
func (c *OnboardbaseClient) GetSecret(request SecretRequest) (*SecretResponse, error) {
	response, err := c.performRequest(
		&performRequestConfig{
			path:    ObbSecretsEndpointPath,
			method:  "GET",
			headers: headers{},
			params:  request.buildQueryParams(),
			body:    httpRequestBody{},
		})
	if err != nil {
		return nil, err
	}

	var data secretResponseBody
	if err := json.Unmarshal(response.Body, &data); err != nil {
		return nil, &APIError{Err: err, Message: "unable to unmarshal secret payload", Data: string(response.Body)}
	}

	secrets, _ := c.getSecretsFromPayload(data.Data)
	secret := secrets[request.Name]

	if secret == "" {
		return nil, &APIError{Message: fmt.Sprintf("secret %s for project '%s' and environment '%s' not found", request.Name, request.Project, request.Environment)}
	}

	return &SecretResponse{Name: request.Name, Value: secrets[request.Name]}, nil
}

// DeleteSecret removes a secret from Onboardbase.
func (c *OnboardbaseClient) DeleteSecret(request SecretRequest) error {
	secretsrequest := SecretsRequest{
		Project:     request.Project,
		Environment: request.Environment,
	}

	secretsData, _, err := c.makeGetSecretsRequest(secretsrequest)
	if err != nil {
		return err
	}
	secrets, err := c.mapSecretsByPlainKey(secretsData.Data)
	if err != nil {
		return err
	}
	secret, ok := secrets[request.Name]
	if !ok || secret.ID == "" {
		return nil
	}

	params := request.buildQueryParams()
	deleteSecretDto := &DeleteSecretsRequest{
		SecretID: secret.ID,
	}
	body, jsonErr := json.Marshal(deleteSecretDto)
	if jsonErr != nil {
		return &APIError{Err: jsonErr, Message: "unable to unmarshal delete secrets payload"}
	}
	_, err = c.performRequest(&performRequestConfig{
		path:    ObbSecretsEndpointPath,
		method:  "DELETE",
		headers: headers{},
		params:  params,
		body:    body,
	})
	if err != nil {
		return err
	}

	return nil
}

func (c *OnboardbaseClient) makeGetSecretsRequest(request SecretsRequest) (*secretResponseBody, *apiResponse, error) {
	response, apiErr := c.performRequest(&performRequestConfig{
		path:    ObbSecretsEndpointPath,
		method:  "GET",
		headers: headers{},
		params:  request.buildQueryParams(),
		body:    httpRequestBody{},
	})
	if apiErr != nil {
		return nil, nil, apiErr
	}

	var data *secretResponseBody
	if err := json.Unmarshal(response.Body, &data); err != nil {
		return nil, nil, &APIError{Err: err, Message: "unable to unmarshal secret payload", Data: string(response.Body)}
	}
	return data, response, nil
}

// GetSecrets retrieves multiple secrets from Onboardbase.
func (c *OnboardbaseClient) GetSecrets(request SecretsRequest) (*SecretsResponse, error) {
	data, response, err := c.makeGetSecretsRequest(request)
	if err != nil {
		return nil, err
	}

	secrets, _ := c.getSecretsFromPayload(data.Data)
	return &SecretsResponse{Secrets: secrets, Body: response.Body}, nil
}

func (r *SecretsRequest) buildQueryParams() queryParams {
	params := queryParams{}

	if r.Project != "" {
		params["project"] = r.Project
	}

	if r.Environment != "" {
		params["environment"] = r.Environment
	}

	return params
}

func (r *SecretRequest) buildQueryParams() queryParams {
	params := queryParams{}

	if r.Project != "" {
		params["project"] = r.Project
	}

	if r.Environment != "" {
		params["environment"] = r.Environment
	}

	return params
}

type performRequestConfig struct {
	path    string
	method  string
	headers headers
	params  queryParams
	body    httpRequestBody
}

func (c *OnboardbaseClient) performRequest(config *performRequestConfig) (*apiResponse, error) {
	urlStr := c.BaseURL().String() + config.path
	reqURL, err := url.Parse(urlStr)
	if err != nil {
		return nil, &APIError{Err: err, Message: fmt.Sprintf("invalid API URL: %s", urlStr)}
	}

	var bodyReader io.Reader
	if config.body != nil {
		bodyReader = bytes.NewReader(config.body)
	} else {
		bodyReader = http.NoBody
	}

	// timeout this request after 20 seconds
	ctx, cancel := context.WithTimeout(context.Background(), HTTPTimeoutDuration)
	defer cancel()

	req, err := http.NewRequestWithContext(ctx, config.method, reqURL.String(), bodyReader)
	if err != nil {
		return nil, &APIError{Err: err, Message: "unable to form HTTP request"}
	}

	req.Header.Set("content-type", "application/json")
	req.Header.Set("user-agent", c.UserAgent)
	req.Header.Set("api_key", c.OnboardbaseAPIKey)

	for key, value := range config.headers {
		req.Header.Set(key, value)
	}

	query := req.URL.Query()
	for key, value := range config.params {
		query.Add(key, value)
	}
	req.URL.RawQuery = query.Encode()

	r, err := c.httpClient.Do(req)

	if err != nil {
		return nil, &APIError{Err: err, Message: "unable to load response"}
	}
	defer func() {
		_ = r.Body.Close()
	}()

	bodyResponse, err := io.ReadAll(r.Body)
	if err != nil {
		return nil, &APIError{Err: err, Message: "unable to read entire response body"}
	}

	response := &apiResponse{HTTPResponse: r, Body: bodyResponse}
	success := isSuccess(r.StatusCode)

	if !success {
		return handlePerformRequestFailure(response)
	}

	if success && err != nil {
		return nil, &APIError{Err: err, Message: "unable to load data from successful response"}
	}
	return response, nil
}

func handlePerformRequestFailure(response *apiResponse) (*apiResponse, *APIError) {
	if contentType := response.HTTPResponse.Header.Get("content-type"); strings.HasPrefix(contentType, "application/json") {
		var errResponse apiErrorResponse
		err := json.Unmarshal(response.Body, &errResponse)
		if err != nil {
			return response, &APIError{Err: err, Message: "unable to unmarshal error JSON payload"}
		}
		return response, &APIError{Err: nil, Message: strings.Join(errResponse.Messages, "\n")}
	}
	return nil, &APIError{Err: fmt.Errorf("%d status code; %d bytes", response.HTTPResponse.StatusCode, len(response.Body)), Message: "unable to load response"}
}

func isSuccess(statusCode int) bool {
	return (statusCode >= 200 && statusCode <= 299) || (statusCode >= 300 && statusCode <= 399)
}

func (e *APIError) Error() string {
	message := fmt.Sprintf("Onboardbase API Client Error: %s", e.Message)
	if underlyingError := e.Err; underlyingError != nil {
		message = fmt.Sprintf("%s\n%s", message, underlyingError.Error())
	}
	if e.Data != "" {
		message = fmt.Sprintf("%s\nData: %s", message, e.Data)
	}
	return message
}
