// Package features parses the AGENTTEAM_DEV_FEATURES gate and applies env defaults
// derived from the active feature set (MESHX, ULTRA, TIDB_ZERO).
package features

import (
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"runtime"
	"sort"
	"strings"

	"github.com/tidbcloud/AgentTeam/dev/internal/env"
)

const (
	EnvName          = "AGENTTEAM_DEV_FEATURES"
	MeshXRuntimeEnv  = "AGENTTEAM_MESHX_ENABLED"
	LocalTiDBTagEnv  = "RETAIN_DATA"
	RetainDataDirEnv = "AGENTTEAM_DEV_RETAIN_DATA_DIR"
)

type Feature string

const (
	MeshX    Feature = "MESHX"
	ULTRA    Feature = "ULTRA"
	TiDBZero Feature = "TIDB_ZERO"
)

const (
	defaultMeshXAutomationDockerImages                  = "ubuntu-latest=ubuntu:24.04"
	defaultMeshXAutomationCredentialsMasterKey          = "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY="
	defaultMeshXPersonalAccessTokenCredentialsMasterKey = "cGVyc29uYWwtYWNjZXNzLXRva2VuLWRldi1rZXktMzI="
)

// Set is an unordered collection of features.
type Set map[Feature]struct{}

// Options is the resolved set of dev options derived from the environment.
type Options struct {
	Features Set
	RepoRoot string
}

// Has reports whether s contains f.
func (s Set) Has(f Feature) bool {
	if len(s) == 0 {
		return false
	}
	_, ok := s[f]
	return ok
}

func (s Set) String() string {
	if len(s) == 0 {
		return ""
	}
	items := make([]string, 0, len(s))
	for f := range s {
		items = append(items, string(f))
	}
	sort.Strings(items)
	return strings.Join(items, ",")
}

// MeshXEnabled reports whether the MESHX feature is on.
func (o Options) MeshXEnabled() bool { return o.Features.Has(MeshX) }

// UltraEnabled reports whether the ULTRA feature is on.
func (o Options) UltraEnabled() bool { return o.Features.Has(ULTRA) }

// TiDBZeroEnabled reports whether the TIDB_ZERO feature is on.
func (o Options) TiDBZeroEnabled() bool { return o.Features.Has(TiDBZero) }

// Load parses AGENTTEAM_DEV_FEATURES and validates it against platform support.
func Load(repoRoot string) (Options, error) {
	raw := os.Getenv(EnvName)
	features, err := parse(raw)
	if err != nil {
		return Options{}, err
	}
	if err := checkUltraPlatformSupport(features); err != nil {
		return Options{}, err
	}
	if len(features) > 0 {
		slog.Info("enabled dev features", "features", features.String())
	}
	return Options{Features: features, RepoRoot: repoRoot}, nil
}

// checkUltraPlatformSupport rejects ULTRA on platforms where the internal tiup
// mirror has no playground component.
func checkUltraPlatformSupport(features Set) error {
	return checkUltraPlatformSupportFor(features, runtime.GOOS, runtime.GOARCH)
}

func checkUltraPlatformSupportFor(features Set, goos, goarch string) error {
	if !features.Has(ULTRA) {
		return nil
	}
	if goos == "darwin" && goarch == "arm64" {
		return fmt.Errorf("ULTRA dev runtime is not supported on darwin/arm64 (the internal tiup mirror lacks a playground build for this platform); run `make docker-dev` instead")
	}
	return nil
}

// ApplyEnvironment sets derived env vars (MeshX runtime, AGS storage, etc.)
// based on opts. It must be called before launching child processes.
func ApplyEnvironment(opts Options) error {
	if !opts.MeshXEnabled() {
		return nil
	}
	os.Setenv(MeshXRuntimeEnv, "true")
	env.SetDefault("MESHX_DATABASE_DSN", "root@tcp(127.0.0.1:4000)/agentteam_meshx?multiStatements=true&parseTime=true&loc=UTC&time_zone=%27%2B00%3A00%27")
	env.SetDefault("MESHX_AUTH_TRUSTED_JWT_CLAIMS", "true")
	env.SetDefault("MESHX_AUTH_TRUSTED_JWT_CLAIMS_PROTECTED_LISTENER", "true")
	if strings.TrimSpace(os.Getenv("MESHX_AGS_DATABASE_DSN")) == "" {
		os.Setenv("MESHX_AGS_DATABASE_DSN", defaultAGSDSN(opts))
	}
	if shouldUseDefaultAGSGitRepoDir(os.Getenv("MESHX_AGS_GIT_REPO_DIR"), opts.RepoRoot) {
		os.Setenv("MESHX_AGS_GIT_REPO_DIR", defaultAGSGitRepoDir(opts.RepoRoot))
	}
	if shouldUseDefaultPublicRepoCacheDir(os.Getenv("MESHX_KB_PUBLIC_REPO_CACHE_DIR"), opts.RepoRoot) {
		os.Setenv("MESHX_KB_PUBLIC_REPO_CACHE_DIR", defaultPublicRepoCacheDir(opts.RepoRoot))
	}
	env.SetDefault("MESHX_AUTOMATION_DOCKER_IMAGES", defaultMeshXAutomationDockerImages)
	env.SetDefault("MESHX_AUTOMATION_CREDENTIALS_MASTER_KEY", defaultMeshXAutomationCredentialsMasterKey)
	env.SetDefault("MESHX_PERSONAL_ACCESS_TOKEN_CREDENTIALS_MASTER_KEY", defaultMeshXPersonalAccessTokenCredentialsMasterKey)
	if err := ensureSQLiteDSNDir(os.Getenv("MESHX_AGS_DATABASE_DSN")); err != nil {
		return err
	}
	if dir := strings.TrimSpace(os.Getenv("MESHX_AGS_GIT_REPO_DIR")); dir != "" {
		if err := os.MkdirAll(dir, 0o755); err != nil {
			return fmt.Errorf("create AGS git repository dir %q: %w", dir, err)
		}
	}
	if dir := strings.TrimSpace(os.Getenv("MESHX_KB_PUBLIC_REPO_CACHE_DIR")); dir != "" {
		if err := os.MkdirAll(dir, 0o755); err != nil {
			return fmt.Errorf("create public repository cache dir %q: %w", dir, err)
		}
	}
	return nil
}

// LocalTiDBTag returns the tiup --tag value for the active RETAIN_DATA setting.
func LocalTiDBTag() string {
	if retain := os.Getenv(LocalTiDBTagEnv); retain != "" {
		if strings.HasPrefix(retain, "agentteam-") {
			return retain
		}
		return "agentteam-dev-tidb-" + retain
	}
	return "agentteam-dev-tidb"
}

func defaultAGSDSN(opts Options) string {
	if opts.UltraEnabled() || opts.TiDBZeroEnabled() {
		return "root@tcp(127.0.0.1:4000)/agentteam_ags?multiStatements=true&parseTime=true&loc=UTC"
	}
	return "sqlite://" + filepath.Join(defaultMeshXAGSDataDir(opts.RepoRoot), "ags.db") + "?_foreign_keys=on"
}

func defaultAGSGitRepoDir(repoRoot string) string {
	return filepath.Join(defaultMeshXAGSDataDir(repoRoot), "ags-repos")
}

func defaultPublicRepoCacheDir(repoRoot string) string {
	return filepath.Join(defaultMeshXAGSDataDir(repoRoot), "public-repo-cache")
}

func shouldUseDefaultAGSGitRepoDir(raw string, repoRoot string) bool {
	trimmed := strings.TrimSpace(raw)
	if trimmed == "" {
		return true
	}
	cleaned := filepath.Clean(trimmed)
	legacyRelative := filepath.Clean(filepath.Join(".data", "meshx", "ags-repos"))
	legacyRootLocal := filepath.Clean(filepath.Join(repoRoot, ".data", "meshx", "ags-repos"))
	return cleaned == legacyRelative || cleaned == legacyRootLocal
}

func shouldUseDefaultPublicRepoCacheDir(raw string, repoRoot string) bool {
	trimmed := strings.TrimSpace(raw)
	if trimmed == "" {
		return true
	}
	cleaned := filepath.Clean(trimmed)
	legacyRelative := filepath.Clean(filepath.Join(".data", "meshx", "public-repo-cache"))
	legacyRootLocal := filepath.Clean(filepath.Join(repoRoot, ".data", "meshx", "public-repo-cache"))
	return cleaned == legacyRelative || cleaned == legacyRootLocal
}

func defaultMeshXAGSDataDir(repoRoot string) string {
	if strings.TrimSpace(os.Getenv(LocalTiDBTagEnv)) == "" {
		return filepath.Join(repoRoot, ".data", "meshx")
	}
	base := strings.TrimSpace(os.Getenv(RetainDataDirEnv))
	if base == "" {
		if home, err := os.UserHomeDir(); err == nil && strings.TrimSpace(home) != "" {
			base = filepath.Join(home, ".agentteam", "dev-data")
		} else {
			base = filepath.Join(repoRoot, ".data", "retained")
		}
	}
	return filepath.Join(base, safeRetainDataPathSegment(LocalTiDBTag()), "meshx")
}

func safeRetainDataPathSegment(raw string) string {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return "default"
	}
	replacer := strings.NewReplacer("/", "_", "\\", "_", ":", "_")
	return replacer.Replace(raw)
}

func ensureSQLiteDSNDir(dsn string) error {
	path := sqliteDSNPath(dsn)
	if path == "" {
		return nil
	}
	dir := filepath.Dir(path)
	if dir == "." || dir == "" {
		return nil
	}
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return fmt.Errorf("create SQLite database dir %q: %w", dir, err)
	}
	return nil
}

func sqliteDSNPath(dsn string) string {
	raw := strings.TrimSpace(dsn)
	lower := strings.ToLower(raw)
	if lower == ":memory:" || strings.Contains(lower, "mode=memory") {
		return ""
	}
	switch {
	case strings.HasPrefix(lower, "file:"):
		return trimSQLiteDSNQuery(raw[len("file:"):])
	case strings.HasPrefix(lower, "sqlite://"):
		return trimSQLiteDSNQuery(raw[len("sqlite://"):])
	case strings.HasPrefix(lower, "sqlite:"):
		path := raw[len("sqlite:"):]
		path = strings.TrimPrefix(path, "//")
		return trimSQLiteDSNQuery(path)
	default:
		return ""
	}
}

func trimSQLiteDSNQuery(path string) string {
	if idx := strings.IndexByte(path, '?'); idx >= 0 {
		return path[:idx]
	}
	return path
}

func parse(raw string) (Set, error) {
	features := Set{}
	for _, token := range strings.FieldsFunc(raw, func(r rune) bool { return r == ',' || r == ' ' || r == '\t' || r == '\n' }) {
		feature := Feature(strings.ToUpper(strings.TrimSpace(token)))
		if feature == "" {
			continue
		}
		switch feature {
		case MeshX, ULTRA, TiDBZero:
			features[feature] = struct{}{}
		case "FTS":
			features[ULTRA] = struct{}{}
		default:
			return nil, fmt.Errorf("unknown %s feature %q (supported: MESHX, ULTRA, TIDB_ZERO; FTS is accepted as a legacy alias)", EnvName, token)
		}
	}
	if features.Has(ULTRA) && features.Has(TiDBZero) {
		return nil, fmt.Errorf("%s: ULTRA and TIDB_ZERO are mutually exclusive TiDB providers", EnvName)
	}
	return features, nil
}
