package core

import (
	"context"
	"encoding/json"
	"fmt"
	"reflect"
	"strings"

	"ragflow/internal/harness/core/schema"
)

// ---- Reflection-based ToolInfo generation ----

// ToolSchemaOptions configures schema generation.
type ToolSchemaOptions struct {
	DescriptionTag string // struct tag to use for field descriptions (default: "description")
}

// DefaultToolSchemaOptions returns the default schema generation options.
func DefaultToolSchemaOptions() *ToolSchemaOptions {
	return &ToolSchemaOptions{DescriptionTag: "description"}
}

// GenerateToolInfo generates a *schema.ToolInfo from a function's parameter type
// using reflection. The function must have the signature:
//
//	func(ctx context.Context, args *T) (string, error)
//
// where T is a struct with json tags.
func GenerateToolInfo[T any](name string, desc string, fn any, opts ...*ToolSchemaOptions) (*schema.ToolInfo, error) {
	opt := DefaultToolSchemaOptions()
	if len(opts) > 0 && opts[0] != nil {
		opt = opts[0]
	}

	// Build param schema from T
	var param T
	t := reflect.TypeOf(param)
	if t.Kind() == reflect.Ptr {
		t = t.Elem()
	}

	inputSchema, err := structToJSONSchema(t, opt.DescriptionTag)
	if err != nil {
		return nil, fmt.Errorf("generate schema for %s: %w", name, err)
	}

	return &schema.ToolInfo{
		Name:        name,
		Description: desc,
		InputSchema: inputSchema,
	}, nil
}

// structToJSONSchema converts a struct type to a JSON Schema map.
func structToJSONSchema(t reflect.Type, descTag string) (map[string]interface{}, error) {
	if t.Kind() != reflect.Struct {
		// For non-struct types, just use a simple schema
		return map[string]interface{}{
			"type": "object",
			"properties": map[string]interface{}{
				"value": map[string]interface{}{"type": jsonTypeName(t)},
			},
		}, nil
	}

	schema := map[string]interface{}{
		"type":       "object",
		"properties": make(map[string]interface{}),
	}

	props := schema["properties"].(map[string]interface{})
	var requiredFields []string

	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		if !field.IsExported() {
			continue
		}

		propName := fieldNameFromTag(field)
		if propName == "" || propName == "-" {
			continue
		}

		propSchema := fieldToJSONSchema(field, descTag)
		if propSchema != nil {
			// Collect required fields into top-level array per JSON Schema spec
			if field.Tag.Get("required") == "true" {
				requiredFields = append(requiredFields, propName)
			}
			delete(propSchema, "required") // remove from per-property position
			props[propName] = propSchema
		}
	}

	if len(requiredFields) > 0 {
		schema["required"] = requiredFields
	}

	return schema, nil
}

// fieldNameFromTag extracts the JSON field name from a struct field's tags.
func fieldNameFromTag(field reflect.StructField) string {
	if tag := field.Tag.Get("json"); tag != "" {
		return strings.Split(tag, ",")[0]
	}
	return strings.ToLower(field.Name)
}

// fieldToJSONSchema generates a JSON schema for a single struct field.
func fieldToJSONSchema(field reflect.StructField, descTag string) map[string]interface{} {
	s := map[string]interface{}{
		"type": jsonTypeName(field.Type),
	}

	if desc := field.Tag.Get(descTag); desc != "" {
		s["description"] = desc
	}

	if enum := field.Tag.Get("enum"); enum != "" {
		s["enum"] = strings.Split(enum, ",")
	}

	if field.Tag.Get("required") == "true" {
		// Required is handled at the parent schema level (top-level array).
		// This field tag is read in structToJSONSchema.
	}

	// Handle nested structs
	if field.Type.Kind() == reflect.Struct {
		nested, err := structToJSONSchema(field.Type, descTag)
		if err == nil {
			return nested
		}
	}

	// Handle pointer or slice element type
	elemType := field.Type
	if elemType.Kind() == reflect.Ptr || elemType.Kind() == reflect.Slice {
		elemType = elemType.Elem()
		if elemType.Kind() == reflect.Struct {
			nested, err := structToJSONSchema(elemType, descTag)
			if err == nil {
				return nested
			}
		}
		s["type"] = jsonTypeName(elemType)
		if field.Type.Kind() == reflect.Slice {
			s["type"] = "array"
			s["items"] = map[string]interface{}{"type": jsonTypeName(elemType)}
		}
	}

	return s
}

// jsonTypeName returns the JSON Schema type name for a Go type.
func jsonTypeName(t reflect.Type) string {
	switch t.Kind() {
	case reflect.String:
		return "string"
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
		reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
		return "integer"
	case reflect.Float32, reflect.Float64:
		return "number"
	case reflect.Bool:
		return "boolean"
	case reflect.Slice, reflect.Array:
		return "array"
	case reflect.Map:
		return "object"
	default:
		return "string"
	}
}

// ---- ReflectTool: create a Tool from any function ----

// ReflectTool creates a Tool from a function by automatically generating
// the ToolInfo schema via reflection. The function must have the signature:
//
//	func(ctx context.Context, args *T) (string, error)
//
// Example:
//
//	type WeatherArgs struct {
//	    City string `json:"city" description:"The city name"`
//	}
//	tool := ReflectTool("get_weather", "Get current weather", myFunc)
func ReflectTool[T any](name, desc string, fn func(context.Context, *T) (string, error)) (*ReflectToolImpl[T], error) {
	info, err := GenerateToolInfo[T](name, desc, fn)
	if err != nil {
		return nil, err
	}
	return &ReflectToolImpl[T]{
		name: name,
		desc: desc,
		fn:   fn,
		info: info,
	}, nil
}

// ReflectToolImpl is a Tool backed by a function with reflection-generated schema.
type ReflectToolImpl[T any] struct {
	name string
	desc string
	fn   func(context.Context, *T) (string, error)
	info *schema.ToolInfo
}

func (t *ReflectToolImpl[T]) Name() string               { return t.name }
func (t *ReflectToolImpl[T]) Description() string        { return t.desc }
func (t *ReflectToolImpl[T]) ToolInfo() *schema.ToolInfo { return t.info }

func (t *ReflectToolImpl[T]) Invoke(ctx context.Context, argsJSON string, opts ...ToolOption) (string, error) {
	var args T
	if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
		return "", fmt.Errorf("unmarshal args for %s: %w", t.name, err)
	}
	return t.fn(ctx, &args)
}

func (t *ReflectToolImpl[T]) Stream(ctx context.Context, argsJSON string, opts ...ToolOption) (*schema.StreamReader[string], error) {
	result, err := t.Invoke(ctx, argsJSON, opts...)
	if err != nil {
		return nil, err
	}
	return schema.StreamReaderFromArray([]string{result}), nil
}

// MustReflectTool is like ReflectTool but panics on error (for use in init()).
func MustReflectTool[T any](name, desc string, fn func(context.Context, *T) (string, error)) *ReflectToolImpl[T] {
	t, err := ReflectTool(name, desc, fn)
	if err != nil {
		panic(fmt.Sprintf("MustReflectTool(%s): %v", name, err))
	}
	return t
}

// ---- Convenience constructors for migration compatibility ----

// InferTool creates a Tool by reflecting on the struct type T to
// automatically generate the JSON input schema. The function must have
// signature: func(ctx context.Context, args *T) (string, error).
func InferTool[T any](ctx context.Context, fn func(context.Context, *T) (string, error)) (*ReflectToolImpl[T], error) {
	name := reflect.TypeOf((*T)(nil)).Elem().Name()
	return ReflectTool[T](name, "", fn)
}

// InferToolWithName creates a Tool with an explicit name and description,
// using reflection for the input schema.
func InferToolWithName[T any](name, desc string, fn func(context.Context, *T) (string, error)) (*ReflectToolImpl[T], error) {
	return ReflectTool[T](name, desc, fn)
}

// NewTool creates a Tool with an explicitly provided ToolInfo.
// The generic parameter T is the struct type for argument unmarshalling.
func NewTool[T any](info *schema.ToolInfo, fn func(context.Context, *T) (string, error)) Tool {
	return &toolWithInfo[T]{
		info: info,
		fn:   fn,
	}
}

// toolWithInfo is a simple Tool backed by an explicit ToolInfo.
type toolWithInfo[T any] struct {
	info *schema.ToolInfo
	fn   func(context.Context, *T) (string, error)
}

func (t *toolWithInfo[T]) Name() string               { return t.info.Name }
func (t *toolWithInfo[T]) Description() string        { return t.info.Description }
func (t *toolWithInfo[T]) ToolInfo() *schema.ToolInfo { return t.info }

func (t *toolWithInfo[T]) Invoke(ctx context.Context, argsJSON string, opts ...ToolOption) (string, error) {
	var args T
	if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
		return "", fmt.Errorf("unmarshal args for %s: %w", t.info.Name, err)
	}
	return t.fn(ctx, &args)
}

func (t *toolWithInfo[T]) Stream(ctx context.Context, argsJSON string, opts ...ToolOption) (*schema.StreamReader[string], error) {
	result, err := t.Invoke(ctx, argsJSON, opts...)
	if err != nil {
		return nil, err
	}
	return schema.StreamReaderFromArray([]string{result}), nil
}

// GoStructToToolInfo converts a Go struct type T to a *schema.ToolInfo
// for use with NewTool or manual binding.
func GoStructToToolInfo[T any](name, desc string) (*schema.ToolInfo, error) {
	return GenerateToolInfo[T](name, desc, nil)
}
