// Package api implements the 'micro api' command — a lightweight
// HTTP-to-RPC gateway that proxies JSON requests to go-micro services.
//
// Usage:
//
//	micro api                        # listen on :8080
//	micro api --address :3000        # custom port
//
// Requests:
//
//	POST /service/endpoint  →  RPC call to service.endpoint
//	GET  /health            →  {"status":"ok"}
//
// The request body is forwarded as-is (JSON). The Micro-Endpoint
// header can also be used to specify the endpoint.
package api

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/signal"
	"sort"
	"strings"
	"syscall"

	"github.com/urfave/cli/v2"
	"go-micro.dev/v5/broker"
	"go-micro.dev/v5/client"
	"go-micro.dev/v5/cmd"
	codecBytes "go-micro.dev/v5/codec/bytes"
	"go-micro.dev/v5/registry"
	"go-micro.dev/v5/store"
)

func init() {
	cmd.Register(&cli.Command{
		Name:  "api",
		Usage: "Run a lightweight HTTP-to-RPC API gateway",
		Description: `Start an HTTP gateway that proxies JSON requests to go-micro services.

Requests are routed by URL path:
  POST /service/endpoint  →  calls service.endpoint via RPC
  GET  /                  →  lists available services and endpoints

Examples:
  # Start on default port
  micro api

  # Custom port
  micro api --address :3000

  # Call a service through the gateway
  curl -XPOST -d '{"name":"Alice"}' http://localhost:8080/greeter/Greeter.Hello

  # Or use the Micro-Endpoint header
  curl -XPOST -H 'Micro-Endpoint: Greeter.Hello' \
    -d '{"name":"Alice"}' http://localhost:8080/greeter`,
		Flags: []cli.Flag{
			&cli.StringFlag{
				Name:    "address",
				Usage:   "Address to listen on",
				Value:   ":8080",
				EnvVars: []string{"MICRO_API_ADDRESS"},
			},
		},
		Action: run,
	})
}

func run(c *cli.Context) error {
	addr := c.String("address")

	mux := http.NewServeMux()

	mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
	})

	// Framework primitives under /micro/
	registerFrameworkRoutes(mux)

	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")

		path := strings.TrimPrefix(r.URL.Path, "/")
		path = strings.TrimSuffix(path, "/")

		// Root: list services
		if path == "" {
			listServices(w)
			return
		}

		// Parse service/endpoint from path
		parts := strings.SplitN(path, "/", 2)
		serviceName := parts[0]
		endpoint := ""
		if len(parts) > 1 {
			endpoint = parts[1]
		}

		// Allow Micro-Endpoint header to override
		if h := r.Header.Get("Micro-Endpoint"); h != "" {
			endpoint = h
		}

		if endpoint == "" {
			describeService(w, serviceName)
			return
		}

		// Proxy RPC call
		body, err := io.ReadAll(r.Body)
		if err != nil {
			writeError(w, http.StatusBadRequest, "failed to read body: "+err.Error())
			return
		}
		if len(body) == 0 {
			body = []byte("{}")
		}

		req := client.DefaultClient.NewRequest(serviceName, endpoint, &codecBytes.Frame{Data: body})
		var rsp codecBytes.Frame

		if err := client.DefaultClient.Call(r.Context(), req, &rsp); err != nil {
			writeError(w, http.StatusInternalServerError, err.Error())
			return
		}

		w.Header().Set("Content-Type", "application/json")
		w.Write(rsp.Data)
	})

	fmt.Println()
	fmt.Println("  \033[1mmicro api\033[0m")
	fmt.Println()
	fmt.Printf("  Listening    \033[36m%s\033[0m\n", addr)
	fmt.Println()
	fmt.Println("  Routes:")
	fmt.Println("    \033[32mGET\033[0m  /                           List services")
	fmt.Println("    \033[32mGET\033[0m  /{service}                  Describe a service")
	fmt.Println("    \033[33mPOST\033[0m /{service}/{endpoint}       Call an endpoint")
	fmt.Println("    \033[32mGET\033[0m  /health                     Health check")
	fmt.Println()
	fmt.Println("  Framework:")
	fmt.Println("    \033[32mGET\033[0m  /micro/registry             List registered services")
	fmt.Println("    \033[32mGET\033[0m  /micro/registry/{name}      Describe a service")
	fmt.Println("    \033[32mGET\033[0m  /micro/store                List store keys")
	fmt.Println("    \033[32mGET\033[0m  /micro/store/{key}          Read a record")
	fmt.Println("    \033[33mPOST\033[0m /micro/store/{key}          Write a record")
	fmt.Println("    \033[33mPOST\033[0m /micro/broker/{topic}       Publish a message")
	fmt.Println()

	server := &http.Server{Addr: addr, Handler: mux}

	go func() {
		if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			fmt.Fprintf(os.Stderr, "server error: %v\n", err)
			os.Exit(1)
		}
	}()

	sig := make(chan os.Signal, 1)
	signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
	<-sig

	fmt.Println("\nShutting down...")
	return server.Close()
}

func listServices(w http.ResponseWriter) {
	services, err := registry.ListServices()
	if err != nil {
		writeError(w, http.StatusInternalServerError, err.Error())
		return
	}

	sort.Slice(services, func(i, j int) bool {
		return services[i].Name < services[j].Name
	})

	type svcInfo struct {
		Name      string   `json:"name"`
		Endpoints []string `json:"endpoints,omitempty"`
	}

	var result []svcInfo
	for _, svc := range services {
		info := svcInfo{Name: svc.Name}
		full, err := registry.GetService(svc.Name)
		if err == nil && len(full) > 0 {
			for _, ep := range full[0].Endpoints {
				info.Endpoints = append(info.Endpoints, ep.Name)
			}
		}
		result = append(result, info)
	}

	json.NewEncoder(w).Encode(result)
}

func describeService(w http.ResponseWriter, name string) {
	services, err := registry.GetService(name)
	if err != nil || len(services) == 0 {
		writeError(w, http.StatusNotFound, "service not found: "+name)
		return
	}

	type epInfo struct {
		Name     string            `json:"name"`
		Metadata map[string]string `json:"metadata,omitempty"`
	}

	svc := services[0]
	var endpoints []epInfo
	for _, ep := range svc.Endpoints {
		endpoints = append(endpoints, epInfo{
			Name:     ep.Name,
			Metadata: ep.Metadata,
		})
	}

	json.NewEncoder(w).Encode(map[string]any{
		"name":      svc.Name,
		"version":   svc.Version,
		"endpoints": endpoints,
		"nodes":     len(svc.Nodes),
	})
}

func writeError(w http.ResponseWriter, code int, msg string) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(code)
	json.NewEncoder(w).Encode(map[string]string{"error": msg})
}

// registerFrameworkRoutes adds /micro/* routes for registry, broker, and store.
func registerFrameworkRoutes(mux *http.ServeMux) {
	// Registry
	mux.HandleFunc("/micro/registry", func(w http.ResponseWriter, r *http.Request) {
		listServices(w)
	})
	mux.HandleFunc("/micro/registry/", func(w http.ResponseWriter, r *http.Request) {
		name := strings.TrimPrefix(r.URL.Path, "/micro/registry/")
		if name == "" {
			listServices(w)
			return
		}
		describeService(w, name)
	})

	// Store
	mux.HandleFunc("/micro/store", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		keys, err := store.List()
		if err != nil {
			writeError(w, http.StatusInternalServerError, err.Error())
			return
		}
		json.NewEncoder(w).Encode(keys)
	})
	mux.HandleFunc("/micro/store/", func(w http.ResponseWriter, r *http.Request) {
		key := strings.TrimPrefix(r.URL.Path, "/micro/store/")
		if key == "" {
			w.Header().Set("Content-Type", "application/json")
			keys, _ := store.List()
			json.NewEncoder(w).Encode(keys)
			return
		}

		w.Header().Set("Content-Type", "application/json")

		switch r.Method {
		case http.MethodGet:
			records, err := store.Read(key)
			if err != nil {
				writeError(w, http.StatusInternalServerError, err.Error())
				return
			}
			if len(records) == 0 {
				writeError(w, http.StatusNotFound, "key not found")
				return
			}
			w.Write(records[0].Value)
		case http.MethodPost:
			body, _ := io.ReadAll(r.Body)
			if err := store.Write(&store.Record{Key: key, Value: body}); err != nil {
				writeError(w, http.StatusInternalServerError, err.Error())
				return
			}
			json.NewEncoder(w).Encode(map[string]string{"status": "ok", "key": key})
		default:
			writeError(w, http.StatusMethodNotAllowed, "use GET or POST")
		}
	})

	// Broker
	mux.HandleFunc("/micro/broker/", func(w http.ResponseWriter, r *http.Request) {
		topic := strings.TrimPrefix(r.URL.Path, "/micro/broker/")
		if topic == "" {
			writeError(w, http.StatusBadRequest, "topic required: /micro/broker/{topic}")
			return
		}
		if r.Method != http.MethodPost {
			writeError(w, http.StatusMethodNotAllowed, "use POST to publish")
			return
		}
		body, _ := io.ReadAll(r.Body)
		b := broker.DefaultBroker
		if err := b.Connect(); err != nil {
			writeError(w, http.StatusInternalServerError, "broker connect: "+err.Error())
			return
		}
		if err := b.Publish(topic, &broker.Message{Body: body}); err != nil {
			writeError(w, http.StatusInternalServerError, err.Error())
			return
		}
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]string{"status": "ok", "topic": topic})
	})
}
