// SPDX-License-Identifier: GPL-3.0-or-later

// Package client provides an HTTP client for the backend API.
package client

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"
	"time"
)

// Client is an HTTP client for the backend API.
type Client struct {
	baseURL    string
	httpClient *http.Client
}

// Options contains client configuration options.
type Options struct {
	// BaseURL is the backend API URL
	BaseURL string
	// Timeout is the request timeout
	Timeout time.Duration
}

// DefaultOptions returns the default client options.
func DefaultOptions() *Options {
	return &Options{
		BaseURL: "http://localhost:62774",
		Timeout: 3 * time.Minute, // Increased for Tor latency
	}
}

// New creates a new API client.
func New(opts *Options) *Client {
	if opts == nil {
		opts = DefaultOptions()
	}

	return &Client{
		baseURL: opts.BaseURL,
		httpClient: &http.Client{
			Timeout: opts.Timeout,
		},
	}
}

// NewWithURL creates a new API client with the given base URL.
// This is a convenience function for testing.
func NewWithURL(baseURL string) *Client {
	return New(&Options{
		BaseURL: baseURL,
		Timeout: 3 * time.Minute, // Increased for Tor latency
	})
}

// BaseURL returns the client's base URL.
func (c *Client) BaseURL() string {
	return c.baseURL
}

// Get performs a GET request.
func (c *Client) Get(ctx context.Context, path string) (*http.Response, error) {
	return c.do(ctx, http.MethodGet, path, nil)
}

// Post performs a POST request.
func (c *Client) Post(ctx context.Context, path string, body io.Reader) (*http.Response, error) {
	return c.do(ctx, http.MethodPost, path, body)
}

// Put performs a PUT request.
func (c *Client) Put(ctx context.Context, path string, body io.Reader) (*http.Response, error) {
	return c.do(ctx, http.MethodPut, path, body)
}

// Delete performs a DELETE request.
func (c *Client) Delete(ctx context.Context, path string) (*http.Response, error) {
	return c.do(ctx, http.MethodDelete, path, nil)
}

// do performs an HTTP request.
func (c *Client) do(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) {
	url := c.baseURL + path

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

	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	req.Header.Set("Accept", "application/json")

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("request failed: %w", err)
	}

	return resp, nil
}

// GetJSON performs a GET request and decodes JSON response.
func (c *Client) GetJSON(ctx context.Context, path string, result interface{}) error {
	resp, err := c.Get(ctx, path)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return c.parseError(resp)
	}

	if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
		return fmt.Errorf("failed to decode response: %w", err)
	}

	return nil
}

// PostJSON performs a POST request with JSON body and decodes JSON response.
func (c *Client) PostJSON(ctx context.Context, path string, body, result interface{}) error {
	var bodyReader io.Reader
	if body != nil {
		data, err := json.Marshal(body)
		if err != nil {
			return fmt.Errorf("failed to encode body: %w", err)
		}
		bodyReader = &jsonReader{data: data}
	}

	resp, err := c.Post(ctx, path, bodyReader)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return c.parseError(resp)
	}

	if result != nil {
		if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
			return fmt.Errorf("failed to decode response: %w", err)
		}
	}

	return nil
}

// parseError parses an error response from the API.
func (c *Client) parseError(resp *http.Response) error {
	body, _ := io.ReadAll(resp.Body)

	var apiErr struct {
		Error   string `json:"error"`
		Message string `json:"message"`
	}

	if err := json.Unmarshal(body, &apiErr); err == nil && (apiErr.Error != "" || apiErr.Message != "") {
		msg := apiErr.Error
		if msg == "" {
			msg = apiErr.Message
		}
		return &APIError{
			StatusCode: resp.StatusCode,
			Message:    msg,
		}
	}

	return &APIError{
		StatusCode: resp.StatusCode,
		Message:    string(body),
	}
}

// APIError represents an API error response.
type APIError struct {
	StatusCode int
	Message    string
}

// Error represents an API error with code and details.
type Error struct {
	StatusCode int    `json:"-"`
	Code       string `json:"code"`
	Message    string `json:"message"`
	Details    string `json:"details,omitempty"`
}

// Error returns the error message.
func (e *APIError) Error() string {
	return fmt.Sprintf("API error %d: %s", e.StatusCode, e.Message)
}

// Error returns the error message.
func (e *Error) Error() string {
	if e.Code != "" {
		return fmt.Sprintf("[%s] %s", e.Code, e.Message)
	}
	return e.Message
}

// IsConnectionError checks if an error is a connection error.
func IsConnectionError(err error) bool {
	if err == nil {
		return false
	}
	errStr := err.Error()
	return strings.Contains(errStr, "connection refused") ||
		strings.Contains(errStr, "no such host") ||
		strings.Contains(errStr, "network is unreachable") ||
		strings.Contains(errStr, "dial tcp") ||
		strings.Contains(errStr, "context deadline exceeded")
}

// IsNotFound returns whether the error is a 404 Not Found.
func (e *APIError) IsNotFound() bool {
	return e.StatusCode == http.StatusNotFound
}

// IsUnauthorized returns whether the error is a 401 Unauthorized.
func (e *APIError) IsUnauthorized() bool {
	return e.StatusCode == http.StatusUnauthorized
}

// jsonReader wraps a byte slice as an io.Reader.
type jsonReader struct {
	data []byte
	pos  int
}

// Read implements io.Reader.
func (r *jsonReader) Read(p []byte) (n int, err error) {
	if r.pos >= len(r.data) {
		return 0, io.EOF
	}
	n = copy(p, r.data[r.pos:])
	r.pos += n
	return n, nil
}

// Ping checks if the backend is reachable.
func (c *Client) Ping(ctx context.Context) error {
	resp, err := c.Get(ctx, "/health")
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("backend returned status %d", resp.StatusCode)
	}

	return nil
}

// get performs a GET request and decodes JSON response.
func (c *Client) get(ctx context.Context, path string, result interface{}) error {
	return c.GetJSON(ctx, path, result)
}

// post performs a POST request with JSON body and decodes JSON response.
func (c *Client) post(ctx context.Context, path string, body, result interface{}) error {
	return c.PostJSON(ctx, path, body, result)
}

// put performs a PUT request with JSON body and decodes JSON response.
func (c *Client) put(ctx context.Context, path string, body, result interface{}) error {
	var bodyReader io.Reader
	if body != nil {
		data, err := json.Marshal(body)
		if err != nil {
			return fmt.Errorf("failed to encode body: %w", err)
		}
		bodyReader = &jsonReader{data: data}
	}

	resp, err := c.Put(ctx, path, bodyReader)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return c.parseError(resp)
	}

	if result != nil {
		if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
			return fmt.Errorf("failed to decode response: %w", err)
		}
	}

	return nil
}

// delete performs a DELETE request and optionally decodes JSON response.
func (c *Client) delete(ctx context.Context, path string, result interface{}) error {
	resp, err := c.Delete(ctx, path)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return c.parseError(resp)
	}

	if result != nil {
		if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
			return fmt.Errorf("failed to decode response: %w", err)
		}
	}

	return nil
}

// deleteWithBody performs a DELETE request with JSON body and decodes JSON response.
func (c *Client) deleteWithBody(ctx context.Context, path string, body, result interface{}) error {
	var bodyReader io.Reader
	if body != nil {
		data, err := json.Marshal(body)
		if err != nil {
			return fmt.Errorf("failed to encode body: %w", err)
		}
		bodyReader = &jsonReader{data: data}
	}

	resp, err := c.do(ctx, http.MethodDelete, path, bodyReader)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return c.parseError(resp)
	}

	if result != nil {
		if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
			return fmt.Errorf("failed to decode response: %w", err)
		}
	}

	return nil
}
