// 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"
)

// 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)
}

// 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)

	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":
		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
	case "recipes":
		page, limit := normalizeRecipePage(arguments.Page, arguments.Limit)
		query := strings.TrimSpace(arguments.Query)

		var (
			recipes []cooked.RecipeCard
			err     error
		)
		if query == "" {
			recipes, err = s.backend.ListRecipes(ctx, page, limit)
		} else {
			recipes, err = s.backend.SearchRecipes(ctx, query, page)
			if len(recipes) > limit {
				recipes = recipes[: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
	default:
		return nil, ReadOutput{}, fmt.Errorf("unsupported read target %q in this slice", arguments.Target)
	}
}

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. Use target recipes to list saved recipe IDs and titles, or include query to search saved recipes. Recipe results omit thumbnails. This slice does not read a single recipe.",
		Annotations: &sdk.ToolAnnotations{
			Title:         "Read Cooked data",
			ReadOnlyHint:  true,
			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 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 or recipes."`
	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"`
}

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