// Package cryptopayclient — клиент API Coinetic (только стандартная библиотека Go).
// Coinetic — торговое название; имя пакета и схема подписи CP-HMAC не меняются.
// Скопируйте файл в свой проект: отдельной зависимости нет.
package cryptopayclient

import (
	"bytes"
	"context"
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"
)

// Error — ошибка API: HTTP-статус и машинный код (см. docs/integration/errors.md).
type Error struct {
	Status  int
	Code    string
	Message string
}

func (e *Error) Error() string {
	return fmt.Sprintf("cryptopay: %d %s: %s", e.Status, e.Code, e.Message)
}

// SignRequest — подпись CP-HMAC: HMAC-SHA256(secret, METHOD\nPATH\nts\nnonce\nhex(sha256(body))).
func SignRequest(secret, method, pathWithQuery string, ts int64, nonce string, body []byte) string {
	bh := sha256.Sum256(body)
	m := hmac.New(sha256.New, []byte(secret))
	fmt.Fprintf(m, "%s\n%s\n%d\n%s\n%s", method, pathWithQuery, ts, nonce, hex.EncodeToString(bh[:]))
	return hex.EncodeToString(m.Sum(nil))
}

// VerifyWebhook проверяет X-Webhook-Signature (несколько v1= через запятую при ротации) и
// окно X-Webhook-Timestamp. rawBody — тело как пришло, до разбора JSON.
func VerifyWebhook(secret string, h http.Header, rawBody []byte, now time.Time, maxSkew time.Duration) error {
	sig, tsStr := h.Get("X-Webhook-Signature"), h.Get("X-Webhook-Timestamp")
	ts, err := strconv.ParseInt(tsStr, 10, 64)
	if sig == "" || err != nil || ts <= 0 {
		return &Error{400, "no_signature", "нет подписи"}
	}
	if d := now.Sub(time.Unix(ts, 0)); d > maxSkew || d < -maxSkew {
		return &Error{400, "stale", "метка времени вне окна"}
	}
	m := hmac.New(sha256.New, []byte(secret))
	m.Write([]byte(tsStr + "."))
	m.Write(rawBody)
	want := []byte("v1=" + hex.EncodeToString(m.Sum(nil)))
	for _, p := range strings.Split(sig, ",") {
		if hmac.Equal([]byte(strings.TrimSpace(p)), want) {
			return nil
		}
	}
	return &Error{401, "bad_signature", "подпись не совпадает"}
}

// PaidStatuses — статусы, после которых можно выдавать товар.
var PaidStatuses = map[string]bool{"paid": true, "overpaid": true}

type Client struct {
	BaseURL string // https://pay.example.com
	KeyID   string // ak_...
	Secret  string
	HTTP    *http.Client
}

// Invoice — поля счёта, которые обычно нужны магазину. Суммы — десятичные строки, не float.
type Invoice struct {
	ID          string            `json:"id"`
	OrderID     string            `json:"order_id"`
	Status      string            `json:"status"`
	Price       string            `json:"price_amount"`
	Currency    string            `json:"price_currency"`
	CheckoutURL string            `json:"checkout_url"`
	Paid        string            `json:"paid_amount"`
	Outstanding string            `json:"outstanding_amount"`
	ExpiresAt   time.Time         `json:"expires_at"`
	Metadata    map[string]string `json:"metadata,omitempty"`
}

type CreateInvoice struct {
	OrderID       string            `json:"order_id"`
	PriceAmount   string            `json:"price_amount"`
	PriceCurrency string            `json:"price_currency"`
	AllowedAssets []string          `json:"allowed_assets,omitempty"`
	ExpiresIn     int               `json:"expires_in_seconds,omitempty"` // 300..86400
	CallbackURL   string            `json:"callback_url,omitempty"`
	ReturnURL     string            `json:"return_url,omitempty"`
	Metadata      map[string]string `json:"metadata,omitempty"`
}

func (c *Client) CreateInvoice(ctx context.Context, in CreateInvoice, idempotencyKey string) (*Invoice, error) {
	var out Invoice
	return &out, c.do(ctx, http.MethodPost, "/v1/invoices", in, idempotencyKey, &out)
}

func (c *Client) GetInvoice(ctx context.Context, id string) (*Invoice, error) {
	var out Invoice
	return &out, c.do(ctx, http.MethodGet, "/v1/invoices/"+url.PathEscape(id), nil, "", &out)
}

func (c *Client) do(ctx context.Context, method, path string, body any, idem string, out any) error {
	var raw []byte
	if body != nil {
		var err error
		if raw, err = json.Marshal(body); err != nil {
			return err
		}
	}
	nb := make([]byte, 16)
	if _, err := rand.Read(nb); err != nil {
		return err
	}
	nonce, ts := hex.EncodeToString(nb), time.Now().Unix()
	req, err := http.NewRequestWithContext(ctx, method, strings.TrimRight(c.BaseURL, "/")+path, bytes.NewReader(raw))
	if err != nil {
		return err
	}
	req.Header.Set("Authorization", fmt.Sprintf("CP-HMAC key=%s, ts=%d, nonce=%s, sig=%s",
		c.KeyID, ts, nonce, SignRequest(c.Secret, method, path, ts, nonce, raw)))
	req.Header.Set("Accept", "application/json")
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	if idem != "" {
		req.Header.Set("Idempotency-Key", idem)
	}
	hc := c.HTTP
	if hc == nil {
		hc = &http.Client{Timeout: 10 * time.Second}
	}
	resp, err := hc.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	data, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
	if err != nil {
		return err
	}
	if resp.StatusCode >= 400 {
		var e struct {
			Error struct{ Code, Message string } `json:"error"`
		}
		_ = json.Unmarshal(data, &e)
		if e.Error.Code == "" {
			e.Error.Code, e.Error.Message = "http_error", resp.Status
		}
		return &Error{resp.StatusCode, e.Error.Code, e.Error.Message}
	}
	if out == nil {
		return nil
	}
	return json.Unmarshal(data, out)
}

// IsCode — удобная проверка кода ошибки API.
func IsCode(err error, code string) bool {
	var e *Error
	return errors.As(err, &e) && e.Code == code
}
