package envexpand

import (
	"bufio"
	"fmt"
	"os"
	"regexp"
	"strings"
)

var placeholderRE = regexp.MustCompile(`\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}`)

const maxIterations = 32

// ExpandString replaces {{NAME}} placeholders using vars.
func ExpandString(s string, vars map[string]string) string {
	return placeholderRE.ReplaceAllStringFunc(s, func(match string) string {
		name := match[2 : len(match)-2]
		if v, ok := vars[name]; ok {
			return v
		}
		return match
	})
}

// ExpandMap resolves {{VAR}} references until stable or maxIterations is reached.
func ExpandMap(vars map[string]string) (map[string]string, error) {
	out := make(map[string]string, len(vars))
	for k, v := range vars {
		out[k] = v
	}

	for i := 0; i < maxIterations; i++ {
		changed := false
		for k, v := range out {
			next := ExpandString(v, out)
			if next != v {
				changed = true
				out[k] = next
			}
		}
		if !changed {
			if unresolved := findUnresolved(out); len(unresolved) > 0 {
				return nil, fmt.Errorf("envexpand: unresolved placeholders: %s", strings.Join(unresolved, ", "))
			}
			return out, nil
		}
	}

	return nil, fmt.Errorf("envexpand: expansion did not converge after %d iterations", maxIterations)
}

func findUnresolved(vars map[string]string) []string {
	seen := make(map[string]bool)
	for _, v := range vars {
		for _, name := range placeholderRE.FindAllStringSubmatch(v, -1) {
			seen[name[1]] = true
		}
	}
	names := make([]string, 0, len(seen))
	for n := range seen {
		names = append(names, "{{"+n+"}}")
	}
	for i := 0; i < len(names); i++ {
		for j := i + 1; j < len(names); j++ {
			if names[j] < names[i] {
				names[i], names[j] = names[j], names[i]
			}
		}
	}
	return names
}

// ParseLines reads KEY=VALUE pairs from dotenv-style content (no expansion).
func ParseLines(lines []string) (map[string]string, error) {
	vars := make(map[string]string)

	for i, line := range lines {
		line = strings.TrimSpace(line)
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}

		key, value, ok := strings.Cut(line, "=")
		if !ok {
			return nil, fmt.Errorf("envexpand: invalid line %d: missing '='", i+1)
		}

		key = strings.TrimSpace(key)
		if key == "" {
			return nil, fmt.Errorf("envexpand: invalid line %d: empty key", i+1)
		}

		value = strings.TrimSpace(value)
		if len(value) >= 2 {
			if (value[0] == '"' && value[len(value)-1] == '"') ||
				(value[0] == '\'' && value[len(value)-1] == '\'') {
				value = value[1 : len(value)-1]
			}
		}

		vars[key] = value
	}

	return vars, nil
}

// LoadFile parses a .env file without expanding placeholders.
func LoadFile(path string) (map[string]string, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}
	lines := strings.Split(strings.ReplaceAll(string(data), "\r\n", "\n"), "\n")
	return ParseLines(lines)
}

// LoadExpandFile parses and expands a .env file.
func LoadExpandFile(path string) (map[string]string, error) {
	vars, err := LoadFile(path)
	if err != nil {
		return nil, err
	}
	return ExpandMap(vars)
}

// WriteFile writes KEY=VALUE lines (no quoting; values are used as-is).
func WriteFile(path string, vars map[string]string) error {
	var keys []string
	for k := range vars {
		keys = append(keys, k)
	}
	// Stable output: sort keys
	for i := 0; i < len(keys); i++ {
		for j := i + 1; j < len(keys); j++ {
			if keys[j] < keys[i] {
				keys[i], keys[j] = keys[j], keys[i]
			}
		}
	}

	var b strings.Builder
	for _, k := range keys {
		b.WriteString(k)
		b.WriteByte('=')
		b.WriteString(vars[k])
		b.WriteByte('\n')
	}

	return os.WriteFile(path, []byte(b.String()), 0o600)
}

// ApplyFile loads and expands a .env file, then sets variables in the process
// environment. Existing environment variables are not overwritten.
func ApplyFile(path string) error {
	vars, err := LoadExpandFile(path)
	if err != nil {
		return err
	}
	ApplyMap(vars, false)
	return nil
}

// ApplyMap sets variables in the process environment.
// When override is false, existing environment variables are kept.
func ApplyMap(vars map[string]string, override bool) {
	for k, v := range vars {
		if !override {
			if _, exists := os.LookupEnv(k); exists {
				continue
			}
		}
		_ = os.Setenv(k, v)
	}
}

// Render reads an input .env, expands placeholders, and writes the result.
func Render(inPath, outPath string) error {
	vars, err := LoadExpandFile(inPath)
	if err != nil {
		return err
	}
	return WriteFile(outPath, vars)
}

// RenderToWriter expands and writes dotenv format to w.
func RenderToWriter(inPath string, w interface{ Write([]byte) (int, error) }) error {
	vars, err := LoadExpandFile(inPath)
	if err != nil {
		return err
	}

	var keys []string
	for k := range vars {
		keys = append(keys, k)
	}
	for i := 0; i < len(keys); i++ {
		for j := i + 1; j < len(keys); j++ {
			if keys[j] < keys[i] {
				keys[i], keys[j] = keys[j], keys[i]
			}
		}
	}

	for _, k := range keys {
		if _, err := fmt.Fprintf(w, "%s=%s\n", k, vars[k]); err != nil {
			return err
		}
	}
	return nil
}

// ParseReader is a helper for tests and stdin.
func ParseReader(r interface {
	ReadString(byte) (string, error)
}) (map[string]string, error) {
	var lines []string
	for {
		line, err := r.ReadString('\n')
		lines = append(lines, strings.TrimSuffix(line, "\n"))
		if err != nil {
			break
		}
	}
	return ParseLines(lines)
}

// ParseFileLines loads via bufio for large files (alias to LoadFile).
func ParseFile(path string) (map[string]string, error) {
	f, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer f.Close()

	var lines []string
	sc := bufio.NewScanner(f)
	for sc.Scan() {
		lines = append(lines, sc.Text())
	}
	if err := sc.Err(); err != nil {
		return nil, err
	}
	return ParseLines(lines)
}
