package pki

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"time"

	"github.com/hashicorp/vault/sdk/helper/certutil"
	"github.com/hashicorp/vault/sdk/logical"
)

// This allows us to record the version of the migration code within the log entry
// in case we find out in the future that something was horribly wrong with the migration,
// and we need to perform it again...
const (
	latestMigrationVersion = 2
	legacyBundleShimID     = issuerID("legacy-entry-shim-id")
	legacyBundleShimKeyID  = keyID("legacy-entry-shim-key-id")
)

type legacyBundleMigrationLog struct {
	Hash             string    `json:"hash"`
	Created          time.Time `json:"created"`
	CreatedIssuer    issuerID  `json:"issuer_id"`
	CreatedKey       keyID     `json:"key_id"`
	MigrationVersion int       `json:"migrationVersion"`
}

type migrationInfo struct {
	isRequired       bool
	legacyBundle     *certutil.CertBundle
	legacyBundleHash string
	migrationLog     *legacyBundleMigrationLog
}

func getMigrationInfo(ctx context.Context, s logical.Storage) (migrationInfo, error) {
	migrationInfo := migrationInfo{
		isRequired:       false,
		legacyBundle:     nil,
		legacyBundleHash: "",
		migrationLog:     nil,
	}

	var err error
	_, migrationInfo.legacyBundle, err = getLegacyCertBundle(ctx, s)
	if err != nil {
		return migrationInfo, err
	}

	migrationInfo.migrationLog, err = getLegacyBundleMigrationLog(ctx, s)
	if err != nil {
		return migrationInfo, err
	}

	migrationInfo.legacyBundleHash, err = computeHashOfLegacyBundle(migrationInfo.legacyBundle)
	if err != nil {
		return migrationInfo, err
	}

	// Even if there isn't anything to migrate, we always want to write out the log entry
	// as that will trigger the secondary clusters to toggle/wake up
	if (migrationInfo.migrationLog == nil) ||
		(migrationInfo.migrationLog.Hash != migrationInfo.legacyBundleHash) ||
		(migrationInfo.migrationLog.MigrationVersion != latestMigrationVersion) {
		migrationInfo.isRequired = true
	}

	return migrationInfo, nil
}

func migrateStorage(ctx context.Context, b *backend, s logical.Storage) error {
	migrationInfo, err := getMigrationInfo(ctx, s)
	if err != nil {
		return err
	}

	if !migrationInfo.isRequired {
		// No migration was deemed to be required.
		return nil
	}

	var issuerIdentifier issuerID
	var keyIdentifier keyID
	sc := b.makeStorageContext(ctx, s)
	if migrationInfo.legacyBundle != nil {
		// Generate a unique name for the migrated items in case things were to be re-migrated again
		// for some weird reason in the future...
		migrationName := fmt.Sprintf("current-%d", time.Now().Unix())

		b.Logger().Info("performing PKI migration to new keys/issuers layout")
		anIssuer, aKey, err := sc.writeCaBundle(migrationInfo.legacyBundle, migrationName, migrationName)
		if err != nil {
			return err
		}
		b.Logger().Info("Migration generated the following ids and set them as defaults",
			"issuer id", anIssuer.ID, "key id", aKey.ID)
		issuerIdentifier = anIssuer.ID
		keyIdentifier = aKey.ID

		// Since we do not have all the mount information available we must schedule
		// the CRL to be rebuilt at a later time.
		b.crlBuilder.requestRebuildIfActiveNode(b)
	}

	if migrationInfo.migrationLog != nil && migrationInfo.migrationLog.MigrationVersion == 1 {
		// We've seen a bundle with migration version 1; this means an
		// earlier version of the code ran which didn't have the fix for
		// correct write order in rebuildIssuersChains(...). Rather than
		// having every user read the migrated active issuer and see if
		// their chains need rebuilding, we'll schedule a one-off chain
		// migration here.
		b.Logger().Info(fmt.Sprintf("%v: performing maintenance rebuild of ca_chains", b.backendUUID))
		if err := sc.rebuildIssuersChains(nil); err != nil {
			return err
		}
	}

	// We always want to write out this log entry as the secondary clusters leverage this path to wake up
	// if they were upgraded prior to the primary cluster's migration occurred.
	err = setLegacyBundleMigrationLog(ctx, s, &legacyBundleMigrationLog{
		Hash:             migrationInfo.legacyBundleHash,
		Created:          time.Now(),
		CreatedIssuer:    issuerIdentifier,
		CreatedKey:       keyIdentifier,
		MigrationVersion: latestMigrationVersion,
	})
	if err != nil {
		return err
	}

	b.Logger().Info(fmt.Sprintf("%v: succeeded in migrating to issuer storage version %v", b.backendUUID, latestMigrationVersion))

	return nil
}

func computeHashOfLegacyBundle(bundle *certutil.CertBundle) (string, error) {
	hasher := sha256.New()
	// Generate an empty hash if the bundle does not exist.
	if bundle != nil {
		// We only hash the main certificate and the certs within the CAChain,
		// assuming that any sort of change that occurred would have influenced one of those two fields.
		if _, err := hasher.Write([]byte(bundle.Certificate)); err != nil {
			return "", err
		}
		for _, cert := range bundle.CAChain {
			if _, err := hasher.Write([]byte(cert)); err != nil {
				return "", err
			}
		}
	}
	return hex.EncodeToString(hasher.Sum(nil)), nil
}

func getLegacyBundleMigrationLog(ctx context.Context, s logical.Storage) (*legacyBundleMigrationLog, error) {
	entry, err := s.Get(ctx, legacyMigrationBundleLogKey)
	if err != nil {
		return nil, err
	}

	if entry == nil {
		return nil, nil
	}

	lbm := &legacyBundleMigrationLog{}
	err = entry.DecodeJSON(lbm)
	if err != nil {
		// If we can't decode our bundle, lets scrap it and assume a blank value,
		// re-running the migration will at most bring back an older certificate/private key
		return nil, nil
	}
	return lbm, nil
}

func setLegacyBundleMigrationLog(ctx context.Context, s logical.Storage, lbm *legacyBundleMigrationLog) error {
	json, err := logical.StorageEntryJSON(legacyMigrationBundleLogKey, lbm)
	if err != nil {
		return err
	}

	return s.Put(ctx, json)
}

func getLegacyCertBundle(ctx context.Context, s logical.Storage) (*issuerEntry, *certutil.CertBundle, error) {
	entry, err := s.Get(ctx, legacyCertBundlePath)
	if err != nil {
		return nil, nil, err
	}

	if entry == nil {
		return nil, nil, nil
	}

	cb := &certutil.CertBundle{}
	err = entry.DecodeJSON(cb)
	if err != nil {
		return nil, nil, err
	}

	// Fake a storage entry with backwards compatibility in mind.
	issuer := &issuerEntry{
		ID:                   legacyBundleShimID,
		KeyID:                legacyBundleShimKeyID,
		Name:                 "legacy-entry-shim",
		Certificate:          cb.Certificate,
		CAChain:              cb.CAChain,
		SerialNumber:         cb.SerialNumber,
		LeafNotAfterBehavior: certutil.ErrNotAfterBehavior,
	}
	issuer.Usage.ToggleUsage(AllIssuerUsages)

	return issuer, cb, nil
}
