// Package server wires the MCP protocol layer to the tools registry:
// it registers the 14 registry tools on an MCP server and routes every
// tools/call to tools.Dispatch through a backend client built from the
// runtime configuration.
package server

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"

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

	"security-center-mcp/internal/client"
	"security-center-mcp/internal/config"
	"security-center-mcp/internal/tools"
)

const (
	// serverName and serverVersion identify this server in the MCP
	// initialize handshake.
	serverName    = "security-center-mcp"
	serverVersion = "0.1.0"
)

// New builds the MCP server: every registry tool is exposed with its
// name, description and input schema verbatim; schemas are handed to
// the SDK as raw JSON so validation stays in the tools layer.
func New(cfg config.Config) *mcp.Server {
	c := client.New(cfg)
	s := mcp.NewServer(&mcp.Implementation{
		Name:    serverName,
		Version: serverVersion,
	}, nil)
	for _, tool := range tools.Registry() {
		schema, err := json.Marshal(tool.InputSchema)
		if err != nil {
			panic(fmt.Sprintf("server: marshal input schema of %q: %v", tool.Name, err))
		}
		s.AddTool(&mcp.Tool{
			Name:        tool.Name,
			Description: tool.Description,
			InputSchema: json.RawMessage(schema),
		}, callHandler(c))
	}
	return s
}

// callHandler returns the ToolHandler shared by every registered tool:
// it decodes the raw arguments and delegates to tools.Dispatch.
//
// Error mapping (plan §4.4):
//   - malformed JSON arguments and *tools.ArgError surface as JSON-RPC
//     invalid params (-32602), since they are call-shape problems;
//   - structured backend/transport errors (Dispatch's {"error": ...}
//     result) are returned as tool results with IsError set, so the LLM
//     can read them and self-correct;
//   - anything else (e.g. an unknown tool name, which the SDK already
//     rejects before reaching the handler) is a plain protocol error.
func callHandler(c *client.Client) mcp.ToolHandler {
	return func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		args := map[string]any{}
		if len(req.Params.Arguments) > 0 {
			if err := json.Unmarshal(req.Params.Arguments, &args); err != nil {
				return nil, invalidParams("arguments must be a JSON object: %v", err)
			}
		}

		data, err := tools.Dispatch(ctx, c, req.Params.Name, args)
		if err != nil {
			var argErr *tools.ArgError
			if errors.As(err, &argErr) {
				return nil, invalidParams("%s", argErr.Msg)
			}
			return nil, err
		}

		payload, err := json.Marshal(data)
		if err != nil {
			return nil, fmt.Errorf("server: marshal result of %q: %w", req.Params.Name, err)
		}
		return &mcp.CallToolResult{
			Content: []mcp.Content{&mcp.TextContent{Text: string(payload)}},
			IsError: isStructuredError(data),
		}, nil
	}
}

// invalidParams builds a JSON-RPC invalid params (-32602) error. The
// SDK preserves the code of a *jsonrpc.Error on the wire.
func invalidParams(format string, args ...any) *jsonrpc.Error {
	return &jsonrpc.Error{
		Code:    jsonrpc.CodeInvalidParams,
		Message: fmt.Sprintf(format, args...),
	}
}

// isStructuredError reports whether data is Dispatch's structured
// error result ({"error": {...}}), as opposed to ordinary tool data.
func isStructuredError(data any) bool {
	m, ok := data.(map[string]any)
	if !ok {
		return false
	}
	_, hasError := m["error"]
	return hasError
}
