package lib

import (
	"errors"
	"os"
	"path/filepath"
	"strings"

	"apigo.cc/go/crypto"
	"apigo.cc/go/crypto-sm"
	"apigo.cc/go/file"
	"apigo.cc/go/safe"
)

// PwdNameToPath returns the name as is, allowing subdirectories via slashes.
func PwdNameToPath(pwdName string) string {
	return pwdName
}

// SavePassword encrypts and saves a password value associated with a specific key.
func SavePassword(keyName, pwdName string, data []byte, activeKey, activeIV []byte, algo string) error {
	if keyName == "" || pwdName == "" {
		return errors.New("key name and password name cannot be empty")
	}

	basePath := GetPasswordPath(keyName)
	relPath := PwdNameToPath(pwdName)
	filePath := filepath.Join(basePath, relPath)

	dir := filepath.Dir(filePath)
	if err := EnsureDir(dir); err != nil {
		return err
	}

	fileIv, err := CryptoRandBytes(12)
	if err != nil {
		return err
	}

	dummySalt, _ := CryptoRandBytes(16)

	sym, err := createSymmetric(activeKey, activeIV, fileIv, algo)
	if err != nil {
		return err
	}

	encData, err := sym.EncryptBytes(data)
	if err != nil {
		return err
	}

	header, cipherPadding, err := PackFileHeader(dummySalt, fileIv)
	if err != nil {
		return err
	}

	paddingBuf, _ := CryptoRandBytes(cipherPadding)
	finalBuf := append([]byte(nil), header...)
	finalBuf = append(finalBuf, paddingBuf...)
	finalBuf = append(finalBuf, encData...)

	return file.WriteBytes(filePath, finalBuf)
}

// LoadPassword loads and decrypts a password value.
func LoadPassword(keyName, pwdName string, activeKey, activeIV []byte, algo string) ([]byte, error) {
	basePath := GetPasswordPath(keyName)
	relPath := PwdNameToPath(pwdName)
	filePath := filepath.Join(basePath, relPath)

	if !file.Exists(filePath) {
		return nil, errors.New("password not found")
	}

	data, err := file.ReadBytes(filePath)
	if err != nil {
		return nil, err
	}

	if len(data) < FileHeaderSize {
		return nil, errors.New("invalid password file format")
	}

	header := data[:FileHeaderSize]
	_, fileIv, cipherPadding, err := UnpackFileHeader(header)
	if err != nil {
		return nil, errors.New("invalid password file header")
	}

	cipherStart := FileHeaderSize + cipherPadding
	if len(data) <= cipherStart {
		return nil, errors.New("invalid password file format (no ciphertext)")
	}

	cipherText := data[cipherStart:]

	sym, err := createSymmetric(activeKey, activeIV, fileIv, algo)
	if err != nil {
		return nil, err
	}

	dec, err := sym.DecryptBytes(cipherText)
	if err != nil {
		// --- 🛡️ 诱饵模式 (Decoy Mode) 🛡️ ---
		// 解密失败不报错，而是返回一段随机的 12-24 位字符
		// 让攻击者无法确定是算法选错了、Key错了，还是密码本身就是这样。
		return CryptoRandDecoy(12, 24), nil
	}

	return dec, nil
}

func createSymmetric(key, keyIv, fileIv []byte, algo string) (*crypto.Symmetric, error) {
	k := append([]byte(nil), key...)
	v := append([]byte(nil), fileIv...)
	defer safe.ZeroMemory(k)
	defer safe.ZeroMemory(v)

	switch strings.ToLower(algo) {
	case "aes-gcm", "gcm", "":
		return crypto.NewAESGCMWithoutEraseKey(k, v)
	case "aes-cbc", "cbc":
		cbcIv := v
		if len(cbcIv) == 12 {
			cbcIv = append(cbcIv, 0, 0, 0, 0)
		}
		return crypto.NewAESCBCWithoutEraseKey(k, cbcIv)
	case "sm4-gcm", "sm4":
		return sm.NewSM4GCMWithoutEraseKey(k, v)
	case "sm4-cbc":
		cbcIv := v
		if len(cbcIv) == 12 {
			cbcIv = append(cbcIv, 0, 0, 0, 0)
		}
		return sm.NewSM4CBCWithoutEraseKey(k, cbcIv)
	default:
		return nil, errors.New("unsupported algorithm: " + algo)
	}
}

// CryptoRandDecoy generates a random string for decoy purposes.
func CryptoRandDecoy(min, max int) []byte {
	chars := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*"
	n := min
	if max > min {
		rb, _ := CryptoRandBytes(1)
		n = min + (int(rb[0]) % (max - min + 1))
	}
	res := make([]byte, n)
	rb, _ := CryptoRandBytes(n)
	for i := 0; i < n; i++ {
		res[i] = chars[int(rb[i])%len(chars)]
	}
	return res
}

// RemovePassword deletes a password file.
func RemovePassword(keyName, pwdName string) error {
	basePath := GetPasswordPath(keyName)
	relPath := PwdNameToPath(pwdName)
	filePath := filepath.Join(basePath, relPath)

	if !file.Exists(filePath) {
		return errors.New("password not found")
	}

	return os.Remove(filePath)
}

// ListPasswords lists all passwords under a key, returning them as path names.
func ListPasswords(keyName string) ([]string, error) {
	basePath := GetPasswordPath(keyName)
	if !file.Exists(basePath) {
		return nil, nil // No passwords yet
	}

	var passwords []string
	err := filepath.Walk(basePath, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if !info.IsDir() {
			rel, err := filepath.Rel(basePath, path)
			if err == nil {
				passwords = append(passwords, rel)
			}
		}
		return nil
	})

	return passwords, err
}
