package main

import (
	"context"
	"flag"
	"fmt"
	"log/slog"
	"net/http"
	"os"
	"path/filepath"
	"strings"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"

	"fn-registry/registry"
)

const version = "0.1.0"

type config struct {
	httpAddr     string
	bind         string
	enableRun    bool
	enableWrite  bool
	registryRoot string
	logLevel     string
}

func main() {
	var cfg config
	flag.StringVar(&cfg.httpAddr, "http", "", "Listen on HTTP+SSE address (e.g. :7733). Empty = stdio.")
	flag.StringVar(&cfg.bind, "bind", "127.0.0.1", "HTTP bind address. Use 0.0.0.0 only with REGISTRY_API_TOKEN set.")
	flag.BoolVar(&cfg.enableRun, "enable-run", false, "Enable fn_run tool (executes registry functions/pipelines).")
	flag.BoolVar(&cfg.enableWrite, "enable-write", false, "Enable fn_create_function tool (writes files + runs fn index).")
	flag.StringVar(&cfg.registryRoot, "registry-root", "", "Override FN_REGISTRY_ROOT.")
	flag.StringVar(&cfg.logLevel, "log-level", "info", "Log level: debug, info, warn, error.")
	flag.Parse()

	// Slog → stderr (stdio JSON-RPC owns stdout).
	lvl := parseLevel(cfg.logLevel)
	slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: lvl})))

	root, err := resolveRoot(cfg.registryRoot)
	if err != nil {
		slog.Error("resolve registry root", "err", err)
		os.Exit(1)
	}
	dbPath := filepath.Join(root, "registry.db")
	if _, err := os.Stat(dbPath); err != nil {
		slog.Error("registry.db not found", "path", dbPath, "hint", "set FN_REGISTRY_ROOT")
		os.Exit(1)
	}

	db, err := registry.Open(dbPath)
	if err != nil {
		slog.Error("open registry.db", "err", err)
		os.Exit(1)
	}
	defer db.Close()

	srv := server.NewMCPServer(
		"registry_mcp",
		version,
		server.WithToolCapabilities(true),
	)

	registerTools(srv, db, root, cfg)

	slog.Info("starting registry_mcp",
		"version", version,
		"root", root,
		"transport", transportLabel(cfg),
		"enable_run", cfg.enableRun,
		"enable_write", cfg.enableWrite,
	)

	if cfg.httpAddr == "" {
		if err := server.ServeStdio(srv); err != nil {
			slog.Error("stdio server", "err", err)
			os.Exit(1)
		}
		return
	}

	if err := serveHTTP(srv, cfg); err != nil {
		slog.Error("http server", "err", err)
		os.Exit(1)
	}
}

func transportLabel(cfg config) string {
	if cfg.httpAddr == "" {
		return "stdio"
	}
	return fmt.Sprintf("http %s%s", cfg.bind, cfg.httpAddr)
}

func parseLevel(s string) slog.Level {
	switch strings.ToLower(s) {
	case "debug":
		return slog.LevelDebug
	case "warn":
		return slog.LevelWarn
	case "error":
		return slog.LevelError
	default:
		return slog.LevelInfo
	}
}

func resolveRoot(override string) (string, error) {
	if override != "" {
		return filepath.Abs(override)
	}
	if env := os.Getenv("FN_REGISTRY_ROOT"); env != "" {
		return filepath.Abs(env)
	}
	cwd, err := os.Getwd()
	if err != nil {
		return "", err
	}
	dir := cwd
	for {
		if _, err := os.Stat(filepath.Join(dir, "registry.db")); err == nil {
			return dir, nil
		}
		if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
			// Found a go.mod but no registry.db here: keep going up.
		}
		parent := filepath.Dir(dir)
		if parent == dir {
			break
		}
		dir = parent
	}
	return "", fmt.Errorf("registry.db not found upward from %s", cwd)
}

func registerTools(s *server.MCPServer, db *registry.DB, root string, cfg config) {
	deps := &deps{db: db, root: root, cfg: cfg}

	// Read-only tools — always on.
	s.AddTool(searchTool(), mcp.NewTypedToolHandler(deps.handleSearch))
	s.AddTool(showTool(), mcp.NewTypedToolHandler(deps.handleShow))
	s.AddTool(codeTool(), mcp.NewTypedToolHandler(deps.handleCode))
	s.AddTool(listDomainsTool(), mcp.NewTypedToolHandler(deps.handleListDomains))
	s.AddTool(usesTool(), mcp.NewTypedToolHandler(deps.handleUses))
	s.AddTool(doctorTool(), mcp.NewTypedToolHandler(deps.handleDoctor))

	// Mutating tools — opt-in.
	if cfg.enableRun {
		s.AddTool(runTool(), mcp.NewTypedToolHandler(deps.handleRun))
	}
	if cfg.enableWrite {
		s.AddTool(createFunctionTool(), mcp.NewTypedToolHandler(deps.handleCreateFunction))
	}
}

// deps carries state into tool handlers.
type deps struct {
	db   *registry.DB
	root string
	cfg  config
}

// serveHTTP hosts the MCP server over Streamable HTTP with optional bearer auth.
func serveHTTP(s *server.MCPServer, cfg config) error {
	addr := cfg.bind + cfg.httpAddr

	httpSrv := server.NewStreamableHTTPServer(s)

	token := os.Getenv("REGISTRY_API_TOKEN")
	if cfg.bind == "0.0.0.0" && token == "" {
		return fmt.Errorf("--bind 0.0.0.0 requires REGISTRY_API_TOKEN")
	}

	mux := http.NewServeMux()
	if token != "" {
		mux.Handle("/", authMiddleware(token, httpSrv))
	} else {
		mux.Handle("/", httpSrv)
	}

	slog.Info("listening http", "addr", addr)
	return http.ListenAndServe(addr, mux)
}

func authMiddleware(token string, next http.Handler) http.Handler {
	expected := "Bearer " + token
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Header.Get("Authorization") != expected {
			http.Error(w, "unauthorized", http.StatusUnauthorized)
			return
		}
		next.ServeHTTP(w, r)
	})
}

// silence unused-import for context if no tool uses it directly here
var _ = context.Background
