package main

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"text/tabwriter"

	"fn-registry/registry"
)

const dbName = "registry.db"

func main() {
	if len(os.Args) < 2 {
		printUsage()
		os.Exit(1)
	}

	switch os.Args[1] {
	case "index":
		cmdIndex()
	case "search":
		cmdSearch(os.Args[2:])
	case "list":
		cmdList(os.Args[2:])
	case "show":
		cmdShow(os.Args[2:])
	case "add":
		cmdAdd(os.Args[2:])
	case "ops":
		cmdOps(os.Args[2:])
	case "proposal":
		cmdProposal(os.Args[2:])
	case "run":
		cmdRun(os.Args[2:])
	case "check":
		cmdCheck(os.Args[2:])
	case "project":
		cmdProject(os.Args[2:])
	case "app":
		cmdApp(os.Args[2:])
	case "analysis":
		cmdAnalysis(os.Args[2:])
	case "sync":
		cmdSync(os.Args[2:])
	case "vault":
		cmdVault(os.Args[2:])
	case "doctor":
		cmdDoctor(os.Args[2:])
	case "match":
		cmdMatch(os.Args[2:])
	case "help", "-h", "--help":
		printUsage()
	default:
		fmt.Fprintf(os.Stderr, "unknown command: %s\n", os.Args[1])
		printUsage()
		os.Exit(1)
	}
}

func printUsage() {
	fmt.Println(`fn — registry CLI

Usage:
  fn index                                 Regenera registry.db desde los .md
  fn search [-k kind] [-p purity] [-l lang] [-d domain] <query>
  fn list   [-k kind] [-d domain] [-l lang]
  fn show   <id>                           Muestra entrada completa
  fn add    [-k kind]                      Abre $EDITOR con template
  fn run    <id_or_name> [args...]          Ejecuta funcion/pipeline (go/py/bash)
  fn check  params                          Lista funciones sin params_schema
  fn ops    <subcommand>                   Gestiona operations.db (fn ops help)
  fn proposal <add|list|show|update>       Gestiona proposals
  fn project <init|list|show|status>       Gestiona proyectos
  fn app     <list|clone|pull>             Gestiona apps externas (Gitea)
  fn analysis <list|clone|pull>            Gestiona analyses externas (Gitea)
  fn sync    [status|locations]            Sincroniza con servidor central
  fn vault   <list|search|index|info>      Gestiona y busca en data vaults
  fn doctor  [artefacts|services|sync|uses-functions|unused] [--json]
                                           Diagnostico read-only del registry
  fn match   [--top N] [--format json|text] [--min-score F] "<cmd>"
                                           Fuzzy match entre comando shell y funciones del registry`)
}

func root() string {
	// Walk up from the binary or cwd to find go.mod
	dir, _ := os.Getwd()
	for {
		if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
			return dir
		}
		parent := filepath.Dir(dir)
		if parent == dir {
			break
		}
		dir = parent
	}
	// Fallback to cwd
	cwd, _ := os.Getwd()
	return cwd
}

func openDB() *registry.DB {
	db, err := registry.Open(filepath.Join(root(), dbName))
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}
	return db
}

// --- index ---

func cmdIndex() {
	r := root()
	db, err := registry.Open(filepath.Join(r, dbName))
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}
	defer db.Close()

	result, err := registry.Index(db, r)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}

	// Flush WAL to main db file so external readers (e.g. Metabase) see changes.
	db.WalCheckpoint()

	// Sync registry.db to Metabase mount directory if it exists.
	metabaseCopy := filepath.Join(r, ".metabase-registry", "registry.db")
	if _, err := os.Stat(filepath.Dir(metabaseCopy)); err == nil {
		src := filepath.Join(r, dbName)
		data, err := os.ReadFile(src)
		if err == nil {
			if err := os.WriteFile(metabaseCopy, data, 0666); err != nil {
				fmt.Fprintf(os.Stderr, "warning: could not sync to metabase: %v\n", err)
			}
		}
	}

	fmt.Printf("Indexed %d functions, %d types, %d apps, %d analysis, %d projects, %d vaults, %d modules, %d unit_tests\n",
		result.Functions, result.Types, result.Apps, result.Analysis, result.Projects, result.Vaults, result.Modules, result.UnitTests)
	for _, e := range result.ValidationErrors {
		fmt.Fprintf(os.Stderr, "  INVALID: %s\n", e)
	}
	for _, w := range result.Warnings {
		fmt.Fprintf(os.Stderr, "  WARN: %s\n", w)
	}
	for _, e := range result.Errors {
		fmt.Fprintf(os.Stderr, "  ERROR: %s\n", e)
	}
}

// --- search ---

func cmdSearch(args []string) {
	var kind, purity, lang, domain, query string
	i := 0
	for i < len(args) {
		switch args[i] {
		case "-k":
			i++
			kind = args[i]
		case "-p":
			i++
			purity = args[i]
		case "-l":
			i++
			lang = args[i]
		case "-d":
			i++
			domain = args[i]
		default:
			query = args[i]
		}
		i++
	}

	db := openDB()
	defer db.Close()

	fns, err := db.SearchFunctions(query, registry.Kind(kind), registry.Purity(purity), lang, domain)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}

	types, err := db.SearchTypes(query, lang, domain)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}

	apps, err := db.SearchApps(query, lang, domain)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}

	analyses, err := db.SearchAnalysis(query, lang, domain)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}

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

	if len(fns) == 0 && len(types) == 0 && len(apps) == 0 && len(analyses) == 0 && len(projects) == 0 {
		fmt.Println("No results.")
		return
	}

	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
	if len(fns) > 0 {
		fmt.Fprintln(w, "KIND\tID\tPURITY\tDESCRIPTION")
		for _, f := range fns {
			desc := truncate(f.Description, 60)
			fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", f.Kind, f.ID, f.Purity, desc)
		}
	}
	if len(types) > 0 {
		if len(fns) > 0 {
			fmt.Fprintln(w)
		}
		fmt.Fprintln(w, "ALGEBRAIC\tID\tDESCRIPTION")
		for _, t := range types {
			desc := truncate(t.Description, 60)
			fmt.Fprintf(w, "%s\t%s\t%s\n", t.Algebraic, t.ID, desc)
		}
	}
	if len(apps) > 0 {
		if len(fns) > 0 || len(types) > 0 {
			fmt.Fprintln(w)
		}
		fmt.Fprintln(w, "APP\tID\tLANG\tDESCRIPTION")
		for _, a := range apps {
			desc := truncate(a.Description, 60)
			fmt.Fprintf(w, "app\t%s\t%s\t%s\n", a.ID, a.Lang, desc)
		}
	}
	if len(analyses) > 0 {
		if len(fns) > 0 || len(types) > 0 || len(apps) > 0 {
			fmt.Fprintln(w)
		}
		fmt.Fprintln(w, "ANALYSIS\tID\tLANG\tDESCRIPTION")
		for _, a := range analyses {
			desc := truncate(a.Description, 60)
			fmt.Fprintf(w, "analysis\t%s\t%s\t%s\n", a.ID, a.Lang, desc)
		}
	}
	if len(projects) > 0 {
		if len(fns) > 0 || len(types) > 0 || len(apps) > 0 || len(analyses) > 0 {
			fmt.Fprintln(w)
		}
		fmt.Fprintln(w, "PROJECT\tID\tDESCRIPTION")
		for _, p := range projects {
			desc := truncate(p.Description, 60)
			fmt.Fprintf(w, "project\t%s\t%s\n", p.ID, desc)
		}
	}
	w.Flush()
}

// --- list ---

func cmdList(args []string) {
	var kind, domain, lang string
	i := 0
	for i < len(args) {
		switch args[i] {
		case "-k":
			i++
			kind = args[i]
		case "-d":
			i++
			domain = args[i]
		case "-l":
			i++
			lang = args[i]
		}
		i++
	}

	db := openDB()
	defer db.Close()

	fns, err := db.SearchFunctions("", registry.Kind(kind), "", lang, domain)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}

	types, err := db.SearchTypes("", lang, domain)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}

	apps, err := db.SearchApps("", lang, domain)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}

	analyses, err := db.SearchAnalysis("", lang, domain)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}

	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
	if len(fns) > 0 {
		fmt.Fprintln(w, "KIND\tID\tPURITY\tVERSION\tDOMAIN")
		for _, f := range fns {
			fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", f.Kind, f.ID, f.Purity, f.Version, f.Domain)
		}
	}
	if len(types) > 0 {
		if len(fns) > 0 {
			fmt.Fprintln(w)
		}
		fmt.Fprintln(w, "ALGEBRAIC\tID\tVERSION\tDOMAIN")
		for _, t := range types {
			fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", t.Algebraic, t.ID, t.Version, t.Domain)
		}
	}
	if len(apps) > 0 {
		if len(fns) > 0 || len(types) > 0 {
			fmt.Fprintln(w)
		}
		fmt.Fprintln(w, "APP\tID\tLANG\tDOMAIN")
		for _, a := range apps {
			fmt.Fprintf(w, "app\t%s\t%s\t%s\n", a.ID, a.Lang, a.Domain)
		}
	}
	if len(analyses) > 0 {
		if len(fns) > 0 || len(types) > 0 || len(apps) > 0 {
			fmt.Fprintln(w)
		}
		fmt.Fprintln(w, "ANALYSIS\tID\tLANG\tDOMAIN")
		for _, a := range analyses {
			fmt.Fprintf(w, "analysis\t%s\t%s\t%s\n", a.ID, a.Lang, a.Domain)
		}
	}
	projects, err := db.ListAllProjects()
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}

	if len(projects) > 0 {
		if len(fns) > 0 || len(types) > 0 || len(apps) > 0 || len(analyses) > 0 {
			fmt.Fprintln(w)
		}
		fmt.Fprintln(w, "PROJECT\tID\tDESCRIPTION")
		for _, p := range projects {
			fmt.Fprintf(w, "project\t%s\t%s\n", p.ID, truncate(p.Description, 60))
		}
	}
	if len(fns) == 0 && len(types) == 0 && len(apps) == 0 && len(analyses) == 0 && len(projects) == 0 {
		fmt.Println("Registry is empty. Run 'fn index' first.")
	}
	w.Flush()
}

// --- show ---

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

	db := openDB()
	defer db.Close()

	f, errF := db.GetFunction(id)
	if errF == nil {
		printFunction(f)
		return
	}

	t, errT := db.GetType(id)
	if errT == nil {
		printType(t)
		return
	}

	a, errA := db.GetApp(id)
	if errA == nil {
		printApp(a)
		return
	}

	an, errAn := db.GetAnalysis(id)
	if errAn == nil {
		printAnalysisEntry(an)
		return
	}

	p, errP := db.GetProject(id)
	if errP == nil {
		printProjectEntry(p)
		return
	}

	v, errV := db.GetVault(id)
	if errV == nil {
		printVaultEntry(v)
		return
	}

	m, errM := db.GetModule(id)
	if errM == nil {
		printModule(m)
		return
	}

	fmt.Fprintf(os.Stderr, "not found: %s\n", id)
	os.Exit(1)
}

func printModule(m *registry.Module) {
	fmt.Printf("ID:          %s\n", m.ID)
	fmt.Printf("Name:        %s\n", m.Name)
	fmt.Printf("Version:     %s\n", m.Version)
	fmt.Printf("Lang:        %s\n", m.Lang)
	fmt.Printf("Description: %s\n", m.Description)
	if len(m.Members) > 0 {
		fmt.Printf("Members:     %s\n", strings.Join(m.Members, ", "))
	}
	if len(m.Tags) > 0 {
		fmt.Printf("Tags:        %s\n", strings.Join(m.Tags, ", "))
	}
	if m.DirPath != "" {
		fmt.Printf("DirPath:     %s\n", m.DirPath)
	}
	if m.RepoURL != "" {
		fmt.Printf("RepoURL:     %s\n", m.RepoURL)
	}
	if m.Documentation != "" {
		fmt.Printf("\nDocumentation:\n%s\n", m.Documentation)
	}
	if m.Notes != "" {
		fmt.Printf("\nNotes:\n%s\n", m.Notes)
	}
}

func printFunction(f *registry.Function) {
	fmt.Printf("ID:          %s\n", f.ID)
	fmt.Printf("Name:        %s\n", f.Name)
	fmt.Printf("Kind:        %s\n", f.Kind)
	fmt.Printf("Lang:        %s\n", f.Lang)
	fmt.Printf("Domain:      %s\n", f.Domain)
	fmt.Printf("Version:     %s\n", f.Version)
	fmt.Printf("Purity:      %s\n", f.Purity)
	fmt.Printf("Signature:   %s\n", f.Signature)
	fmt.Printf("Description: %s\n", f.Description)
	fmt.Printf("Tags:        %s\n", strings.Join(f.Tags, ", "))
	fmt.Printf("File:        %s\n", f.FilePath)
	if len(f.UsesFunctions) > 0 {
		fmt.Printf("Uses fns:    %s\n", strings.Join(f.UsesFunctions, ", "))
	}
	if len(f.UsesTypes) > 0 {
		fmt.Printf("Uses types:  %s\n", strings.Join(f.UsesTypes, ", "))
	}
	if len(f.Returns) > 0 {
		fmt.Printf("Returns:     %s\n", strings.Join(f.Returns, ", "))
	}
	if f.ErrorType != "" {
		fmt.Printf("Error type:  %s\n", f.ErrorType)
	}
	if len(f.Imports) > 0 {
		fmt.Printf("Imports:     %s\n", strings.Join(f.Imports, ", "))
	}
	if f.ParamsSchema != "" {
		fmt.Printf("Params:      %s\n", f.ParamsSchema)
	}
	if f.Example != "" {
		fmt.Printf("\nExample:\n%s\n", f.Example)
	}
	if f.Notes != "" {
		fmt.Printf("\nNotes:\n%s\n", f.Notes)
	}
	if f.Documentation != "" {
		fmt.Printf("\nDocumentation:\n%s\n", f.Documentation)
	}
	if f.Code != "" {
		fmt.Printf("\nCode:\n%s\n", f.Code)
	}
	if f.SourceRepo != "" {
		fmt.Printf("Source repo: %s\n", f.SourceRepo)
		fmt.Printf("Source license: %s\n", f.SourceLicense)
		fmt.Printf("Source file: %s\n", f.SourceFile)
	}
	if f.Kind == registry.KindComponent {
		fmt.Printf("Framework:   %s\n", f.Framework)
		if f.HasState != nil {
			fmt.Printf("Has state:   %v\n", *f.HasState)
		}
		if len(f.Emits) > 0 {
			fmt.Printf("Emits:       %s\n", strings.Join(f.Emits, ", "))
		}
	}
}

func printType(t *registry.Type) {
	fmt.Printf("ID:          %s\n", t.ID)
	fmt.Printf("Name:        %s\n", t.Name)
	fmt.Printf("Lang:        %s\n", t.Lang)
	fmt.Printf("Domain:      %s\n", t.Domain)
	fmt.Printf("Version:     %s\n", t.Version)
	fmt.Printf("Algebraic:   %s\n", t.Algebraic)
	fmt.Printf("Description: %s\n", t.Description)
	fmt.Printf("Tags:        %s\n", strings.Join(t.Tags, ", "))
	fmt.Printf("File:        %s\n", t.FilePath)
	if len(t.UsesTypes) > 0 {
		fmt.Printf("Uses types:  %s\n", strings.Join(t.UsesTypes, ", "))
	}
	if t.SourceRepo != "" {
		fmt.Printf("Source repo: %s\n", t.SourceRepo)
		fmt.Printf("Source license: %s\n", t.SourceLicense)
		fmt.Printf("Source file: %s\n", t.SourceFile)
	}
	if t.Definition != "" {
		fmt.Printf("\nDefinition:\n%s\n", t.Definition)
	}
	if t.Examples != "" {
		fmt.Printf("\nExamples:\n%s\n", t.Examples)
	}
	if t.Notes != "" {
		fmt.Printf("\nNotes:\n%s\n", t.Notes)
	}
	if t.Documentation != "" {
		fmt.Printf("\nDocumentation:\n%s\n", t.Documentation)
	}
	if t.Code != "" {
		fmt.Printf("\nCode:\n%s\n", t.Code)
	}
}

func printApp(a *registry.App) {
	fmt.Printf("ID:          %s\n", a.ID)
	fmt.Printf("Name:        %s\n", a.Name)
	fmt.Printf("Lang:        %s\n", a.Lang)
	fmt.Printf("Domain:      %s\n", a.Domain)
	fmt.Printf("Description: %s\n", a.Description)
	fmt.Printf("Tags:        %s\n", strings.Join(a.Tags, ", "))
	fmt.Printf("Dir:         %s\n", a.DirPath)
	if a.RepoURL != "" {
		fmt.Printf("Repo URL:    %s\n", a.RepoURL)
	}
	if a.Framework != "" {
		fmt.Printf("Framework:   %s\n", a.Framework)
	}
	if a.EntryPoint != "" {
		fmt.Printf("Entry point: %s\n", a.EntryPoint)
	}
	if len(a.UsesFunctions) > 0 {
		fmt.Printf("Uses fns:    %s\n", strings.Join(a.UsesFunctions, ", "))
	}
	if len(a.UsesTypes) > 0 {
		fmt.Printf("Uses types:  %s\n", strings.Join(a.UsesTypes, ", "))
	}
	if len(a.UsesModules) > 0 {
		fmt.Printf("Uses mods:   %s\n", strings.Join(a.UsesModules, ", "))
	}
	if a.Notes != "" {
		fmt.Printf("\nNotes:\n%s\n", a.Notes)
	}
	if a.Documentation != "" {
		fmt.Printf("\nDocumentation:\n%s\n", a.Documentation)
	}
}

func printAnalysisEntry(a *registry.Analysis) {
	fmt.Printf("ID:          %s\n", a.ID)
	fmt.Printf("Name:        %s\n", a.Name)
	fmt.Printf("Lang:        %s\n", a.Lang)
	fmt.Printf("Domain:      %s\n", a.Domain)
	fmt.Printf("Description: %s\n", a.Description)
	fmt.Printf("Tags:        %s\n", strings.Join(a.Tags, ", "))
	fmt.Printf("Dir:         %s\n", a.DirPath)
	if a.RepoURL != "" {
		fmt.Printf("Repo URL:    %s\n", a.RepoURL)
	}
	if a.Framework != "" {
		fmt.Printf("Framework:   %s\n", a.Framework)
	}
	if a.EntryPoint != "" {
		fmt.Printf("Entry point: %s\n", a.EntryPoint)
	}
	if len(a.UsesFunctions) > 0 {
		fmt.Printf("Uses fns:    %s\n", strings.Join(a.UsesFunctions, ", "))
	}
	if len(a.UsesTypes) > 0 {
		fmt.Printf("Uses types:  %s\n", strings.Join(a.UsesTypes, ", "))
	}
	if a.Notes != "" {
		fmt.Printf("\nNotes:\n%s\n", a.Notes)
	}
	if a.Documentation != "" {
		fmt.Printf("\nDocumentation:\n%s\n", a.Documentation)
	}
}

func printProjectEntry(p *registry.Project) {
	fmt.Printf("ID:          %s\n", p.ID)
	fmt.Printf("Name:        %s\n", p.Name)
	fmt.Printf("Description: %s\n", p.Description)
	fmt.Printf("Tags:        %s\n", strings.Join(p.Tags, ", "))
	fmt.Printf("Dir:         %s\n", p.DirPath)
	if p.RepoURL != "" {
		fmt.Printf("Repo URL:    %s\n", p.RepoURL)
	}
	if p.Notes != "" {
		fmt.Printf("\nNotes:\n%s\n", p.Notes)
	}
	if p.Documentation != "" {
		fmt.Printf("\nDocumentation:\n%s\n", p.Documentation)
	}
}

func printVaultEntry(v *registry.Vault) {
	fmt.Printf("ID:          %s\n", v.ID)
	fmt.Printf("Name:        %s\n", v.Name)
	if v.ProjectID != "" {
		fmt.Printf("Project:     %s\n", v.ProjectID)
	}
	fmt.Printf("Description: %s\n", v.Description)
	if v.Path != "" {
		fmt.Printf("Path:        %s\n", v.Path)
	}
	fmt.Printf("Symlink:     %v\n", v.Symlink)
	fmt.Printf("Tags:        %s\n", strings.Join(v.Tags, ", "))
}

// --- check ---

func cmdCheck(args []string) {
	if len(args) < 1 {
		fmt.Fprintln(os.Stderr, "usage: fn check <subcommand>\n\nSubcommands:\n  params    Lista funciones sin params_schema documentado")
		os.Exit(1)
	}
	switch args[0] {
	case "params":
		cmdCheckParams()
	default:
		fmt.Fprintf(os.Stderr, "unknown check subcommand: %s\n", args[0])
		os.Exit(1)
	}
}

func cmdCheckParams() {
	db := openDB()
	defer db.Close()

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

	var missing []registry.Function
	for _, f := range fns {
		if f.ParamsSchema == "" {
			missing = append(missing, f)
		}
	}

	if len(missing) == 0 {
		fmt.Printf("All %d functions have params_schema documented.\n", len(fns))
		return
	}

	fmt.Printf("%d/%d functions missing params_schema:\n\n", len(missing), len(fns))
	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
	fmt.Fprintln(w, "ID\tLANG\tDOMAIN\tKIND")
	for _, f := range missing {
		fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", f.ID, f.Lang, f.Domain, f.Kind)
	}
	w.Flush()
}

// --- add ---

func cmdAdd(args []string) {
	kind := "function"
	for i := 0; i < len(args); i++ {
		if args[i] == "-k" {
			i++
			kind = args[i]
		}
	}

	editor := os.Getenv("EDITOR")
	if editor == "" {
		editor = "vi"
	}

	r := root()
	var templatePath string
	switch kind {
	case "function":
		templatePath = filepath.Join(r, "docs", "templates", "function.md")
	case "pipeline":
		templatePath = filepath.Join(r, "docs", "templates", "pipeline.md")
	case "component":
		templatePath = filepath.Join(r, "docs", "templates", "component.md")
	case "app":
		templatePath = filepath.Join(r, "docs", "templates", "app.md")
	case "analysis":
		templatePath = filepath.Join(r, "docs", "templates", "analysis.md")
	case "project":
		templatePath = filepath.Join(r, "docs", "templates", "project.md")
	default:
		fmt.Fprintf(os.Stderr, "unknown kind: %s (use function, pipeline, component, app, analysis, or project)\n", kind)
		os.Exit(1)
	}

	if _, err := os.Stat(templatePath); os.IsNotExist(err) {
		fmt.Fprintf(os.Stderr, "template not found: %s\n", templatePath)
		os.Exit(1)
	}

	fmt.Printf("Template: %s\n", templatePath)
	fmt.Printf("Copy and edit the template, then run 'fn index' to register it.\n")
	fmt.Printf("\n  cp %s functions/<domain>/<name>.md\n  $EDITOR functions/<domain>/<name>.md\n  fn index\n", templatePath)
}

// --- add type ---

func truncate(s string, max int) string {
	if len(s) <= max {
		return s
	}
	return s[:max-3] + "..."
}
