package main

import (
	"bytes"
	"context"
	"embed"
	"encoding/json"
	"log"
	"net/http"
	"os"
	"strings"

	"rnpay/coinman"

	"github.com/redis/go-redis/v9"
)

const redisKeyCallback = "coinman:callback_url"

var (
	cm  *coinman.Coinman
	rdb *redis.Client
	ctx = context.Background()
)

//go:embed admin.html
var adminFS embed.FS

func main() {
	redisAddr := getEnv("REDIS_ADDR", "localhost:6379")
	notifyStream := getEnv("NOTIFY_STREAM", "")
	port := getEnv("PORT", ":16001")

	rdb = redis.NewClient(&redis.Options{Addr: redisAddr})
	if err := rdb.Ping(ctx).Err(); err != nil {
		log.Fatalf("Redis: %v", err)
	}
	if u := getEnv("CALLBACK_URL", ""); u != "" {
		rdb.Set(ctx, redisKeyCallback, u, 0)
	}

	cm = coinman.New(rdb)
	cm.OnOrderComplete(func(addr string, order coinman.OrderInfo) {
		url, _ := rdb.Get(ctx, redisKeyCallback).Result()
		if url == "" {
			return
		}
		body, _ := json.Marshal(map[string]interface{}{"event": "usdt.received", "address": addr, "order": order})
		resp, err := http.Post(url, "application/json", bytes.NewReader(body))
		if err != nil {
			log.Printf("[coinman] callback err: %v", err)
			return
		}
		resp.Body.Close()
		log.Printf("[coinman] callback -> %d", resp.StatusCode)
	})
	if notifyStream != "" {
		cm.SetNotifyStream(notifyStream)
	}
	cm.Start()

	http.HandleFunc("/start", cors(handleStart))
	http.HandleFunc("/addresses", cors(handleAddresses))
	http.HandleFunc("/addresses/add", cors(handleAddressesAdd))
	http.HandleFunc("/addresses/remove", cors(handleAddressesRemove))
	http.HandleFunc("/config/callback", cors(handleConfigCallback))
	http.HandleFunc("/admin", func(w http.ResponseWriter, r *http.Request) {
		data, _ := adminFS.ReadFile("admin.html")
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		w.Write(data)
	})

	log.Printf("coinman listen %s (REDIS=%s, STREAM=%s)", port, redisAddr, boolStr(notifyStream != ""))
	log.Fatal(http.ListenAndServe(port, nil))
}

func handleConfigCallback(w http.ResponseWriter, r *http.Request) {
	if r.Method == http.MethodGet {
		url, _ := rdb.Get(ctx, redisKeyCallback).Result()
		jsonResp(w, true, "ok", map[string]interface{}{"callback_url": url})
		return
	}
	if r.Method != http.MethodPost {
		jsonResp(w, false, "Method not allowed", nil)
		return
	}
	var req struct {
		CallbackURL string `json:"callback_url"`
	}
	_ = json.NewDecoder(r.Body).Decode(&req)
	url := strings.TrimSpace(req.CallbackURL)
	if url == "" {
		rdb.Del(ctx, redisKeyCallback)
		jsonResp(w, true, "ok", map[string]interface{}{"callback_url": ""})
		return
	}
	rdb.Set(ctx, redisKeyCallback, url, 0)
	jsonResp(w, true, "ok", map[string]interface{}{"callback_url": url})
}

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

func boolStr(b bool) string {
	if b {
		return "yes"
	}
	return "no"
}

func cors(h http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS")
		w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
		if r.Method == http.MethodOptions {
			return
		}
		h(w, r)
	}
}

func jsonResp(w http.ResponseWriter, ok bool, msg string, data interface{}) {
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": ok, "msg": msg, "data": data})
}

func handleStart(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		jsonResp(w, false, "Method not allowed", nil)
		return
	}
	cm.Start()
	jsonResp(w, true, "started", nil)
}

func handleAddresses(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		jsonResp(w, false, "Method not allowed", nil)
		return
	}
	list := cm.ListPaymentAddresses()
	jsonResp(w, true, "ok", map[string]interface{}{"addresses": list})
}

func handleAddressesAdd(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		jsonResp(w, false, "Method not allowed", nil)
		return
	}
	var req struct {
		Addresses []string `json:"addresses"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		jsonResp(w, false, "invalid json", nil)
		return
	}
	addrs := trimAddrs(req.Addresses)
	if len(addrs) == 0 {
		jsonResp(w, false, "addresses required", nil)
		return
	}
	cm.AddPaymentAddress(addrs)
	jsonResp(w, true, "ok", map[string]interface{}{"added": addrs})
}

func handleAddressesRemove(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		jsonResp(w, false, "Method not allowed", nil)
		return
	}
	var req struct {
		Addresses []string `json:"addresses"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		jsonResp(w, false, "invalid json", nil)
		return
	}
	addrs := trimAddrs(req.Addresses)
	if len(addrs) == 0 {
		jsonResp(w, false, "addresses required", nil)
		return
	}
	cm.RemovePaymentAddress(addrs)
	jsonResp(w, true, "ok", map[string]interface{}{"removed": addrs})
}

func trimAddrs(s []string) []string {
	out := make([]string, 0, len(s))
	for _, a := range s {
		a = strings.TrimSpace(a)
		if a != "" {
			out = append(out, a)
		}
	}
	return out
}
