package secret

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

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

// KeychainUnlockerMetadata extends UnlockerMetadata with keychain-specific data
type KeychainUnlockerMetadata struct {
	UnlockerMetadata
	// Age keypair information
	AgePublicKey string `json:"age_public_key"`
	// Keychain item name
	KeychainItemName string `json:"keychain_item_name"`
}

// KeychainUnlocker represents a macOS Keychain-protected unlocker
type KeychainUnlocker struct {
	Directory string
	Metadata  UnlockerMetadata
	fs        afero.Fs
}

// KeychainData represents the data stored in the macOS keychain
type KeychainData struct {
	AgePublicKey         string `json:"age_public_key"`
	AgePrivKeyPassphrase string `json:"age_priv_key_passphrase"`
	EncryptedLongtermKey string `json:"encrypted_longterm_key"`
}

// GetIdentity implements Unlocker interface for Keychain-based unlockers
func (k *KeychainUnlocker) GetIdentity() (*age.X25519Identity, error) {
	DebugWith("Getting keychain unlocker identity",
		slog.String("unlocker_id", k.GetID()),
		slog.String("unlocker_type", k.GetType()),
	)

	// Step 1: Get keychain item name
	keychainItemName, err := k.GetKeychainItemName()
	if err != nil {
		Debug("Failed to get keychain item name", "error", err, "unlocker_id", k.GetID())
		return nil, fmt.Errorf("failed to get keychain item name: %w", err)
	}

	// Step 2: Retrieve data from keychain
	Debug("Retrieving data from macOS keychain", "keychain_item", keychainItemName)
	keychainDataBytes, err := retrieveFromKeychain(keychainItemName)
	if err != nil {
		Debug("Failed to retrieve data from keychain", "error", err, "keychain_item", keychainItemName)
		return nil, fmt.Errorf("failed to retrieve data from keychain: %w", err)
	}

	DebugWith("Retrieved data from keychain",
		slog.String("unlocker_id", k.GetID()),
		slog.Int("data_length", len(keychainDataBytes)),
	)

	// Step 3: Parse keychain data
	var keychainData KeychainData
	if err := json.Unmarshal(keychainDataBytes, &keychainData); err != nil {
		Debug("Failed to parse keychain data", "error", err, "unlocker_id", k.GetID())
		return nil, fmt.Errorf("failed to parse keychain data: %w", err)
	}

	Debug("Parsed keychain data successfully", "unlocker_id", k.GetID())

	// Step 4: Read the encrypted age private key from filesystem
	agePrivKeyPath := filepath.Join(k.Directory, "priv.age")
	Debug("Reading encrypted age private key", "path", agePrivKeyPath)

	encryptedAgePrivKeyData, err := afero.ReadFile(k.fs, agePrivKeyPath)
	if err != nil {
		Debug("Failed to read encrypted age private key", "error", err, "path", agePrivKeyPath)
		return nil, fmt.Errorf("failed to read encrypted age private key: %w", err)
	}

	DebugWith("Read encrypted age private key",
		slog.String("unlocker_id", k.GetID()),
		slog.Int("encrypted_length", len(encryptedAgePrivKeyData)),
	)

	// Step 5: Decrypt the age private key using the passphrase from keychain
	Debug("Decrypting age private key with keychain passphrase", "unlocker_id", k.GetID())
	agePrivKeyData, err := DecryptWithPassphrase(encryptedAgePrivKeyData, keychainData.AgePrivKeyPassphrase)
	if err != nil {
		Debug("Failed to decrypt age private key with keychain passphrase", "error", err, "unlocker_id", k.GetID())
		return nil, fmt.Errorf("failed to decrypt age private key with keychain passphrase: %w", err)
	}

	DebugWith("Successfully decrypted age private key with keychain passphrase",
		slog.String("unlocker_id", k.GetID()),
		slog.Int("decrypted_length", len(agePrivKeyData)),
	)

	// Step 6: Parse the decrypted age private key
	Debug("Parsing decrypted age private key", "unlocker_id", k.GetID())
	ageIdentity, err := age.ParseX25519Identity(string(agePrivKeyData))
	if err != nil {
		Debug("Failed to parse age private key", "error", err, "unlocker_id", k.GetID())
		return nil, fmt.Errorf("failed to parse age private key: %w", err)
	}

	DebugWith("Successfully parsed keychain age identity",
		slog.String("unlocker_id", k.GetID()),
		slog.String("public_key", ageIdentity.Recipient().String()),
	)

	return ageIdentity, nil
}

// GetType implements Unlocker interface
func (k *KeychainUnlocker) GetType() string {
	return "keychain"
}

// GetMetadata implements Unlocker interface
func (k *KeychainUnlocker) GetMetadata() UnlockerMetadata {
	return k.Metadata
}

// GetDirectory implements Unlocker interface
func (k *KeychainUnlocker) GetDirectory() string {
	return k.Directory
}

// GetID implements Unlocker interface
func (k *KeychainUnlocker) GetID() string {
	return k.Metadata.ID
}

// ID implements Unlocker interface - generates ID from keychain item name
func (k *KeychainUnlocker) ID() string {
	// Generate ID using keychain item name
	keychainItemName, err := k.GetKeychainItemName()
	if err != nil {
		// Fallback to metadata ID if we can't read the keychain item name
		return k.Metadata.ID
	}
	return fmt.Sprintf("%s-keychain", keychainItemName)
}

// Remove implements Unlocker interface - removes the keychain unlocker
func (k *KeychainUnlocker) Remove() error {
	// Step 1: Get keychain item name
	keychainItemName, err := k.GetKeychainItemName()
	if err != nil {
		Debug("Failed to get keychain item name during removal", "error", err, "unlocker_id", k.GetID())
		return fmt.Errorf("failed to get keychain item name: %w", err)
	}

	// Step 2: Remove from keychain
	Debug("Removing keychain item", "keychain_item", keychainItemName)
	if err := deleteFromKeychain(keychainItemName); err != nil {
		Debug("Failed to remove keychain item", "error", err, "keychain_item", keychainItemName)
		return fmt.Errorf("failed to remove keychain item: %w", err)
	}

	// Step 3: Remove directory
	Debug("Removing keychain unlocker directory", "directory", k.Directory)
	if err := k.fs.RemoveAll(k.Directory); err != nil {
		Debug("Failed to remove keychain unlocker directory", "error", err, "directory", k.Directory)
		return fmt.Errorf("failed to remove keychain unlocker directory: %w", err)
	}

	Debug("Successfully removed keychain unlocker", "unlocker_id", k.GetID(), "keychain_item", keychainItemName)
	return nil
}

// NewKeychainUnlocker creates a new KeychainUnlocker instance
func NewKeychainUnlocker(fs afero.Fs, directory string, metadata UnlockerMetadata) *KeychainUnlocker {
	return &KeychainUnlocker{
		Directory: directory,
		Metadata:  metadata,
		fs:        fs,
	}
}

// GetKeychainItemName returns the keychain item name from metadata
func (k *KeychainUnlocker) GetKeychainItemName() (string, error) {
	// Load the metadata
	metadataPath := filepath.Join(k.Directory, "unlocker-metadata.json")
	metadataData, err := afero.ReadFile(k.fs, metadataPath)
	if err != nil {
		return "", fmt.Errorf("failed to read keychain metadata: %w", err)
	}

	var keychainMetadata KeychainUnlockerMetadata
	if err := json.Unmarshal(metadataData, &keychainMetadata); err != nil {
		return "", fmt.Errorf("failed to parse keychain metadata: %w", err)
	}

	return keychainMetadata.KeychainItemName, nil
}

// generateKeychainUnlockerName generates a unique name for the keychain unlocker
func generateKeychainUnlockerName(vaultName string) (string, error) {
	hostname, err := os.Hostname()
	if err != nil {
		return "", fmt.Errorf("failed to get hostname: %w", err)
	}

	// Format: secret-<vault>-<hostname>-<date>
	enrollmentDate := time.Now().Format("2006-01-02")
	return fmt.Sprintf("secret-%s-%s-%s", vaultName, hostname, enrollmentDate), nil
}

// CreateKeychainUnlocker creates a new keychain unlocker and stores it in the vault
func CreateKeychainUnlocker(fs afero.Fs, stateDir string) (*KeychainUnlocker, error) {
	// Check if we're on macOS
	if err := checkMacOSAvailable(); err != nil {
		return nil, err
	}

	// Get current vault using the GetCurrentVault function from the same package
	vault, err := GetCurrentVault(fs, stateDir)
	if err != nil {
		return nil, fmt.Errorf("failed to get current vault: %w", err)
	}

	// Generate the keychain item name
	keychainItemName, err := generateKeychainUnlockerName(vault.GetName())
	if err != nil {
		return nil, fmt.Errorf("failed to generate keychain item name: %w", err)
	}

	// Create unlocker directory using the keychain item name as the directory name
	vaultDir, err := vault.GetDirectory()
	if err != nil {
		return nil, fmt.Errorf("failed to get vault directory: %w", err)
	}

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

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

	// Step 2: Generate a random passphrase for encrypting the age private key
	agePrivKeyPassphrase, err := generateRandomPassphrase(64)
	if err != nil {
		return nil, fmt.Errorf("failed to generate age private key passphrase: %w", err)
	}

	// Step 3: Store age public key as plaintext
	agePublicKeyString := ageIdentity.Recipient().String()
	agePubKeyPath := filepath.Join(unlockerDir, "pub.age")
	if err := afero.WriteFile(fs, agePubKeyPath, []byte(agePublicKeyString), FilePerms); err != nil {
		return nil, fmt.Errorf("failed to write age public key: %w", err)
	}

	// Step 4: Encrypt age private key with the generated passphrase and store on disk
	agePrivateKeyBytes := []byte(ageIdentity.String())
	encryptedAgePrivKey, err := EncryptWithPassphrase(agePrivateKeyBytes, agePrivKeyPassphrase)
	if err != nil {
		return nil, fmt.Errorf("failed to encrypt age private key with passphrase: %w", err)
	}

	agePrivKeyPath := filepath.Join(unlockerDir, "priv.age")
	if err := afero.WriteFile(fs, agePrivKeyPath, encryptedAgePrivKey, FilePerms); err != nil {
		return nil, fmt.Errorf("failed to write encrypted age private key: %w", err)
	}

	// Step 5: Get or derive the long-term private key
	var ltPrivKeyData []byte

	// Check if mnemonic is available in environment variable
	if envMnemonic := os.Getenv(EnvMnemonic); envMnemonic != "" {
		// Use mnemonic directly to derive long-term key
		ltIdentity, err := agehd.DeriveIdentity(envMnemonic, 0)
		if err != nil {
			return nil, fmt.Errorf("failed to derive long-term key from mnemonic: %w", err)
		}
		ltPrivKeyData = []byte(ltIdentity.String())
	} else {
		// Get the vault to access current unlocker
		currentUnlocker, err := vault.GetCurrentUnlocker()
		if err != nil {
			return nil, fmt.Errorf("failed to get current unlocker: %w", err)
		}

		// Get the current unlocker identity
		currentUnlockerIdentity, err := currentUnlocker.GetIdentity()
		if err != nil {
			return nil, fmt.Errorf("failed to get current unlocker identity: %w", err)
		}

		// Get encrypted long-term key from current unlocker, handling different types
		var encryptedLtPrivKey []byte
		switch currentUnlocker := currentUnlocker.(type) {
		case *PassphraseUnlocker:
			// Read the encrypted long-term private key from passphrase unlocker
			encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlocker.GetDirectory(), "longterm.age"))
			if err != nil {
				return nil, fmt.Errorf("failed to read encrypted long-term key from current passphrase unlocker: %w", err)
			}

		case *PGPUnlocker:
			// Read the encrypted long-term private key from PGP unlocker
			encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlocker.GetDirectory(), "longterm.age"))
			if err != nil {
				return nil, fmt.Errorf("failed to read encrypted long-term key from current PGP unlocker: %w", err)
			}

		case *KeychainUnlocker:
			// Read the encrypted long-term private key from another keychain unlocker
			encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlocker.GetDirectory(), "longterm.age"))
			if err != nil {
				return nil, fmt.Errorf("failed to read encrypted long-term key from current keychain unlocker: %w", err)
			}

		default:
			return nil, fmt.Errorf("unsupported current unlocker type for keychain unlocker creation")
		}

		// Decrypt long-term private key using current unlocker
		ltPrivKeyData, err = DecryptWithIdentity(encryptedLtPrivKey, currentUnlockerIdentity)
		if err != nil {
			return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
		}
	}

	// Step 6: Encrypt long-term private key to the new age unlocker
	encryptedLtPrivKeyToAge, err := EncryptToRecipient(ltPrivKeyData, ageIdentity.Recipient())
	if err != nil {
		return nil, fmt.Errorf("failed to encrypt long-term private key to age unlocker: %w", err)
	}

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

	// Step 7: Prepare keychain data
	keychainData := KeychainData{
		AgePublicKey:         agePublicKeyString,
		AgePrivKeyPassphrase: agePrivKeyPassphrase,
		EncryptedLongtermKey: hex.EncodeToString(encryptedLtPrivKeyToAge),
	}

	keychainDataBytes, err := json.Marshal(keychainData)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal keychain data: %w", err)
	}

	// Step 8: Store data in keychain
	if err := storeInKeychain(keychainItemName, keychainDataBytes); err != nil {
		return nil, fmt.Errorf("failed to store data in keychain: %w", err)
	}

	// Step 9: Create and write enhanced metadata
	// Generate the key ID directly using the keychain item name
	keyID := fmt.Sprintf("%s-keychain", keychainItemName)

	keychainMetadata := KeychainUnlockerMetadata{
		UnlockerMetadata: UnlockerMetadata{
			ID:        keyID,
			Type:      "keychain",
			CreatedAt: time.Now(),
			Flags:     []string{"keychain", "macos"},
		},
		AgePublicKey:     agePublicKeyString,
		KeychainItemName: keychainItemName,
	}

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

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

	return &KeychainUnlocker{
		Directory: unlockerDir,
		Metadata:  keychainMetadata.UnlockerMetadata,
		fs:        fs,
	}, nil
}

// checkMacOSAvailable verifies that we're running on macOS and security command is available
func checkMacOSAvailable() error {
	cmd := exec.Command("/usr/bin/security", "help")
	if err := cmd.Run(); err != nil {
		return fmt.Errorf("macOS security command not available: %w (keychain unlockers are only supported on macOS)", err)
	}
	return nil
}

// storeInKeychain stores data in the macOS keychain using the security command
func storeInKeychain(itemName string, data []byte) error {
	cmd := exec.Command("/usr/bin/security", "add-generic-password",
		"-a", itemName,
		"-s", itemName,
		"-w", string(data),
		"-U") // Update if exists

	if err := cmd.Run(); err != nil {
		return fmt.Errorf("failed to store item in keychain: %w", err)
	}

	return nil
}

// retrieveFromKeychain retrieves data from the macOS keychain using the security command
func retrieveFromKeychain(itemName string) ([]byte, error) {
	cmd := exec.Command("/usr/bin/security", "find-generic-password",
		"-a", itemName,
		"-s", itemName,
		"-w") // Return password only

	output, err := cmd.Output()
	if err != nil {
		return nil, fmt.Errorf("failed to retrieve item from keychain: %w", err)
	}

	// Remove trailing newline if present
	if len(output) > 0 && output[len(output)-1] == '\n' {
		output = output[:len(output)-1]
	}

	return output, nil
}

// deleteFromKeychain removes an item from the macOS keychain using the security command
func deleteFromKeychain(itemName string) error {
	cmd := exec.Command("/usr/bin/security", "delete-generic-password",
		"-a", itemName,
		"-s", itemName)

	if err := cmd.Run(); err != nil {
		return fmt.Errorf("failed to delete item from keychain: %w", err)
	}

	return nil
}

// generateRandomPassphrase generates a random passphrase for encrypting the age private key
func generateRandomPassphrase(length int) (string, error) {
	return generateRandomString(length, "0123456789abcdef")
}
