// Package env loads .env files, computes defaults, and exposes DSN helpers
// for the AgentTeam dev tool. It is the single source of truth for the dev-time
// environment.
package env

import (
	"bufio"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
)

// Load copies .env.example to .env if missing, applies defaults, and populates
// the process environment from .env.
func Load(repoRoot string) {
	syncEnvFile(repoRoot)
	loadDotEnv(repoRoot)
	setDefaults()
}

// FindRepoRoot walks up from the current directory until it finds a directory
// containing both go.mod and SPECS.md.
func FindRepoRoot() (string, error) {
	dir, err := os.Getwd()
	if err != nil {
		return "", err
	}
	for {
		if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
			if _, err := os.Stat(filepath.Join(dir, "SPECS.md")); err == nil {
				return dir, nil
			}
		}
		if p := filepath.Dir(dir); p == dir {
			return "", fmt.Errorf("no repo root found")
		} else {
			dir = p
		}
	}
}

func syncEnvFile(repoRoot string) {
	dst := filepath.Join(repoRoot, ".env")
	if _, err := os.Stat(dst); err != nil {
		if data, err := os.ReadFile(filepath.Join(repoRoot, ".env.example")); err == nil {
			os.WriteFile(dst, data, 0644)
			slog.Info("created .env from .env.example")
		}
	}
}

func loadDotEnv(repoRoot string) {
	f, err := os.Open(filepath.Join(repoRoot, ".env"))
	if err != nil {
		return
	}
	defer f.Close()
	sc := bufio.NewScanner(f)
	for sc.Scan() {
		line := strings.TrimSpace(sc.Text())
		if line == "" || line[0] == '#' {
			continue
		}
		if k, v, ok := parseEnvLine(line); ok {
			os.Setenv(k, v)
		}
	}
}

func parseEnvLine(line string) (string, string, bool) {
	line = strings.TrimPrefix(line, "export ")
	i := strings.Index(line, "=")
	if i < 0 {
		return "", "", false
	}
	k := strings.TrimSpace(line[:i])
	v := strings.TrimSpace(line[i+1:])
	if len(v) >= 2 {
		if (v[0] == '"' && v[len(v)-1] == '"') || (v[0] == '\'' && v[len(v)-1] == '\'') {
			v = v[1 : len(v)-1]
		}
	}
	return k, v, true
}

// SetDefaults seeds dev-time defaults for env vars that aren't already set.
func SetDefaults() {
	setDefaults()
}

func setDefaults() {
	setDefault("AGENTTEAM_AUTH_REGISTRATION_MODE", "open")
	setDefault("AGENTTEAM_PUBLIC_BASE_URL", "http://localhost:3001")
	setDefault("AGENTTEAM_DATABASE_DSN", "root@tcp(127.0.0.1:4000)/agentteam_core?multiStatements=true&parseTime=true&loc=UTC&time_zone=%27%2B00%3A00%27")
}

// SetDefault sets name=value in the environment if name is unset.
func SetDefault(key, value string) {
	setDefault(key, value)
}

func setDefault(key, value string) {
	if os.Getenv(key) == "" {
		os.Setenv(key, value)
	}
}

var dsnPortRe = regexp.MustCompile(`@tcp\([^)]*:(\d+)\)`)

// ExtractDSNPort returns the tcp port embedded in a mysql DSN, or "".
func ExtractDSNPort(dsn string) string {
	if m := dsnPortRe.FindStringSubmatch(dsn); len(m) >= 2 {
		return m[1]
	}
	return ""
}

// IsLocalDSN reports whether dsn targets localhost on the given port.
func IsLocalDSN(dsn, port string) bool {
	return strings.Contains(dsn, "@tcp(localhost:"+port+")") ||
		strings.Contains(dsn, "@tcp(127.0.0.1:"+port+")") ||
		strings.Contains(dsn, "@tcp([::1]:"+port+")")
}

// ExtractDSNDatabase returns the database name embedded in a mysql DSN.
func ExtractDSNDatabase(dsn string) string {
	if i := strings.Index(dsn, ")/"); i >= 0 {
		rest := dsn[i+2:]
		if j := strings.IndexAny(rest, "?"); j >= 0 {
			rest = rest[:j]
		}
		return strings.Trim(rest, " /")
	}
	return ""
}

// ReplaceDSNDatabase swaps the database name in dsn.
func ReplaceDSNDatabase(dsn, database string) string {
	idx := strings.Index(dsn, ")/")
	if idx < 0 {
		return dsn
	}
	prefix := dsn[:idx+2]
	rest := dsn[idx+2:]
	if qi := strings.Index(rest, "?"); qi >= 0 {
		return prefix + database + rest[qi:]
	}
	return prefix + database
}

// ReplaceEnv returns a copy of env (the slice form returned by os.Environ)
// with name=value substituted in (or appended).
func ReplaceEnv(env []string, name string, value string) []string {
	out := make([]string, 0, len(env)+1)
	replaced := false
	for _, entry := range env {
		current, _, ok := strings.Cut(entry, "=")
		if ok && current == name {
			if !replaced {
				out = append(out, name+"="+value)
				replaced = true
			}
			continue
		}
		out = append(out, entry)
	}
	if !replaced {
		out = append(out, name+"="+value)
	}
	return out
}

// EnvInt parses name from the environment with a fallback and minimum.
func EnvInt(name string, fallback int, min int) int {
	value, err := strconv.Atoi(os.Getenv(name))
	if err != nil || value < min {
		return fallback
	}
	return value
}

// EscapeSQLIdentifier doubles backticks inside a MySQL identifier.
func EscapeSQLIdentifier(value string) string {
	return strings.ReplaceAll(value, "`", "``")
}
