package main

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"

	"fn-registry/registry"
)

func cmdRun(args []string) {
	if len(args) == 0 {
		fmt.Fprintln(os.Stderr, "usage: fn run <id_or_name> [args...]")
		os.Exit(1)
	}

	idOrName := args[0]
	passArgs := args[1:]

	registryRoot := root()
	dbPath := filepath.Join(registryRoot, dbName)

	db, err := registry.Open(dbPath)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: cannot open registry: %v\n", err)
		os.Exit(1)
	}
	defer db.Close()

	fn, err := resolveFunction(db, idOrName)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}

	if fn.FilePath == "" {
		fmt.Fprintf(os.Stderr, "error: %s has no file_path in registry\n", fn.ID)
		os.Exit(1)
	}

	absPath := filepath.Join(registryRoot, fn.FilePath)
	if _, err := os.Stat(absPath); os.IsNotExist(err) {
		fmt.Fprintf(os.Stderr, "error: file not found: %s\n", absPath)
		os.Exit(1)
	}

	cmd, err := buildCommand(fn, db, registryRoot, absPath, passArgs)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}

	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	cmd.Stdin = os.Stdin

	fmt.Fprintf(os.Stderr, "[fn run] %s (%s/%s) %s\n", fn.ID, fn.Lang, fn.Kind, strings.Join(passArgs, " "))

	if err := cmd.Run(); err != nil {
		if exitErr, ok := err.(*exec.ExitError); ok {
			os.Exit(exitErr.ExitCode())
		}
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}
}

func resolveFunction(db *registry.DB, idOrName string) (*registry.Function, error) {
	fn, err := db.GetFunction(idOrName)
	if err == nil {
		return fn, nil
	}

	fns, err := db.GetFunctionsByName(idOrName)
	if err != nil {
		return nil, fmt.Errorf("lookup failed: %w", err)
	}
	if len(fns) == 0 {
		return nil, fmt.Errorf("function %q not found (tried as ID and name)", idOrName)
	}
	if len(fns) == 1 {
		return &fns[0], nil
	}

	var b strings.Builder
	fmt.Fprintf(&b, "ambiguous name %q — found %d matches:\n", idOrName, len(fns))
	for _, f := range fns {
		fmt.Fprintf(&b, "  %s  (%s/%s)\n", f.ID, f.Lang, f.Kind)
	}
	fmt.Fprintf(&b, "use the full ID to disambiguate")
	return nil, fmt.Errorf("%s", b.String())
}

func buildCommand(fn *registry.Function, db *registry.DB, registryRoot, absPath string, args []string) (*exec.Cmd, error) {
	switch fn.Lang {
	case "go":
		return buildGoCommand(fn, registryRoot, absPath, args)
	case "py":
		return buildPyRunnerCommand(fn, db, registryRoot, args)
	case "bash":
		return buildBashCommand(absPath, args)
	case "ts":
		return buildTsCommand(registryRoot, absPath, args)
	case "cpp":
		return buildCppCommand(fn, registryRoot, absPath, args)
	default:
		return nil, fmt.Errorf("unsupported lang %q for execution", fn.Lang)
	}
}

func buildGoCommand(fn *registry.Function, registryRoot, absPath string, args []string) (*exec.Cmd, error) {
	dir := filepath.Dir(absPath)
	env := append(os.Environ(), "CGO_ENABLED=1")

	// If directory has main.go → go run . (pipelines and standalone executables)
	mainGo := filepath.Join(dir, "main.go")
	if _, err := os.Stat(mainGo); err == nil {
		cmdArgs := append([]string{"run", "."}, args...)
		cmd := exec.Command("go", cmdArgs...)
		cmd.Dir = dir
		cmd.Env = env
		return cmd, nil
	}

	// Library code: if it has tests → go test
	if fn.Tested && fn.TestFilePath != "" {
		testAbs := filepath.Join(registryRoot, fn.TestFilePath)
		if _, err := os.Stat(testAbs); err == nil {
			relPkg, _ := filepath.Rel(registryRoot, dir)
			pkgPath := "./" + filepath.ToSlash(relPkg)
			cmdArgs := append([]string{"test", "-v", "-count=1", "-tags", "fts5", pkgPath}, args...)
			cmd := exec.Command("go", cmdArgs...)
			cmd.Dir = registryRoot
			cmd.Env = env
			return cmd, nil
		}
	}

	// No tests: go vet (compilation check)
	relPkg, _ := filepath.Rel(registryRoot, dir)
	pkgPath := "./" + filepath.ToSlash(relPkg)
	cmdArgs := []string{"vet", "-tags", "fts5", pkgPath}
	cmd := exec.Command("go", cmdArgs...)
	cmd.Dir = registryRoot
	cmd.Env = env
	fmt.Fprintf(os.Stderr, "[fn run] %s is library code without tests — running go vet\n", fn.ID)
	return cmd, nil
}


func buildBashCommand(absPath string, args []string) (*exec.Cmd, error) {
	cmdArgs := append([]string{absPath}, args...)
	cmd := exec.Command("bash", cmdArgs...)
	cmd.Dir = filepath.Dir(absPath)
	return cmd, nil
}

func buildTsCommand(registryRoot, absPath string, args []string) (*exec.Cmd, error) {
	tsxBin := filepath.Join(registryRoot, "frontend", "node_modules", ".bin", "tsx")
	if _, err := os.Stat(tsxBin); os.IsNotExist(err) {
		return nil, fmt.Errorf("tsx not found — run: cd frontend && pnpm add -D tsx")
	}

	cmdArgs := append([]string{absPath}, args...)
	cmd := exec.Command(tsxBin, cmdArgs...)
	cmd.Dir = filepath.Dir(absPath)
	return cmd, nil
}
