package main

import (
	"encoding/json"
	"log"
	"maps"
	"net/http"
	"strconv"
	"strings"
	"sync"
	"time"
)

const (
	addr              = ":8080"
	readHeaderTimeout = 5 * time.Second
)

var spendLimits = &spendLimitStore{items: map[string]map[string]any{}}

type spendLimitStore struct {
	mu    sync.Mutex
	items map[string]map[string]any
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodGet {
			http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
			return
		}
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte(`{"status":"ok"}`))
	})
	mux.HandleFunc("/api/v1/billing/tenants/", handleTenantBilling)

	log.Printf("cp9 billing plan stub listening on %s", addr)
	server := &http.Server{
		Addr:              addr,
		Handler:           mux,
		ReadHeaderTimeout: readHeaderTimeout,
	}
	if err := server.ListenAndServe(); err != nil {
		log.Fatal(err)
	}
}

func handleTenantBilling(w http.ResponseWriter, r *http.Request) {
	tenantIDText, billingPath, ok := splitTenantBillingPath(r.URL.Path)
	if !ok {
		http.NotFound(w, r)
		return
	}
	tenantID, err := strconv.ParseUint(tenantIDText, 10, 64)
	if err != nil || tenantID == 0 {
		http.Error(w, "tenant_id must be a positive integer", http.StatusBadRequest)
		return
	}

	switch billingPath {
	case "plans":
		if r.Method != http.MethodGet {
			http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
			return
		}
		tenantPlans(w, tenantID)
	case "billing_action_check":
		if r.Method != http.MethodPost {
			http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
			return
		}
		billingActionCheck(w)
	case "measures/dryrun", "measures":
		if r.Method != http.MethodPost {
			http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
			return
		}
		measure(w, r)
	case "spend_limits":
		handleSpendLimits(w, r, tenantID)
	default:
		if suffix, ok := strings.CutPrefix(billingPath, "spend_limits/"); ok {
			handleSpendLimitByID(w, r, tenantID, suffix)
			return
		}
		http.NotFound(w, r)
	}
}

func splitTenantBillingPath(path string) (tenantIDText, billingPath string, ok bool) {
	const prefix = "/api/v1/billing/tenants/"
	if !strings.HasPrefix(path, prefix) {
		return "", "", false
	}
	rest := strings.TrimPrefix(path, prefix)
	parts := strings.SplitN(rest, "/", 2)
	if len(parts) != 2 || strings.TrimSpace(parts[0]) == "" || strings.TrimSpace(parts[1]) == "" {
		return "", "", false
	}
	return strings.TrimSpace(parts[0]), strings.Trim(parts[1], "/ "), true
}

func tenantPlans(w http.ResponseWriter, tenantID uint64) {
	w.Header().Set("Content-Type", "application/json")
	if err := json.NewEncoder(w).Encode(map[string]any{
		"tenant_id":          tenantID,
		"tenant_id_str":      strconv.FormatUint(tenantID, 10),
		"effective_plan":     "on_demand",
		"effective_discount": 0,
		"poc_credits":        0,
		"dev_tier_plan_info": map[string]any{
			"plan":   "free_trial",
			"status": "not_started",
		},
		"poc_plan_info": map[string]any{
			"plan":   "poc",
			"status": "not_started",
		},
		"ondemand_plan_info": map[string]any{
			"plan":   "on_demand",
			"status": "active",
		},
	}); err != nil {
		log.Printf("encode tenant plan response: %v", err)
	}
}

func billingActionCheck(w http.ResponseWriter) {
	w.Header().Set("Content-Type", "application/json")
	if err := json.NewEncoder(w).Encode(map[string]any{
		"pass":   true,
		"reason": "",
	}); err != nil {
		log.Printf("encode billing action check response: %v", err)
	}
}

func measure(w http.ResponseWriter, r *http.Request) {
	var req struct {
		CostID string         `json:"cost_id"`
		Cost   int64          `json:"cost"`
		Labels map[string]any `json:"labels"`
		Unit   string         `json:"unit"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "decode measure request: "+err.Error(), http.StatusBadRequest)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	if err := json.NewEncoder(w).Encode(map[string]any{
		"binded_package_id": 0,
		"consumed_source":   "cp9",
		"cost":              req.Cost,
		"cost_id":           req.CostID,
		"cost_time_stamp":   time.Now().Unix(),
		"invoice_id":        0,
		"package_id":        0,
		"package_version":   0,
		"price":             "1",
		"raw_price":         "1",
		"unit":              req.Unit,
	}); err != nil {
		log.Printf("encode dryrun response: %v", err)
	}
}

func handleSpendLimits(w http.ResponseWriter, r *http.Request, tenantID uint64) {
	switch r.Method {
	case http.MethodGet:
		listSpendLimits(w, r, tenantID)
	case http.MethodPost:
		createSpendLimit(w, r, tenantID)
	default:
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
	}
}

func handleSpendLimitByID(w http.ResponseWriter, r *http.Request, tenantID uint64, suffix string) {
	settingIDText := strings.TrimSuffix(strings.Trim(suffix, "/ "), "/info")
	settingID, err := strconv.ParseUint(settingIDText, 10, 64)
	if err != nil || settingID == 0 {
		http.NotFound(w, r)
		return
	}

	if strings.HasSuffix(suffix, "/info") {
		if r.Method != http.MethodGet {
			http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
			return
		}
		spendLimits.mu.Lock()
		setting := spendLimits.byIDLocked(tenantID, settingID)
		spendLimits.mu.Unlock()
		if setting == nil {
			http.NotFound(w, r)
			return
		}
		out := cloneMap(setting)
		out["costs"] = "0"
		out["start_date"] = time.Now().UTC().Format(time.RFC3339)
		writeJSON(w, out)
		return
	}

	switch r.Method {
	case http.MethodPut:
		updateSpendLimit(w, r, tenantID, settingID)
	case http.MethodDelete:
		deleteSpendLimit(w, tenantID, settingID)
	default:
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
	}
}

func listSpendLimits(w http.ResponseWriter, r *http.Request, tenantID uint64) {
	rangeIDFilter, _ := strconv.ParseUint(strings.TrimSpace(r.URL.Query().Get("range_id")), 10, 64)
	spendLimits.mu.Lock()
	defer spendLimits.mu.Unlock()
	var out []map[string]any
	for _, item := range spendLimits.items {
		if uintFromAny(item["tenant_id"]) != tenantID {
			continue
		}
		if rangeIDFilter != 0 && uintFromAny(item["range_id"]) != rangeIDFilter {
			continue
		}
		out = append(out, cloneMap(item))
	}
	if out == nil {
		if rangeIDFilter == 0 {
			out = []map[string]any{}
		} else {
			setting := normalizedSpendLimit(tenantID, map[string]any{"range_id": rangeIDFilter})
			spendLimits.items[spendLimitKey(tenantID, uintFromAny(setting["spendlimit_setting_id"]))] = setting
			out = []map[string]any{cloneMap(setting)}
		}
	}
	writeJSON(w, out)
}

func createSpendLimit(w http.ResponseWriter, r *http.Request, tenantID uint64) {
	body := map[string]any{}
	decoder := json.NewDecoder(r.Body)
	decoder.UseNumber()
	if err := decoder.Decode(&body); err != nil {
		http.Error(w, "decode spend limit request: "+err.Error(), http.StatusBadRequest)
		return
	}
	setting := normalizedSpendLimit(tenantID, body)
	spendLimits.mu.Lock()
	spendLimits.items[spendLimitKey(tenantID, uintFromAny(setting["spendlimit_setting_id"]))] = setting
	spendLimits.mu.Unlock()
	writeJSON(w, setting)
}

func updateSpendLimit(w http.ResponseWriter, r *http.Request, tenantID, settingID uint64) {
	body := map[string]any{}
	decoder := json.NewDecoder(r.Body)
	decoder.UseNumber()
	if err := decoder.Decode(&body); err != nil {
		http.Error(w, "decode spend limit request: "+err.Error(), http.StatusBadRequest)
		return
	}
	setting := normalizedSpendLimit(tenantID, body)
	setting["spendlimit_setting_id"] = settingID
	spendLimits.mu.Lock()
	spendLimits.items[spendLimitKey(tenantID, settingID)] = setting
	spendLimits.mu.Unlock()
	writeJSON(w, setting)
}

func deleteSpendLimit(w http.ResponseWriter, tenantID, settingID uint64) {
	spendLimits.mu.Lock()
	delete(spendLimits.items, spendLimitKey(tenantID, settingID))
	spendLimits.mu.Unlock()
	writeJSON(w, map[string]any{})
}

func normalizedSpendLimit(tenantID uint64, in map[string]any) map[string]any {
	out := cloneMap(in)
	out["tenant_id"] = tenantID
	rangeID := uintFromAny(out["range_id"])
	settingID := uintFromAny(out["spendlimit_setting_id"])
	if settingID == 0 {
		settingID = tenantID*1_000_000_000 + rangeID
		if settingID == 0 {
			settingID = tenantID*1_000_000_000 + 1
		}
		out["spendlimit_setting_id"] = settingID
	}
	if _, ok := out["range_type"]; !ok {
		out["range_type"] = "cluster"
	}
	if _, ok := out["interval"]; !ok {
		out["interval"] = "monthly"
	}
	if _, ok := out["cost_type"]; !ok {
		out["cost_type"] = "total"
	}
	if _, ok := out["biz_type"]; !ok {
		out["biz_type"] = "serverless_cluster"
	}
	if _, ok := out["status"]; !ok {
		out["status"] = "active"
	}
	if _, ok := out["limit"]; !ok {
		out["limit"] = "0"
	}
	if _, ok := out["biz_data"]; !ok {
		out["biz_data"] = `{"free_credits":"0","invoice_limit":"0","remaining_credits":"0","active_total_credits":"0","recharge_plan_id":0}`
	}
	return out
}

func (s *spendLimitStore) byIDLocked(tenantID, settingID uint64) map[string]any {
	return s.items[spendLimitKey(tenantID, settingID)]
}

func spendLimitKey(tenantID, settingID uint64) string {
	return strconv.FormatUint(tenantID, 10) + "/" + strconv.FormatUint(settingID, 10)
}

func uintFromAny(value any) uint64 {
	switch v := value.(type) {
	case uint64:
		return v
	case uint:
		return uint64(v)
	case int:
		if v > 0 {
			return uint64(v)
		}
	case int64:
		if v > 0 {
			return uint64(v)
		}
	case float64:
		if v > 0 {
			return uint64(v)
		}
	case json.Number:
		n, _ := strconv.ParseUint(v.String(), 10, 64)
		return n
	case string:
		n, _ := strconv.ParseUint(strings.TrimSpace(v), 10, 64)
		return n
	}
	return 0
}

func cloneMap(in map[string]any) map[string]any {
	out := make(map[string]any, len(in))
	maps.Copy(out, in)
	return out
}

func writeJSON(w http.ResponseWriter, value any) {
	w.Header().Set("Content-Type", "application/json")
	if err := json.NewEncoder(w).Encode(value); err != nil {
		log.Printf("encode response: %v", err)
	}
}
