package vault

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

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

// GetCurrentUnlocker returns the current unlocker for this vault
func (v *Vault) GetCurrentUnlocker() (secret.Unlocker, error) {
	secret.DebugWith("Getting current unlocker", slog.String("vault_name", v.Name))

	vaultDir, err := v.GetDirectory()
	if err != nil {
		secret.Debug("Failed to get vault directory for unlocker", "error", err, "vault_name", v.Name)
		return nil, err
	}

	currentUnlockerPath := filepath.Join(vaultDir, "current-unlocker")

	// Check if the symlink exists
	_, err = v.fs.Stat(currentUnlockerPath)
	if err != nil {
		secret.Debug("Failed to stat current unlocker symlink", "error", err, "path", currentUnlockerPath)
		return nil, fmt.Errorf("failed to read current unlocker: %w", err)
	}

	// Resolve the symlink to get the target directory
	var unlockerDir string
	if _, ok := v.fs.(*afero.OsFs); ok {
		secret.Debug("Resolving unlocker symlink (real filesystem)")
		// For real filesystems, resolve the symlink properly
		unlockerDir, err = ResolveVaultSymlink(v.fs, currentUnlockerPath)
		if err != nil {
			secret.Debug("Failed to resolve unlocker symlink", "error", err, "symlink_path", currentUnlockerPath)
			return nil, fmt.Errorf("failed to resolve current unlocker symlink: %w", err)
		}
	} else {
		secret.Debug("Reading unlocker path (mock filesystem)")
		// Fallback for mock filesystems: read the path from file contents
		unlockerDirBytes, err := afero.ReadFile(v.fs, currentUnlockerPath)
		if err != nil {
			secret.Debug("Failed to read unlocker path file", "error", err, "path", currentUnlockerPath)
			return nil, fmt.Errorf("failed to read current unlocker: %w", err)
		}
		unlockerDir = strings.TrimSpace(string(unlockerDirBytes))
	}

	secret.DebugWith("Resolved unlocker directory",
		slog.String("unlocker_dir", unlockerDir),
		slog.String("vault_name", v.Name),
	)

	// Read unlocker metadata
	metadataPath := filepath.Join(unlockerDir, "unlocker-metadata.json")
	secret.Debug("Reading unlocker metadata", "path", metadataPath)

	metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
	if err != nil {
		secret.Debug("Failed to read unlocker metadata", "error", err, "path", metadataPath)
		return nil, fmt.Errorf("failed to read unlocker metadata: %w", err)
	}

	var metadata UnlockerMetadata
	if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
		secret.Debug("Failed to parse unlocker metadata", "error", err, "path", metadataPath)
		return nil, fmt.Errorf("failed to parse unlocker metadata: %w", err)
	}

	secret.DebugWith("Parsed unlocker metadata",
		slog.String("unlocker_id", metadata.ID),
		slog.String("unlocker_type", metadata.Type),
		slog.Time("created_at", metadata.CreatedAt),
		slog.Any("flags", metadata.Flags),
	)

	// Create unlocker instance using direct constructors with filesystem
	var unlocker secret.Unlocker
	// Convert our metadata to secret.UnlockerMetadata
	secretMetadata := secret.UnlockerMetadata(metadata)
	switch metadata.Type {
	case "passphrase":
		secret.Debug("Creating passphrase unlocker instance", "unlocker_id", metadata.ID)
		unlocker = secret.NewPassphraseUnlocker(v.fs, unlockerDir, secretMetadata)
	case "pgp":
		secret.Debug("Creating PGP unlocker instance", "unlocker_id", metadata.ID)
		unlocker = secret.NewPGPUnlocker(v.fs, unlockerDir, secretMetadata)
	case "keychain":
		secret.Debug("Creating keychain unlocker instance", "unlocker_id", metadata.ID)
		unlocker = secret.NewKeychainUnlocker(v.fs, unlockerDir, secretMetadata)
	default:
		secret.Debug("Unsupported unlocker type", "type", metadata.Type, "unlocker_id", metadata.ID)
		return nil, fmt.Errorf("unsupported unlocker type: %s", metadata.Type)
	}

	secret.DebugWith("Successfully created unlocker instance",
		slog.String("unlocker_type", unlocker.GetType()),
		slog.String("unlocker_id", unlocker.GetID()),
		slog.String("vault_name", v.Name),
	)

	return unlocker, nil
}

// ListUnlockers returns a list of available unlockers for this vault
func (v *Vault) ListUnlockers() ([]UnlockerMetadata, error) {
	vaultDir, err := v.GetDirectory()
	if err != nil {
		return nil, err
	}

	unlockersDir := filepath.Join(vaultDir, "unlockers.d")

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

	// List directories in unlockers.d
	files, err := afero.ReadDir(v.fs, unlockersDir)
	if err != nil {
		return nil, fmt.Errorf("failed to read unlockers directory: %w", err)
	}

	var unlockers []UnlockerMetadata
	for _, file := range files {
		if file.IsDir() {
			// Read metadata file
			metadataPath := filepath.Join(unlockersDir, file.Name(), "unlocker-metadata.json")
			exists, err := afero.Exists(v.fs, metadataPath)
			if err != nil {
				continue
			}
			if !exists {
				continue
			}

			metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
			if err != nil {
				continue
			}

			var metadata UnlockerMetadata
			if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
				continue
			}

			unlockers = append(unlockers, metadata)
		}
	}

	return unlockers, nil
}

// RemoveUnlocker removes an unlocker from this vault
func (v *Vault) RemoveUnlocker(unlockerID string) error {
	vaultDir, err := v.GetDirectory()
	if err != nil {
		return err
	}

	// Find the unlocker directory and create the unlocker instance
	unlockersDir := filepath.Join(vaultDir, "unlockers.d")

	// List directories in unlockers.d
	files, err := afero.ReadDir(v.fs, unlockersDir)
	if err != nil {
		return fmt.Errorf("failed to read unlockers directory: %w", err)
	}

	var unlocker secret.Unlocker
	var unlockerDirPath string
	for _, file := range files {
		if file.IsDir() {
			// Read metadata file
			metadataPath := filepath.Join(unlockersDir, file.Name(), "unlocker-metadata.json")
			exists, err := afero.Exists(v.fs, metadataPath)
			if err != nil || !exists {
				continue
			}

			metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
			if err != nil {
				continue
			}

			var metadata UnlockerMetadata
			if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
				continue
			}

			if metadata.ID == unlockerID {
				unlockerDirPath = filepath.Join(unlockersDir, file.Name())

				// Convert our metadata to secret.UnlockerMetadata
				secretMetadata := secret.UnlockerMetadata(metadata)

				// Create the appropriate unlocker instance
				switch metadata.Type {
				case "passphrase":
					unlocker = secret.NewPassphraseUnlocker(v.fs, unlockerDirPath, secretMetadata)
				case "pgp":
					unlocker = secret.NewPGPUnlocker(v.fs, unlockerDirPath, secretMetadata)
				case "keychain":
					unlocker = secret.NewKeychainUnlocker(v.fs, unlockerDirPath, secretMetadata)
				default:
					return fmt.Errorf("unsupported unlocker type: %s", metadata.Type)
				}
				break
			}
		}
	}

	if unlocker == nil {
		return fmt.Errorf("unlocker with ID %s not found", unlockerID)
	}

	// Use the unlocker's Remove method
	return unlocker.Remove()
}

// SelectUnlocker selects an unlocker as current for this vault
func (v *Vault) SelectUnlocker(unlockerID string) error {
	vaultDir, err := v.GetDirectory()
	if err != nil {
		return err
	}

	// Find the unlocker directory by ID
	unlockersDir := filepath.Join(vaultDir, "unlockers.d")

	// List directories in unlockers.d to find the unlocker
	files, err := afero.ReadDir(v.fs, unlockersDir)
	if err != nil {
		return fmt.Errorf("failed to read unlockers directory: %w", err)
	}

	var targetUnlockerDir string
	for _, file := range files {
		if file.IsDir() {
			// Read metadata file
			metadataPath := filepath.Join(unlockersDir, file.Name(), "unlocker-metadata.json")
			exists, err := afero.Exists(v.fs, metadataPath)
			if err != nil || !exists {
				continue
			}

			metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
			if err != nil {
				continue
			}

			var metadata UnlockerMetadata
			if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
				continue
			}

			if metadata.ID == unlockerID {
				targetUnlockerDir = filepath.Join(unlockersDir, file.Name())
				break
			}
		}
	}

	if targetUnlockerDir == "" {
		return fmt.Errorf("unlocker with ID %s not found", unlockerID)
	}

	// Create/update current unlocker symlink
	currentUnlockerPath := filepath.Join(vaultDir, "current-unlocker")

	// Remove existing symlink if it exists
	if exists, _ := afero.Exists(v.fs, currentUnlockerPath); exists {
		if err := v.fs.Remove(currentUnlockerPath); err != nil {
			secret.Debug("Failed to remove existing unlocker symlink", "error", err, "path", currentUnlockerPath)
		}
	}

	// Create new symlink
	return afero.WriteFile(v.fs, currentUnlockerPath, []byte(targetUnlockerDir), secret.FilePerms)
}

// CreatePassphraseUnlocker creates a new passphrase-protected unlocker
func (v *Vault) CreatePassphraseUnlocker(passphrase string) (*secret.PassphraseUnlocker, error) {
	vaultDir, err := v.GetDirectory()
	if err != nil {
		return nil, fmt.Errorf("failed to get vault directory: %w", err)
	}

	// Create unlocker directory with timestamp
	timestamp := time.Now().Format("2006-01-02.15.04")
	unlockerDir := filepath.Join(vaultDir, "unlockers.d", "passphrase")
	if err := v.fs.MkdirAll(unlockerDir, secret.DirPerms); err != nil {
		return nil, fmt.Errorf("failed to create unlocker directory: %w", err)
	}

	// Generate new age keypair for unlocker
	unlockerIdentity, err := age.GenerateX25519Identity()
	if err != nil {
		return nil, fmt.Errorf("failed to generate unlocker: %w", err)
	}

	// Write public key
	pubKeyPath := filepath.Join(unlockerDir, "pub.age")
	if err := afero.WriteFile(v.fs, pubKeyPath, []byte(unlockerIdentity.Recipient().String()), secret.FilePerms); err != nil {
		return nil, fmt.Errorf("failed to write unlocker public key: %w", err)
	}

	// Encrypt private key with passphrase
	privKeyData := []byte(unlockerIdentity.String())
	encryptedPrivKey, err := secret.EncryptWithPassphrase(privKeyData, passphrase)
	if err != nil {
		return nil, fmt.Errorf("failed to encrypt unlocker private key: %w", err)
	}

	// Write encrypted private key
	privKeyPath := filepath.Join(unlockerDir, "priv.age")
	if err := afero.WriteFile(v.fs, privKeyPath, encryptedPrivKey, secret.FilePerms); err != nil {
		return nil, fmt.Errorf("failed to write encrypted unlocker private key: %w", err)
	}

	// Create metadata
	unlockerID := fmt.Sprintf("%s-passphrase", timestamp)
	metadata := UnlockerMetadata{
		ID:        unlockerID,
		Type:      "passphrase",
		CreatedAt: time.Now(),
		Flags:     []string{},
	}

	// Write metadata
	metadataBytes, err := json.MarshalIndent(metadata, "", "  ")
	if err != nil {
		return nil, fmt.Errorf("failed to marshal metadata: %w", err)
	}

	metadataPath := filepath.Join(unlockerDir, "unlocker-metadata.json")
	if err := afero.WriteFile(v.fs, metadataPath, metadataBytes, secret.FilePerms); err != nil {
		return nil, fmt.Errorf("failed to write unlocker metadata: %w", err)
	}

	// Encrypt long-term private key to this unlocker if vault is unlocked
	if !v.Locked() {
		ltPrivKey := []byte(v.GetLongTermKey().String())
		encryptedLtPrivKey, err := secret.EncryptToRecipient(ltPrivKey, unlockerIdentity.Recipient())
		if err != nil {
			return nil, fmt.Errorf("failed to encrypt long-term private key: %w", err)
		}

		ltPrivKeyPath := filepath.Join(unlockerDir, "longterm.age")
		if err := afero.WriteFile(v.fs, ltPrivKeyPath, encryptedLtPrivKey, secret.FilePerms); err != nil {
			return nil, fmt.Errorf("failed to write encrypted long-term private key: %w", err)
		}
	}

	// Select this unlocker as current
	if err := v.SelectUnlocker(unlockerID); err != nil {
		return nil, fmt.Errorf("failed to select new unlocker: %w", err)
	}

	// Convert our metadata to secret.UnlockerMetadata for the constructor
	secretMetadata := secret.UnlockerMetadata(metadata)

	return secret.NewPassphraseUnlocker(v.fs, unlockerDir, secretMetadata), nil
}