package proxmox

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

// doer is the minimal HTTP surface the client needs; *http.Client satisfies it.
// Tests inject a mock to exercise decoding/error paths without a live host.
type doer interface {
	Do(*http.Request) (*http.Response, error)
}

// Config configures a Client (the API backend).
type Config struct {
	// Endpoint is the API base, e.g. "https://127.0.0.1:8006". The "/api2/json"
	// suffix is added by the client.
	Endpoint string
	// Node is the Proxmox node name (e.g. "demo-felhom"). Confirm on the box
	// (GET /nodes), never hard-code — see proxmox-platform.md §1.
	Node string
	// Token is the full API token "USER@REALM!TOKENID=SECRET". Never logged.
	Token string
	// TLS selects how the host cert is trusted.
	TLS TLSConfig
	// HTTPTimeout bounds a single HTTP round-trip (not a whole task wait).
	// Defaults to 30s.
	HTTPTimeout time.Duration
}

// Client is the API backend: a typed REST client for one Proxmox host. It is the
// default path for everything the scoped token can do. It never shells out.
type Client struct {
	base  string // "<endpoint>/api2/json"
	node  string
	token string
	http  doer
}

// NewClient builds an API client. It validates required config and constructs the
// TLS-pinned transport.
func NewClient(cfg Config) (*Client, error) {
	if cfg.Endpoint == "" {
		return nil, fmt.Errorf("proxmox: endpoint is required")
	}
	if cfg.Node == "" {
		return nil, fmt.Errorf("proxmox: node is required")
	}
	if cfg.Token == "" {
		return nil, fmt.Errorf("proxmox: API token is required")
	}
	tlsCfg, err := cfg.TLS.build()
	if err != nil {
		return nil, err
	}
	timeout := cfg.HTTPTimeout
	if timeout == 0 {
		timeout = 30 * time.Second
	}
	hc := &http.Client{
		Timeout:   timeout,
		Transport: &http.Transport{TLSClientConfig: tlsCfg},
	}
	return &Client{
		base:  strings.TrimRight(cfg.Endpoint, "/") + "/api2/json",
		node:  cfg.Node,
		token: cfg.Token,
		http:  hc,
	}, nil
}

// Node returns the configured node name.
func (c *Client) Node() string { return c.node }

// get performs GET <path> and decodes the {"data": ...} envelope into out.
func (c *Client) get(ctx context.Context, path string, out any) error {
	return c.do(ctx, http.MethodGet, path, nil, out)
}

// postForm performs a form-encoded POST/PUT and decodes the envelope into out.
// out may be nil when the caller does not need the body.
func (c *Client) postForm(ctx context.Context, method, path string, params url.Values, out any) error {
	var body io.Reader
	if params != nil {
		body = strings.NewReader(params.Encode())
	}
	return c.doBody(ctx, method, path, body, "application/x-www-form-urlencoded", out)
}

func (c *Client) do(ctx context.Context, method, path string, body io.Reader, out any) error {
	return c.doBody(ctx, method, path, body, "", out)
}

// doBody is the single HTTP chokepoint: builds the request, sets auth, executes,
// maps non-2xx to APIError, and decodes the data envelope.
func (c *Client) doBody(ctx context.Context, method, path string, body io.Reader, contentType string, out any) error {
	req, err := http.NewRequestWithContext(ctx, method, c.base+path, body)
	if err != nil {
		return fmt.Errorf("proxmox: building request: %w", err)
	}
	req.Header.Set("Authorization", "PVEAPIToken="+c.token)
	req.Header.Set("Accept", "application/json")
	if contentType != "" {
		req.Header.Set("Content-Type", contentType)
	}

	resp, err := c.http.Do(req)
	if err != nil {
		return fmt.Errorf("proxmox: %s %s: %w", method, path, err)
	}
	defer resp.Body.Close()

	raw, err := io.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("proxmox: reading %s %s response: %w", method, path, err)
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return newAPIError(resp.StatusCode, method, path, string(raw))
	}
	if out == nil {
		return nil
	}
	var env struct {
		Data json.RawMessage `json:"data"`
	}
	if err := json.Unmarshal(raw, &env); err != nil {
		return fmt.Errorf("proxmox: decoding %s %s envelope: %w", method, path, err)
	}
	if len(env.Data) == 0 || bytes.Equal(env.Data, []byte("null")) {
		return nil // no data (e.g. a sync PUT /config)
	}
	if err := json.Unmarshal(env.Data, out); err != nil {
		return fmt.Errorf("proxmox: decoding %s %s data: %w", method, path, err)
	}
	return nil
}

// dataString runs a request expecting the "data" field to be a bare string
// (the UPID returned by async mutating ops). Returns "" with no error when the
// response carries no data (some sync ops).
func (c *Client) dataString(ctx context.Context, method, path string, params url.Values) (string, error) {
	var s string
	if err := c.postForm(ctx, method, path, params, &s); err != nil {
		return "", err
	}
	return s, nil
}
