package vault

import (
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"time"

	"git.eeqj.de/sneak/secret/internal/secret"
	"git.eeqj.de/sneak/secret/pkg/agehd"
	"github.com/spf13/afero"
)

// Register the GetCurrentVault function with the secret package
func init() {
	secret.RegisterGetCurrentVaultFunc(func(fs afero.Fs, stateDir string) (secret.VaultInterface, error) {
		return GetCurrentVault(fs, stateDir)
	})
}

// isValidVaultName validates vault names according to the format [a-z0-9\.\-\_]+
// Note: We don't allow slashes in vault names unlike secret names
func isValidVaultName(name string) bool {
	if name == "" {
		return false
	}
	matched, _ := regexp.MatchString(`^[a-z0-9\.\-\_]+$`, name)
	return matched
}

// ResolveVaultSymlink resolves the currentvault symlink by reading either the symlink target or file contents
// This function is designed to work on both Unix and Windows systems, as well as with in-memory filesystems
func ResolveVaultSymlink(fs afero.Fs, symlinkPath string) (string, error) {
	secret.Debug("resolveVaultSymlink starting", "symlink_path", symlinkPath)

	// First try to handle the path as a real symlink (works on Unix systems)
	if _, ok := fs.(*afero.OsFs); ok {
		secret.Debug("Using real filesystem symlink resolution")

		// Check if the symlink exists
		secret.Debug("Checking symlink target", "symlink_path", symlinkPath)
		target, err := os.Readlink(symlinkPath)
		if err == nil {
			secret.Debug("Symlink points to", "target", target)

			// On real filesystem, we need to handle relative symlinks
			// by resolving them relative to the symlink's directory
			if !filepath.IsAbs(target) {
				// Get the current directory before changing
				originalDir, err := os.Getwd()
				if err != nil {
					return "", fmt.Errorf("failed to get current directory: %w", err)
				}
				secret.Debug("Got current directory", "original_dir", originalDir)

				// Change to the symlink's directory
				symlinkDir := filepath.Dir(symlinkPath)
				secret.Debug("Changing to symlink directory", "symlink_path", symlinkDir)
				secret.Debug("About to call os.Chdir - this might hang if symlink is broken")
				if err := os.Chdir(symlinkDir); err != nil {
					return "", fmt.Errorf("failed to change to symlink directory: %w", err)
				}
				secret.Debug("Changed to symlink directory successfully - os.Chdir completed")

				// Get the absolute path of the target
				secret.Debug("Getting absolute path of current directory")
				absolutePath, err := os.Getwd()
				if err != nil {
					// Try to restore original directory before returning error
					_ = os.Chdir(originalDir)
					return "", fmt.Errorf("failed to get absolute path: %w", err)
				}
				secret.Debug("Got absolute path", "absolute_path", absolutePath)

				// Restore the original directory
				secret.Debug("Restoring original directory", "original_dir", originalDir)
				if err := os.Chdir(originalDir); err != nil {
					return "", fmt.Errorf("failed to restore original directory: %w", err)
				}
				secret.Debug("Restored original directory successfully")

				// Use the absolute path of the target
				target = absolutePath
			}

			secret.Debug("resolveVaultSymlink completed successfully", "result", target)
			return target, nil
		}
	}

	// Fallback: treat it as a regular file containing the target path
	secret.Debug("Fallback: trying to read regular file with target path")

	fileData, err := afero.ReadFile(fs, symlinkPath)
	if err != nil {
		secret.Debug("Failed to read target path file", "error", err)
		return "", fmt.Errorf("failed to read vault symlink: %w", err)
	}

	target := string(fileData)
	secret.Debug("Read target path from file", "target", target)

	secret.Debug("resolveVaultSymlink completed via fallback", "result", target)
	return target, nil
}

// GetCurrentVault gets the current vault from the file system
func GetCurrentVault(fs afero.Fs, stateDir string) (*Vault, error) {
	secret.Debug("Getting current vault", "state_dir", stateDir)

	// Check if the current vault symlink exists
	currentVaultPath := filepath.Join(stateDir, "currentvault")

	secret.Debug("Checking current vault symlink", "path", currentVaultPath)
	_, err := fs.Stat(currentVaultPath)
	if err != nil {
		secret.Debug("Failed to stat current vault symlink", "error", err, "path", currentVaultPath)
		return nil, fmt.Errorf("failed to read current vault symlink: %w", err)
	}

	secret.Debug("Current vault symlink exists")

	// Resolve the symlink to get the actual vault directory
	secret.Debug("Resolving vault symlink")
	targetPath, err := ResolveVaultSymlink(fs, currentVaultPath)
	if err != nil {
		return nil, err
	}

	secret.Debug("Resolved vault symlink", "target_path", targetPath)

	// Extract the vault name from the path
	// The path will be something like "/path/to/vaults.d/default"
	vaultName := filepath.Base(targetPath)
	secret.Debug("Extracted vault name", "vault_name", vaultName)

	secret.Debug("Current vault resolved", "vault_name", vaultName, "target_path", targetPath)

	// Create and return the vault
	return NewVault(fs, stateDir, vaultName), nil
}

// ListVaults lists all vaults in the state directory
func ListVaults(fs afero.Fs, stateDir string) ([]string, error) {
	vaultsDir := filepath.Join(stateDir, "vaults.d")

	// Check if vaults directory exists
	exists, err := afero.DirExists(fs, vaultsDir)
	if err != nil {
		return nil, fmt.Errorf("failed to check if vaults directory exists: %w", err)
	}
	if !exists {
		return []string{}, nil
	}

	// Read the vaults directory
	entries, err := afero.ReadDir(fs, vaultsDir)
	if err != nil {
		return nil, fmt.Errorf("failed to read vaults directory: %w", err)
	}

	// Extract vault names
	var vaults []string
	for _, entry := range entries {
		if entry.IsDir() {
			vaults = append(vaults, entry.Name())
		}
	}

	return vaults, nil
}

// CreateVault creates a new vault
func CreateVault(fs afero.Fs, stateDir string, name string) (*Vault, error) {
	secret.Debug("Creating new vault", "name", name, "state_dir", stateDir)

	// Validate vault name
	if !isValidVaultName(name) {
		secret.Debug("Invalid vault name provided", "vault_name", name)
		return nil, fmt.Errorf("invalid vault name '%s': must match pattern [a-z0-9.\\-_]+", name)
	}
	secret.Debug("Vault name validation passed", "vault_name", name)

	// Create vault directory structure
	vaultDir := filepath.Join(stateDir, "vaults.d", name)
	secret.Debug("Creating vault directory structure", "vault_dir", vaultDir)

	// Create main vault directory
	if err := fs.MkdirAll(vaultDir, secret.DirPerms); err != nil {
		return nil, fmt.Errorf("failed to create vault directory: %w", err)
	}

	// Create secrets directory
	secretsDir := filepath.Join(vaultDir, "secrets.d")
	if err := fs.MkdirAll(secretsDir, secret.DirPerms); err != nil {
		return nil, fmt.Errorf("failed to create secrets directory: %w", err)
	}

	// Create unlockers directory
	unlockersDir := filepath.Join(vaultDir, "unlockers.d")
	if err := fs.MkdirAll(unlockersDir, secret.DirPerms); err != nil {
		return nil, fmt.Errorf("failed to create unlockers directory: %w", err)
	}

	// Check if mnemonic is available in environment
	mnemonic := os.Getenv(secret.EnvMnemonic)
	var derivationIndex uint32
	var publicKeyHash string

	if mnemonic != "" {
		secret.Debug("Mnemonic found in environment, deriving long-term key", "vault", name)

		// Get the next available derivation index for this mnemonic
		var err error
		derivationIndex, err = GetNextDerivationIndex(fs, stateDir, mnemonic)
		if err != nil {
			return nil, fmt.Errorf("failed to get next derivation index: %w", err)
		}

		// Derive the long-term key using the actual derivation index
		ltIdentity, err := agehd.DeriveIdentity(mnemonic, derivationIndex)
		if err != nil {
			return nil, fmt.Errorf("failed to derive long-term key: %w", err)
		}

		// Write the public key
		ltPubKey := ltIdentity.Recipient().String()
		ltPubKeyPath := filepath.Join(vaultDir, "pub.age")
		if err := afero.WriteFile(fs, ltPubKeyPath, []byte(ltPubKey), secret.FilePerms); err != nil {
			return nil, fmt.Errorf("failed to write long-term public key: %w", err)
		}
		secret.Debug("Wrote long-term public key", "path", ltPubKeyPath)

		// Compute public key hash from index 0 (same for all vaults with this mnemonic)
		// This is used to identify which vaults belong to the same mnemonic family
		identity0, err := agehd.DeriveIdentity(mnemonic, 0)
		if err != nil {
			return nil, fmt.Errorf("failed to derive identity for index 0: %w", err)
		}
		publicKeyHash = ComputeDoubleSHA256([]byte(identity0.Recipient().String()))
	} else {
		secret.Debug("No mnemonic in environment, vault created without long-term key", "vault", name)
		// Use 0 for derivation index when no mnemonic is provided
		derivationIndex = 0
	}

	// Save vault metadata
	metadata := &VaultMetadata{
		CreatedAt:       time.Now(),
		DerivationIndex: derivationIndex,
		PublicKeyHash:   publicKeyHash,
	}
	if err := SaveVaultMetadata(fs, vaultDir, metadata); err != nil {
		return nil, fmt.Errorf("failed to save vault metadata: %w", err)
	}

	// Select the newly created vault as current
	secret.Debug("Selecting newly created vault as current", "name", name)
	if err := SelectVault(fs, stateDir, name); err != nil {
		return nil, fmt.Errorf("failed to select vault: %w", err)
	}

	// Create and return the vault
	secret.Debug("Successfully created vault", "name", name)
	return NewVault(fs, stateDir, name), nil
}

// SelectVault selects the given vault as the current vault
func SelectVault(fs afero.Fs, stateDir string, name string) error {
	secret.Debug("Selecting vault", "vault_name", name, "state_dir", stateDir)

	// Validate vault name
	if !isValidVaultName(name) {
		secret.Debug("Invalid vault name provided", "vault_name", name)
		return fmt.Errorf("invalid vault name '%s': must match pattern [a-z0-9.\\-_]+", name)
	}
	secret.Debug("Vault name validation passed", "vault_name", name)

	// Check if vault exists
	vaultDir := filepath.Join(stateDir, "vaults.d", name)
	exists, err := afero.DirExists(fs, vaultDir)
	if err != nil {
		return fmt.Errorf("failed to check if vault exists: %w", err)
	}
	if !exists {
		return fmt.Errorf("vault %s does not exist", name)
	}

	// Create or update the current vault symlink/file
	currentVaultPath := filepath.Join(stateDir, "currentvault")
	targetPath := filepath.Join(stateDir, "vaults.d", name)

	// First try to remove existing symlink if it exists
	if _, err := fs.Stat(currentVaultPath); err == nil {
		secret.Debug("Removing existing current vault symlink", "path", currentVaultPath)
		// Ignore errors from Remove as we'll try to create/update it anyway.
		// On some systems, removing a symlink may fail but the subsequent create may still succeed.
		_ = fs.Remove(currentVaultPath)
	}

	// Try to create a real symlink first (works on Unix systems)
	if _, ok := fs.(*afero.OsFs); ok {
		secret.Debug("Creating vault symlink", "target", targetPath, "link", currentVaultPath)
		if err := os.Symlink(targetPath, currentVaultPath); err == nil {
			secret.Debug("Successfully selected vault", "vault_name", name)
			return nil
		}
		// If symlink creation fails, fall back to regular file
	}

	// Fallback: create a regular file with the target path
	secret.Debug("Fallback: creating regular file with target path", "target", targetPath)
	if err := afero.WriteFile(fs, currentVaultPath, []byte(targetPath), secret.FilePerms); err != nil {
		return fmt.Errorf("failed to select vault: %w", err)
	}

	secret.Debug("Successfully selected vault", "vault_name", name)
	return nil
}
