package secrets

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

// NodeSecrets holds per-node secret material to inject at burn time.
// These should live in Vault at kv/data/atlas/nodes/<hostname>.
type NodeSecrets struct {
	AtlasPassword string            `json:"atlas_password,omitempty"`
	RootPassword  string            `json:"root_password,omitempty"`
	K3sToken      string            `json:"k3s_token,omitempty"`
	CloudInit     string            `json:"cloud_init,omitempty"`
	Extra         map[string]string `json:"extra,omitempty"`
}

// Client fetches node secrets from Vault using either a token or AppRole.
type Client struct {
	Addr     string
	Token    string
	RoleID   string
	SecretID string
	Client   *http.Client
}

// NewFromEnv builds a client from VAULT_ADDR, VAULT_TOKEN, VAULT_ROLE_ID, VAULT_SECRET_ID.
func NewFromEnv() *Client {
	return &Client{
		Addr:     os.Getenv("VAULT_ADDR"),
		Token:    os.Getenv("VAULT_TOKEN"),
		RoleID:   os.Getenv("VAULT_ROLE_ID"),
		SecretID: os.Getenv("VAULT_SECRET_ID"),
		Client: &http.Client{
			Timeout: 10 * time.Second,
		},
	}
}

// LoginIfNeeded performs AppRole login if no token is present.
func (c *Client) LoginIfNeeded(ctx context.Context) error {
	if c.Token != "" || c.RoleID == "" || c.SecretID == "" {
		return nil
	}
	body := map[string]string{"role_id": c.RoleID, "secret_id": c.SecretID}
	var buf bytes.Buffer
	if err := json.NewEncoder(&buf).Encode(body); err != nil {
		return err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("%s/v1/auth/approle/login", strings.TrimSuffix(c.Addr, "/")), &buf)
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/json")
	resp, err := c.httpClient().Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("approle login failed: %s", resp.Status)
	}
	var r struct {
		Auth struct {
			ClientToken string `json:"client_token"`
		} `json:"auth"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
		return err
	}
	if r.Auth.ClientToken == "" {
		return fmt.Errorf("approle login returned empty token")
	}
	c.Token = r.Auth.ClientToken
	return nil
}

// FetchNode loads per-node secret material because burn-time injection needs
// a single read path that can fall back to empty secrets when Vault has no row.
// FetchNode pulls kv/data/atlas/nodes/<hostname>.
func (c *Client) FetchNode(ctx context.Context, hostname string) (*NodeSecrets, error) {
	if err := c.LoginIfNeeded(ctx); err != nil {
		return nil, err
	}
	url := fmt.Sprintf("%s/v1/kv/data/atlas/nodes/%s", strings.TrimSuffix(c.Addr, "/"), hostname)
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return nil, err
	}
	if c.Token != "" {
		req.Header.Set("X-Vault-Token", c.Token)
	}
	resp, err := c.httpClient().Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode == http.StatusNotFound {
		return &NodeSecrets{}, nil
	}
	if resp.StatusCode != http.StatusOK {
		b, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("vault fetch %s: %s: %s", hostname, resp.Status, string(b))
	}
	var r struct {
		Data struct {
			Data NodeSecrets `json:"data"`
		} `json:"data"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
		return nil, err
	}
	return &r.Data.Data, nil
}

func (c *Client) httpClient() *http.Client {
	if c.Client != nil {
		return c.Client
	}
	return http.DefaultClient
}
