package rest

import (
	"errors"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"strings"
	"sync"

	"gopkg.in/yaml.v3"
)

// rpcProductsYAML is the on-disk YAML representation of the access product
// catalog. It matches config/rpc_products.yaml at the repo root.
type rpcProductsYAML struct {
	Products []accessProduct `yaml:"products"`
}

// accessProduct also has to carry YAML tags so a single struct drives both
// the JSON API response and the on-disk config. (JSON tags are unchanged.)
// These yaml tags mirror the json tags exactly to avoid drift.
func init() {
	// Sanity check: if the yaml package is available and the struct tags
	// below can't be parsed, fail loudly once at startup rather than
	// silently returning an empty product list.
	var _ yaml.Unmarshaler
}

// Keep the YAML-aware struct tags co-located with the existing JSON tags
// by redeclaring accessProduct here is *not* an option (duplicate decl),
// so we use an explicit intermediate with both sets of tags for loading
// and then copy into the existing accessProduct.
type rpcProductsYAMLEntry struct {
	Slug               string   `yaml:"slug"`
	Name               string   `yaml:"name"`
	Provider           string   `yaml:"provider"`
	VMID               int      `yaml:"vmid"`
	HTTPURL            string   `yaml:"http_url"`
	WSURL              string   `yaml:"ws_url"`
	DefaultTier        string   `yaml:"default_tier"`
	RequiresApproval   bool     `yaml:"requires_approval"`
	BillingModel       string   `yaml:"billing_model"`
	Description        string   `yaml:"description"`
	UseCases           []string `yaml:"use_cases"`
	ManagementFeatures []string `yaml:"management_features"`
}

type rpcProductsYAMLFile struct {
	Products []rpcProductsYAMLEntry `yaml:"products"`
}

var (
	rpcProductsOnce sync.Once
	rpcProductsVal  []accessProduct
)

// rpcAccessProductCatalog returns the current access product catalog,
// loading it from disk on first call. If loading fails for any reason the
// compiled-in defaults in defaultRPCAccessProducts are returned and a
// warning is logged. Callers should treat the returned slice as read-only.
func rpcAccessProductCatalog() []accessProduct {
	rpcProductsOnce.Do(func() {
		loaded, path, err := loadRPCAccessProducts()
		switch {
		case err != nil:
			log.Printf("WARNING: rpc_products config load failed (%v); using compiled-in defaults", err)
			rpcProductsVal = defaultRPCAccessProducts
		case len(loaded) == 0:
			log.Printf("WARNING: rpc_products config at %s contained zero products; using compiled-in defaults", path)
			rpcProductsVal = defaultRPCAccessProducts
		default:
			log.Printf("rpc_products: loaded %d products from %s", len(loaded), path)
			rpcProductsVal = loaded
		}
	})
	return rpcProductsVal
}

// loadRPCAccessProducts reads the YAML catalog from disk and returns the
// parsed products along with the path it actually read from. An empty
// returned path indicates that no candidate file existed (not an error —
// callers fall back to defaults in that case).
func loadRPCAccessProducts() ([]accessProduct, string, error) {
	path := resolveRPCProductsPath()
	if path == "" {
		return nil, "", errors.New("no rpc_products.yaml found (set RPC_PRODUCTS_PATH or place config/rpc_products.yaml next to the binary)")
	}
	raw, err := os.ReadFile(path) // #nosec G304 -- path comes from env/repo-known locations
	if err != nil {
		return nil, path, fmt.Errorf("read %s: %w", path, err)
	}
	var decoded rpcProductsYAMLFile
	if err := yaml.Unmarshal(raw, &decoded); err != nil {
		return nil, path, fmt.Errorf("parse %s: %w", path, err)
	}
	products := make([]accessProduct, 0, len(decoded.Products))
	seen := make(map[string]struct{}, len(decoded.Products))
	for i, entry := range decoded.Products {
		if strings.TrimSpace(entry.Slug) == "" {
			return nil, path, fmt.Errorf("%s: product[%d] has empty slug", path, i)
		}
		if _, dup := seen[entry.Slug]; dup {
			return nil, path, fmt.Errorf("%s: duplicate product slug %q", path, entry.Slug)
		}
		seen[entry.Slug] = struct{}{}
		if strings.TrimSpace(entry.HTTPURL) == "" {
			return nil, path, fmt.Errorf("%s: product %q is missing http_url", path, entry.Slug)
		}
		products = append(products, accessProduct{
			Slug:               entry.Slug,
			Name:               entry.Name,
			Provider:           entry.Provider,
			VMID:               entry.VMID,
			HTTPURL:            strings.TrimSpace(entry.HTTPURL),
			WSURL:              strings.TrimSpace(entry.WSURL),
			DefaultTier:        entry.DefaultTier,
			RequiresApproval:   entry.RequiresApproval,
			BillingModel:       entry.BillingModel,
			Description:        strings.TrimSpace(entry.Description),
			UseCases:           entry.UseCases,
			ManagementFeatures: entry.ManagementFeatures,
		})
	}
	return products, path, nil
}

// resolveRPCProductsPath searches for the YAML catalog in precedence order:
//   1. $RPC_PRODUCTS_PATH (absolute or relative to cwd)
//   2. $EXPLORER_BACKEND_DIR/config/rpc_products.yaml
//   3. <cwd>/backend/config/rpc_products.yaml
//   4. <cwd>/config/rpc_products.yaml
//
// Returns "" when no candidate exists.
func resolveRPCProductsPath() string {
	if explicit := strings.TrimSpace(os.Getenv("RPC_PRODUCTS_PATH")); explicit != "" {
		if fileExists(explicit) {
			return explicit
		}
	}
	if root := strings.TrimSpace(os.Getenv("EXPLORER_BACKEND_DIR")); root != "" {
		candidate := filepath.Join(root, "config", "rpc_products.yaml")
		if fileExists(candidate) {
			return candidate
		}
	}
	for _, candidate := range []string{
		filepath.Join("backend", "config", "rpc_products.yaml"),
		filepath.Join("config", "rpc_products.yaml"),
	} {
		if fileExists(candidate) {
			return candidate
		}
	}
	return ""
}

// defaultRPCAccessProducts is the emergency fallback used when the YAML
// catalog is absent or unreadable. Kept in sync with config/rpc_products.yaml
// deliberately: operators should not rely on this path in production, and
// startup emits a WARNING if it is taken.
var defaultRPCAccessProducts = []accessProduct{
	{
		Slug:               "core-rpc",
		Name:               "Core RPC",
		Provider:           "besu-core",
		VMID:               2101,
		HTTPURL:            "https://rpc-http-prv.d-bis.org",
		WSURL:              "wss://rpc-ws-prv.d-bis.org",
		DefaultTier:        "enterprise",
		RequiresApproval:   true,
		BillingModel:       "contract",
		Description:        "Private Chain 138 Core RPC for operator-grade administration and sensitive workloads.",
		UseCases:           []string{"core deployments", "operator automation", "private infrastructure integration"},
		ManagementFeatures: []string{"dedicated API key", "higher rate ceiling", "operator-oriented access controls"},
	},
	{
		Slug:               "alltra-rpc",
		Name:               "Alltra RPC",
		Provider:           "alltra",
		VMID:               2102,
		HTTPURL:            "http://192.168.11.212:8545",
		WSURL:              "ws://192.168.11.212:8546",
		DefaultTier:        "pro",
		RequiresApproval:   false,
		BillingModel:       "subscription",
		Description:        "Dedicated Alltra-managed RPC lane for partner traffic, subscription access, and API-key-gated usage.",
		UseCases:           []string{"tenant RPC access", "managed partner workloads", "metered commercial usage"},
		ManagementFeatures: []string{"subscription-ready key issuance", "rate governance", "partner-specific traffic lane"},
	},
	{
		Slug:               "thirdweb-rpc",
		Name:               "Thirdweb RPC",
		Provider:           "thirdweb",
		VMID:               2103,
		HTTPURL:            "http://192.168.11.217:8545",
		WSURL:              "ws://192.168.11.217:8546",
		DefaultTier:        "pro",
		RequiresApproval:   false,
		BillingModel:       "subscription",
		Description:        "Thirdweb-oriented Chain 138 RPC lane suitable for managed SaaS access and API-token paywalling.",
		UseCases:           []string{"thirdweb integrations", "commercial API access", "managed dApp traffic"},
		ManagementFeatures: []string{"API token issuance", "usage tiering", "future paywall/subscription hooks"},
	},
}
