package storage

import (
	"crypto"
	"encoding/json"
	"encoding/pem"
	"errors"
	"fmt"
	"log/slog"
	"net/url"
	"os"
	"path/filepath"
	"strings"

	"github.com/go-acme/lego/v5/certcrypto"
	"github.com/go-acme/lego/v5/log"
)

const RootPathWarningMessage = `!!!! HEADS UP !!!!

Your account credentials have been saved in your
configuration directory at "%s".

You should make a secure backup of this folder now. This
configuration directory will also contain private keys
generated by lego and certificates obtained from the ACME
server. Making regular backups of this folder is ideal.
`

const (
	baseAccountsRootFolderName = "accounts"
	accountFileName            = "account.json"
)

type PrivateKeyNotFound struct {
	AccountID string
}

func (e *PrivateKeyNotFound) Error() string {
	return fmt.Sprintf("no private key found for account %q", e.AccountID)
}

// AccountsStorage A storage for account data.
//
// rootPath:
//
//	./.lego/accounts/
//	     │      └── root accounts directory
//	     └── "path" option
//
// rootUserPath:
//
//	./.lego/accounts/localhost_14000/foo@example.com/
//	     │      │             │             └── accountID
//	     │      │             └── CA server ("server" option)
//	     │      └── root accounts directory
//	     └── "path" option
//
// keyPath:
//
//	./.lego/accounts/localhost_14000/foo@example.com/foo@example.com.key
//	     │      │             │             │            └── private key
//	     │      │             │             └── accountID
//	     │      │             └── CA server ("server" option)
//	     │      └── root accounts directory
//	     └── "path" option
//
// accountFilePath:
//
//	./.lego/accounts/localhost_14000/foo@example.com/account.json
//	     │      │             │             │            └── account file
//	     │      │             │             └── accountID
//	     │      │             └── CA server ("server" option)
//	     │      └── root accounts directory
//	     └── "path" option
type AccountsStorage struct {
	rootPath string
}

// NewAccountsStorage Creates a new AccountsStorage.
func NewAccountsStorage(basePath string) *AccountsStorage {
	return &AccountsStorage{
		rootPath: filepath.Join(basePath, baseAccountsRootFolderName),
	}
}

// GetRootPath returns the root path of the storage of the accounts.
func (s *AccountsStorage) GetRootPath() string {
	return s.rootPath
}

// Save saves the account to a file (only the JSON file).
func (s *AccountsStorage) Save(account *Account) error {
	if account.ID == "" {
		account.ID = account.GetID()
	}

	jsonBytes, err := json.MarshalIndent(account, "", "\t")
	if err != nil {
		return err
	}

	server, err := url.Parse(account.Server)
	if err != nil {
		return fmt.Errorf("invalid server URL %q: %w", account.Server, err)
	}

	accountFilePath := s.getAccountFilePath(server, account.GetID())

	err = os.MkdirAll(filepath.Dir(accountFilePath), 0o700)
	if err != nil {
		return fmt.Errorf("create the directory %q for the account: %w", filepath.Dir(accountFilePath), err)
	}

	return os.WriteFile(accountFilePath, jsonBytes, filePerm)
}

// SavePrivateKey saves the private key to a file defined by the account.
func (s *AccountsStorage) SavePrivateKey(account *Account) error {
	effectiveAccountID := account.GetID()

	server, err := url.Parse(account.Server)
	if err != nil {
		return fmt.Errorf("invalid server URL %q: %w", account.Server, err)
	}

	err = s.savePrivateKey(server, effectiveAccountID, account.key)
	if err != nil {
		return fmt.Errorf("save the private key (accountID: %s): %w", effectiveAccountID, err)
	}

	return nil
}

// Get gets an account from a file or creates a new one (the files are saved).
func (s *AccountsStorage) Get(server string, keyType certcrypto.KeyType, email, accountID string) (*Account, error) {
	serverURL, err := url.Parse(server)
	if err != nil {
		return nil, fmt.Errorf("invalid server URL %q: %w", server, err)
	}

	effectiveAccountID := GetEffectiveAccountID(email, accountID)

	if !s.existsAccountFile(serverURL, effectiveAccountID) {
		var account *Account

		account, err = s.createAccount(serverURL, keyType, email, accountID)
		if err != nil {
			return nil, err
		}

		err = s.Save(account)
		if err != nil {
			return nil, err
		}

		return account, nil
	}

	return s.getAccount(serverURL, keyType, effectiveAccountID)
}

// createAccount creates a new account.
func (s *AccountsStorage) createAccount(server *url.URL, keyType certcrypto.KeyType, email, accountID string) (*Account, error) {
	effectiveAccountID := GetEffectiveAccountID(email, accountID)

	privateKey, err := s.createPrivateKey(server, keyType, effectiveAccountID)
	if err != nil {
		return nil, err
	}

	return &Account{
		Server:  server.String(),
		ID:      effectiveAccountID,
		Email:   email,
		KeyType: keyType,
		key:     privateKey,
	}, nil
}

// getAccount gets the account from a file.
// It will flag the account as needing recovery if the registration is missing.
// And it will also create a new private key if it doesn't exist (and save the private key file).
func (s *AccountsStorage) getAccount(server *url.URL, keyType certcrypto.KeyType, effectiveAccountID string) (*Account, error) {
	accountFilePath := s.getAccountFilePath(server, effectiveAccountID)

	account, err := ReadJSONFile[Account](accountFilePath)
	if err != nil {
		return nil, fmt.Errorf("could not read the account file %q: %w", accountFilePath, err)
	}

	if fixIntegrity(account, server.String(), keyType) {
		err = s.Save(account)
		if err != nil {
			return nil, fmt.Errorf("could not save the account file: %w", err)
		}
	}

	account.key, err = s.readPrivateKey(server, effectiveAccountID)
	if err != nil {
		var privateKeyNotFound *PrivateKeyNotFound

		if !errors.As(err, &privateKeyNotFound) {
			return nil, err
		}

		// TODO(ldez): debug level?
		log.Info("No key found for the account. Generating a new private key.",
			slog.String("accountID", effectiveAccountID),
			slog.Any("keyType", keyType),
		)

		// The private key was regenerated, so the registration is no longer valid.
		account.Registration = nil

		account.key, err = s.createPrivateKey(server, keyType, effectiveAccountID)
		if err != nil {
			return nil, fmt.Errorf("new private key creation: %w", err)
		}

		err := s.Save(account)
		if err != nil {
			return nil, fmt.Errorf("could not save the account file: %w", err)
		}

		return account, nil
	}

	if account.Registration != nil && account.Registration.Status != "" {
		return account, nil
	}

	// The registration is missing, but there is an existing private key, so the account needs recovery.
	account.NeedsRecovery = true

	return account, nil
}

// createPrivateKey generates a new private key and saves it to a file.
func (s *AccountsStorage) createPrivateKey(server *url.URL, keyType certcrypto.KeyType, effectiveAccountID string) (crypto.Signer, error) {
	privateKey, err := certcrypto.GeneratePrivateKey(keyType)
	if err != nil {
		return nil, fmt.Errorf("private key generation (accountID: %s): %w", effectiveAccountID, err)
	}

	err = s.savePrivateKey(server, effectiveAccountID, privateKey)
	if err != nil {
		return nil, fmt.Errorf("save the private key (accountID: %s): %w", effectiveAccountID, err)
	}

	return privateKey, nil
}

func (s *AccountsStorage) savePrivateKey(server *url.URL, effectiveAccountID string, privateKey crypto.Signer) error {
	accKeyPath := s.getAccountKeyPath(server, effectiveAccountID)
	keysPath := filepath.Dir(accKeyPath)

	err := CreateNonExistingFolder(keysPath)
	if err != nil {
		return fmt.Errorf("could not check/create the directory %q for the account (accountID: %s): %w", keysPath, effectiveAccountID, err)
	}

	err = savePrivateKey(accKeyPath, privateKey)
	if err != nil {
		return fmt.Errorf("save the private key (accountID: %s): %w", effectiveAccountID, err)
	}

	// TODO(ldez): debug level?
	log.Info("Private key saved.", slog.String("filepath", accKeyPath))

	return nil
}

// readPrivateKey reads the private key from a file.
func (s *AccountsStorage) readPrivateKey(server *url.URL, effectiveAccountID string) (crypto.Signer, error) {
	accKeyPath := s.getAccountKeyPath(server, effectiveAccountID)

	if _, err := os.Stat(accKeyPath); os.IsNotExist(err) {
		return nil, &PrivateKeyNotFound{AccountID: effectiveAccountID}
	} else if err != nil {
		return nil, err
	}

	privateKey, err := ReadPrivateKeyFile(accKeyPath)
	if err != nil {
		return nil, fmt.Errorf("error loading the private key file %q: %w", accKeyPath, err)
	}

	return privateKey, nil
}

// existsAccountFile checks if the account file exists.
func (s *AccountsStorage) existsAccountFile(server *url.URL, effectiveAccountID string) bool {
	accountFilePath := s.getAccountFilePath(server, effectiveAccountID)

	if _, err := os.Stat(accountFilePath); os.IsNotExist(err) {
		return false
	} else if err != nil {
		log.Fatal("Could not read the account file.",
			slog.String("filepath", accountFilePath),
			log.ErrorAttr(err),
		)
	}

	return true
}

// getAccountKeyPath returns the account private key path.
func (s *AccountsStorage) getAccountKeyPath(server *url.URL, effectiveAccountID string) string {
	return filepath.Join(s.getRootUserPath(server, effectiveAccountID), effectiveAccountID+".key")
}

// getAccountFilePath returns the account file path.
func (s *AccountsStorage) getAccountFilePath(server *url.URL, effectiveAccountID string) string {
	return filepath.Join(s.getRootUserPath(server, effectiveAccountID), accountFileName)
}

// getRootUserPath returns the path to the root folder for an account.
func (s *AccountsStorage) getRootUserPath(server *url.URL, effectiveAccountID string) string {
	return filepath.Join(s.rootPath, sanitizeHost(server), effectiveAccountID)
}

func savePrivateKey(filename string, privateKey crypto.Signer) error {
	err := os.WriteFile(filename, pem.EncodeToMemory(certcrypto.PEMBlock(privateKey)), filePerm)
	if err != nil {
		return fmt.Errorf("PEM encoding: %w", err)
	}

	return nil
}

func sanitizeHost(uri *url.URL) string {
	return strings.NewReplacer(":", "_", "/", "_", `\`, "_").Replace(uri.Host)
}

// To "autofix" account from previous versions.
func fixIntegrity(account *Account, server string, keyType certcrypto.KeyType) bool {
	var toSave bool

	if account.KeyType == "" {
		account.KeyType = keyType
		toSave = true
	}

	if account.Server == "" {
		account.Server = server
		toSave = true
	}

	return toSave
}
