package cli

import (
	"fmt"
	"io"
	"regexp"
	"strings"

	"github.com/denn-gubsky/loomcycle/cmd/loomcycle/embedded"
	"gopkg.in/yaml.v3"
)

// RunTrueNASQuestions generates the "Advanced environment options" block of the
// TrueNAS catalog install form (RFC AR Phase 2) from the binary's embedded
// .env.insecure.example (RFC AQ). It emits one `env_options` dict question whose
// attrs are a typed, documented field per non-secret knob — keeping the form in
// lockstep with the env catalogue (no hand-authored drift; RFC AR §11 open-Q #1).
//
// Splice the output under `questions:` in deploy/truenas/catalog/questions.yaml;
// the catalog template loops `values.env_options` → add_env. Regenerate whenever
// .env.insecure.example changes.
func RunTrueNASQuestions(args []string, stdout, stderr io.Writer) int {
	if len(args) != 0 {
		return fail(stderr, "usage: loomcycle truenas-questions (no arguments)")
	}
	q := tnVar{
		Variable: "env_options",
		Label:    "Advanced environment options",
		Group:    "Advanced configuration",
		Description: "Any other non-secret knob from the loomcycle env catalogue " +
			"(.env.insecure.example). Leave a field blank to use the binary's built-in " +
			"default; each field's help shows the example value.",
		Schema: tnSchema{Type: "dict", Attrs: parseEnvCatalogue(string(embedded.EnvTemplate()))},
	}
	out, err := yaml.Marshal([]tnVar{q})
	if err != nil {
		return failOp(stderr, "marshal questions: %v", err)
	}
	fmt.Fprint(stdout, "# GENERATED by `loomcycle truenas-questions` from the embedded\n"+
		"# .env.insecure.example. Splice under `questions:` in\n"+
		"# deploy/truenas/catalog/questions.yaml — do not hand-edit. Regenerate on env change.\n")
	stdout.Write(out)
	return 0
}

// tnVar / tnSchema model the subset of the TrueNAS questions.yaml schema this
// generator emits (a dict question with string attrs). yaml field order matches
// the catalog convention (variable, label, group, description, schema).
type tnVar struct {
	Variable    string   `yaml:"variable"`
	Label       string   `yaml:"label"`
	Group       string   `yaml:"group,omitempty"`
	Description string   `yaml:"description,omitempty"`
	Schema      tnSchema `yaml:"schema"`
}

type tnSchema struct {
	Type    string  `yaml:"type"`
	Default string  `yaml:"default"`
	Attrs   []tnVar `yaml:"attrs,omitempty"`
}

var envKnobRe = regexp.MustCompile(`^([A-Z][A-Z0-9_]*)=(.*)$`)

// envSkip are knobs the catalog form already covers via dedicated questions (so
// they must not appear twice) or that the container fixes.
var envSkip = map[string]bool{
	"LOOMCYCLE_LISTEN_ADDR":     true, // fixed 0.0.0.0:8787 in the container
	"LOOMCYCLE_DATA_DIR":        true, // fixed /data
	"LOOMCYCLE_CONFIG_DIR":      true, // fixed /config
	"LOOMCYCLE_CONFIG_FILES":    true, // the overlay is mounted, not listed
	"LOOMCYCLE_PRESETS":         true, // the presets multiselect
	"LOOMCYCLE_STORAGE_BACKEND": true, // fixed postgres
	"LOOMCYCLE_PG_AUTOMIGRATE":  true, // the auto-migrate toggle
}

// envSecretSuffix mirrors the CLAUDE.md secret-name patterns. A knob whose name
// ends in one of these is a SECRET and must never surface as a non-private form
// field — secrets have dedicated `private: true` questions. Defensive: the
// non-secret env catalogue shouldn't contain these, but never emit one if it does.
var envSecretSuffix = []string{"_KEY", "_TOKEN", "_SECRET", "_AUTH", "_PASSWORD", "_PEPPER", "_DSN", "_CREDENTIAL"}

func envIsSecret(key string) bool {
	for _, s := range envSecretSuffix {
		if strings.HasSuffix(key, s) {
			return true
		}
	}
	return false
}

// parseEnvCatalogue walks the env-template text and turns each non-secret,
// not-already-covered knob (`KEY=val` or a commented `# KEY=val`) into a string
// question. The prose comment lines immediately preceding a knob become its help
// text; a blank line or a `─── section ───` rule breaks that association.
func parseEnvCatalogue(s string) []tnVar {
	var attrs []tnVar
	var comment []string
	seen := map[string]bool{}
	for _, raw := range strings.Split(s, "\n") {
		line := strings.TrimSpace(raw)
		if line == "" {
			comment = nil
			continue
		}
		isComment := strings.HasPrefix(line, "#")
		body := line
		if isComment {
			body = strings.TrimSpace(strings.TrimLeft(line, "#"))
		}
		// A decorative section rule (── … ──) resets the help association.
		if body == "" || strings.HasPrefix(body, "─") {
			comment = nil
			continue
		}
		if m := envKnobRe.FindStringSubmatch(body); m != nil {
			key, val := m[1], stripInlineComment(m[2])
			if !envSkip[key] && !envIsSecret(key) && !seen[key] {
				seen[key] = true
				attrs = append(attrs, tnVar{
					Variable:    key,
					Label:       key,
					Description: buildHelp(comment, val),
					Schema:      tnSchema{Type: "string", Default: ""},
				})
			}
			comment = nil
			continue
		}
		if isComment {
			comment = append(comment, body)
		} else {
			comment = nil // a stray non-comment, non-knob line
		}
	}
	return attrs
}

// stripInlineComment drops a trailing ` # …` from an env value (e.g.
// `https://ollama.com   # optional override`).
func stripInlineComment(val string) string {
	if i := strings.Index(val, " #"); i >= 0 {
		val = val[:i]
	}
	return strings.TrimSpace(val)
}

// buildHelp joins the accumulated prose into one help string and appends the
// example value; the result is length-capped so a long comment block stays usable.
func buildHelp(comment []string, example string) string {
	help := strings.TrimSpace(strings.Join(comment, " "))
	if example != "" {
		if help != "" {
			help += " "
		}
		help += "(example: " + example + ")"
	}
	const cap = 400
	if len(help) > cap {
		help = help[:cap-1] + "…"
	}
	return help
}
