package secret

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

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

// Variables to allow overriding in tests
var (
	// GPGEncryptFunc is the function used for GPG encryption
	// Can be overridden in tests to provide a non-interactive implementation
	GPGEncryptFunc = gpgEncryptDefault

	// GPGDecryptFunc is the function used for GPG decryption
	// Can be overridden in tests to provide a non-interactive implementation
	GPGDecryptFunc = gpgDecryptDefault
)

// PGPUnlockerMetadata extends UnlockerMetadata with PGP-specific data
type PGPUnlockerMetadata struct {
	UnlockerMetadata
	// GPG key ID used for encryption
	GPGKeyID string `json:"gpg_key_id"`
}

// PGPUnlocker represents a PGP-protected unlocker
type PGPUnlocker struct {
	Directory string
	Metadata  UnlockerMetadata
	fs        afero.Fs
}

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

	// Step 1: Read the encrypted age private key from filesystem
	agePrivKeyPath := filepath.Join(p.Directory, "priv.age.gpg")
	Debug("Reading PGP-encrypted age private key", "path", agePrivKeyPath)

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

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

	// Step 2: Decrypt the age private key using GPG
	Debug("Decrypting age private key with GPG", "unlocker_id", p.GetID())
	agePrivKeyData, err := GPGDecryptFunc(encryptedAgePrivKeyData)
	if err != nil {
		Debug("Failed to decrypt age private key with GPG", "error", err, "unlocker_id", p.GetID())
		return nil, fmt.Errorf("failed to decrypt age private key with GPG: %w", err)
	}

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

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

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

	return ageIdentity, nil
}

// GetType implements Unlocker interface
func (p *PGPUnlocker) GetType() string {
	return "pgp"
}

// GetMetadata implements Unlocker interface
func (p *PGPUnlocker) GetMetadata() UnlockerMetadata {
	return p.Metadata
}

// GetDirectory implements Unlocker interface
func (p *PGPUnlocker) GetDirectory() string {
	return p.Directory
}

// GetID implements Unlocker interface - generates ID from GPG key ID
func (p *PGPUnlocker) GetID() string {
	// Generate ID using GPG key ID: <keyid>-pgp
	gpgKeyID, err := p.GetGPGKeyID()
	if err != nil {
		// Fallback to creation time-based ID if we can't read the GPG key ID
		createdAt := p.Metadata.CreatedAt
		return fmt.Sprintf("%s-pgp", createdAt.Format("2006-01-02.15.04"))
	}
	return fmt.Sprintf("%s-pgp", gpgKeyID)
}

// Remove implements Unlocker interface - removes the PGP unlocker
func (p *PGPUnlocker) Remove() error {
	// For PGP unlockers, we just need to remove the directory
	// No external resources (like keychain items) to clean up
	if err := p.fs.RemoveAll(p.Directory); err != nil {
		return fmt.Errorf("failed to remove PGP unlocker directory: %w", err)
	}
	return nil
}

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

// GetGPGKeyID returns the GPG key ID from metadata
func (p *PGPUnlocker) GetGPGKeyID() (string, error) {
	// Load the metadata
	metadataPath := filepath.Join(p.Directory, "unlocker-metadata.json")
	metadataData, err := afero.ReadFile(p.fs, metadataPath)
	if err != nil {
		return "", fmt.Errorf("failed to read PGP metadata: %w", err)
	}

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

	return pgpMetadata.GPGKeyID, nil
}

// generatePGPUnlockerName generates a unique name for the PGP unlocker based on hostname and date
func generatePGPUnlockerName() (string, error) {
	hostname, err := os.Hostname()
	if err != nil {
		return "", fmt.Errorf("failed to get hostname: %w", err)
	}

	// Format: hostname-pgp-YYYY-MM-DD
	enrollmentDate := time.Now().Format("2006-01-02")
	return fmt.Sprintf("%s-pgp-%s", hostname, enrollmentDate), nil
}

// CreatePGPUnlocker creates a new PGP unlocker and stores it in the vault
func CreatePGPUnlocker(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnlocker, error) {
	// Check if GPG is available
	if err := checkGPGAvailable(); err != nil {
		return nil, err
	}

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

	// Generate the unlocker name based on hostname and date
	unlockerName, err := generatePGPUnlockerName()
	if err != nil {
		return nil, fmt.Errorf("failed to generate unlocker name: %w", err)
	}

	// Create unlocker directory using the generated 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", unlockerName)
	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 PGP unlocker
	ageIdentity, err := age.GenerateX25519Identity()
	if err != nil {
		return nil, fmt.Errorf("failed to generate age keypair: %w", err)
	}

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

	// Step 3: 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)
			}

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

		// Step 6: 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 7: 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 8: Encrypt age private key to the GPG key ID
	agePrivateKeyBytes := []byte(ageIdentity.String())
	encryptedAgePrivKey, err := GPGEncryptFunc(agePrivateKeyBytes, gpgKeyID)
	if err != nil {
		return nil, fmt.Errorf("failed to encrypt age private key with GPG: %w", err)
	}

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

	// Step 9: Create and write enhanced metadata
	pgpMetadata := PGPUnlockerMetadata{
		UnlockerMetadata: UnlockerMetadata{
			Type:      "pgp",
			CreatedAt: time.Now(),
			Flags:     []string{"gpg", "encrypted"},
		},
		GPGKeyID: gpgKeyID,
	}

	metadataBytes, err := json.MarshalIndent(pgpMetadata, "", "  ")
	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 &PGPUnlocker{
		Directory: unlockerDir,
		Metadata:  pgpMetadata.UnlockerMetadata,
		fs:        fs,
	}, nil
}

// checkGPGAvailable verifies that GPG is available
func checkGPGAvailable() error {
	cmd := exec.Command("gpg", "--version")
	if err := cmd.Run(); err != nil {
		return fmt.Errorf("GPG not available: %w (make sure 'gpg' command is installed and in PATH)", err)
	}
	return nil
}

// gpgEncryptDefault is the default implementation of GPG encryption
func gpgEncryptDefault(data []byte, keyID string) ([]byte, error) {
	cmd := exec.Command("gpg", "--trust-model", "always", "--armor", "--encrypt", "-r", keyID)
	cmd.Stdin = strings.NewReader(string(data))

	output, err := cmd.Output()
	if err != nil {
		return nil, fmt.Errorf("GPG encryption failed: %w", err)
	}

	return output, nil
}

// gpgDecryptDefault is the default implementation of GPG decryption
func gpgDecryptDefault(encryptedData []byte) ([]byte, error) {
	cmd := exec.Command("gpg", "--quiet", "--decrypt")
	cmd.Stdin = strings.NewReader(string(encryptedData))

	output, err := cmd.Output()
	if err != nil {
		return nil, fmt.Errorf("GPG decryption failed: %w", err)
	}

	return output, nil
}
