package ns

import (
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
)

// ParseNSMd reads an ns.md file, extracts the YAML frontmatter, and
// parses it into a *NSConfig. The body after the closing `---` is
// discarded (namespace declarations do not require body preservation
// like jobspecs do under R-015; we keep the parser minimal and
// consistent with internal/config/markdown.go).
//
// Frontmatter keys (R-014):
//
//	kind: Namespace        (required; must be "Namespace")
//	name: <ns-name>        (required)
//	parents: ["a", "b"]   (optional; default empty)
//	inherits_env: true     (optional; default true)
//	inherits_secrets: true (optional; default true)
//	constraints: ["k=v"]  (optional; parsed into cfg.Constraints)
//	quota: {...}           (optional; parsed but not surfaced here)
//	acl: {...}             (optional; parsed but not surfaced here)
//
// The parser is a minimal hand-rolled YAML-ish key:value reader (no
// new dependencies; gopkg.in/yaml.v3 is intentionally NOT added). It
// supports flat scalar keys and the inline flow-array form
// `["a", "b"]` for `parents`. Nested mappings (quota, acl) are
// recognized as keys but their contents are currently ignored — they
// are reserved for later phases.
func ParseNSMd(path string) (*NSConfig, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("read %s: %w", path, err)
	}
	content := string(data)

	block, ok := extractFrontmatter(content)
	if !ok {
		return nil, fmt.Errorf("parse %s: missing frontmatter", path)
	}
	if strings.TrimSpace(block) == "" {
		return nil, fmt.Errorf("parse %s: missing frontmatter", path)
	}

	cfg, err := parseNSFrontmatter(block, path)
	if err != nil {
		return nil, err
	}
	if cfg.Name == "" {
		return nil, fmt.Errorf("parse %s: missing name", path)
	}
	return cfg, nil
}

// ParseNSMdWithBody is like ParseNSMd but also returns the markdown
// body (the content after the closing `---` delimiter). Used by the
// ns inherit / set-constraint editors that rewrite frontmatter while
// preserving the body.
func ParseNSMdWithBody(path string) (*NSConfig, string, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, "", fmt.Errorf("read %s: %w", path, err)
	}
	content := string(data)

	block, ok := extractFrontmatter(content)
	if !ok {
		return nil, "", fmt.Errorf("parse %s: missing frontmatter", path)
	}
	if strings.TrimSpace(block) == "" {
		return nil, "", fmt.Errorf("parse %s: missing frontmatter", path)
	}

	cfg, err := parseNSFrontmatter(block, path)
	if err != nil {
		return nil, "", err
	}
	if cfg.Name == "" {
		return nil, "", fmt.Errorf("parse %s: missing name", path)
	}
	body := bodyAfterFrontmatter(content)
	return cfg, body, nil
}

// bodyAfterFrontmatter returns the content after the closing frontmatter
// delimiter of an ns.md file. If no frontmatter is present, the whole
// input is returned unchanged.
func bodyAfterFrontmatter(content string) string {
	trimmed := strings.TrimLeft(content, "\r\n\t ")
	if !strings.HasPrefix(trimmed, "---") {
		return content
	}
	rest := trimmed[3:]
	rest = strings.TrimLeft(rest, "\r\n")
	idx := strings.Index(rest, "\n---")
	if idx < 0 {
		return content
	}
	after := rest[idx+4:]
	after = strings.TrimLeft(after, "\r\n")
	return after
}

// extractFrontmatter returns the YAML block between the first pair of
// `---` delimiters and whether a frontmatter block was present.
func extractFrontmatter(content string) (string, bool) {
	trimmed := strings.TrimLeft(content, "\r\n\t ")
	if !strings.HasPrefix(trimmed, "---") {
		return "", false
	}
	rest := trimmed[3:]
	rest = strings.TrimLeft(rest, "\r\n")
	idx := strings.Index(rest, "\n---")
	if idx < 0 {
		return "", false
	}
	return rest[:idx], true
}

// parseNSFrontmatter parses a minimal YAML-ish frontmatter block into
// a *NSConfig. See ParseNSMd for the supported keys.
func parseNSFrontmatter(block, path string) (*NSConfig, error) {
	cfg := &NSConfig{
		InheritsEnv:     true,
		InheritsSecrets: true,
	}
	kind := ""

	lines := strings.Split(block, "\n")
	for lineNo, raw := range lines {
		line := stripNSComment(raw)
		if strings.TrimSpace(line) == "" {
			continue
		}
		if countIndent(line) > 0 {
			// Indented line under a nested mapping header (quota, acl).
			// Recognized but ignored at this phase.
			continue
		}
		key, val, ok := splitKV(strings.TrimSpace(line))
		if !ok {
			return nil, fmt.Errorf("parse %s: line %d: malformed key:value", path, lineNo+1)
		}
		switch key {
		case "kind":
			kind = strings.TrimSpace(unquote(val))
		case "name":
			cfg.Name = strings.TrimSpace(unquote(val))
		case "parents":
			parents, err := parseStringArray(val)
			if err != nil {
				return nil, fmt.Errorf("parse %s: line %d: parents: %w", path, lineNo+1, err)
			}
			cfg.Parents = parents
		case "inherits_env":
			cfg.InheritsEnv = parseBool(val)
		case "inherits_secrets":
			cfg.InheritsSecrets = parseBool(val)
		case "constraints":
			cons, err := parseStringArray(val)
			if err != nil {
				return nil, fmt.Errorf("parse %s: line %d: constraints: %w", path, lineNo+1, err)
			}
			cfg.Constraints = cons
		case "quota", "acl":
			// Reserved nested-mapping keys; recognized, contents ignored.
		default:
			// Unknown keys are ignored (forward-compat with future
			// frontmatter additions).
		}
	}

	if kind == "" {
		return nil, fmt.Errorf("parse %s: missing kind", path)
	}
	if kind != "Namespace" {
		return nil, fmt.Errorf("parse %s: kind %q is not %q", path, kind, "Namespace")
	}
	return cfg, nil
}

// parseStringArray parses an inline YAML flow-array of scalars, e.g.
// `["a", "b"]` or `['a', 'b']` or `[a, b]`. Returns an error if the
// value is not a flow-array. Empty array `[]` returns nil.
func parseStringArray(val string) ([]string, error) {
	val = strings.TrimSpace(val)
	if val == "" {
		return nil, nil
	}
	if !strings.HasPrefix(val, "[") || !strings.HasSuffix(val, "]") {
		return nil, fmt.Errorf("expected [..] array, got %q", val)
	}
	inner := strings.TrimSpace(val[1 : len(val)-1])
	if inner == "" {
		return nil, nil
	}
	parts := splitFlowItems(inner)
	out := make([]string, 0, len(parts))
	for _, p := range parts {
		p = strings.TrimSpace(p)
		if p == "" {
			continue
		}
		out = append(out, unquote(p))
	}
	return out, nil
}

// splitFlowItems splits a comma-separated flow-array body, respecting
// single and double quotes.
func splitFlowItems(s string) []string {
	var out []string
	inSingle := false
	inDouble := false
	start := 0
	for i := 0; i < len(s); i++ {
		c := s[i]
		switch c {
		case '\'':
			if !inDouble {
				inSingle = !inSingle
			}
		case '"':
			if !inSingle {
				inDouble = !inDouble
			}
		case ',':
			if !inSingle && !inDouble {
				out = append(out, s[start:i])
				start = i + 1
			}
		}
	}
	out = append(out, s[start:])
	return out
}

// parseBool parses a YAML-ish bool (true/false/yes/no), defaulting to
// true for empty (matches the inherits_* defaults).
func parseBool(val string) bool {
	switch strings.ToLower(strings.TrimSpace(unquote(val))) {
	case "false", "no", "off", "0":
		return false
	default:
		return true
	}
}

// ParseNSMdDir walks `<root>/*/ns.md`, parses each, and returns the
// config map keyed by namespace name. The `cluster` directory is
// skipped (it is not a namespace). The `_defaults` namespace MUST
// exist; if missing, an error is returned.
func ParseNSMdDir(root string) (map[string]*NSConfig, error) {
	entries, err := os.ReadDir(root)
	if err != nil {
		return nil, fmt.Errorf("read namespace root %s: %w", root, err)
	}

	configs := make(map[string]*NSConfig)
	var found []string
	for _, ent := range entries {
		if !ent.IsDir() {
			continue
		}
		if ent.Name() == "cluster" {
			continue
		}
		nsMd := filepath.Join(root, ent.Name(), "ns.md")
		info, err := os.Stat(nsMd)
		if err != nil || info.IsDir() {
			continue
		}
		cfg, err := ParseNSMd(nsMd)
		if err != nil {
			return nil, err
		}
		// The directory name and the frontmatter `name` should match;
		// we key by the frontmatter name (canonical) but also accept
		// the directory name if frontmatter name is missing (the
		// parser already errors on missing name, so this is defensive).
		key := cfg.Name
		if key == "" {
			key = ent.Name()
		}
		if _, dup := configs[key]; dup {
			return nil, fmt.Errorf("duplicate namespace %q (from %s)", key, nsMd)
		}
		configs[key] = cfg
		found = append(found, key)
	}

	if _, ok := configs[defaultsName]; !ok {
		sort.Strings(found)
		names := strings.Join(found, ", ")
		if names == "" {
			names = "(none)"
		}
		return nil, fmt.Errorf("namespace root %s: implicit root %q not found (found: %s)", root, defaultsName, names)
	}
	return configs, nil
}

func countIndent(s string) int {
	n := 0
	for _, r := range s {
		if r == ' ' || r == '\t' {
			n++
			continue
		}
		break
	}
	return n
}

func splitKV(s string) (key, val string, ok bool) {
	idx := strings.Index(s, ":")
	if idx < 0 {
		return "", "", false
	}
	key = strings.TrimSpace(s[:idx])
	val = strings.TrimSpace(s[idx+1:])
	if key == "" {
		return "", "", false
	}
	return key, val, true
}

func stripNSComment(s string) string {
	inSingle := false
	inDouble := false
	for i := 0; i < len(s); i++ {
		c := s[i]
		switch c {
		case '\'':
			if !inDouble {
				inSingle = !inSingle
			}
		case '"':
			if !inSingle {
				inDouble = !inDouble
			}
		case '#':
			if !inSingle && !inDouble {
				if i == 0 || s[i-1] == ' ' || s[i-1] == '\t' {
					return s[:i]
				}
			}
		}
	}
	return s
}

func unquote(s string) string {
	if len(s) >= 2 {
		if (s[0] == '"' && s[len(s)-1] == '"') || (s[0] == '\'' && s[len(s)-1] == '\'') {
			return s[1 : len(s)-1]
		}
	}
	return s
}
