package resolvemcp

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

	"github.com/mark3labs/mcp-go/mcp"

	"github.com/bitechdev/ResolveSpec/pkg/common"
	"github.com/bitechdev/ResolveSpec/pkg/logger"
	"github.com/bitechdev/ResolveSpec/pkg/reflection"
)

// toolName builds the MCP tool name for a given operation and model.
func toolName(operation, schema, entity string) string {
	if schema == "" {
		return fmt.Sprintf("%s_%s", operation, entity)
	}
	return fmt.Sprintf("%s_%s_%s", operation, schema, entity)
}

// registerModelTools registers the four CRUD tools and resource for a model.
func registerModelTools(h *Handler, schema, entity string, model interface{}) {
	info := buildModelInfo(schema, entity, model)
	registerReadTool(h, schema, entity, info)
	registerCreateTool(h, schema, entity, info)
	registerUpdateTool(h, schema, entity, info)
	registerDeleteTool(h, schema, entity, info)
	registerModelResource(h, schema, entity, info)

	logger.Info("[resolvemcp] Registered MCP tools for %s", info.fullName)
}

// --------------------------------------------------------------------------
// Model introspection
// --------------------------------------------------------------------------

// modelInfo holds pre-computed metadata for a model used in tool descriptions.
type modelInfo struct {
	fullName      string // e.g. "public.users"
	pkName        string // e.g. "id"
	columns       []columnInfo
	relationNames []string
	schemaDoc     string // formatted multi-line schema listing
}

type columnInfo struct {
	jsonName  string
	sqlName   string
	goType    string
	sqlType   string
	isPrimary bool
	isUnique  bool
	isFK      bool
	nullable  bool
}

// buildModelInfo extracts column metadata and pre-builds the schema documentation string.
func buildModelInfo(schema, entity string, model interface{}) modelInfo {
	info := modelInfo{
		fullName: buildModelName(schema, entity),
		pkName:   reflection.GetPrimaryKeyName(model),
	}

	// Unwrap to base struct type
	modelType := reflect.TypeOf(model)
	for modelType != nil && (modelType.Kind() == reflect.Pointer || modelType.Kind() == reflect.Slice) {
		modelType = modelType.Elem()
	}
	if modelType == nil || modelType.Kind() != reflect.Struct {
		return info
	}

	details := reflection.GetModelColumnDetail(reflect.New(modelType).Elem())

	for _, d := range details {
		// Derive the JSON name from the struct field
		jsonName := fieldJSONName(modelType, d.Name)
		if jsonName == "" || jsonName == "-" {
			continue
		}

		// Skip relation fields (slice or user-defined struct that isn't time.Time).
		fieldType, found := modelType.FieldByName(d.Name)
		var unwrappedType reflect.Type
		isSQLType := false
		if found {
			ft := fieldType.Type
			if sqlType, ok := unwrapSQLType(ft); ok {
				unwrappedType = sqlType
				ft = sqlType
				isSQLType = true
			} else if ft.Kind() == reflect.Pointer {
				ft = ft.Elem()
			}
			isUserStruct := ft.Kind() == reflect.Struct && ft.Name() != "Time" && ft.PkgPath() != ""
			if !isSQLType && (ft.Kind() == reflect.Slice || isUserStruct) {
				info.relationNames = append(info.relationNames, jsonName)
				continue
			}
		}

		sqlName := d.SQLName
		if sqlName == "" {
			sqlName = jsonName
		}

		// Derive Go type name, unwrapping pointer if needed.
		goType := d.DataType
		if isSQLType {
			goType = unwrappedType.Name()
		}
		if goType == "" && found {
			ft := fieldType.Type
			for ft.Kind() == reflect.Pointer {
				ft = ft.Elem()
			}
			goType = ft.Name()
		}

		// isPrimary: use both the GORM-tag detection and a name comparison against
		// the known primary key (handles camelCase "primaryKey" tags correctly).
		isPrimary := d.SQLKey == "primary_key" ||
			(info.pkName != "" && (sqlName == info.pkName || jsonName == info.pkName))

		ci := columnInfo{
			jsonName:  jsonName,
			sqlName:   sqlName,
			goType:    goType,
			sqlType:   d.SQLDataType,
			isPrimary: isPrimary,
			isUnique:  d.SQLKey == "unique" || d.SQLKey == "uniqueindex",
			isFK:      d.SQLKey == "foreign_key",
			nullable:  isSQLType || d.Nullable,
		}
		info.columns = append(info.columns, ci)
	}

	info.schemaDoc = buildSchemaDoc(info)
	return info
}

// unwrapSQLType returns the value type wrapped by a spectypes SQL value. These
// types are scalar columns even when their Go representation is a struct or a
// slice (for example, SqlNull[string] and SqlJSONB).
func unwrapSQLType(t reflect.Type) (reflect.Type, bool) {
	for t.Kind() == reflect.Pointer {
		t = t.Elem()
	}

	if t.PkgPath() != "github.com/bitechdev/ResolveSpec/pkg/spectypes" {
		return nil, false
	}
	if t.Kind() == reflect.Struct {
		if value, ok := t.FieldByName("Val"); ok {
			return value.Type, true
		}
	}
	return t, true
}

// fieldJSONName returns the JSON tag name for a struct field, falling back to the field name.
func fieldJSONName(modelType reflect.Type, fieldName string) string {
	field, ok := modelType.FieldByName(fieldName)
	if !ok {
		return fieldName
	}
	tag := field.Tag.Get("json")
	if tag == "" {
		return fieldName
	}
	parts := strings.SplitN(tag, ",", 2)
	if parts[0] == "" {
		return fieldName
	}
	return parts[0]
}

// buildSchemaDoc builds a human-readable column listing for inclusion in tool descriptions.
func buildSchemaDoc(info modelInfo) string {
	if len(info.columns) == 0 {
		return ""
	}

	var sb strings.Builder
	sb.WriteString("Columns:\n")
	for _, c := range info.columns {
		line := fmt.Sprintf("  • %s", c.jsonName)

		typeDesc := c.goType
		if c.sqlType != "" {
			typeDesc = c.sqlType
		}
		if typeDesc != "" {
			line += fmt.Sprintf(" (%s)", typeDesc)
		}

		var flags []string
		if c.isPrimary {
			flags = append(flags, "primary key")
		}
		if c.isUnique {
			flags = append(flags, "unique")
		}
		if c.isFK {
			flags = append(flags, "foreign key")
		}
		if !c.nullable && !c.isPrimary {
			flags = append(flags, "not null")
		} else if c.nullable {
			flags = append(flags, "nullable")
		}
		if len(flags) > 0 {
			line += " — " + strings.Join(flags, ", ")
		}

		sb.WriteString(line + "\n")
	}

	if len(info.relationNames) > 0 {
		sb.WriteString("Relations (preloadable): " + strings.Join(info.relationNames, ", ") + "\n")
	}

	return sb.String()
}

// columnNameList returns a comma-separated list of JSON column names (for descriptions).
func columnNameList(cols []columnInfo) string {
	names := make([]string, len(cols))
	for i, c := range cols {
		names[i] = c.jsonName
	}
	return strings.Join(names, ", ")
}

// writableColumnNames returns JSON names for all non-primary-key columns.
func writableColumnNames(cols []columnInfo) []string {
	var names []string
	for _, c := range cols {
		if !c.isPrimary {
			names = append(names, c.jsonName)
		}
	}
	return names
}

// --------------------------------------------------------------------------
// Read tool
// --------------------------------------------------------------------------

func registerReadTool(h *Handler, schema, entity string, info modelInfo) {
	name := toolName("read", schema, entity)

	var descParts []string
	descParts = append(descParts, fmt.Sprintf("Read records from the '%s' database table.", info.fullName))
	if info.pkName != "" {
		descParts = append(descParts, fmt.Sprintf("Primary key: '%s'. Pass it via 'id' to fetch a single record.", info.pkName))
	}
	if info.schemaDoc != "" {
		descParts = append(descParts, info.schemaDoc)
	}
	descParts = append(descParts,
		"Pagination: use 'limit'/'offset' for offset-based paging, or 'cursor_forward'/'cursor_backward' (pass the primary key value of the last/first record on the current page) for cursor-based paging.",
		"Filtering: each filter object requires 'column' (JSON field name) and 'operator'. Supported operators: = != > < >= <= like ilike in is_null is_not_null. Combine with 'logic_operator': AND (default) or OR.",
		"Sorting: each sort object requires 'column' and 'direction' (asc or desc).",
	)
	if len(info.relationNames) > 0 {
		descParts = append(descParts, fmt.Sprintf("Preloadable relations: %s. Pass relation name in 'preloads'.", strings.Join(info.relationNames, ", ")))
	}

	description := strings.Join(descParts, "\n\n")

	filterDesc := `Array of filter objects. Example: [{"column":"status","operator":"=","value":"active"},{"column":"age","operator":">","value":18,"logic_operator":"AND"}]`
	if len(info.columns) > 0 {
		filterDesc += fmt.Sprintf(" Available columns: %s.", columnNameList(info.columns))
	}

	sortDesc := `Array of sort objects. Example: [{"column":"created_at","direction":"desc"}]`
	if len(info.columns) > 0 {
		sortDesc += fmt.Sprintf(" Available columns: %s.", columnNameList(info.columns))
	}

	tool := mcp.NewTool(name,
		mcp.WithDescription(description),
		mcp.WithString("id",
			mcp.Description(fmt.Sprintf("Primary key (%s) of a single record to fetch. Omit to return multiple records.", info.pkName)),
		),
		mcp.WithNumber("limit",
			mcp.Description("Maximum number of records to return per page. Recommended: 10–100."),
		),
		mcp.WithNumber("offset",
			mcp.Description("Number of records to skip (for offset-based pagination). Use with 'limit'."),
		),
		mcp.WithString("cursor_forward",
			mcp.Description(fmt.Sprintf("Cursor for the next page: pass the '%s' value of the last record on the current page. Requires 'sort' to be set.", info.pkName)),
		),
		mcp.WithString("cursor_backward",
			mcp.Description(fmt.Sprintf("Cursor for the previous page: pass the '%s' value of the first record on the current page. Requires 'sort' to be set.", info.pkName)),
		),
		mcp.WithArray("columns",
			mcp.Description(fmt.Sprintf("Columns to include in the result. Omit to return all columns. Available: %s.", columnNameList(info.columns))),
		),
		mcp.WithArray("omit_columns",
			mcp.Description(fmt.Sprintf("Columns to exclude from the result. Available: %s.", columnNameList(info.columns))),
		),
		mcp.WithArray("filters",
			mcp.Description(filterDesc),
		),
		mcp.WithArray("sort",
			mcp.Description(sortDesc),
		),
		mcp.WithArray("preloads",
			mcp.Description(buildPreloadDesc(info)),
		),
	)

	h.mcpServer.AddTool(tool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		args := req.GetArguments()
		id, _ := args["id"].(string)
		options := parseRequestOptions(args)

		data, metadata, err := h.executeRead(ctx, schema, entity, id, options)
		if err != nil {
			return mcp.NewToolResultError(err.Error()), nil
		}

		return marshalResult(map[string]interface{}{
			"success":  true,
			"data":     data,
			"metadata": metadata,
		})
	})
}

func buildPreloadDesc(info modelInfo) string {
	if len(info.relationNames) == 0 {
		return `Array of relation preload objects. Each object: {"relation":"RelationName"}. No relations defined on this model.`
	}
	return fmt.Sprintf(
		`Array of relation preload objects. Each object: {"relation":"RelationName","columns":["col1","col2"]}. Available relations: %s.`,
		strings.Join(info.relationNames, ", "),
	)
}

// --------------------------------------------------------------------------
// Create tool
// --------------------------------------------------------------------------

func registerCreateTool(h *Handler, schema, entity string, info modelInfo) {
	name := toolName("create", schema, entity)

	writable := writableColumnNames(info.columns)

	var descParts []string
	descParts = append(descParts, fmt.Sprintf("Create one or more new records in the '%s' table.", info.fullName))
	if len(writable) > 0 {
		descParts = append(descParts, fmt.Sprintf("Writable fields: %s.", strings.Join(writable, ", ")))
	}
	if info.pkName != "" {
		descParts = append(descParts, fmt.Sprintf("The primary key ('%s') is typically auto-generated — omit it unless you need to supply it explicitly.", info.pkName))
	}
	descParts = append(descParts,
		"Pass a single JSON object to 'data' to create one record. Pass an array of objects to create multiple records in a single transaction (all succeed or all fail).",
	)
	if info.schemaDoc != "" {
		descParts = append(descParts, info.schemaDoc)
	}

	description := strings.Join(descParts, "\n\n")

	dataDesc := "Record fields to create."
	if len(writable) > 0 {
		dataDesc += fmt.Sprintf(" Writable fields: %s.", strings.Join(writable, ", "))
	}
	dataDesc += " Pass a single object or an array of objects."

	tool := mcp.NewTool(name,
		mcp.WithDescription(description),
		mcp.WithObject("data",
			mcp.Description(dataDesc),
			mcp.Required(),
		),
	)

	h.mcpServer.AddTool(tool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		args := req.GetArguments()
		data, ok := args["data"]
		if !ok {
			return mcp.NewToolResultError("missing required argument: data"), nil
		}

		result, err := h.executeCreate(ctx, schema, entity, data)
		if err != nil {
			return mcp.NewToolResultError(err.Error()), nil
		}

		return marshalResult(map[string]interface{}{
			"success": true,
			"data":    result,
		})
	})
}

// --------------------------------------------------------------------------
// Update tool
// --------------------------------------------------------------------------

func registerUpdateTool(h *Handler, schema, entity string, info modelInfo) {
	name := toolName("update", schema, entity)

	writable := writableColumnNames(info.columns)

	var descParts []string
	descParts = append(descParts, fmt.Sprintf("Update an existing record in the '%s' table.", info.fullName))
	if info.pkName != "" {
		descParts = append(descParts, fmt.Sprintf("Identify the record by its primary key ('%s') via the 'id' argument or by including '%s' inside 'data'.", info.pkName, info.pkName))
	}
	if len(writable) > 0 {
		descParts = append(descParts, fmt.Sprintf("Updatable fields: %s.", strings.Join(writable, ", ")))
	}
	descParts = append(descParts,
		"Only non-null, non-empty fields in 'data' are applied — existing values are preserved for fields you omit. Returns the merged record as stored.",
	)
	if info.schemaDoc != "" {
		descParts = append(descParts, info.schemaDoc)
	}

	description := strings.Join(descParts, "\n\n")

	idDesc := fmt.Sprintf("Primary key ('%s') of the record to update. Can also be included inside 'data'.", info.pkName)

	dataDesc := "Fields to update (non-null, non-empty values are merged into the existing record)."
	if len(writable) > 0 {
		dataDesc += fmt.Sprintf(" Updatable fields: %s.", strings.Join(writable, ", "))
	}

	tool := mcp.NewTool(name,
		mcp.WithDescription(description),
		mcp.WithString("id",
			mcp.Description(idDesc),
		),
		mcp.WithObject("data",
			mcp.Description(dataDesc),
			mcp.Required(),
		),
	)

	h.mcpServer.AddTool(tool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		args := req.GetArguments()
		id, _ := args["id"].(string)

		data, ok := args["data"]
		if !ok {
			return mcp.NewToolResultError("missing required argument: data"), nil
		}
		dataMap, ok := data.(map[string]interface{})
		if !ok {
			return mcp.NewToolResultError("data must be an object"), nil
		}

		result, err := h.executeUpdate(ctx, schema, entity, id, dataMap)
		if err != nil {
			return mcp.NewToolResultError(err.Error()), nil
		}

		return marshalResult(map[string]interface{}{
			"success": true,
			"data":    result,
		})
	})
}

// --------------------------------------------------------------------------
// Delete tool
// --------------------------------------------------------------------------

func registerDeleteTool(h *Handler, schema, entity string, info modelInfo) {
	name := toolName("delete", schema, entity)

	descParts := []string{
		fmt.Sprintf("Delete a record from the '%s' table by its primary key.", info.fullName),
	}
	if info.pkName != "" {
		descParts = append(descParts, fmt.Sprintf("Pass the '%s' value of the record to delete via the 'id' argument.", info.pkName))
	}
	descParts = append(descParts, "Returns the deleted record. This operation is irreversible.")

	description := strings.Join(descParts, " ")

	tool := mcp.NewTool(name,
		mcp.WithDescription(description),
		mcp.WithString("id",
			mcp.Description(fmt.Sprintf("Primary key ('%s') of the record to delete.", info.pkName)),
			mcp.Required(),
		),
	)

	h.mcpServer.AddTool(tool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		args := req.GetArguments()
		id, _ := args["id"].(string)

		result, err := h.executeDelete(ctx, schema, entity, id)
		if err != nil {
			return mcp.NewToolResultError(err.Error()), nil
		}

		return marshalResult(map[string]interface{}{
			"success": true,
			"data":    result,
		})
	})
}

// --------------------------------------------------------------------------
// Resource registration
// --------------------------------------------------------------------------

func registerModelResource(h *Handler, schema, entity string, info modelInfo) {
	resourceURI := info.fullName

	var resourceDesc strings.Builder
	fmt.Fprintf(&resourceDesc, "Database table: %s", info.fullName)
	if info.pkName != "" {
		fmt.Fprintf(&resourceDesc, " (primary key: %s)", info.pkName)
	}
	if info.schemaDoc != "" {
		resourceDesc.WriteString("\n\n")
		resourceDesc.WriteString(info.schemaDoc)
	}

	resource := mcp.NewResource(
		resourceURI,
		entity,
		mcp.WithResourceDescription(resourceDesc.String()),
		mcp.WithMIMEType("application/json"),
	)

	h.mcpServer.AddResource(resource, func(ctx context.Context, req mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
		limit := 100
		options := common.RequestOptions{Limit: &limit}

		data, metadata, err := h.executeRead(ctx, schema, entity, "", options)
		if err != nil {
			return nil, err
		}

		payload := map[string]interface{}{
			"data":     data,
			"metadata": metadata,
		}
		jsonBytes, err := json.Marshal(payload)
		if err != nil {
			return nil, fmt.Errorf("error marshaling resource: %w", err)
		}

		return []mcp.ResourceContents{
			mcp.TextResourceContents{
				URI:      req.Params.URI,
				MIMEType: "application/json",
				Text:     string(jsonBytes),
			},
		}, nil
	})
}

// --------------------------------------------------------------------------
// Argument parsing helpers
// --------------------------------------------------------------------------

// parseRequestOptions converts raw MCP tool arguments into common.RequestOptions.
func parseRequestOptions(args map[string]interface{}) common.RequestOptions {
	options := common.RequestOptions{}

	if v, ok := args["limit"]; ok {
		switch n := v.(type) {
		case float64:
			limit := int(n)
			options.Limit = &limit
		case int:
			options.Limit = &n
		}
	}

	if v, ok := args["offset"]; ok {
		switch n := v.(type) {
		case float64:
			offset := int(n)
			options.Offset = &offset
		case int:
			options.Offset = &n
		}
	}

	if v, ok := args["cursor_forward"].(string); ok {
		options.CursorForward = v
	}
	if v, ok := args["cursor_backward"].(string); ok {
		options.CursorBackward = v
	}

	options.Columns = parseStringArray(args["columns"])
	options.OmitColumns = parseStringArray(args["omit_columns"])
	options.Filters = parseFilters(args["filters"])
	options.Sort = parseSortOptions(args["sort"])
	options.Preload = parsePreloadOptions(args["preloads"])

	return options
}

func parseStringArray(raw interface{}) []string {
	if raw == nil {
		return nil
	}
	items, ok := raw.([]interface{})
	if !ok {
		return nil
	}
	result := make([]string, 0, len(items))
	for _, item := range items {
		if s, ok := item.(string); ok {
			result = append(result, s)
		}
	}
	return result
}

func parseFilters(raw interface{}) []common.FilterOption {
	if raw == nil {
		return nil
	}
	items, ok := raw.([]interface{})
	if !ok {
		return nil
	}
	result := make([]common.FilterOption, 0, len(items))
	for _, item := range items {
		b, err := json.Marshal(item)
		if err != nil {
			continue
		}
		var f common.FilterOption
		if err := json.Unmarshal(b, &f); err != nil {
			continue
		}
		if f.Column == "" || f.Operator == "" {
			continue
		}
		if strings.EqualFold(f.LogicOperator, "or") {
			f.LogicOperator = "OR"
		} else {
			f.LogicOperator = "AND"
		}
		result = append(result, f)
	}
	return result
}

func parseSortOptions(raw interface{}) []common.SortOption {
	if raw == nil {
		return nil
	}
	items, ok := raw.([]interface{})
	if !ok {
		return nil
	}
	result := make([]common.SortOption, 0, len(items))
	for _, item := range items {
		b, err := json.Marshal(item)
		if err != nil {
			continue
		}
		var s common.SortOption
		if err := json.Unmarshal(b, &s); err != nil {
			continue
		}
		if s.Column == "" {
			continue
		}
		result = append(result, s)
	}
	return result
}

func parsePreloadOptions(raw interface{}) []common.PreloadOption {
	if raw == nil {
		return nil
	}
	items, ok := raw.([]interface{})
	if !ok {
		return nil
	}
	result := make([]common.PreloadOption, 0, len(items))
	for _, item := range items {
		b, err := json.Marshal(item)
		if err != nil {
			continue
		}
		var p common.PreloadOption
		if err := json.Unmarshal(b, &p); err != nil {
			continue
		}
		if p.Relation == "" {
			continue
		}
		result = append(result, p)
	}
	return result
}

// marshalResult marshals a value to JSON and returns it as an MCP text result.
func marshalResult(v interface{}) (*mcp.CallToolResult, error) {
	b, err := json.Marshal(v)
	if err != nil {
		return mcp.NewToolResultError(fmt.Sprintf("error marshaling result: %v", err)), nil
	}
	return mcp.NewToolResultText(string(b)), nil
}
