// Package romu is the Romu client: every request is signed for you (standard library only).
package romu

import (
	"bytes"
	"crypto/ed25519"
	"crypto/rand"
	"crypto/sha256"
	"crypto/x509"
	"encoding/base64"
	"encoding/hex"
	"encoding/pem"
	"errors"
	"io"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"
)

// Client holds your key_id and private key.
type Client struct {
	BaseURL string
	KeyID   string
	Key     ed25519.PrivateKey
}

// NewClient reads the PEM file made by openssl genpkey.
func NewClient(baseURL, keyID, keyFile string) (*Client, error) {
	raw, err := os.ReadFile(keyFile)
	if err != nil {
		return nil, err
	}
	block, _ := pem.Decode(raw)
	if block == nil {
		return nil, errors.New("romu: no PEM block in the key file")
	}
	parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes)
	if err != nil {
		return nil, err
	}
	key, ok := parsed.(ed25519.PrivateKey)
	if !ok {
		return nil, errors.New("romu: the key is not Ed25519")
	}
	return &Client{BaseURL: baseURL, KeyID: keyID, Key: key}, nil
}

// StringToSign is nine lines joined with "\n", no newline after the last one.
func StringToSign(keyID string, timestamp int64, nonce, method, path, query string, body []byte, idempotencyKey string) string {
	sum := sha256.Sum256(body)
	return strings.Join([]string{"custody.api.request.v1", keyID, strconv.FormatInt(timestamp, 10), nonce,
		method, path, query, hex.EncodeToString(sum[:]), idempotencyKey}, "\n")
}

// Get sends one signed GET.
func (c *Client) Get(path, query string) (int, []byte, error) {
	return c.Do(http.MethodGet, path, query, nil, "")
}

// Do sends one signed request. The query goes out exactly as it was signed.
func (c *Client) Do(method, path, query string, body []byte, idempotencyKey string) (int, []byte, error) {
	random := make([]byte, 18)
	if _, err := rand.Read(random); err != nil {
		return 0, nil, err
	}
	nonce := base64.RawURLEncoding.EncodeToString(random)
	timestamp := time.Now().Unix()
	message := StringToSign(c.KeyID, timestamp, nonce, method, path, query, body, idempotencyKey)

	target := c.BaseURL + path
	if query != "" {
		target += "?" + query
	}
	req, err := http.NewRequest(method, target, bytes.NewReader(body))
	if err != nil {
		return 0, nil, err
	}
	req.Header.Set("X-Custody-Key-Id", c.KeyID)
	req.Header.Set("X-Custody-Timestamp", strconv.FormatInt(timestamp, 10))
	req.Header.Set("X-Custody-Nonce", nonce)
	req.Header.Set("X-Custody-Signature", base64.RawURLEncoding.EncodeToString(ed25519.Sign(c.Key, []byte(message))))
	if idempotencyKey != "" {
		req.Header.Set("Idempotency-Key", idempotencyKey)
	}
	if len(body) > 0 {
		req.Header.Set("Content-Type", "application/json")
	}
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return 0, nil, err
	}
	defer res.Body.Close()
	out, err := io.ReadAll(res.Body)
	return res.StatusCode, out, err
}
