// Package profile stores the optional VFace identity in an age-encrypted vault.
package profile

import (
	"bytes"
	"crypto/ed25519"
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"time"

	"aegis/internal/cryptokit"
	"filippo.io/age"
	agearmor "filippo.io/age/armor"
)

const (
	Version           = 1
	MinPasswordLength = 12
	maxVaultBytes     = 1 << 20
)

type Profile struct {
	Version            int       `json:"version"`
	Username           string    `json:"username"`
	Email              string    `json:"email"`
	PublicKey          string    `json:"ed25519_public_key"`
	PrivateKey         string    `json:"ed25519_private_key"`
	AgeIdentity        string    `json:"age_identity,omitempty"`
	YubiKeyFingerprint string    `json:"yubikey_signing_fingerprint,omitempty"`
	CreatedAt          time.Time `json:"created_at"`
}

func DefaultPath() (string, error) {
	directory, err := os.UserConfigDir()
	if err != nil {
		return "", fmt.Errorf("locate profile directory: %w", err)
	}
	return filepath.Join(directory, "aegis", "profile.json.age"), nil
}

func Generate(username, email string) (Profile, error) {
	username = strings.TrimSpace(username)
	email = strings.TrimSpace(email)
	if username == "" || email == "" {
		return Profile{}, errors.New("VFace username and email are required")
	}
	if strings.ContainsAny(username+email, "\x00\r\n|") {
		return Profile{}, errors.New("VFace username and email contain forbidden characters")
	}
	publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		return Profile{}, fmt.Errorf("generate VFace Ed25519 key pair: %w", err)
	}
	return Profile{
		Version:    Version,
		Username:   username,
		Email:      email,
		PublicKey:  base64.StdEncoding.EncodeToString(publicKey),
		PrivateKey: base64.StdEncoding.EncodeToString(privateKey),
		CreatedAt:  time.Now().UTC(),
	}, nil
}

func Save(path string, value Profile, password string) error {
	if err := validatePassword(password); err != nil {
		return err
	}
	if err := Validate(value); err != nil {
		return err
	}
	plaintext, err := json.MarshalIndent(value, "", "  ")
	if err != nil {
		return fmt.Errorf("encode VFace profile: %w", err)
	}
	recipient, err := age.NewScryptRecipient(password)
	if err != nil {
		return fmt.Errorf("create profile password recipient: %w", err)
	}
	var encrypted bytes.Buffer
	armor := agearmor.NewWriter(&encrypted)
	writer, err := age.Encrypt(armor, recipient)
	if err != nil {
		_ = armor.Close()
		return fmt.Errorf("create profile vault: %w", err)
	}
	if _, err := writer.Write(plaintext); err != nil {
		_ = writer.Close()
		_ = armor.Close()
		return fmt.Errorf("write profile vault: %w", err)
	}
	if err := writer.Close(); err != nil {
		_ = armor.Close()
		return fmt.Errorf("close profile vault: %w", err)
	}
	if err := armor.Close(); err != nil {
		return fmt.Errorf("close profile armor: %w", err)
	}
	return atomicWrite(path, encrypted.Bytes())
}

func Load(path string, password string) (Profile, error) {
	if err := validatePassword(password); err != nil {
		return Profile{}, err
	}
	ciphertext, err := os.ReadFile(path)
	if err != nil {
		return Profile{}, fmt.Errorf("read profile vault: %w", err)
	}
	if len(ciphertext) > maxVaultBytes {
		return Profile{}, errors.New("profile vault is too large")
	}
	identity, err := age.NewScryptIdentity(password)
	if err != nil {
		return Profile{}, fmt.Errorf("create profile password identity: %w", err)
	}
	reader, err := age.Decrypt(agearmor.NewReader(bytes.NewReader(ciphertext)), identity)
	if err != nil {
		return Profile{}, errors.New("cannot unlock profile vault")
	}
	plaintext, err := io.ReadAll(io.LimitReader(reader, maxVaultBytes+1))
	if err != nil {
		return Profile{}, fmt.Errorf("read profile vault: %w", err)
	}
	if len(plaintext) > maxVaultBytes {
		return Profile{}, errors.New("profile vault contents are too large")
	}
	var value Profile
	if err := json.Unmarshal(plaintext, &value); err != nil {
		return Profile{}, errors.New("profile vault contains invalid data")
	}
	if err := Validate(value); err != nil {
		return Profile{}, fmt.Errorf("validate profile vault: %w", err)
	}
	return value, nil
}

func Validate(value Profile) error {
	if value.Version != Version {
		return fmt.Errorf("unsupported VFace profile version %d", value.Version)
	}
	if value.Username == "" || value.Email == "" || strings.ContainsAny(value.Username+value.Email, "\x00\r\n|") {
		return errors.New("invalid VFace username or email")
	}
	publicKey, err := decodePublicKey(value.PublicKey)
	if err != nil {
		return err
	}
	if strings.TrimSpace(value.PrivateKey) == "" {
		if strings.TrimSpace(value.YubiKeyFingerprint) == "" {
			return errors.New("profile needs an encrypted private key or a YubiKey signing fingerprint")
		}
		return nil
	}
	privateKey, err := decodePrivateKey(value.PrivateKey)
	if err != nil {
		return err
	}
	derived := privateKey.Public().(ed25519.PublicKey)
	if !bytes.Equal(publicKey, derived) {
		return errors.New("VFace public and private keys do not match")
	}
	return nil
}

func validatePassword(password string) error {
	if len([]rune(password)) < MinPasswordLength {
		return fmt.Errorf("profile password must contain at least %d characters", MinPasswordLength)
	}
	return nil
}

func decodePublicKey(value string) (ed25519.PublicKey, error) {
	canonical, err := cryptokit.CanonicalEd25519PublicKey(value)
	if err != nil {
		return nil, fmt.Errorf("invalid VFace public key: %w", err)
	}
	decoded, err := base64.StdEncoding.DecodeString(canonical)
	if err != nil {
		return nil, errors.New("invalid VFace public key encoding")
	}
	return ed25519.PublicKey(decoded), nil
}

func decodePrivateKey(value string) (ed25519.PrivateKey, error) {
	decoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(value))
	if err != nil || len(decoded) != ed25519.PrivateKeySize {
		return nil, errors.New("invalid VFace private key")
	}
	return ed25519.PrivateKey(decoded), nil
}

func atomicWrite(path string, data []byte) error {
	if path == "" {
		return errors.New("profile vault path is required")
	}
	directory := filepath.Dir(path)
	if err := os.MkdirAll(directory, 0o700); err != nil {
		return fmt.Errorf("create profile directory: %w", err)
	}
	if err := os.Chmod(directory, 0o700); err != nil {
		return fmt.Errorf("protect profile directory: %w", err)
	}
	temporary, err := os.CreateTemp(directory, ".profile-*.tmp")
	if err != nil {
		return fmt.Errorf("create profile temporary file: %w", err)
	}
	temporaryName := temporary.Name()
	defer os.Remove(temporaryName)
	if err := temporary.Chmod(0o600); err != nil {
		_ = temporary.Close()
		return err
	}
	if _, err := temporary.Write(data); err != nil {
		_ = temporary.Close()
		return fmt.Errorf("write profile vault: %w", err)
	}
	if err := temporary.Sync(); err != nil {
		_ = temporary.Close()
		return fmt.Errorf("sync profile vault: %w", err)
	}
	if err := temporary.Close(); err != nil {
		return err
	}
	if err := os.Rename(temporaryName, path); err != nil {
		return fmt.Errorf("commit profile vault: %w", err)
	}
	return nil
}
