// Command gencfg generates examples/config.example.yaml from the
// internal/platform/config package. It extracts struct field doc comments from the
// Go source via go/ast and uses [config.ExampleConfig] for example values,
// producing a fully-documented YAML reference file that cannot drift from
// the code.
//
// Usage (via go generate in internal/platform/config):
//
//	go generate ./internal/platform/config
//
// Optional sections (MQTT, Signal, Forge, etc.) are emitted as commented-out
// YAML blocks so new users see the full configuration surface without
// accidentally enabling services they have not configured. The set of
// optional keys is defined by [optionalKeys].
package main

import (
	"bytes"
	"flag"
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"os"
	"path/filepath"
	"reflect"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/nugget/thane-ai-agent/internal/platform/config"
	"gopkg.in/yaml.v3"
)

// optionalKeys lists the top-level yaml keys in Config that should be
// emitted as commented-out YAML blocks. All other keys are emitted normally.
var optionalKeys = map[string]bool{
	"anthropic":       true,
	"mqtt":            true,
	"person":          true,
	"unifi":           true,
	"signal":          true,
	"carddav":         true,
	"companion":       true,
	"identity":        true,
	"attachments":     true,
	"provenance":      true,
	"forge":           true,
	"email":           true,
	"archive":         true,
	"extraction":      true,
	"episodic":        true,
	"agent":           true,
	"delegate":        true,
	"mcp":             true,
	"prewarm":         true,
	"state_window":    true,
	"search":          true,
	"media":           true,
	"metacognitive":   true,
	"loops":           true,
	"capability_tags": true,
	"channel_tags":    true,
	"timezone":        true,
	"extra_path":      true,
	"pricing":         true,
	"debug":           true,
	"paths":           true,
	"doc_roots":       true,
	"log_level":       true,
	"log_format":      true,
}

// commentMap maps TypeName → FieldName → doc comment text as written in the
// source, trimmed of surrounding whitespace.
type commentMap map[string]map[string]string

func main() {
	var srcDir string
	var outFile string

	flag.StringVar(&srcDir, "srcdir", "", "directory containing config package source files")
	flag.StringVar(&outFile, "out", "", "output file path (empty = stdout)")
	flag.Parse()

	if srcDir == "" {
		fmt.Fprintln(os.Stderr, "gencfg: -srcdir required")
		os.Exit(1)
	}

	comments, err := extractComments(srcDir)
	if err != nil {
		fmt.Fprintf(os.Stderr, "gencfg: extract comments: %v\n", err)
		os.Exit(1)
	}

	cfg := config.ExampleConfig()
	doc := buildDocument(cfg, comments)

	var buf bytes.Buffer
	buf.WriteString("# Thane Configuration\n")
	buf.WriteString("# Generated by: go generate ./internal/platform/config\n")
	buf.WriteString("# Do not edit manually — update example.go and re-run go generate.\n")
	buf.WriteString("#\n")
	buf.WriteString("# Config search order:\n")
	buf.WriteString("#   ./config.yaml, ~/Thane/config.yaml, ~/.config/thane/config.yaml,\n")
	buf.WriteString("#   /config/config.yaml, /usr/local/etc/thane/config.yaml, /etc/thane/config.yaml\n")
	buf.WriteString("# Override: thane -config /path/to/config.yaml serve\n")
	buf.WriteString("#\n")
	buf.WriteString("# Environment variables are expanded (e.g., ${HOME}).\n")
	buf.WriteString("# Protect secrets: chmod 600 config.yaml\n")
	buf.WriteString("#\n")
	buf.WriteString("# Sections marked with '# (optional)' are commented out.\n")
	buf.WriteString("# Uncomment and configure as needed.\n")
	buf.WriteString("\n")

	enc := yaml.NewEncoder(&buf)
	enc.SetIndent(2)
	if err := enc.Encode(doc); err != nil {
		fmt.Fprintf(os.Stderr, "gencfg: encode yaml: %v\n", err)
		os.Exit(1)
	}
	enc.Close()

	// yaml.Encoder appends a trailing "---\n" document separator; strip it.
	out := bytes.TrimSuffix(buf.Bytes(), []byte("---\n"))

	if outFile == "" {
		os.Stdout.Write(out)
		return
	}
	if err := os.WriteFile(outFile, out, 0o644); err != nil {
		fmt.Fprintf(os.Stderr, "gencfg: write %s: %v\n", outFile, err)
		os.Exit(1)
	}
	fmt.Fprintf(os.Stderr, "gencfg: wrote %s (%d bytes)\n", outFile, len(out))
}

// extractComments parses all .go files in dir and returns a commentMap
// mapping TypeName → FieldName → doc comment text, trimmed of surrounding
// whitespace. Comments are stored verbatim as written in the Go source.
func extractComments(dir string) (commentMap, error) {
	fset := token.NewFileSet()
	entries, err := os.ReadDir(dir)
	if err != nil {
		return nil, err
	}
	var files []*ast.File
	for _, e := range entries {
		if e.IsDir() || !strings.HasSuffix(e.Name(), ".go") || strings.HasSuffix(e.Name(), "_test.go") {
			continue
		}
		f, err := parser.ParseFile(fset, filepath.Join(dir, e.Name()), nil, parser.ParseComments)
		if err != nil {
			return nil, err
		}
		files = append(files, f)
	}

	result := make(commentMap)
	for _, file := range files {
		ast.Inspect(file, func(n ast.Node) bool {
			ts, ok := n.(*ast.TypeSpec)
			if !ok {
				return true
			}
			st, ok := ts.Type.(*ast.StructType)
			if !ok {
				return true
			}

			typeName := ts.Name.Name
			if result[typeName] == nil {
				result[typeName] = make(map[string]string)
			}

			for _, field := range st.Fields.List {
				if field.Doc == nil || len(field.Names) == 0 {
					continue
				}
				text := strings.TrimSpace(field.Doc.Text())
				for _, name := range field.Names {
					result[typeName][name.Name] = text
				}
			}
			return true
		})
	}
	return result, nil
}

// buildDocument assembles the root yaml.DocumentNode for the Config.
// Required sections (not in optionalKeys) are emitted as normal YAML
// mapping entries with field doc comments as HeadComments. Optional
// sections are marshaled, each line prefixed with "# ", and attached
// as HeadComment on the immediately following required key — so the
// commented-out block appears in the right position in the output.
func buildDocument(cfg *config.Config, comments commentMap) *yaml.Node {
	t := reflect.TypeOf(*cfg)
	v := reflect.ValueOf(*cfg)

	mapNode := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
	docNode := &yaml.Node{Kind: yaml.DocumentNode, Content: []*yaml.Node{mapNode}}

	// pendingComment accumulates commented-out optional sections until the
	// next required key is found, at which point it is attached as that
	// key's HeadComment.
	var pendingComment strings.Builder

	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		if !field.IsExported() {
			continue
		}

		yamlKey := yamlTagKey(field)
		if yamlKey == "" || yamlKey == "-" {
			continue
		}

		fieldDoc := comments["Config"][field.Name]
		fv := v.Field(i)
		ft := field.Type

		if optionalKeys[yamlKey] {
			// Build commented-out YAML for this section and accumulate.
			commented := buildCommentedSection(yamlKey, fv, ft, fieldDoc, comments)
			pendingComment.WriteString(commented)
		} else {
			// Build the required key node.
			keyNode := &yaml.Node{Kind: yaml.ScalarNode, Value: yamlKey, Tag: "!!str"}

			var header strings.Builder
			if pendingComment.Len() > 0 {
				header.WriteString(pendingComment.String())
				pendingComment.Reset()
			}
			if fieldDoc != "" {
				if header.Len() > 0 {
					header.WriteString("#\n")
				}
				header.WriteString(formatComment(fieldDoc))
			}
			if header.Len() > 0 {
				keyNode.HeadComment = strings.TrimRight(header.String(), "\n")
			}

			valNode := buildValueNode(fv, ft, comments)
			mapNode.Content = append(mapNode.Content, keyNode, valNode)
		}
	}

	// Any remaining optional sections (after the last required key) get
	// attached as FootComment on the last emitted value node.
	if pendingComment.Len() > 0 && len(mapNode.Content) > 0 {
		last := mapNode.Content[len(mapNode.Content)-1]
		fc := strings.TrimRight(pendingComment.String(), "\n")
		if last.FootComment != "" {
			last.FootComment += "\n" + fc
		} else {
			last.FootComment = fc
		}
	}

	return docNode
}

// buildCommentedSection marshals the field as a key: value YAML block, with
// per-field doc comments included, and renders every line commented out.
// The fieldDoc appears as an "(optional)" header so users understand the
// section is not required. The comments map is used for per-field guidance
// within the block.
//
// yaml.v3 renders HeadComment nodes as "  # comment text" lines. When we
// prefix the whole block with "# ", those lines would become "# # comment
// text" (double-hashed). To avoid that, we detect inner HeadComment lines
// and strip their leading "#" so they render as clean single-hash comments
// inside the outer "# " prefix.
func buildCommentedSection(key string, v reflect.Value, t reflect.Type, fieldDoc string, comments commentMap) string {
	var buf strings.Builder

	// Section header: blank comment separator + doc summary.
	buf.WriteString("#\n# (optional) ")
	if fieldDoc != "" {
		// First sentence of the doc comment is sufficient here.
		first := strings.SplitN(fieldDoc, "\n", 2)[0]
		buf.WriteString(first)
	} else {
		buf.WriteString(key + " configuration")
	}
	buf.WriteString("\n")

	// Build the value node with full field comments so users see per-field
	// documentation inside the commented-out block.
	valNode := buildValueNode(v, t, comments)

	wrapDoc := &yaml.Node{
		Kind: yaml.DocumentNode,
		Content: []*yaml.Node{
			{
				Kind: yaml.MappingNode,
				Content: []*yaml.Node{
					{Kind: yaml.ScalarNode, Value: key, Tag: "!!str"},
					valNode,
				},
			},
		},
	}

	var yamlBuf bytes.Buffer
	enc := yaml.NewEncoder(&yamlBuf)
	enc.SetIndent(2)
	enc.Encode(wrapDoc) //nolint:errcheck // best-effort
	enc.Close()

	// Drop the leading "---\n" document separator if present.
	raw := yamlBuf.String()
	raw = strings.TrimPrefix(raw, "---\n")

	for _, line := range strings.Split(strings.TrimRight(raw, "\n"), "\n") {
		// yaml.v3 renders HeadComments as indented "  # text" lines.
		// Detect them by checking whether the trimmed line starts with "#",
		// then strip the inner "#" prefix so the outer "# " produces clean
		// single-hash comment lines rather than "# # text".
		trimmed := strings.TrimLeft(line, " \t")
		if strings.HasPrefix(trimmed, "#") {
			spaces := line[:len(line)-len(trimmed)]
			commentText := strings.TrimPrefix(trimmed, "#")
			commentText = strings.TrimPrefix(commentText, " ")
			if commentText == "" {
				buf.WriteString("#\n")
			} else {
				buf.WriteString("# " + spaces + commentText + "\n")
			}
		} else {
			buf.WriteString("# ")
			buf.WriteString(line)
			buf.WriteString("\n")
		}
	}

	return buf.String()
}

// buildValueNode recursively constructs a yaml.Node for the given reflect
// value. When comments is non-nil, struct field doc comments are attached
// as HeadComments on key nodes.
func buildValueNode(v reflect.Value, t reflect.Type, comments commentMap) *yaml.Node {
	// Dereference pointers. A nil pointer in the required (non-commented)
	// path emits "null". In a commented-out section the caller already
	// has a non-nil value from ExampleConfig, but if somehow we reach nil
	// here, synthesize a zero value so the YAML block is still meaningful.
	if t.Kind() == reflect.Ptr {
		if v.IsNil() {
			zv := reflect.New(t.Elem()).Elem()
			return buildValueNode(zv, t.Elem(), comments)
		}
		return buildValueNode(v.Elem(), t.Elem(), comments)
	}

	// time.Duration is stored as int64 but should appear as a human-readable
	// string in YAML (e.g., "3m", "30s").
	if t == reflect.TypeOf(time.Duration(0)) {
		d := time.Duration(v.Int())
		return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: formatDuration(d)}
	}

	switch t.Kind() {
	case reflect.Struct:
		return buildStructNode(v, t, comments)

	case reflect.Slice:
		seqNode := &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq"}
		for i := 0; i < v.Len(); i++ {
			seqNode.Content = append(seqNode.Content, buildValueNode(v.Index(i), t.Elem(), comments))
		}
		return seqNode

	case reflect.Map:
		mapNode := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
		keys := v.MapKeys()
		// Sort for deterministic output.
		sort.Slice(keys, func(i, j int) bool {
			return fmt.Sprintf("%v", keys[i]) < fmt.Sprintf("%v", keys[j])
		})
		for _, k := range keys {
			keyNode := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: fmt.Sprintf("%v", k)}
			valNode := buildValueNode(v.MapIndex(k), t.Elem(), comments)
			mapNode.Content = append(mapNode.Content, keyNode, valNode)
		}
		return mapNode

	case reflect.String:
		s := v.String()
		n := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: s}
		if s == "" {
			n.Style = yaml.DoubleQuotedStyle
		}
		return n

	case reflect.Bool:
		val := "false"
		if v.Bool() {
			val = "true"
		}
		return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!bool", Value: val}

	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!int", Value: fmt.Sprintf("%d", v.Int())}

	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
		return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!int", Value: fmt.Sprintf("%d", v.Uint())}

	case reflect.Float32, reflect.Float64:
		f := v.Float()
		// Always include a decimal point so yaml parsers keep the float type.
		val := strconv.FormatFloat(f, 'f', -1, 64)
		if !strings.Contains(val, ".") {
			val += ".0"
		}
		return &yaml.Node{Kind: yaml.ScalarNode, Value: val}

	case reflect.Interface:
		if v.IsNil() {
			return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!null", Value: "null"}
		}
		return buildValueNode(v.Elem(), v.Elem().Type(), comments)

	default:
		return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: fmt.Sprintf("%v", v.Interface())}
	}
}

// buildStructNode builds a yaml.MappingNode for a struct, attaching field
// doc comments as HeadComments when comments is non-nil.
func buildStructNode(v reflect.Value, t reflect.Type, comments commentMap) *yaml.Node {
	mapNode := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
	typeName := t.Name()

	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		if !field.IsExported() {
			continue
		}

		yamlKey := yamlTagKey(field)
		if yamlKey == "" || yamlKey == "-" {
			continue
		}

		fv := v.Field(i)
		ft := field.Type

		// Nil pointers reach buildValueNode which synthesizes a zero value,
		// so all fields appear in the generated YAML regardless of whether
		// the pointer was set in ExampleConfig. This ensures the example
		// covers the full config surface (e.g. LoggingConfig.ContentArchiveDir).

		keyNode := &yaml.Node{Kind: yaml.ScalarNode, Value: yamlKey, Tag: "!!str"}

		if comments != nil {
			if comment := comments[typeName][field.Name]; comment != "" {
				keyNode.HeadComment = formatComment(comment)
			}
		}

		valNode := buildValueNode(fv, ft, comments)
		mapNode.Content = append(mapNode.Content, keyNode, valNode)
	}

	return mapNode
}

// yamlTagKey returns the YAML key name from a struct field's yaml tag.
// Returns the field name lowercased if no yaml tag is present, or "" if
// the tag is explicitly "-".
func yamlTagKey(field reflect.StructField) string {
	tag := field.Tag.Get("yaml")
	if tag == "" {
		return strings.ToLower(field.Name)
	}
	key, _, _ := strings.Cut(tag, ",")
	return key
}

// formatDuration converts a time.Duration to a clean YAML string, trimming
// redundant zero components ("3m0s" → "3m", "1h0m0s" → "1h").
func formatDuration(d time.Duration) string {
	if d == 0 {
		return "0s"
	}
	s := d.String()
	// Trim trailing "0s" when preceded by minutes.
	if strings.HasSuffix(s, "m0s") {
		s = strings.TrimSuffix(s, "0s")
	}
	// Trim trailing "0m" when preceded by hours.
	if strings.HasSuffix(s, "h0m") {
		s = strings.TrimSuffix(s, "0m")
	}
	return s
}

// formatComment formats a multi-line doc comment string as YAML comment
// lines (each prefixed with "# "). Blank lines are preserved as "# ".
func formatComment(text string) string {
	var buf strings.Builder
	for _, line := range strings.Split(strings.TrimRight(text, "\n"), "\n") {
		buf.WriteString("# ")
		buf.WriteString(line)
		buf.WriteString("\n")
	}
	return strings.TrimRight(buf.String(), "\n")
}
