//
//  Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//

// Package component — e2e fixture stubs and compat shims.
//
// The test fixtures under internal/agent/dsl/testdata reference
// seven component names that are registered here: Retrieval,
// TavilySearch, ExeSQL, Generate, Answer, Iteration,
// IterationItem. Their bodies are deliberately trivial — they
// echo a stable, template-friendly output shape and never call
// the network or DB. The contract is "registered, non-panicking,
// and produces outputs downstream templates can resolve", not
// "do something useful". The Universe A wrappers in
// universe_a_wrappers.go and the real production bodies in
// their own .go files replace these stubs in production paths.
//
// The seven names were chosen by enumerating the component_name
// values in the testdata fixtures (see the `examples` var in
// internal/agent/canvas/dsl_examples_test.go). Keeping the list
// in sync with the fixture set is a single-source-of-truth
// discipline: if a new fixture references a name not in this file,
// the e2e test's compile+invoke loop will surface the gap with a
// clear factory error.
package component

import (
	"context"
	"fmt"

	"ragflow/internal/agent/runtime"
)

// ----- Retrieval -----

const componentNameRetrieval = "Retrieval"

// RetrievalStub is a fixture stub for the Retrieval component. It
// returns an empty `formalized_content` so downstream templates
// that reference `{retrieval:0@formalized_content}` resolve to an
// empty string. The real production component (Dealer / KGSearch
// path) is registered as newRetrievalComponent in
// universe_a_wrappers.go and is the body that actually runs in
// production.
type RetrievalStub struct{}

// NewRetrievalStub constructs a Retrieval stub. params is accepted
// for API parity but unused at this stage (the real component
// parses kb_ids / similarity_threshold / top_n from it).
func NewRetrievalStub(_ map[string]any) (Component, error) {
	return &RetrievalStub{}, nil
}

// Name returns the registered component name.
func (r *RetrievalStub) Name() string { return componentNameRetrieval }

// Invoke returns a stub result that downstream templates can
// resolve. `formalized_content` is the field the test fixtures
// reference; empty string is the safe fixture value.
func (r *RetrievalStub) Invoke(_ context.Context, _ map[string]any) (map[string]any, error) {
	return map[string]any{"formalized_content": ""}, nil
}

// Stream mirrors Invoke as a single-chunk SSE stream.
func (r *RetrievalStub) Stream(ctx context.Context, inputs map[string]any) (<-chan map[string]any, error) {
	out, err := r.Invoke(ctx, inputs)
	if err != nil {
		return nil, err
	}
	ch := make(chan map[string]any, 1)
	ch <- out
	close(ch)
	return ch, nil
}

// Inputs returns the DSL param surface.
func (r *RetrievalStub) Inputs() map[string]string {
	return map[string]string{
		"kb_ids":                     "Knowledge base IDs to search over.",
		"similarity_threshold":       "Minimum vector similarity to include a chunk.",
		"keywords_similarity_weight": "BM25 vs vector blend factor (0 = pure vector, 1 = pure BM25).",
		"top_n":                      "Number of top chunks to keep after rerank.",
		"top_k":                      "Number of candidates to retrieve before rerank.",
		"rerank_id":                  "Optional rerank model identifier.",
		"empty_response":             "Fallback message when no chunks pass the threshold.",
	}
}

// Outputs returns the public output surface.
func (r *RetrievalStub) Outputs() map[string]string {
	return map[string]string{
		"formalized_content": "Rendered chunks for downstream LLM prompts.",
	}
}

// ----- TavilySearch -----

const componentNameTavilySearch = "TavilySearch"

// TavilySearchStub is a fixture stub for the TavilySearch tool. The
// real implementation (see internal/agent/tool/tavily.go) calls
// the Tavily HTTP API; this stub returns an empty result so the
// canvas e2e flow runs without network access.
type TavilySearchStub struct{}

// NewTavilySearchStub constructs a TavilySearch stub.
func NewTavilySearchStub(_ map[string]any) (Component, error) {
	return &TavilySearchStub{}, nil
}

// Name returns the registered component name.
func (t *TavilySearchStub) Name() string { return componentNameTavilySearch }

// Invoke returns an empty `formalized_content` so downstream
// templates resolve.
func (t *TavilySearchStub) Invoke(_ context.Context, _ map[string]any) (map[string]any, error) {
	return map[string]any{"formalized_content": ""}, nil
}

// Stream mirrors Invoke.
func (t *TavilySearchStub) Stream(ctx context.Context, inputs map[string]any) (<-chan map[string]any, error) {
	out, err := t.Invoke(ctx, inputs)
	if err != nil {
		return nil, err
	}
	ch := make(chan map[string]any, 1)
	ch <- out
	close(ch)
	return ch, nil
}

// Inputs returns the DSL param surface.
func (t *TavilySearchStub) Inputs() map[string]string {
	return map[string]string{
		"api_key": "Tavily API key.",
		"query":   "Search query template (may reference {iterationitem:0@result}).",
	}
}

// Outputs returns the public output surface.
func (t *TavilySearchStub) Outputs() map[string]string {
	return map[string]string{
		"formalized_content": "Rendered search results for downstream LLM prompts.",
	}
}

// ----- ExeSQL -----

const componentNameExeSQL = "ExeSQL"
const componentNameCodeExec = "CodeExec"

// ExeSQLStub is a fixture stub for the ExeSQL component. The real
// implementation (see internal/agent/tool/exesql.go) opens a MySQL
// connection and runs the user's SQL; this stub returns a fixed
// two-column schema so the e2e flow runs without a database.
type ExeSQLStub struct{}

// NewExeSQLStub constructs an ExeSQL stub.
func NewExeSQLStub(_ map[string]any) (Component, error) {
	return &ExeSQLStub{}, nil
}

// Name returns the registered component name.
func (e *ExeSQLStub) Name() string { return componentNameExeSQL }

// Invoke returns a stable two-column stub result. Downstream
// templates that render SQL output will see headers + an empty row
// — enough for the message surface to format a string.
func (e *ExeSQLStub) Invoke(_ context.Context, _ map[string]any) (map[string]any, error) {
	return map[string]any{
		"columns": []string{"col1", "col2"},
		"rows":    [][]any{{"", ""}},
		"sql":     "",
	}, nil
}

// Stream mirrors Invoke.
func (e *ExeSQLStub) Stream(ctx context.Context, inputs map[string]any) (<-chan map[string]any, error) {
	out, err := e.Invoke(ctx, inputs)
	if err != nil {
		return nil, err
	}
	ch := make(chan map[string]any, 1)
	ch <- out
	close(ch)
	return ch, nil
}

// Inputs returns the DSL param surface.
func (e *ExeSQLStub) Inputs() map[string]string {
	return map[string]string{
		"database": "Database / schema name.",
		"username": "DB user.",
		"host":     "DB host.",
		"port":     "DB port.",
		"password": "DB password.",
		"top_n":    "Limit on rows returned.",
	}
}

// Outputs returns the public output surface.
func (e *ExeSQLStub) Outputs() map[string]string {
	return map[string]string{
		"columns": "Result-set column names.",
		"rows":    "Result-set rows (matrix form).",
		"sql":     "Resolved SQL string.",
	}
}

// ----- Generate -----

const componentNameGenerate = "Generate"

// GenerateStub is a fixture stub for the legacy "Generate"
// component name. The Python DSL used "Generate" for a
// non-tool-using chat call; the Go port renamed the canonical
// name to "LLM" (see llm.go) and registers "Generate" here as a
// thin alias that routes to the LLM factory. Test fixtures that
// still reference the old name compile and run identically to
// LLM-backed flows.
type GenerateStub struct {
	inner *LLMComponent
}

// NewGenerateStub constructs a Generate stub. params is forwarded to
// the LLM factory so Generate and LLM share the same param surface
// (llm_id, prompt, temperature, message_history_window_size, cite).
func NewGenerateStub(params map[string]any) (Component, error) {
	llmParams, err := buildLLMParamFromV1Params(params)
	if err != nil {
		return nil, fmt.Errorf("Generate: %w", err)
	}
	return &GenerateStub{inner: NewLLMComponent(llmParams)}, nil
}

// Name returns the registered component name.
func (g *GenerateStub) Name() string { return componentNameGenerate }

// Invoke delegates to the LLM component.
func (g *GenerateStub) Invoke(ctx context.Context, inputs map[string]any) (map[string]any, error) {
	return g.inner.Invoke(ctx, inputs)
}

// Stream delegates to the LLM component.
func (g *GenerateStub) Stream(ctx context.Context, inputs map[string]any) (<-chan map[string]any, error) {
	return g.inner.Stream(ctx, inputs)
}

// Inputs returns the DSL param surface. Matches LLM's surface
// plus the v1-only message_history_window_size and cite.
func (g *GenerateStub) Inputs() map[string]string {
	return map[string]string{
		"llm_id":                      "LLM model identifier.",
		"prompt":                      "System / user prompt template.",
		"temperature":                 "Sampling temperature (0 = greedy).",
		"message_history_window_size": "How many prior turns to include.",
		"cite":                        "Whether to include source citations in the output.",
	}
}

// Outputs returns the public output surface.
func (g *GenerateStub) Outputs() map[string]string {
	return map[string]string{
		"content": "Assistant text response.",
		"model":   "Resolved model identifier.",
		"tokens":  "Token count for the call.",
	}
}

// buildLLMParamFromV1Params converts the test-fixture Generate params shape
// into the LLMParam shape. legacy fixtures store the user prompt under "prompt"
// (not "user_prompt") and the system prompt is sometimes empty (the
// system role is often folded into "prompt"). We map: prompt →
// UserPrompt, llm_id → ModelID, temperature → Temperature,
// base_url → BaseURL, api_key → APIKey.
func buildLLMParamFromV1Params(p map[string]any) (LLMParam, error) {
	out := LLMParam{}
	if v, ok := p["llm_id"].(string); ok {
		out.ModelID = v
	}
	if v, ok := p["prompt"].(string); ok {
		out.UserPrompt = v
	}
	if v, ok := p["temperature"].(float64); ok {
		out.Temperature = &v
	}
	if v, ok := p["max_tokens"].(float64); ok {
		i := int(v)
		out.MaxTokens = &i
	}
	if v, ok := p["api_key"].(string); ok {
		out.APIKey = v
	}
	if v, ok := p["base_url"].(string); ok {
		out.BaseURL = v
	}
	return out, nil
}

// ----- Answer -----

const componentNameAnswer = "Answer"

// AnswerStub is a fixture stub for the Answer component. Answer
// is the agent's "wait for user" node (it pairs with ExeSQL or
// Message in conversational flows). The real implementation
// pauses the run and resumes on user input via the eino
// interrupt path (see canvas/interrupt_resume.go); the stub
// returns an empty answer immediately so the e2e flow can
// complete.
type AnswerStub struct{}

// NewAnswerStub constructs an Answer stub.
func NewAnswerStub(_ map[string]any) (Component, error) {
	return &AnswerStub{}, nil
}

// Name returns the registered component name.
func (a *AnswerStub) Name() string { return componentNameAnswer }

// Invoke returns an empty answer. Real implementation will block
// until the user provides input; the stub is fire-and-forget so
// the e2e flow doesn't deadlock.
func (a *AnswerStub) Invoke(ctx context.Context, _ map[string]any) (map[string]any, error) {
	// Mirror the no-state-check pattern of Message/Retrieval: we
	// don't read state, but the signature must match.
	if _, _, err := runtime.GetStateFromContext[*runtime.CanvasState](ctx); err != nil {
		return nil, fmt.Errorf("Answer: %w", err)
	}
	return map[string]any{"answer": ""}, nil
}

// Stream mirrors Invoke.
func (a *AnswerStub) Stream(ctx context.Context, inputs map[string]any) (<-chan map[string]any, error) {
	out, err := a.Invoke(ctx, inputs)
	if err != nil {
		return nil, err
	}
	ch := make(chan map[string]any, 1)
	ch <- out
	close(ch)
	return ch, nil
}

// Inputs returns the DSL param surface.
func (a *AnswerStub) Inputs() map[string]string {
	return map[string]string{
		"question": "Optional clarification question to surface to the user.",
	}
}

// Outputs returns the public output surface.
func (a *AnswerStub) Outputs() map[string]string {
	return map[string]string{
		"answer": "User's response text.",
	}
}

// ----- Iteration / IterationItem (alias to Parallel) -----

// componentNameIteration / componentNameIterationItem are the legacy
// v1 names that the front-end may still emit. The Go port's runtime
// uses "Parallel" for the same concept; the two stubs registered
// below therefore alias through to the real Parallel factory. Any
// caller that bypasses dsl.NormalizeForCanvas (and so still sees the
// legacy names) gets Parallel behaviour for free; the canonical
// path through the decoder boundary uses dsl.NormalizeForCanvas's
// fold step (see internal/agent/dsl/normalize.go) to rewrite
// "Iteration" -> "Parallel" and drop LoopItem/IterationItem nodes
// before they reach the registry.
const (
	componentNameIteration     = "Iteration"
	componentNameIterationItem = "IterationItem"
)

// IterationStub delegates every call to the real Parallel
// component. Existence of this type is purely for registry
// compatibility (the v1 "Iteration" name must resolve) and for
// test introspection (RegisteredNames() still returns "Iteration"
// so parallel_test.go assertions do not churn).
type IterationStub struct {
	inner Component
}

// NewIterationStub constructs an Iteration alias by routing to the
// Parallel factory. Any error from the inner factory is surfaced
// unchanged.
func NewIterationStub(params map[string]any) (Component, error) {
	inner, err := New(componentNameParallel, params)
	if err != nil {
		return nil, err
	}
	return &IterationStub{inner: inner}, nil
}

// Name returns the registered (legacy) component name.
func (i *IterationStub) Name() string { return componentNameIteration }

// Invoke delegates to the inner Parallel component.
func (i *IterationStub) Invoke(ctx context.Context, inputs map[string]any) (map[string]any, error) {
	return i.inner.Invoke(ctx, inputs)
}

// Stream delegates to the inner Parallel component.
func (i *IterationStub) Stream(ctx context.Context, inputs map[string]any) (<-chan map[string]any, error) {
	return i.inner.Stream(ctx, inputs)
}

// Inputs mirrors Parallel's surface for introspection.
func (i *IterationStub) Inputs() map[string]string {
	return i.inner.Inputs()
}

// Outputs mirrors Parallel's surface for introspection.
func (i *IterationStub) Outputs() map[string]string {
	return i.inner.Outputs()
}

// IterationItemStub is a fixture stub for the body node of an
// Iteration. The real wiring (parent_id → child routing) is
// engine-side; the stub itself is a passthrough.
type IterationItemStub struct{}

// NewIterationItemStub constructs an IterationItem stub.
func NewIterationItemStub(_ map[string]any) (Component, error) {
	return &IterationItemStub{}, nil
}

// Name returns the registered component name.
func (it *IterationItemStub) Name() string { return componentNameIterationItem }

// Invoke returns a passthrough empty map.
func (it *IterationItemStub) Invoke(_ context.Context, _ map[string]any) (map[string]any, error) {
	return map[string]any{"result": ""}, nil
}

// Stream mirrors Invoke.
func (it *IterationItemStub) Stream(ctx context.Context, inputs map[string]any) (<-chan map[string]any, error) {
	out, err := it.Invoke(ctx, inputs)
	if err != nil {
		return nil, err
	}
	ch := make(chan map[string]any, 1)
	ch <- out
	close(ch)
	return ch, nil
}

// Inputs returns the DSL param surface.
func (it *IterationItemStub) Inputs() map[string]string {
	return map[string]string{
		"item": "The current iteration item, injected by the Iteration parent.",
	}
}

// Outputs returns the public output surface.
func (it *IterationItemStub) Outputs() map[string]string {
	return map[string]string{
		"result": "Body result for the current item.",
	}
}

// ----- registrations -----

// One init per file keeps the registrations grouped and visible.
// Each Register call panics on a duplicate (the registry enforces
// uniqueness), so accidental double-registration in a later refactor
// surfaces as a panic at init time, not as a silent override.
func init() {
	// Primary registration: Retrieval and ExeSQL go through the
	// Universe A delegation wrappers in universe_a_wrappers.go
	// (real eino tool plumbing). The stubs remain available for
	// unit tests that want to assert the "no service wired" path
	// via a direct constructor.
	Register(componentNameRetrieval, newRetrievalComponent)
	// The Python-side
	// The agent canvas uses both a PascalCase "SearchMyDataset"
	// and the original snake_case typo "search_my_dateset"; an
	// intermediate "search_my_dataset" form also exists in some
	// legacy DSLs. The Universe B tool registry accepts all
	// three (see internal/agent/tool/registry.go); Universe A
	// must mirror that surface or older DSLs fall back to
	// "unknown component" at buildNodeBody time. All four
	// registrations resolve to the same wrapper, so swapping
	// the factory in the future only needs to happen once.
	Register("SearchMyDataset", newRetrievalComponent)
	Register("search_my_dataset", newRetrievalComponent)
	Register("search_my_dateset", newRetrievalComponent)
	Register(componentNameTavilySearch, NewTavilySearchStub)
	Register(componentNameExeSQL, newExeSQLComponent)
	Register(componentNameCodeExec, newCodeExecComponent)
	Register(componentNameGenerate, NewGenerateStub)
	Register(componentNameAnswer, NewAnswerStub)
	Register(componentNameIteration, NewIterationStub)
	Register(componentNameIterationItem, NewIterationItemStub)
}
