package secret

import (
	"encoding/json"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"strings"
	"time"

	"filippo.io/age"
	"git.eeqj.de/sneak/secret/pkg/agehd"
	"github.com/spf13/afero"
)

// VaultInterface defines the interface that vault implementations must satisfy
type VaultInterface interface {
	GetDirectory() (string, error)
	AddSecret(name string, value []byte, force bool) error
	GetName() string
	GetFilesystem() afero.Fs
	GetCurrentUnlocker() (Unlocker, error)
	CreatePassphraseUnlocker(passphrase string) (*PassphraseUnlocker, error)
}

// Secret represents a secret in a vault
type Secret struct {
	Name      string
	Directory string
	Metadata  SecretMetadata
	vault     VaultInterface
}

// NewSecret creates a new Secret instance
func NewSecret(vault VaultInterface, name string) *Secret {
	DebugWith("Creating new secret instance",
		slog.String("secret_name", name),
		slog.String("vault_name", vault.GetName()),
	)

	// Convert slashes to percent signs for storage directory name
	storageName := strings.ReplaceAll(name, "/", "%")
	vaultDir, _ := vault.GetDirectory()
	secretDir := filepath.Join(vaultDir, "secrets.d", storageName)

	DebugWith("Secret storage details",
		slog.String("secret_name", name),
		slog.String("storage_name", storageName),
		slog.String("secret_dir", secretDir),
	)

	return &Secret{
		Name:      name,
		Directory: secretDir,
		vault:     vault,
		Metadata: SecretMetadata{
			CreatedAt: time.Now(),
			UpdatedAt: time.Now(),
		},
	}
}

// Save is deprecated - use vault.AddSecret directly which creates versions
// Kept for backward compatibility
func (s *Secret) Save(value []byte, force bool) error {
	DebugWith("Saving secret (deprecated method)",
		slog.String("secret_name", s.Name),
		slog.String("vault_name", s.vault.GetName()),
		slog.Int("value_length", len(value)),
		slog.Bool("force", force),
	)

	err := s.vault.AddSecret(s.Name, value, force)
	if err != nil {
		Debug("Failed to save secret", "error", err, "secret_name", s.Name)
		return err
	}

	Debug("Successfully saved secret", "secret_name", s.Name)
	return nil
}

// GetValue retrieves and decrypts the current version's value using the provided unlocker
func (s *Secret) GetValue(unlocker Unlocker) ([]byte, error) {
	DebugWith("Getting secret value",
		slog.String("secret_name", s.Name),
		slog.String("vault_name", s.vault.GetName()),
	)

	// Check if secret exists
	exists, err := s.Exists()
	if err != nil {
		Debug("Failed to check if secret exists during GetValue", "error", err, "secret_name", s.Name)
		return nil, fmt.Errorf("failed to check if secret exists: %w", err)
	}
	if !exists {
		Debug("Secret not found during GetValue", "secret_name", s.Name, "vault_name", s.vault.GetName())
		return nil, fmt.Errorf("secret %s not found", s.Name)
	}

	Debug("Secret exists, getting current version", "secret_name", s.Name)

	// Get current version
	currentVersion, err := GetCurrentVersion(s.vault.GetFilesystem(), s.Directory)
	if err != nil {
		Debug("Failed to get current version", "error", err, "secret_name", s.Name)
		return nil, fmt.Errorf("failed to get current version: %w", err)
	}

	// Create version object
	version := NewSecretVersion(s.vault, s.Name, currentVersion)

	// Check if we have SB_SECRET_MNEMONIC environment variable for direct decryption
	if envMnemonic := os.Getenv(EnvMnemonic); envMnemonic != "" {
		Debug("Using mnemonic from environment for direct long-term key derivation", "secret_name", s.Name)

		// Get vault directory to read metadata
		vaultDir, err := s.vault.GetDirectory()
		if err != nil {
			Debug("Failed to get vault directory", "error", err, "secret_name", s.Name)
			return nil, fmt.Errorf("failed to get vault directory: %w", err)
		}

		// Load vault metadata to get the correct derivation index
		metadataPath := filepath.Join(vaultDir, "vault-metadata.json")
		metadataBytes, err := afero.ReadFile(s.vault.GetFilesystem(), metadataPath)
		if err != nil {
			Debug("Failed to read vault metadata", "error", err, "path", metadataPath)
			return nil, fmt.Errorf("failed to read vault metadata: %w", err)
		}

		var metadata VaultMetadata
		if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
			Debug("Failed to parse vault metadata", "error", err, "secret_name", s.Name)
			return nil, fmt.Errorf("failed to parse vault metadata: %w", err)
		}

		DebugWith("Using vault derivation index from metadata",
			slog.String("secret_name", s.Name),
			slog.String("vault_name", s.vault.GetName()),
			slog.Uint64("derivation_index", uint64(metadata.DerivationIndex)),
		)

		// Use mnemonic with the vault's derivation index from metadata
		ltIdentity, err := agehd.DeriveIdentity(envMnemonic, metadata.DerivationIndex)
		if err != nil {
			Debug("Failed to derive long-term key from mnemonic for secret", "error", err, "secret_name", s.Name)
			return nil, fmt.Errorf("failed to derive long-term key from mnemonic: %w", err)
		}

		Debug("Successfully derived long-term key from mnemonic", "secret_name", s.Name)

		// Use the long-term key to decrypt the version
		return version.GetValue(ltIdentity)
	}

	Debug("Using unlocker for vault access", "secret_name", s.Name)

	// Use the provided unlocker to get the vault's long-term private key
	if unlocker == nil {
		Debug("No unlocker provided for secret decryption", "secret_name", s.Name)
		return nil, fmt.Errorf("unlocker required to decrypt secret")
	}

	DebugWith("Getting vault's long-term key using unlocker",
		slog.String("secret_name", s.Name),
		slog.String("unlocker_type", unlocker.GetType()),
		slog.String("unlocker_id", unlocker.GetID()),
	)

	// Step 1: Use the unlocker to get the vault's long-term private key
	unlockIdentity, err := unlocker.GetIdentity()
	if err != nil {
		Debug("Failed to get unlocker identity", "error", err, "secret_name", s.Name, "unlocker_type", unlocker.GetType())
		return nil, fmt.Errorf("failed to get unlocker identity: %w", err)
	}

	// Read the encrypted long-term private key from the unlocker directory
	encryptedLtPrivKeyPath := filepath.Join(unlocker.GetDirectory(), "longterm.age")
	Debug("Reading encrypted long-term private key", "path", encryptedLtPrivKeyPath)

	encryptedLtPrivKey, err := afero.ReadFile(s.vault.GetFilesystem(), encryptedLtPrivKeyPath)
	if err != nil {
		Debug("Failed to read encrypted long-term private key", "error", err, "path", encryptedLtPrivKeyPath)
		return nil, fmt.Errorf("failed to read encrypted long-term private key: %w", err)
	}

	// Decrypt the encrypted long-term private key using the unlocker
	Debug("Decrypting long-term private key using unlocker", "secret_name", s.Name)
	ltPrivKeyData, err := DecryptWithIdentity(encryptedLtPrivKey, unlockIdentity)
	if err != nil {
		Debug("Failed to decrypt long-term private key", "error", err, "secret_name", s.Name)
		return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
	}

	// Parse the long-term private key
	Debug("Parsing long-term private key", "secret_name", s.Name)
	ltIdentity, err := age.ParseX25519Identity(string(ltPrivKeyData))
	if err != nil {
		Debug("Failed to parse long-term private key", "error", err, "secret_name", s.Name)
		return nil, fmt.Errorf("failed to parse long-term private key: %w", err)
	}

	DebugWith("Successfully obtained vault's long-term key",
		slog.String("secret_name", s.Name),
		slog.String("public_key", ltIdentity.Recipient().String()),
	)

	// Use the long-term key to decrypt the version
	return version.GetValue(ltIdentity)
}

// LoadMetadata is deprecated - metadata is now per-version and encrypted
func (s *Secret) LoadMetadata() error {
	Debug("LoadMetadata called but is deprecated in versioned model", "secret_name", s.Name)
	// For backward compatibility, we'll populate with basic info
	now := time.Now()
	s.Metadata = SecretMetadata{
		CreatedAt: now,
		UpdatedAt: now,
	}
	return nil
}

// GetMetadata returns the secret metadata (deprecated)
func (s *Secret) GetMetadata() SecretMetadata {
	Debug("GetMetadata called but is deprecated in versioned model", "secret_name", s.Name)
	return s.Metadata
}

// GetEncryptedData is deprecated - data is now stored in versions
func (s *Secret) GetEncryptedData() ([]byte, error) {
	Debug("GetEncryptedData called but is deprecated in versioned model", "secret_name", s.Name)
	return nil, fmt.Errorf("GetEncryptedData is deprecated - use version-specific methods")
}

// Exists checks if the secret exists on disk
func (s *Secret) Exists() (bool, error) {
	DebugWith("Checking if secret exists",
		slog.String("secret_name", s.Name),
		slog.String("vault_name", s.vault.GetName()),
	)

	// Check if the secret directory exists and has a current symlink
	exists, err := afero.DirExists(s.vault.GetFilesystem(), s.Directory)
	if err != nil {
		Debug("Failed to check secret directory existence", "error", err, "secret_dir", s.Directory)
		return false, err
	}

	if !exists {
		Debug("Secret directory does not exist", "secret_dir", s.Directory)
		return false, nil
	}

	// Check if current symlink exists
	_, err = GetCurrentVersion(s.vault.GetFilesystem(), s.Directory)
	if err != nil {
		Debug("No current version found", "error", err, "secret_name", s.Name)
		return false, nil
	}

	DebugWith("Secret existence check result",
		slog.String("secret_name", s.Name),
		slog.Bool("exists", true),
	)

	return true, nil
}

// GetCurrentVault gets the current vault from the file system
// This function is a wrapper around the actual implementation in the vault package
// and exists to break the import cycle.
func GetCurrentVault(fs afero.Fs, stateDir string) (VaultInterface, error) {
	// This is a forward declaration. The actual implementation is provided
	// by the vault package when it calls RegisterGetCurrentVaultFunc.
	if getCurrentVaultFunc == nil {
		return nil, fmt.Errorf("GetCurrentVault function not registered")
	}
	return getCurrentVaultFunc(fs, stateDir)
}

// getCurrentVaultFunc is a function variable that will be set by the vault package
// to implement the actual GetCurrentVault functionality
var getCurrentVaultFunc func(fs afero.Fs, stateDir string) (VaultInterface, error)

// RegisterGetCurrentVaultFunc allows the vault package to register its implementation
// of GetCurrentVault to break the import cycle
func RegisterGetCurrentVaultFunc(fn func(fs afero.Fs, stateDir string) (VaultInterface, error)) {
	getCurrentVaultFunc = fn
}
