package profile

import (
	"crypto/rand"
	"crypto/sha256"
	"fmt"
	"math/big"
	"os"
	"path/filepath"
	"time"

	"go.yaml.in/yaml/v3"
)

// ---------------------------------------------------------------------------
// YAML file helpers
// ---------------------------------------------------------------------------

func loadYAMLFile(path string, v interface{}) error {
	data, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return nil
		}
		return err
	}
	if mode, ok := sensitiveYAMLFileMode(path); ok {
		if err := os.Chmod(path, mode); err != nil {
			return err
		}
	}
	return yaml.Unmarshal(data, v)
}

func withConfigDirLock(configDir string, fn func() error) error {
	lock, err := AcquireLock(configDir)
	if err != nil {
		return fmt.Errorf("failed to acquire file lock: %w", err)
	}
	defer lock.Release()

	return fn()
}

func saveYAMLFile(v interface{}, path string, mgr *Manager) error {
	return withConfigDirLock(filepath.Dir(path), func() error {
		return saveYAMLFileLocked(v, path, mgr)
	})
}

// saveYAMLFileLocked writes YAML assuming the caller already holds the
// config-directory lock for the target file.
func saveYAMLFileLocked(v interface{}, path string, mgr *Manager) error {
	mode := os.FileMode(0o644)
	if secureMode, ok := sensitiveYAMLFileMode(path); ok {
		mode = secureMode
	}
	return saveYAMLFileLockedMode(v, path, mgr, mode)
}

func sensitiveYAMLFileMode(path string) (os.FileMode, bool) {
	switch filepath.Base(path) {
	case configFileName, "agents.yaml", "channels.yaml", "servers.yaml", "apps.yaml", "mcp-servers.yaml", "dbaas.yaml", "databases.yaml":
		return 0o600, true
	default:
		return 0, false
	}
}

func saveYAMLFileLockedMode(v interface{}, path string, mgr *Manager, mode os.FileMode) error {
	data, err := yaml.Marshal(v)
	if err != nil {
		return err
	}

	existing, err := os.ReadFile(path)
	if err == nil && string(existing) == string(data) {
		if info, statErr := os.Stat(path); statErr == nil && info.Mode().Perm() != mode {
			return os.Chmod(path, mode)
		}
		return nil
	}

	if err := writeFileAtomic(path, data, mode); err != nil {
		return err
	}
	// Tell the watcher to ignore only this exact content revision. A later
	// external replacement must still be observed.
	if mgr != nil && mgr.watcher != nil {
		mgr.watcher.SuppressRevision(path, sha256.Sum256(data))
	}
	return os.Chmod(path, mode)
}

func writeFileAtomic(path string, data []byte, mode os.FileMode) error {
	dir := filepath.Dir(path)
	if err := os.MkdirAll(dir, 0755); err != nil {
		return err
	}

	tmp, err := os.CreateTemp(dir, "."+filepath.Base(path)+".tmp-*")
	if err != nil {
		return err
	}
	tmpPath := tmp.Name()
	removeTmp := true
	defer func() {
		if removeTmp {
			_ = os.Remove(tmpPath)
		}
	}()

	if err := tmp.Chmod(mode); err != nil {
		_ = tmp.Close()
		return err
	}
	if _, err := tmp.Write(data); err != nil {
		_ = tmp.Close()
		return err
	}
	if err := tmp.Sync(); err != nil {
		_ = tmp.Close()
		return err
	}
	if err := tmp.Close(); err != nil {
		return err
	}
	if err := os.Rename(tmpPath, path); err != nil {
		return err
	}
	removeTmp = false
	if dirFile, err := os.Open(dir); err == nil {
		_ = dirFile.Sync()
		_ = dirFile.Close()
	}
	return nil
}

// ---------------------------------------------------------------------------
// File watcher
// ---------------------------------------------------------------------------

// StartWatcher starts a filesystem watcher that monitors profile YAML files
// for external changes and reloads them into memory.
func (m *Manager) StartWatcher() error {
	m.mu.Lock()
	defer m.mu.Unlock()
	if m.watcher != nil {
		return nil
	}
	w, err := NewFileWatcher(m)
	if err != nil {
		return err
	}
	w.WatchRuntimeConfigs(m.runtimes)
	m.watcher = w
	return nil
}

// StopWatcher stops the filesystem watcher.
func (m *Manager) StopWatcher() {
	m.mu.Lock()
	defer m.mu.Unlock()
	if m.watcher != nil {
		m.watcher.Close()
		m.watcher = nil
	}
}

// ---------------------------------------------------------------------------
// AgentTeam server ID generation
// ---------------------------------------------------------------------------

const base58Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"

func base58Encode(data []byte) string {
	x := new(big.Int).SetBytes(data)
	base := big.NewInt(58)
	zero := big.NewInt(0)
	mod := new(big.Int)

	var result []byte
	for x.Cmp(zero) > 0 {
		x.DivMod(x, base, mod)
		result = append(result, base58Alphabet[mod.Int64()])
	}

	for _, b := range data {
		if b != 0 {
			break
		}
		result = append(result, base58Alphabet[0])
	}

	for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
		result[i], result[j] = result[j], result[i]
	}
	return string(result)
}

// generateServerID generates a unique server ID with prefix "srv_" followed by
// 12 random base58 characters.
func generateServerID() string {
	const idLen = 12
	// 12 raw bytes always encode to at least 12 base58 characters; truncate to exactly 12.
	const rawLen = 12
	b := make([]byte, rawLen)
	if _, err := rand.Read(b); err != nil {
		for i := range b {
			b[i] = byte(time.Now().UnixNano() >> (uint(i) * 8))
		}
	}
	encoded := base58Encode(b)
	if len(encoded) > idLen {
		encoded = encoded[:idLen]
	}
	return "srv_" + encoded
}

func defaultServerName(legacyKind string) string {
	switch legacyKind {
	case "agentteam":
		return ""
	default:
		return legacyKind
	}
}

// generateAgentID generates a unique agent ID with prefix "agt_" followed by
// 12 random base58 characters.
func generateAgentID() string {
	const idLen = 12
	// 9 random bytes encode to ~12 base58 characters; truncate to exactly 12.
	const rawLen = 9
	b := make([]byte, rawLen)
	if _, err := rand.Read(b); err != nil {
		for i := range b {
			b[i] = byte(time.Now().UnixNano() >> (uint(i) * 8))
		}
	}
	encoded := base58Encode(b)
	if len(encoded) > idLen {
		encoded = encoded[:idLen]
	}
	return "agt_" + encoded
}

// generateDBaaSID generates a unique dbaas service ID with prefix "dbs_"
// followed by 12 random base58 characters.
func generateDBaaSID() string {
	const idLen = 12
	const rawLen = 9
	b := make([]byte, rawLen)
	if _, err := rand.Read(b); err != nil {
		for i := range b {
			b[i] = byte(time.Now().UnixNano() >> (uint(i) * 8))
		}
	}
	encoded := base58Encode(b)
	if len(encoded) > idLen {
		encoded = encoded[:idLen]
	}
	return "dbs_" + encoded
}

// generateDatabaseID generates a unique database instance ID with prefix "dbi_"
// followed by 12 random base58 characters.
func generateDatabaseID() string {
	const idLen = 12
	const rawLen = 9
	b := make([]byte, rawLen)
	if _, err := rand.Read(b); err != nil {
		for i := range b {
			b[i] = byte(time.Now().UnixNano() >> (uint(i) * 8))
		}
	}
	encoded := base58Encode(b)
	if len(encoded) > idLen {
		encoded = encoded[:idLen]
	}
	return "dbi_" + encoded
}
