// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
//
// SPDX-License-Identifier: LicenseRef-MutuaL-1.2

package mcp

import (
	"context"
	"fmt"
	"regexp"
	"strings"

	sdk "github.com/modelcontextprotocol/go-sdk/mcp"
)

const uuidPattern = `^[ \t\r\n]*[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}[ \t\r\n]*$`

const (
	uuidLength             = len("00000000-0000-0000-0000-000000000000")
	resolveRecipePageLimit = 30
	maxResolveRecipePages  = 100
)

var uuidRegex = regexp.MustCompile(uuidPattern)

func validateRecipeID(recipeID string) error { return validateUUID("recipe_id", recipeID) }

func validateProductGroupID(productGroupID string) error {
	return validateUUID("product_group_id", productGroupID)
}

func validateUUID(field, value string) error {
	if !uuidRegex.MatchString(strings.TrimSpace(value)) {
		return fmt.Errorf("%s must be a UUID", field)
	}

	return nil
}

func validateUUIDPrefix(prefix string) error {
	prefix = strings.TrimSpace(prefix)
	if prefix == "" {
		return fmt.Errorf("prefix is required")
	}
	if len(prefix) > uuidLength {
		return fmt.Errorf("prefix must be a UUID prefix")
	}
	for index, char := range prefix {
		switch index {
		case 8, 13, 18, 23:
			if char != '-' {
				return fmt.Errorf("prefix must be a UUID prefix")
			}
		default:
			if !isHex(char) {
				return fmt.Errorf("prefix must be a UUID prefix")
			}
		}
	}

	return nil
}

func isHex(char rune) bool {
	return char >= '0' && char <= '9' || char >= 'a' && char <= 'f' || char >= 'A' && char <= 'F'
}

func (s *Server) callResolveRecipeIDTool(
	ctx context.Context,
	_ *sdk.CallToolRequest,
	arguments ResolveRecipeIDArguments,
) (*sdk.CallToolResult, ResolveRecipeIDOutput, error) {
	prefix := strings.TrimSpace(arguments.Prefix)
	if err := validateUUIDPrefix(prefix); err != nil {
		return nil, ResolveRecipeIDOutput{}, err
	}

	matches, err := s.resolveRecipeIDMatches(ctx, strings.ToLower(prefix))
	if err != nil {
		return nil, ResolveRecipeIDOutput{}, err
	}

	output := ResolveRecipeIDOutput{Recipes: matches}
	if len(matches) > 1 {
		output.Warning = "Multiple saved recipes match this UUID prefix. Use the full recipe_id before mutating or deleting a recipe."
	}

	return newToolResult(formatResolveRecipeID(prefix, output), output), output, nil
}

func (s *Server) resolveRecipeIDMatches(ctx context.Context, prefix string) ([]RecipeSummary, error) {
	matches := []RecipeSummary{}
	for page := 1; page <= maxResolveRecipePages+1; page++ {
		recipes, err := s.backend.ListRecipes(ctx, page, resolveRecipePageLimit)
		if err != nil {
			return nil, err
		}
		if page > maxResolveRecipePages {
			if len(recipes) > 0 {
				return nil, fmt.Errorf(
					"too many saved recipes to resolve recipe_id prefix; narrow the prefix or use the full recipe_id",
				)
			}

			return matches, nil
		}
		for _, recipe := range recipes {
			if strings.HasPrefix(strings.ToLower(recipe.ID), prefix) {
				matches = append(matches, RecipeSummary{ID: recipe.ID, Title: recipe.Title})
			}
		}
		if len(recipes) < resolveRecipePageLimit {
			return matches, nil
		}
	}

	return matches, nil
}

func resolveRecipeIDTool() *sdk.Tool {
	openWorld := true
	return &sdk.Tool{
		Name:        resolveRecipeIDToolName,
		Title:       "Resolve recipe ID",
		Description: "Resolve a UUID prefix to matching saved Cooked recipe IDs and titles. Use this when a recipe ID was copied partially or needs disambiguation.",
		Annotations: &sdk.ToolAnnotations{
			Title:         "Resolve recipe ID",
			ReadOnlyHint:  true,
			OpenWorldHint: &openWorld,
		},
	}
}

func formatResolveRecipeID(prefix string, output ResolveRecipeIDOutput) string {
	if len(output.Recipes) == 0 {
		return fmt.Sprintf("No saved recipes matched UUID prefix %q.", prefix)
	}

	var builder strings.Builder
	fmt.Fprintf(&builder, "Saved recipes matching UUID prefix %q:\n", prefix)
	for _, recipe := range output.Recipes {
		builder.WriteString("- ")
		builder.WriteString(recipe.Title)
		builder.WriteString(" (id: ")
		builder.WriteString(recipe.ID)
		builder.WriteString(")\n")
	}
	if output.Warning != "" {
		builder.WriteByte('\n')
		builder.WriteString(output.Warning)
	}

	return strings.TrimRight(builder.String(), "\n")
}

// ResolveRecipeIDArguments contains resolve_recipe_id tool arguments.
type ResolveRecipeIDArguments struct {
	Prefix string `json:"prefix" jsonschema:"UUID prefix to resolve against saved recipe IDs."`
}

// ResolveRecipeIDOutput is the structured output for resolve_recipe_id.
type ResolveRecipeIDOutput struct {
	Recipes []RecipeSummary `json:"recipes,omitempty"`
	Warning string          `json:"warning,omitempty"`
}
