package localai

import (
	"context"
	"crypto/sha256"
	"crypto/subtle"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"

	"github.com/labstack/echo/v4"
	"github.com/mudler/LocalAI/core/services/nodes"
	"github.com/mudler/LocalAI/core/services/testutil"
	"github.com/mudler/LocalAI/pkg/natsauth"
	"github.com/nats-io/nkeys"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

var _ = DescribeTable("token validation",
	func(expectedToken, providedToken string, wantMatch bool) {
		if expectedToken == "" {
			// No auth required — always matches
			Expect(wantMatch).To(BeTrue(), "no-auth should always pass")
			return
		}

		if providedToken == "" {
			Expect(wantMatch).To(BeFalse(), "empty token should be rejected")
			return
		}

		expectedHash := sha256.Sum256([]byte(expectedToken))
		providedHash := sha256.Sum256([]byte(providedToken))
		match := subtle.ConstantTimeCompare(expectedHash[:], providedHash[:]) == 1

		Expect(match).To(Equal(wantMatch))
	},
	Entry("matching tokens", "my-secret-token", "my-secret-token", true),
	Entry("mismatched tokens", "my-secret-token", "wrong-token", false),
	Entry("empty expected (no auth)", "", "any-token", true),
	Entry("empty provided when expected set", "my-secret-token", "", false),
)

var _ = Describe("Node HTTP handlers", func() {
	var (
		registry *nodes.NodeRegistry
	)

	BeforeEach(func() {
		db := testutil.SetupTestDB()
		var err error
		registry, err = nodes.NewNodeRegistry(db)
		Expect(err).ToNot(HaveOccurred())
	})

	Describe("RegisterNodeEndpoint", func() {
		It("registers a backend node and returns 201", func() {
			e := echo.New()
			body := `{"name":"worker-1","address":"10.0.0.1:50051"}`
			req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body))
			req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
			rec := httptest.NewRecorder()
			c := e.NewContext(req, rec)

			handler := RegisterNodeEndpoint(registry, "", true, nil, "", natsauth.Config{})
			Expect(handler(c)).To(Succeed())
			Expect(rec.Code).To(Equal(http.StatusCreated))

			var resp map[string]any
			Expect(json.Unmarshal(rec.Body.Bytes(), &resp)).To(Succeed())
			Expect(resp["name"]).To(Equal("worker-1"))
			Expect(resp["id"]).ToNot(BeEmpty())
			Expect(resp["status"]).To(Equal(nodes.StatusHealthy))
		})

		It("returns nats_jwt when account seed is configured", func() {
			akp, err := nkeys.CreateAccount()
			Expect(err).ToNot(HaveOccurred())
			seed, err := akp.Seed()
			Expect(err).ToNot(HaveOccurred())

			e := echo.New()
			body := `{"name":"worker-nats","address":"10.0.0.2:50051"}`
			req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body))
			req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
			rec := httptest.NewRecorder()
			c := e.NewContext(req, rec)

			natsCfg := natsauth.Config{AccountSeed: string(seed)}
			handler := RegisterNodeEndpoint(registry, "", true, nil, "", natsCfg)
			Expect(handler(c)).To(Succeed())
			Expect(rec.Code).To(Equal(http.StatusCreated))

			var resp map[string]any
			Expect(json.Unmarshal(rec.Body.Bytes(), &resp)).To(Succeed())
			Expect(resp["nats_jwt"]).ToNot(BeEmpty())
		})

		It("returns 400 when name is missing", func() {
			e := echo.New()
			body := `{"address":"10.0.0.1:50051"}`
			req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body))
			req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
			rec := httptest.NewRecorder()
			c := e.NewContext(req, rec)

			handler := RegisterNodeEndpoint(registry, "", true, nil, "", natsauth.Config{})
			Expect(handler(c)).To(Succeed())
			Expect(rec.Code).To(Equal(http.StatusBadRequest))

			var resp map[string]any
			Expect(json.Unmarshal(rec.Body.Bytes(), &resp)).To(Succeed())
			errObj, ok := resp["error"].(map[string]any)
			Expect(ok).To(BeTrue())
			Expect(errObj["message"]).To(ContainSubstring("name is required"))
		})

		It("returns 400 when name exceeds 255 characters", func() {
			e := echo.New()
			longName := strings.Repeat("x", 256)
			body := `{"name":"` + longName + `","address":"10.0.0.1:50051"}`
			req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body))
			req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
			rec := httptest.NewRecorder()
			c := e.NewContext(req, rec)

			handler := RegisterNodeEndpoint(registry, "", true, nil, "", natsauth.Config{})
			Expect(handler(c)).To(Succeed())
			Expect(rec.Code).To(Equal(http.StatusBadRequest))

			var resp map[string]any
			Expect(json.Unmarshal(rec.Body.Bytes(), &resp)).To(Succeed())
			errObj, ok := resp["error"].(map[string]any)
			Expect(ok).To(BeTrue())
			Expect(errObj["message"]).To(ContainSubstring("exceeds 255 characters"))
		})

		It("returns 400 when address is missing for backend node type", func() {
			e := echo.New()
			body := `{"name":"worker-no-addr"}`
			req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body))
			req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
			rec := httptest.NewRecorder()
			c := e.NewContext(req, rec)

			handler := RegisterNodeEndpoint(registry, "", true, nil, "", natsauth.Config{})
			Expect(handler(c)).To(Succeed())
			Expect(rec.Code).To(Equal(http.StatusBadRequest))

			var resp map[string]any
			Expect(json.Unmarshal(rec.Body.Bytes(), &resp)).To(Succeed())
			errObj, ok := resp["error"].(map[string]any)
			Expect(ok).To(BeTrue())
			Expect(errObj["message"]).To(ContainSubstring("address is required"))
		})

		It("returns 400 when node_type is invalid", func() {
			e := echo.New()
			body := `{"name":"bad-type","address":"10.0.0.1:50051","node_type":"invalid"}`
			req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body))
			req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
			rec := httptest.NewRecorder()
			c := e.NewContext(req, rec)

			handler := RegisterNodeEndpoint(registry, "", true, nil, "", natsauth.Config{})
			Expect(handler(c)).To(Succeed())
			Expect(rec.Code).To(Equal(http.StatusBadRequest))

			var resp map[string]any
			Expect(json.Unmarshal(rec.Body.Bytes(), &resp)).To(Succeed())
			errObj, ok := resp["error"].(map[string]any)
			Expect(ok).To(BeTrue())
			Expect(errObj["message"]).To(ContainSubstring("invalid node_type"))
		})

		It("returns 401 when registration token is wrong", func() {
			e := echo.New()
			body := `{"name":"worker-1","address":"10.0.0.1:50051","token":"wrong-token"}`
			req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body))
			req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
			rec := httptest.NewRecorder()
			c := e.NewContext(req, rec)

			handler := RegisterNodeEndpoint(registry, "correct-token", true, nil, "", natsauth.Config{})
			Expect(handler(c)).To(Succeed())
			Expect(rec.Code).To(Equal(http.StatusUnauthorized))
		})

		It("sets status to pending when autoApprove is false", func() {
			e := echo.New()
			body := `{"name":"pending-worker","address":"10.0.0.1:50051"}`
			req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body))
			req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
			rec := httptest.NewRecorder()
			c := e.NewContext(req, rec)

			handler := RegisterNodeEndpoint(registry, "", false, nil, "", natsauth.Config{})
			Expect(handler(c)).To(Succeed())
			Expect(rec.Code).To(Equal(http.StatusCreated))

			var resp map[string]any
			Expect(json.Unmarshal(rec.Body.Bytes(), &resp)).To(Succeed())
			Expect(resp["status"]).To(Equal(nodes.StatusPending))
		})

		// Regression: a worker re-register used to wipe every UI-added label
		// because the endpoint called SetNodeLabels (replace-all) with only
		// what the worker sent. Operators reported "labels assigned to node
		// do not persist" — the labels survived until the next worker
		// restart, then disappeared.
		It("preserves UI-added labels across worker re-register", func() {
			ctx := context.Background()
			e := echo.New()

			// 1. Worker first-registers with one label.
			body1 := `{"name":"worker-merge","address":"10.0.0.50:50051","labels":{"tier":"a","gpu":"a100"}}`
			req1 := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body1))
			req1.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
			rec1 := httptest.NewRecorder()
			handler := RegisterNodeEndpoint(registry, "", true, nil, "", natsauth.Config{})
			Expect(handler(e.NewContext(req1, rec1))).To(Succeed())
			Expect(rec1.Code).To(Equal(http.StatusCreated))

			node, err := registry.GetByName(ctx, "worker-merge")
			Expect(err).ToNot(HaveOccurred())
			Expect(node).ToNot(BeNil())

			// 2. Operator adds a label via the UI.
			Expect(registry.SetNodeLabel(ctx, node.ID, "owner", "ettore")).To(Succeed())

			// 3. Worker restarts and re-registers, sending its own labels
			//    (different from the UI-added one).
			body2 := `{"name":"worker-merge","address":"10.0.0.50:50051","labels":{"tier":"b","gpu":"a100"}}`
			req2 := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body2))
			req2.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
			rec2 := httptest.NewRecorder()
			Expect(handler(e.NewContext(req2, rec2))).To(Succeed())
			Expect(rec2.Code).To(Equal(http.StatusCreated))

			// 4. Assert the UI-added label survived AND the worker labels updated.
			labels, err := registry.GetNodeLabels(ctx, node.ID)
			Expect(err).ToNot(HaveOccurred())
			byKey := map[string]string{}
			for _, l := range labels {
				byKey[l.Key] = l.Value
			}
			Expect(byKey).To(HaveKeyWithValue("owner", "ettore"),
				"UI-added label must survive a worker re-register")
			Expect(byKey).To(HaveKeyWithValue("tier", "b"),
				"worker label updates must apply on re-register")
			Expect(byKey).To(HaveKeyWithValue("gpu", "a100"))
		})
	})

	Describe("SetSchedulingEndpoint", func() {
		postScheduling := func(body string) *httptest.ResponseRecorder {
			e := echo.New()
			req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body))
			req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
			rec := httptest.NewRecorder()
			c := e.NewContext(req, rec)
			handler := SetSchedulingEndpoint(registry)
			Expect(handler(c)).To(Succeed())
			return rec
		}

		It("persists prefix-cache fields and round-trips them via GET", func() {
			ctx := context.Background()
			rec := postScheduling(`{"model_name":"pc-model","route_policy":"prefix_cache","balance_abs_threshold":3,"min_prefix_match":0.4}`)
			Expect(rec.Code).To(Equal(http.StatusOK))

			cfg, err := registry.GetModelScheduling(ctx, "pc-model")
			Expect(err).ToNot(HaveOccurred())
			Expect(cfg).ToNot(BeNil())
			Expect(cfg.RoutePolicy).To(Equal("prefix_cache"))
			Expect(cfg.BalanceAbsThreshold).To(Equal(3))
			Expect(cfg.MinPrefixMatch).To(BeNumerically("~", 0.4, 1e-9))

			e := echo.New()
			getReq := httptest.NewRequest(http.MethodGet, "/", nil)
			getRec := httptest.NewRecorder()
			gc := e.NewContext(getReq, getRec)
			gc.SetParamNames("model")
			gc.SetParamValues("pc-model")
			Expect(GetSchedulingEndpoint(registry)(gc)).To(Succeed())
			Expect(getRec.Code).To(Equal(http.StatusOK))

			var got nodes.ModelSchedulingConfig
			Expect(json.Unmarshal(getRec.Body.Bytes(), &got)).To(Succeed())
			Expect(got.RoutePolicy).To(Equal("prefix_cache"))
			Expect(got.BalanceAbsThreshold).To(Equal(3))
			Expect(got.MinPrefixMatch).To(BeNumerically("~", 0.4, 1e-9))
		})

		It("returns 400 for an out-of-range min_prefix_match", func() {
			rec := postScheduling(`{"model_name":"bad-mpm","min_prefix_match":2}`)
			Expect(rec.Code).To(Equal(http.StatusBadRequest))
			var resp map[string]any
			Expect(json.Unmarshal(rec.Body.Bytes(), &resp)).To(Succeed())
			errObj, ok := resp["error"].(map[string]any)
			Expect(ok).To(BeTrue())
			Expect(errObj["message"]).To(ContainSubstring("min_prefix_match"))
		})

		It("returns 400 for an unknown route_policy", func() {
			rec := postScheduling(`{"model_name":"bad-policy","route_policy":"bogus"}`)
			Expect(rec.Code).To(Equal(http.StatusBadRequest))
			var resp map[string]any
			Expect(json.Unmarshal(rec.Body.Bytes(), &resp)).To(Succeed())
			errObj, ok := resp["error"].(map[string]any)
			Expect(ok).To(BeTrue())
			Expect(errObj["message"]).To(ContainSubstring("route_policy"))
		})

		It("returns 400 for a balance_rel_threshold between 0 and 1", func() {
			rec := postScheduling(`{"model_name":"bad-rel","balance_rel_threshold":0.5}`)
			Expect(rec.Code).To(Equal(http.StatusBadRequest))
			var resp map[string]any
			Expect(json.Unmarshal(rec.Body.Bytes(), &resp)).To(Succeed())
			errObj, ok := resp["error"].(map[string]any)
			Expect(ok).To(BeTrue())
			Expect(errObj["message"]).To(ContainSubstring("balance_rel_threshold"))
		})

		// Regression for the partial-update footgun: a min/max-only POST used to
		// full-replace every column and silently reset the prefix-cache settings
		// to empty/zero. The pointer-merge must preserve omitted prefix fields.
		It("preserves prefix-cache settings across a min_replicas-only update", func() {
			ctx := context.Background()

			rec := postScheduling(`{"model_name":"merge-model","route_policy":"prefix_cache","min_prefix_match":0.4}`)
			Expect(rec.Code).To(Equal(http.StatusOK))

			// Update only min_replicas - omits all prefix-cache fields.
			rec = postScheduling(`{"model_name":"merge-model","min_replicas":2}`)
			Expect(rec.Code).To(Equal(http.StatusOK))

			cfg, err := registry.GetModelScheduling(ctx, "merge-model")
			Expect(err).ToNot(HaveOccurred())
			Expect(cfg).ToNot(BeNil())
			Expect(cfg.MinReplicas).To(Equal(2), "the provided non-prefix field must update")
			Expect(cfg.RoutePolicy).To(Equal("prefix_cache"), "omitted route_policy must be preserved")
			Expect(cfg.MinPrefixMatch).To(BeNumerically("~", 0.4, 1e-9), "omitted min_prefix_match must be preserved")
		})

		It("updates a prefix-cache field when it is explicitly provided", func() {
			ctx := context.Background()

			rec := postScheduling(`{"model_name":"update-model","route_policy":"prefix_cache","min_prefix_match":0.4}`)
			Expect(rec.Code).To(Equal(http.StatusOK))

			rec = postScheduling(`{"model_name":"update-model","route_policy":"round_robin"}`)
			Expect(rec.Code).To(Equal(http.StatusOK))

			cfg, err := registry.GetModelScheduling(ctx, "update-model")
			Expect(err).ToNot(HaveOccurred())
			Expect(cfg).ToNot(BeNil())
			Expect(cfg.RoutePolicy).To(Equal("round_robin"), "explicitly provided route_policy must update")
			Expect(cfg.MinPrefixMatch).To(BeNumerically("~", 0.4, 1e-9), "omitted min_prefix_match must still be preserved")
		})
	})

	Describe("ListNodesEndpoint", func() {
		It("returns an empty list when no nodes are registered", func() {
			e := echo.New()
			req := httptest.NewRequest(http.MethodGet, "/", nil)
			rec := httptest.NewRecorder()
			c := e.NewContext(req, rec)

			handler := ListNodesEndpoint(registry)
			Expect(handler(c)).To(Succeed())
			Expect(rec.Code).To(Equal(http.StatusOK))

			var list []nodes.BackendNode
			Expect(json.Unmarshal(rec.Body.Bytes(), &list)).To(Succeed())
			Expect(list).To(BeEmpty())
		})

		It("returns registered nodes", func() {
			// Register two nodes directly via the registry
			ctx := context.Background()
			Expect(registry.Register(ctx, &nodes.BackendNode{
				Name:    "alpha",
				Address: "10.0.0.1:50051",
			}, true)).To(Succeed())
			Expect(registry.Register(ctx, &nodes.BackendNode{
				Name:    "beta",
				Address: "10.0.0.2:50051",
			}, true)).To(Succeed())

			e := echo.New()
			req := httptest.NewRequest(http.MethodGet, "/", nil)
			rec := httptest.NewRecorder()
			c := e.NewContext(req, rec)

			handler := ListNodesEndpoint(registry)
			Expect(handler(c)).To(Succeed())
			Expect(rec.Code).To(Equal(http.StatusOK))

			var list []nodes.BackendNode
			Expect(json.Unmarshal(rec.Body.Bytes(), &list)).To(Succeed())
			Expect(list).To(HaveLen(2))
			names := []string{list[0].Name, list[1].Name}
			Expect(names).To(ConsistOf("alpha", "beta"))
		})
	})

	Describe("ListAllNodeModelsEndpoint", func() {
		It("returns an empty list when no models are loaded", func() {
			e := echo.New()
			req := httptest.NewRequest(http.MethodGet, "/", nil)
			rec := httptest.NewRecorder()
			c := e.NewContext(req, rec)

			handler := ListAllNodeModelsEndpoint(registry)
			Expect(handler(c)).To(Succeed())
			Expect(rec.Code).To(Equal(http.StatusOK))

			var list []nodes.NodeModel
			Expect(json.Unmarshal(rec.Body.Bytes(), &list)).To(Succeed())
			Expect(list).To(BeEmpty())
		})

		It("returns loaded models across healthy nodes", func() {
			ctx := context.Background()
			Expect(registry.Register(ctx, &nodes.BackendNode{
				ID: "n1", Name: "alpha", Address: "10.0.0.1:50051", Status: nodes.StatusHealthy,
			}, true)).To(Succeed())
			Expect(registry.SetNodeModel(ctx, "n1", "llama-3.3", 0, "loaded", "10.0.0.1:50051", 0)).To(Succeed())

			e := echo.New()
			req := httptest.NewRequest(http.MethodGet, "/", nil)
			rec := httptest.NewRecorder()
			c := e.NewContext(req, rec)

			handler := ListAllNodeModelsEndpoint(registry)
			Expect(handler(c)).To(Succeed())
			Expect(rec.Code).To(Equal(http.StatusOK))

			var list []nodes.NodeModel
			Expect(json.Unmarshal(rec.Body.Bytes(), &list)).To(Succeed())
			Expect(list).To(HaveLen(1))
			Expect(list[0].ModelName).To(Equal("llama-3.3"))
			Expect(list[0].NodeID).To(Equal("n1"))
		})
	})
})
