/*
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 httpclient provides an HTTP client for interacting with BeyondTrust Workload Credentials API.
// API Documentation: https://docs.beyondtrust.com/bt-docs/docs/secrets-api
package httpclient

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

	btwcutil "github.com/external-secrets/external-secrets/providers/v1/beyondtrustworkloadcredentials/util"
)

const (
	// API documentation URL.
	apiDocsURL = "https://docs.beyondtrust.com/bt-docs/docs/secrets-api"

	// API version header for BeyondTrust Workload Credentials.
	apiVersionHeader = "bt-secrets-api-version"
	apiVersion       = "2026-04-28"

	// Default timeout for HTTP requests.
	defaultTimeout = 30 * time.Second

	// Maximum response body size to prevent unbounded memory allocation.
	maxResponseBytes = 10 << 20 // 10 MiB
)

// Client represents a client for interacting with BeyondTrust Workload Credentials API.
type Client struct {
	httpClient *http.Client
	baseURL    *url.URL
	token      string
}

// NewClient creates a new BeyondTrust Workload Credentials HTTP client.
func NewClient(serverURL, token string) (*Client, error) {
	if err := validateServerURL(serverURL); err != nil {
		return nil, err
	}

	parsedURL, err := url.Parse(strings.TrimSuffix(serverURL, "/"))
	if err != nil {
		return nil, fmt.Errorf("failed to parse server URL %q: %w", serverURL, err)
	}

	return &Client{
		httpClient: &http.Client{
			Timeout: defaultTimeout,
		},
		baseURL: parsedURL,
		token:   token,
	}, nil
}

// NewClientWithCustomCA creates a client using the provided PEM-encoded CA bundle.
func NewClientWithCustomCA(serverURL, token string, caBundlePEM []byte) (*Client, error) {
	if err := validateServerURL(serverURL); err != nil {
		return nil, err
	}

	parsedURL, err := url.Parse(strings.TrimSuffix(serverURL, "/"))
	if err != nil {
		return nil, fmt.Errorf("failed to parse server URL %q: %w", serverURL, err)
	}

	httpClient := &http.Client{
		Timeout: defaultTimeout,
	}

	if len(caBundlePEM) > 0 {
		roots := x509.NewCertPool()
		if !roots.AppendCertsFromPEM(caBundlePEM) {
			return nil, fmt.Errorf("failed to parse CA bundle PEM")
		}
		// Clone the default transport to preserve default settings like ProxyFromEnvironment
		transport := http.DefaultTransport.(*http.Transport).Clone()
		transport.TLSClientConfig = &tls.Config{
			RootCAs:    roots,
			MinVersion: tls.VersionTLS12,
		}
		httpClient.Transport = transport
	}

	return &Client{
		httpClient: httpClient,
		baseURL:    parsedURL,
		token:      token,
	}, nil
}

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

// SetBaseURL sets the base URL for the API.
func (c *Client) SetBaseURL(urlStr string) error {
	if err := validateServerURL(urlStr); err != nil {
		return err
	}

	baseURL, err := url.Parse(strings.TrimSuffix(urlStr, "/"))
	if err != nil {
		return fmt.Errorf("failed to parse base URL %q: %w", urlStr, err)
	}

	c.baseURL = baseURL
	return nil
}

// CheckSession verifies if the current authentication session is valid.
func (c *Client) CheckSession(ctx context.Context) error {
	endpoint := fmt.Sprintf("%s/session", c.baseURL.String())

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, http.NoBody)
	if err != nil {
		return fmt.Errorf("failed to create session check request: %w", err)
	}

	c.setHeaders(req)

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("failed to check session: %w", err)
	}
	defer func() { _ = resp.Body.Close() }()

	body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBytes))
	if err != nil {
		return fmt.Errorf("failed to read session check response: %w", err)
	}

	if resp.StatusCode == http.StatusOK {
		// HTTP 200 indicates a valid session
		return nil
	}

	return parseError(body, resp.StatusCode, "/session")
}

// GetSecret fetches a single secret by name from the specified folder path.
func (c *Client) GetSecret(ctx context.Context, name string, folderPath *string) (*btwcutil.KV, error) {
	path := formatPath(folderPath)

	endpoint := fmt.Sprintf("%s/static/%s", c.baseURL.String(), url.PathEscape(name))

	// The single-secret endpoint uses "folder" while the list endpoint uses "path". These are intentionally different parameter names.
	if folderPath != nil && *folderPath != "" {
		endpoint += fmt.Sprintf("?folder=%s", url.QueryEscape(*folderPath))
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, http.NoBody)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	c.setHeaders(req)

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to fetch secret %q at %q: %w", name, path, err)
	}
	defer func() { _ = resp.Body.Close() }()

	body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBytes))
	if err != nil {
		return nil, fmt.Errorf("failed to read response body: %w", err)
	}

	if resp.StatusCode == http.StatusOK {
		var kv btwcutil.KV
		if err := json.Unmarshal(body, &kv); err != nil {
			return nil, fmt.Errorf("failed to unmarshal secret response: %w", err)
		}
		return &kv, nil
	}

	return nil, parseError(body, resp.StatusCode, fmt.Sprintf("%s/%s", path, name))
}

// GetSecrets fetches a list of secrets at the specified folder path.
func (c *Client) GetSecrets(ctx context.Context, folderPath *string, recursive bool) ([]btwcutil.KVListItem, error) {
	path := formatPath(folderPath)

	endpoint := fmt.Sprintf("%s/static", c.baseURL.String())

	// The list endpoint uses "path" (GET /static?path=...) per the API spec,
	// while the single-secret endpoint uses "folder". These are intentionally different parameter names.
	params := url.Values{}
	if folderPath != nil && *folderPath != "" {
		params.Set("path", *folderPath)
	}
	if recursive {
		params.Set("recursive", "true")
	}

	// Add query string if there are parameters
	if len(params) > 0 {
		endpoint += "?" + params.Encode()
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, http.NoBody)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	c.setHeaders(req)

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to list secrets: %w", err)
	}
	defer func() { _ = resp.Body.Close() }()

	body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBytes))
	if err != nil {
		return nil, fmt.Errorf("failed to read response body: %w", err)
	}

	if resp.StatusCode == http.StatusOK {
		var listResp struct {
			Data  []btwcutil.KVListItem `json:"data"`
			Error string                `json:"error,omitempty"`
		}
		if err := json.Unmarshal(body, &listResp); err != nil {
			return nil, fmt.Errorf("failed to unmarshal list response: %w", err)
		}

		// Check for API error in response body even with 200 status
		if listResp.Error != "" {
			return nil, fmt.Errorf("beyondtrust API error: %s", listResp.Error)
		}

		// Empty folder is valid - return empty list
		if len(listResp.Data) == 0 {
			return []btwcutil.KVListItem{}, nil
		}

		return listResp.Data, nil
	}

	return nil, parseError(body, resp.StatusCode, path)
}

// GenerateDynamicSecret calls the dynamic secret generation endpoint.
func (c *Client) GenerateDynamicSecret(ctx context.Context, secretName string, folderPath *string) (*btwcutil.GeneratedSecret, error) {
	path := formatPath(folderPath)

	endpoint := fmt.Sprintf("%s/dynamic/%s/generate", c.baseURL.String(), url.PathEscape(secretName))

	// Add folder query parameter if specified
	if folderPath != nil && *folderPath != "" {
		endpoint += fmt.Sprintf("?folder=%s", url.QueryEscape(*folderPath))
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, http.NoBody)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	c.setHeaders(req)

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to generate dynamic secret: %w", err)
	}
	defer func() { _ = resp.Body.Close() }()

	body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBytes))
	if err != nil {
		return nil, fmt.Errorf("failed to read response body: %w", err)
	}

	if resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusOK {
		var wrapped struct {
			Secret btwcutil.GeneratedSecret `json:"secret"`
		}
		if err := json.Unmarshal(body, &wrapped); err != nil {
			return nil, fmt.Errorf("failed to unmarshal generated secret response: %w", err)
		}
		return &wrapped.Secret, nil
	}

	return nil, parseError(body, resp.StatusCode, path)
}

// setHeaders adds required headers to the HTTP request.
func (c *Client) setHeaders(req *http.Request) {
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token))
	req.Header.Set(apiVersionHeader, apiVersion)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Accept", "application/json")
}

// validateServerURL checks if the provided server URL is valid.
func validateServerURL(server string) error {
	server = strings.TrimSpace(server)
	if server == "" {
		return fmt.Errorf("server URL is required")
	}

	parsedURL, err := url.ParseRequestURI(server)
	if err != nil {
		return fmt.Errorf("invalid server URL %q: %w", server, err)
	}

	// Validate that the URL has both scheme and host
	if parsedURL.Scheme == "" {
		return fmt.Errorf("server URL %q is missing scheme (e.g., https://)", server)
	}
	if parsedURL.Host == "" {
		return fmt.Errorf("server URL %q is missing host", server)
	}

	return nil
}

// formatPath returns the string value of the given path pointer.
func formatPath(pathPtr *string) string {
	if pathPtr == nil || *pathPtr == "" {
		return "/"
	}
	return *pathPtr
}

// parseError attempts to parse an error response from the API.
func parseError(body []byte, statusCode int, path string) error {
	var errResp errorResponse

	// Try to parse structured error response
	if err := json.Unmarshal(body, &errResp); err == nil {
		var msg strings.Builder

		if errResp.Error != "" {
			msg.WriteString(errResp.Error)
		}

		if errResp.Message != "" {
			if msg.Len() > 0 {
				msg.WriteString(": ")
			}
			msg.WriteString(errResp.Message)
		}

		// Include details if present
		if len(errResp.Details) > 0 {
			detailsJSON, _ := json.Marshal(errResp.Details)
			if msg.Len() > 0 {
				msg.WriteString(" ")
			}
			msg.WriteString(fmt.Sprintf("(details: %s)", string(detailsJSON)))
		}

		if msg.Len() > 0 {
			return &APIError{
				StatusCode: statusCode,
				Message:    fmt.Sprintf("API error (HTTP %d): %s at path %q (see %s)", statusCode, msg.String(), path, apiDocsURL),
				Path:       path,
			}
		}
	}

	// Fallback error
	return &APIError{
		StatusCode: statusCode,
		Message:    fmt.Sprintf("API error (HTTP %d): unexpected response at path %q (see %s)", statusCode, path, apiDocsURL),
		Path:       path,
	}
}

// Ensure Client implements btwcutil.Client interface.
var _ btwcutil.Client = (*Client)(nil)

// NewBeyondtrustWorkloadCredentialsClient is a wrapper for backward compatibility.
func NewBeyondtrustWorkloadCredentialsClient(server, token string) (btwcutil.Client, error) {
	return NewClient(server, token)
}

// NewBeyondtrustWorkloadCredentialsClientWithCustomCA is a wrapper for backward compatibility.
func NewBeyondtrustWorkloadCredentialsClientWithCustomCA(server, token string, caBundlePEM []byte) (btwcutil.Client, error) {
	return NewClientWithCustomCA(server, token, caBundlePEM)
}
