// Package payments gates the one-time $10 lifetime Premium membership on
// CoinPay (coinpayportal.com). It talks to the CoinPay REST API directly over
// HTTP — no `coinpay` CLI needs to be installed on the host.
//
// Config (env):
//
//	COINPAY_API_KEY                Bearer key for the CoinPay API (cp_live_…)
//	AGENTBBS_COINPAY_MERCHANT_ID   merchant/business id payments are created under
//	AGENTBBS_COINPAY_API_URL       API base (default https://coinpayportal.com/api)
//	AGENTBBS_PREMIUM_AMOUNT        fiat amount (default 10)
//	AGENTBBS_PREMIUM_CURRENCY      fiat currency (default USD)
//	AGENTBBS_PREMIUM_BLOCKCHAIN    settlement chain (default eth)
//
// Operators can also grant pod time manually: `agentbbs grant-pod <user> N`.
package payments

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

// PodTerm is how much access one manual pod grant buys (grant-pod CLI).
const PodTerm = 31 * 24 * time.Hour

// PremiumPriceLabel is the human-readable price for the one-time lifetime
// membership offered at join@.
const PremiumPriceLabel = "$10 (lifetime)"

// Premium charge parameters — overridable via env so the offer can change
// without a rebuild.
func PremiumAmount() string     { return envOr("AGENTBBS_PREMIUM_AMOUNT", "10") }
func PremiumCurrency() string   { return envOr("AGENTBBS_PREMIUM_CURRENCY", "USD") }
func PremiumBlockchain() string { return envOr("AGENTBBS_PREMIUM_BLOCKCHAIN", "eth") }

// MerchantID is the CoinPay merchant/business id payments are created under.
func MerchantID() string { return os.Getenv("AGENTBBS_COINPAY_MERCHANT_ID") }

func apiKey() string { return os.Getenv("COINPAY_API_KEY") }
func apiBase() string {
	return trimSlash(envOr("AGENTBBS_COINPAY_API_URL", "https://coinpayportal.com/api"))
}

// APIConfigured reports whether live CoinPay calls can be made.
func APIConfigured() bool { return apiKey() != "" && MerchantID() != "" }

func envOr(k, def string) string {
	if v := os.Getenv(k); v != "" {
		return v
	}
	return def
}

func trimSlash(s string) string {
	for len(s) > 0 && s[len(s)-1] == '/' {
		s = s[:len(s)-1]
	}
	return s
}

// Charge is a created CoinPay payment a user must fund: a unique deposit
// address, the crypto amount, the fiat amount it settles, and the payment id
// (store it to verify settlement later).
type Charge struct {
	Address      string
	CryptoAmount string
	Currency     string
	FiatAmount   string
	FiatCurrency string
	ID           string
	QR           string
}

// PremiumReference derives a stable memo for a user's lifetime membership from
// their key fingerprint; sent as payment metadata for reconciliation.
func PremiumReference(pubkeyFP string) string { return Reference("premium", pubkeyFP) }

// Reference derives a stable, short payment reference for a user+plan.
func Reference(plan, pubkeyFP string) string {
	mac := hmac.New(sha256.New, []byte("agentbbs."+plan))
	mac.Write([]byte(pubkeyFP))
	return "abbs-" + plan + "-" + hex.EncodeToString(mac.Sum(nil))[:12]
}

// coinpayPayment is the (subset of the) CoinPay payment object, returned
// wrapped as {"payment": {…}}.
type coinpayPayment struct {
	ID           string `json:"id"`
	Status       string `json:"status"`
	Address      string `json:"payment_address"`
	CryptoAmount string `json:"crypto_amount"`
	CryptoCurr   string `json:"crypto_currency"`
	QR           string `json:"qr_code"`
}

type paymentEnvelope struct {
	Payment coinpayPayment `json:"payment"`
}

// coinpayDo performs an authenticated CoinPay API call and decodes the
// {"payment":{…}} envelope.
func coinpayDo(ctx context.Context, method, path string, body any) (*paymentEnvelope, error) {
	var rdr io.Reader
	if body != nil {
		b, err := json.Marshal(body)
		if err != nil {
			return nil, err
		}
		rdr = bytes.NewReader(b)
	}
	req, err := http.NewRequestWithContext(ctx, method, apiBase()+path, rdr)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Authorization", "Bearer "+apiKey())
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<16))
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, fmt.Errorf("coinpay %s %s: %s: %s", method, path, resp.Status, bytesTrim(raw))
	}
	var env paymentEnvelope
	if err := json.Unmarshal(raw, &env); err != nil {
		return nil, fmt.Errorf("coinpay %s %s: bad response: %w", method, path, err)
	}
	return &env, nil
}

func bytesTrim(b []byte) string { return string(bytes.TrimSpace(b)) }

// CreatePremiumCharge creates a CoinPay payment for the lifetime membership and
// returns the deposit address, amount, and payment id (store the id to verify
// settlement later). created is false when CoinPay isn't configured.
func CreatePremiumCharge(ref string) (Charge, bool, error) {
	if !APIConfigured() {
		return Charge{}, false, nil
	}
	amount := json.Number(PremiumAmount())
	if f, err := strconv.ParseFloat(PremiumAmount(), 64); err == nil {
		amount = json.Number(strconv.FormatFloat(f, 'f', -1, 64))
	}
	body := map[string]any{
		"business_id": MerchantID(),
		"amount":      amount,
		"currency":    PremiumCurrency(),
		"blockchain":  toUpper(PremiumBlockchain()),
		"description": "AgentBBS Premium membership (lifetime)",
		"metadata":    map[string]string{"ref": ref},
	}
	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
	defer cancel()
	env, err := coinpayDo(ctx, http.MethodPost, "/payments/create", body)
	if err != nil {
		return Charge{}, false, err
	}
	p := env.Payment
	if p.Address == "" {
		return Charge{}, false, fmt.Errorf("coinpay: payment created without an address")
	}
	return Charge{
		Address:      p.Address,
		CryptoAmount: p.CryptoAmount,
		Currency:     p.CryptoCurr,
		FiatAmount:   PremiumAmount(),
		FiatCurrency: PremiumCurrency(),
		ID:           p.ID,
		QR:           p.QR,
	}, true, nil
}

// VerifyPremium reports whether a created premium payment has settled. payID is
// the CoinPay payment id from CreatePremiumCharge. checked is false when we
// could not reach CoinPay (caller falls back to store state); paid is true on a
// confirmed/forwarded status.
func VerifyPremium(payID string) (paid bool, checked bool) {
	if payID == "" || !APIConfigured() {
		return false, false
	}
	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()
	env, err := coinpayDo(ctx, http.MethodGet, "/payments/"+url.PathEscape(payID), nil)
	if err != nil {
		return false, false // network/API error — fall back to store state
	}
	switch env.Payment.Status {
	case "confirmed", "forwarded", "completed", "paid":
		return true, true
	default:
		return false, true
	}
}

func toUpper(s string) string {
	b := []byte(s)
	for i, c := range b {
		if c >= 'a' && c <= 'z' {
			b[i] = c - 32
		}
	}
	return string(b)
}
