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

// Package server wires up the MCP server with tools and transport selection.
package server

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"

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

	"git.secluded.site/sb-mcp/internal/config"
	"git.secluded.site/sb-mcp/internal/silverbullet"
)

// Version is set at build time via ldflags.
var Version = "dev"

// New creates a fully-wired MCP server with all tools registered.
func New(cfg *config.Config) *mcp.Server {
	sbClient := silverbullet.New(cfg.SBURL, silverbullet.Auth{
		User:  cfg.SBUser,
		Pass:  cfg.SBPass,
		Token: cfg.SBToken,
	})
	defaultTimeout := cfg.DefaultTimeout
	if defaultTimeout == 0 {
		defaultTimeout = 120
	}

	server := mcp.NewServer(&mcp.Implementation{
		Name:    "sb-mcp",
		Version: Version,
	}, &mcp.ServerOptions{})

	// Register tools with the SB client closed over
	mcp.AddTool(server, &mcp.Tool{
		Name: "execute_lua",
		Description: "Execute a Space Lua script on the SilverBullet instance. " +
			"Always call `usage` first to load the Space Lua reference and API guide.",
		Annotations: &mcp.ToolAnnotations{
			Title:           "Execute Lua",
			ReadOnlyHint:    false,
			DestructiveHint: ptrBool(true),
			IdempotentHint:  false,
			OpenWorldHint:   ptrBool(true),
		},
	}, makeExecuteLuaHandler(sbClient, defaultTimeout))

	mcp.AddTool(server, &mcp.Tool{
		Name:        "screenshot",
		Description: "Capture the current SilverBullet viewport as a PNG image.",
		Annotations: &mcp.ToolAnnotations{
			Title:          "Screenshot",
			ReadOnlyHint:   true,
			IdempotentHint: false,
			OpenWorldHint:  ptrBool(false),
		},
	}, makeScreenshotHandler(sbClient))

	mcp.AddTool(server, &mcp.Tool{
		Name:        "console_logs",
		Description: "Retrieve recent console log entries from SilverBullet for debugging.",
		Annotations: &mcp.ToolAnnotations{
			Title:          "Console Logs",
			ReadOnlyHint:   true,
			IdempotentHint: false,
			OpenWorldHint:  ptrBool(false),
		},
	}, makeConsoleLogsHandler(sbClient))

	mcp.AddTool(server, &mcp.Tool{
		Name: "usage",
		Description: "Load the full SilverBullet usage guide: Space Lua syntax, available APIs (space.*, editor.*, net.*, query), gotchas, and best practices. " +
			"Always call this before `execute_lua`.",
		Annotations: &mcp.ToolAnnotations{
			Title:          "Usage Guide",
			ReadOnlyHint:   true,
			IdempotentHint: true,
			OpenWorldHint:  ptrBool(false),
		},
	}, makeUsageHandler())

	return server
}

// Run starts the MCP server with the appropriate transport.
// If cfg.HTTPAddr is set, uses streamable HTTP; otherwise uses stdio.
func Run(ctx context.Context, cfg *config.Config) error {
	server := New(cfg)

	if cfg.HTTPAddr != "" {
		handler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server {
			return server
		}, &mcp.StreamableHTTPOptions{
			DisableLocalhostProtection: true,
		})
		log.Printf("sb-mcp: MCP server listening on %s", cfg.HTTPAddr)
		return http.ListenAndServe(cfg.HTTPAddr, handler)
	}

	// Stdio transport with logging to stderr
	t := &mcp.LoggingTransport{
		Transport: &mcp.StdioTransport{},
		Writer:    os.Stderr,
	}
	log.Printf("sb-mcp: starting stdio transport")
	return server.Run(ctx, t)
}

// ptrBool returns a pointer to the given bool value.
func ptrBool(b bool) *bool {
	return &b
}

// makeExecuteLuaHandler returns a tool handler that closes over the SB client.
func makeExecuteLuaHandler(client *silverbullet.Client, defaultTimeout int) func(context.Context, *mcp.CallToolRequest, ExecuteLuaParams) (*mcp.CallToolResult, any, error) {
	return func(ctx context.Context, req *mcp.CallToolRequest, params ExecuteLuaParams) (*mcp.CallToolResult, any, error) {
		timeout := params.Timeout
		if timeout == 0 {
			timeout = defaultTimeout
		}

		result, err := client.ExecuteLua(ctx, params.Script, timeout)
		if err != nil {
			return nil, nil, err
		}

		if result.Error != "" {
			return &mcp.CallToolResult{
				Content: []mcp.Content{
					&mcp.TextContent{Text: fmt.Sprintf("Lua error: %s", result.Error)},
				},
				IsError: true,
			}, nil, nil
		}

		formatted := formatLuaResult(result.Result)

		return &mcp.CallToolResult{
			Content: []mcp.Content{
				&mcp.TextContent{Text: formatted},
			},
		}, nil, nil
	}
}

// makeScreenshotHandler returns a tool handler that closes over the SB client.
func makeScreenshotHandler(client *silverbullet.Client) func(context.Context, *mcp.CallToolRequest, ScreenshotParams) (*mcp.CallToolResult, any, error) {
	return func(ctx context.Context, req *mcp.CallToolRequest, params ScreenshotParams) (*mcp.CallToolResult, any, error) {
		data, err := client.Screenshot(ctx)
		if err != nil {
			return nil, nil, err
		}

		return &mcp.CallToolResult{
			Content: []mcp.Content{
				&mcp.ImageContent{
					Data:     data,
					MIMEType: "image/png",
				},
			},
		}, nil, nil
	}
}

// makeConsoleLogsHandler returns a tool handler that closes over the SB client.
func makeConsoleLogsHandler(client *silverbullet.Client) func(context.Context, *mcp.CallToolRequest, ConsoleLogsParams) (*mcp.CallToolResult, any, error) {
	return func(ctx context.Context, req *mcp.CallToolRequest, params ConsoleLogsParams) (*mcp.CallToolResult, any, error) {
		limit := params.Limit
		if limit == 0 {
			limit = 100
		}

		result, err := client.ConsoleLogs(ctx, limit, params.Since)
		if err != nil {
			return nil, nil, err
		}

		logsJSON, err := json.MarshalIndent(result.Logs, "", "  ")
		if err != nil {
			return nil, nil, fmt.Errorf("formatting logs: %w", err)
		}

		return &mcp.CallToolResult{
			Content: []mcp.Content{
				&mcp.TextContent{Text: string(logsJSON)},
			},
		}, nil, nil
	}
}

// makeUsageHandler returns a tool handler that returns the embedded SilverBullet usage guide.
func makeUsageHandler() func(context.Context, *mcp.CallToolRequest, UsageParams) (*mcp.CallToolResult, any, error) {
	return func(_ context.Context, _ *mcp.CallToolRequest, _ UsageParams) (*mcp.CallToolResult, any, error) {
		return &mcp.CallToolResult{
			Content: []mcp.Content{
				&mcp.TextContent{Text: Instructions},
			},
		}, nil, nil
	}
}

// formatLuaResult formats a JSON result from Lua execution for display.
// JSON strings are unescaped to plain text (so markdown content reads cleanly).
// All other values (numbers, bools, null, objects, arrays) pass through as raw JSON.
func formatLuaResult(raw json.RawMessage) string {
	if raw == nil {
		return "null"
	}

	var v any
	if json.Unmarshal(raw, &v) == nil {
		if s, ok := v.(string); ok {
			return s
		}
	}

	return string(raw)
}
