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

// Package mcp implements the MCP surface for cooked-mcp.
package mcp

import (
	"context"
	"fmt"
	"strings"

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

	"git.secluded.site/cooked-mcp/internal/cooked"
)

const (
	serverName                = "Cooked"
	readToolName              = "read"
	previewRecipeTextToolName = "preview_recipe_text"
	saveRecipeToolName        = "save_recipe"
)

// Backend provides the Cooked operations exposed as MCP tools.
type Backend interface {
	ReadShoppingList(ctx context.Context) (cooked.ShoppingList, error)
	ListRecipes(ctx context.Context, page, limit int) ([]cooked.RecipeCard, error)
	SearchRecipes(ctx context.Context, query string, page int) ([]cooked.RecipeCard, error)
	ReadRecipeMetadata(ctx context.Context, recipeID string) (cooked.RecipeMetadata, error)
	ReadRecipeContent(ctx context.Context, recipeID string) (cooked.RecipeContent, error)
	PreviewRecipeText(ctx context.Context, title, text string) (cooked.RecipeTextPreview, error)
	SavePreparedRecipe(ctx context.Context, title, markdown string, portions int) (string, error)
}

// Server exposes Cooked as an MCP server.
type Server struct {
	backend Backend
	sdk     *sdk.Server
}

// NewServer returns an MCP server for a Cooked backend.
func NewServer(backend Backend, version string) *Server {
	server := &Server{backend: backend}
	server.sdk = sdk.NewServer(&sdk.Implementation{Name: serverName, Title: serverName, Version: version}, nil)
	sdk.AddTool(server.sdk, readTool(), server.callReadTool)
	sdk.AddTool(server.sdk, previewRecipeTextTool(), server.callPreviewRecipeTextTool)
	sdk.AddTool(server.sdk, saveRecipeTool(), server.callSaveRecipeTool)

	return server
}

// RunStdio serves MCP over the official SDK stdio transport.
func (s *Server) RunStdio(ctx context.Context) error {
	return s.sdk.Run(ctx, &sdk.StdioTransport{})
}

// CallReadTool invokes the read tool without asserting MCP wire presentation.
func (s *Server) CallReadTool(ctx context.Context, arguments ReadArguments) (ReadOutput, error) {
	_, output, err := s.callReadTool(ctx, nil, arguments)
	if err != nil {
		return ReadOutput{}, err
	}

	return output, nil
}

func (s *Server) callReadTool(
	ctx context.Context,
	_ *sdk.CallToolRequest,
	arguments ReadArguments,
) (*sdk.CallToolResult, ReadOutput, error) {
	switch arguments.Target {
	case "shopping_list":
		return s.callShoppingListReadTool(ctx)
	case "recipe":
		return s.callRecipeReadTool(ctx, arguments.RecipeID)
	case "recipes":
		return s.callRecipesReadTool(ctx, arguments)
	default:
		return nil, ReadOutput{}, fmt.Errorf("unsupported read target %q in this slice", arguments.Target)
	}
}

func (s *Server) callShoppingListReadTool(ctx context.Context) (*sdk.CallToolResult, ReadOutput, error) {
	shoppingList, err := s.backend.ReadShoppingList(ctx)
	if err != nil {
		return nil, ReadOutput{}, err
	}

	output := ReadOutput{
		Aisles:              shoppingList.Aisles,
		ShoppingListRecipes: shoppingList.Recipes,
	}

	return &sdk.CallToolResult{
		Content: []sdk.Content{&sdk.TextContent{Text: formatShoppingList(shoppingList)}},
	}, output, nil
}

func (s *Server) callRecipeReadTool(ctx context.Context, rawRecipeID string) (*sdk.CallToolResult, ReadOutput, error) {
	recipeID := strings.TrimSpace(rawRecipeID)
	if recipeID == "" {
		return nil, ReadOutput{}, fmt.Errorf("recipe_id is required when target is recipe")
	}

	metadata, err := s.backend.ReadRecipeMetadata(ctx, recipeID)
	if err != nil {
		return nil, ReadOutput{}, err
	}
	content, err := s.backend.ReadRecipeContent(ctx, recipeID)
	if err != nil {
		return nil, ReadOutput{}, err
	}

	recipe := RecipeDetail{
		ID:             recipeID,
		Title:          metadata.Title,
		Owner:          metadata.Owner,
		EditPermission: metadata.EditPermission,
		Content:        content.Content,
		Portions:       content.Portions,
	}
	output := ReadOutput{Recipe: &recipe}

	return &sdk.CallToolResult{Content: []sdk.Content{&sdk.TextContent{Text: formatRecipe(recipe)}}}, output, nil
}

func (s *Server) callRecipesReadTool(
	ctx context.Context,
	arguments ReadArguments,
) (*sdk.CallToolResult, ReadOutput, error) {
	page, limit := normalizeRecipePage(arguments.Page, arguments.Limit)
	recipes, err := s.readRecipeCards(ctx, strings.TrimSpace(arguments.Query), page, limit)
	if err != nil {
		return nil, ReadOutput{}, err
	}

	output := ReadOutput{Recipes: recipeSummaries(recipes)}

	return &sdk.CallToolResult{Content: []sdk.Content{&sdk.TextContent{Text: formatRecipes(recipes)}}}, output, nil
}

func (s *Server) readRecipeCards(ctx context.Context, query string, page, limit int) ([]cooked.RecipeCard, error) {
	if query == "" {
		return s.backend.ListRecipes(ctx, page, limit)
	}

	recipes, err := s.backend.SearchRecipes(ctx, query, page)
	if err != nil {
		return nil, err
	}
	if len(recipes) > limit {
		recipes = recipes[:limit]
	}

	return recipes, nil
}

func (s *Server) callPreviewRecipeTextTool(
	ctx context.Context,
	_ *sdk.CallToolRequest,
	arguments PreviewRecipeTextArguments,
) (*sdk.CallToolResult, PreviewRecipeTextOutput, error) {
	title := strings.TrimSpace(arguments.Title)
	if title == "" {
		return nil, PreviewRecipeTextOutput{}, fmt.Errorf("title is required")
	}
	if strings.TrimSpace(arguments.Text) == "" {
		return nil, PreviewRecipeTextOutput{}, fmt.Errorf("text is required")
	}

	preview, err := s.backend.PreviewRecipeText(ctx, title, arguments.Text)
	if err != nil {
		return nil, PreviewRecipeTextOutput{}, err
	}

	output := PreviewRecipeTextOutput{
		Title:    preview.Title,
		Markdown: preview.Markdown,
		Portions: preview.Portions,
	}

	return &sdk.CallToolResult{
		Content: []sdk.Content{&sdk.TextContent{Text: formatRecipeTextPreview(output)}},
	}, output, nil
}

func (s *Server) callSaveRecipeTool(
	ctx context.Context,
	_ *sdk.CallToolRequest,
	arguments SaveRecipeArguments,
) (*sdk.CallToolResult, SaveRecipeOutput, error) {
	source := strings.TrimSpace(arguments.Source)
	switch source {
	case "prepared":
		title := strings.TrimSpace(arguments.Title)
		if title == "" {
			return nil, SaveRecipeOutput{}, fmt.Errorf("title is required when source is prepared")
		}
		if strings.TrimSpace(arguments.Markdown) == "" {
			return nil, SaveRecipeOutput{}, fmt.Errorf("markdown is required when source is prepared")
		}
		if arguments.Portions < 1 {
			return nil, SaveRecipeOutput{}, fmt.Errorf("portions is required when source is prepared")
		}

		recipeID, err := s.backend.SavePreparedRecipe(ctx, title, arguments.Markdown, arguments.Portions)
		if err != nil {
			return nil, SaveRecipeOutput{}, err
		}
		if strings.TrimSpace(recipeID) == "" {
			return nil, SaveRecipeOutput{}, fmt.Errorf("save recipe response missing recipe ID")
		}

		output := SaveRecipeOutput{RecipeID: recipeID}

		return &sdk.CallToolResult{
			Content: []sdk.Content{&sdk.TextContent{Text: formatRecipeSave(output)}},
		}, output, nil
	case "":
		return nil, SaveRecipeOutput{}, fmt.Errorf("source is required")
	default:
		return nil, SaveRecipeOutput{}, fmt.Errorf("unsupported save_recipe source %q in this slice", source)
	}
}

func readTool() *sdk.Tool {
	openWorld := true
	return &sdk.Tool{
		Name:        readToolName,
		Title:       "Read Cooked data",
		Description: "Read Cooked data. Use target shopping_list for the current shopping list, target recipes to list or search saved recipe IDs and titles, or target recipe with recipe_id to read a single recipe. Recipe results omit images and thumbnails.",
		Annotations: &sdk.ToolAnnotations{
			Title:         "Read Cooked data",
			ReadOnlyHint:  true,
			OpenWorldHint: &openWorld,
		},
	}
}

func previewRecipeTextTool() *sdk.Tool {
	openWorld := true
	return &sdk.Tool{
		Name:        previewRecipeTextToolName,
		Title:       "Preview recipe text",
		Description: "Preview raw recipe text as Cooked recipe markdown and portions without saving or updating a recipe.",
		Annotations: &sdk.ToolAnnotations{
			Title:         "Preview recipe text",
			ReadOnlyHint:  true,
			OpenWorldHint: &openWorld,
		},
	}
}

func saveRecipeTool() *sdk.Tool {
	openWorld := true
	return &sdk.Tool{
		Name:        saveRecipeToolName,
		Title:       "Save recipe",
		Description: "Save a recipe. This slice supports source prepared with title, markdown, and portions. Other source values are reserved for later slices.",
		Annotations: &sdk.ToolAnnotations{
			Title:         "Save recipe",
			OpenWorldHint: &openWorld,
		},
	}
}

func normalizeRecipePage(page, limit int) (int, int) {
	if page < 1 {
		page = 1
	}
	if limit < 1 {
		limit = 10
	}
	if limit > 30 {
		limit = 30
	}

	return page, limit
}

func formatShoppingList(shoppingList cooked.ShoppingList) string {
	if len(shoppingList.Aisles) == 0 {
		return "Shopping list is empty."
	}

	var builder strings.Builder
	builder.WriteString("Shopping list:\n")
	for _, aisle := range shoppingList.Aisles {
		builder.WriteString("- ")
		builder.WriteString(aisle.Name)
		builder.WriteString(":")
		if len(aisle.ProductGroups) == 0 {
			builder.WriteString(" no items\n")
			continue
		}
		builder.WriteByte('\n')

		for _, product := range aisle.ProductGroups {
			builder.WriteString("  - ")
			builder.WriteString(product.Name)
			if product.Quantity != "" {
				builder.WriteString(" — ")
				builder.WriteString(product.Quantity)
			}
			builder.WriteString(" (id: ")
			builder.WriteString(product.ID)
			builder.WriteString(")")
			if product.Selected {
				builder.WriteString(" selected")
			}
			builder.WriteByte('\n')
		}
	}

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

func formatRecipes(recipes []cooked.RecipeCard) string {
	if len(recipes) == 0 {
		return "No saved recipes found."
	}

	var builder strings.Builder
	builder.WriteString("Saved recipes:\n")
	for _, recipe := range recipes {
		builder.WriteString("- ")
		builder.WriteString(recipe.Title)
		builder.WriteString(" (id: ")
		builder.WriteString(recipe.ID)
		builder.WriteString(")\n")
	}

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

func formatRecipe(recipe RecipeDetail) string {
	title := recipe.Title
	if title == "" {
		title = "(untitled recipe)"
	}

	var builder strings.Builder
	builder.WriteString("Recipe: ")
	builder.WriteString(title)
	builder.WriteString(" (id: ")
	builder.WriteString(recipe.ID)
	builder.WriteString(")\n")
	if recipe.Owner != "" {
		builder.WriteString("Owner: ")
		builder.WriteString(recipe.Owner)
		builder.WriteByte('\n')
	}
	fmt.Fprintf(&builder, "Edit permission: %t\n", recipe.EditPermission)
	fmt.Fprintf(&builder, "Portions: %d\n", recipe.Portions)

	content := strings.TrimSpace(recipe.Content)
	if content == "" {
		builder.WriteString("\nNo recipe content returned.")
	} else {
		builder.WriteString("\n")
		builder.WriteString(content)
	}

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

func formatRecipeTextPreview(preview PreviewRecipeTextOutput) string {
	title := preview.Title
	if title == "" {
		title = "(untitled preview)"
	}

	var builder strings.Builder
	builder.WriteString("Recipe text preview: ")
	builder.WriteString(title)
	builder.WriteByte('\n')
	fmt.Fprintf(&builder, "Portions: %d\n", preview.Portions)

	markdown := strings.TrimSpace(preview.Markdown)
	if markdown == "" {
		builder.WriteString("\nNo preview markdown returned.")
	} else {
		builder.WriteString("\n")
		builder.WriteString(markdown)
	}

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

func formatRecipeSave(output SaveRecipeOutput) string {
	return "Saved recipe (id: " + output.RecipeID + ")."
}

func recipeSummaries(recipes []cooked.RecipeCard) []RecipeSummary {
	summaries := make([]RecipeSummary, 0, len(recipes))
	for _, recipe := range recipes {
		summaries = append(summaries, RecipeSummary{ID: recipe.ID, Title: recipe.Title})
	}

	return summaries
}

// ReadArguments contains read tool arguments.
type ReadArguments struct {
	Target   string `json:"target"              jsonschema:"Cooked object to read. Use shopping_list, recipes, or recipe."`
	RecipeID string `json:"recipe_id,omitempty" jsonschema:"Recipe ID for target recipe."`
	Query    string `json:"query,omitempty"     jsonschema:"Search query for target recipes. Omit to list saved recipes."`
	Page     int    `json:"page,omitempty"      jsonschema:"Page number for target recipes. Defaults to 1."`
	Limit    int    `json:"limit,omitempty"     jsonschema:"Maximum recipe cards for target recipes. Defaults to 10 and caps at 30."`
}

// ReadOutput is the structured output for the current read tool slice.
type ReadOutput struct {
	Aisles              []cooked.Aisle  `json:"aisles,omitempty"`
	ShoppingListRecipes []string        `json:"shopping_list_recipes,omitempty"`
	Recipes             []RecipeSummary `json:"recipes,omitempty"`
	Recipe              *RecipeDetail   `json:"recipe,omitempty"`
}

// RecipeSummary is the MCP-safe saved recipe summary.
type RecipeSummary struct {
	ID    string `json:"id"`
	Title string `json:"title"`
}

// RecipeDetail is the MCP-safe single recipe output.
type RecipeDetail struct {
	ID             string `json:"id"`
	Title          string `json:"title"`
	Owner          string `json:"owner"`
	EditPermission bool   `json:"edit_permission"`
	Content        string `json:"content"`
	Portions       int    `json:"portions"`
}

// PreviewRecipeTextArguments contains preview_recipe_text tool arguments.
type PreviewRecipeTextArguments struct {
	Title string `json:"title" jsonschema:"Recipe title for the preview."`
	Text  string `json:"text"  jsonschema:"Raw recipe text to preview without saving."`
}

// PreviewRecipeTextOutput is the structured output for preview_recipe_text.
type PreviewRecipeTextOutput struct {
	Title    string `json:"title"`
	Markdown string `json:"markdown"`
	Portions int    `json:"portions"`
}

// SaveRecipeArguments contains save_recipe tool arguments.
type SaveRecipeArguments struct {
	Source   string `json:"source"             jsonschema:"Recipe save source. Supported now: prepared."`
	Title    string `json:"title,omitempty"    jsonschema:"Recipe title for prepared saves."`
	Markdown string `json:"markdown,omitempty" jsonschema:"Recipe markdown for prepared saves."`
	Portions int    `json:"portions,omitempty" jsonschema:"Recipe portions for prepared saves."`
}

// SaveRecipeOutput is the structured output for save_recipe.
type SaveRecipeOutput struct {
	RecipeID string `json:"recipe_id,omitempty"`
}
