package chainbase

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"path/filepath"
	"time"

	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/util/logicerr"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util"
	"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
	policyengine "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
	"go.etcd.io/bbolt"
)

type boltLocalOverrideStorage struct {
	*cfg

	db *bbolt.DB
}

var chainBucket = []byte{0}

var (
	// ErrRootBucketNotFound signals the database has not been properly initialized.
	ErrRootBucketNotFound = logicerr.New("root bucket not found")

	ErrGlobalNamespaceBucketNotFound = logicerr.New("global namespace bucket not found")

	ErrTargetTypeBucketNotFound = logicerr.New("target type bucket not found")

	ErrTargetNameBucketNotFound = logicerr.New("target name bucket not found")

	ErrBucketNotContainsChainID = logicerr.New("chain id not found in bucket")

	errChainIDIsNotSet = errors.New("chain ID is not set")
)

// NewBoltLocalOverrideDatabase returns storage wrapper for storing access policy engine
// local overrides.
//
// chain storage (chainBucket):
// -> global namespace bucket (nBucket):
// --> target bucket (tBucket)
// ---> target name (resource) bucket (rBucket):
//
// |        Key          |      Value        |
// x---------------------x-------------------x
// |  chain id (string)  | serialized chain  |
// x---------------------x-------------------x
//
//nolint:godot
func NewBoltLocalOverrideDatabase(opts ...Option) LocalOverrideDatabase {
	c := defaultCfg()

	for i := range opts {
		opts[i](c)
	}

	return &boltLocalOverrideStorage{
		cfg: c,
	}
}

func (cs *boltLocalOverrideStorage) Init() error {
	return cs.db.Update(func(tx *bbolt.Tx) error {
		_, err := tx.CreateBucketIfNotExists(chainBucket)
		return err
	})
}

func (cs *boltLocalOverrideStorage) Open(context.Context) error {
	err := util.MkdirAllX(filepath.Dir(cs.path), cs.perm)
	if err != nil {
		return fmt.Errorf("can't create dir %s for the chain DB: %w", cs.path, err)
	}

	opts := *bbolt.DefaultOptions
	opts.NoSync = cs.noSync
	opts.Timeout = 100 * time.Millisecond

	cs.db, err = bbolt.Open(cs.path, cs.perm, &opts)
	if err != nil {
		return fmt.Errorf("can't open the chain DB: %w", err)
	}

	cs.db.MaxBatchSize = cs.maxBatchSize
	cs.db.MaxBatchDelay = cs.maxBatchDelay

	return nil
}

func (cs *boltLocalOverrideStorage) Close() error {
	var err error
	if cs.db != nil {
		err = cs.db.Close()
	}
	return err
}

func getTypeBucket(tx *bbolt.Tx, name chain.Name, target policyengine.Target) (*bbolt.Bucket, error) {
	cbucket := tx.Bucket(chainBucket)
	if cbucket == nil {
		return nil, ErrRootBucketNotFound
	}

	nbucket := cbucket.Bucket([]byte(name))
	if nbucket == nil {
		return nil, fmt.Errorf("%w: %w: %s", policyengine.ErrChainNotFound, ErrGlobalNamespaceBucketNotFound, name)
	}
	return nbucket.Bucket([]byte{byte(target.Type)}), nil
}

func normalizeTargetName(target *policyengine.Target) {
	if target.Type == policyengine.Namespace && target.Name == "" {
		target.Name = "root"
	}
}

func getTargetBucket(tx *bbolt.Tx, name chain.Name, target policyengine.Target) (*bbolt.Bucket, error) {
	typeBucket, err := getTypeBucket(tx, name, target)
	if err != nil {
		return nil, err
	}
	if typeBucket == nil {
		return nil, fmt.Errorf("%w: %w: %c", policyengine.ErrChainNotFound, ErrTargetTypeBucketNotFound, target.Type)
	}

	normalizeTargetName(&target)
	rbucket := typeBucket.Bucket([]byte(target.Name))
	if rbucket == nil {
		return nil, fmt.Errorf("%w: %w: %s", policyengine.ErrChainNotFound, ErrTargetNameBucketNotFound, target.Name)
	}
	return rbucket, nil
}

func getTargetBucketCreateIfEmpty(tx *bbolt.Tx, name chain.Name, target policyengine.Target) (*bbolt.Bucket, error) {
	cbucket := tx.Bucket(chainBucket)
	if cbucket == nil {
		return nil, ErrRootBucketNotFound
	}

	nbucket := cbucket.Bucket([]byte(name))
	if nbucket == nil {
		var err error
		nbucket, err = cbucket.CreateBucket([]byte(name))
		if err != nil {
			return nil, fmt.Errorf("could not create a bucket for the global chain name %s: %w", name, err)
		}
	}

	typeBucket := nbucket.Bucket([]byte{byte(target.Type)})
	if typeBucket == nil {
		var err error
		typeBucket, err = nbucket.CreateBucket([]byte{byte(target.Type)})
		if err != nil {
			return nil, fmt.Errorf("could not create a bucket for the target type '%c': %w", target.Type, err)
		}
	}

	normalizeTargetName(&target)
	rbucket := typeBucket.Bucket([]byte(target.Name))
	if rbucket == nil {
		var err error
		rbucket, err = typeBucket.CreateBucket([]byte(target.Name))
		if err != nil {
			return nil, fmt.Errorf("could not create a bucket for the target name %s: %w", target.Name, err)
		}
	}

	return rbucket, nil
}

func (cs *boltLocalOverrideStorage) AddOverride(name chain.Name, target policyengine.Target, c *chain.Chain) (chain.ID, error) {
	if len(c.ID) == 0 {
		return chain.ID{}, errChainIDIsNotSet
	}

	serializedChain := c.Bytes()

	err := cs.db.Update(func(tx *bbolt.Tx) error {
		rbuck, err := getTargetBucketCreateIfEmpty(tx, name, target)
		if err != nil {
			return err
		}
		return rbuck.Put([]byte(c.ID), serializedChain)
	})

	return c.ID, err
}

func (cs *boltLocalOverrideStorage) GetOverride(name chain.Name, target policyengine.Target, chainID chain.ID) (*chain.Chain, error) {
	var serializedChain []byte

	if err := cs.db.View(func(tx *bbolt.Tx) error {
		rbuck, err := getTargetBucket(tx, name, target)
		if err != nil {
			return err
		}
		serializedChain = rbuck.Get([]byte(chainID))
		if serializedChain == nil {
			return fmt.Errorf("%w: %w: %s", policyengine.ErrChainNotFound, ErrBucketNotContainsChainID, chainID)
		}
		serializedChain = bytes.Clone(serializedChain)
		return nil
	}); err != nil {
		return nil, err
	}

	c := &chain.Chain{}
	if err := c.DecodeBytes(serializedChain); err != nil {
		return nil, err
	}
	return c, nil
}

func (cs *boltLocalOverrideStorage) RemoveOverride(name chain.Name, target policyengine.Target, chainID chain.ID) error {
	return cs.db.Update(func(tx *bbolt.Tx) error {
		rbuck, err := getTargetBucket(tx, name, target)
		if err != nil {
			return err
		}
		return rbuck.Delete([]byte(chainID))
	})
}

func (cs *boltLocalOverrideStorage) RemoveOverridesByTarget(name chain.Name, target policyengine.Target) error {
	return cs.db.Update(func(tx *bbolt.Tx) error {
		typeBucket, err := getTypeBucket(tx, name, target)
		if err != nil {
			return err
		}
		normalizeTargetName(&target)
		return typeBucket.DeleteBucket([]byte(target.Name))
	})
}

func (cs *boltLocalOverrideStorage) ListOverrides(name chain.Name, target policyengine.Target) ([]*chain.Chain, error) {
	var serializedChains [][]byte
	var serializedChain []byte
	if err := cs.db.View(func(tx *bbolt.Tx) error {
		rbuck, err := getTargetBucket(tx, name, target)
		if err != nil {
			return err
		}
		return rbuck.ForEach(func(_, v []byte) error {
			serializedChain = bytes.Clone(v)
			serializedChains = append(serializedChains, serializedChain)
			return nil
		})
	}); err != nil {
		if errors.Is(err, policyengine.ErrChainNotFound) {
			return []*chain.Chain{}, nil
		}
		return nil, err
	}
	chains := make([]*chain.Chain, 0, len(serializedChains))
	for _, serializedChain = range serializedChains {
		c := &chain.Chain{}
		if err := c.DecodeBytes(serializedChain); err != nil {
			return nil, err
		}
		chains = append(chains, c)
	}
	return chains, nil
}

func (cs *boltLocalOverrideStorage) DropAllOverrides(name chain.Name) error {
	return cs.db.Update(func(tx *bbolt.Tx) error {
		cbucket := tx.Bucket(chainBucket)
		if cbucket == nil {
			return ErrRootBucketNotFound
		}

		nbucket := cbucket.Bucket([]byte(name))
		if nbucket == nil {
			return fmt.Errorf("%w: %w: global namespace %s", policyengine.ErrChainNotFound, ErrGlobalNamespaceBucketNotFound, name)
		}

		return tx.DeleteBucket([]byte(name))
	})
}

func (cs *boltLocalOverrideStorage) ListOverrideDefinedTargets(name chain.Name) ([]policyengine.Target, error) {
	var targets []policyengine.Target
	if err := cs.db.View(func(tx *bbolt.Tx) error {
		var err error
		targets, err = getTargets(tx, name)
		if err != nil {
			return err
		}
		return nil
	}); err != nil {
		return nil, err
	}
	return targets, nil
}

func getTargets(tx *bbolt.Tx, name chain.Name) ([]policyengine.Target, error) {
	var targets []policyengine.Target
	cbucket := tx.Bucket(chainBucket)
	if cbucket == nil {
		return nil, ErrRootBucketNotFound
	}

	nbucket := cbucket.Bucket([]byte(name))
	if nbucket == nil {
		return nil, fmt.Errorf("%w: %w: %s", policyengine.ErrChainNotFound, ErrGlobalNamespaceBucketNotFound, name)
	}

	if err := nbucket.ForEachBucket(func(k []byte) error {
		ttype := policyengine.TargetType(k[0])
		if err := nbucket.Bucket(k).ForEachBucket(func(k []byte) error {
			targets = append(targets, policyengine.Target{
				Type: ttype,
				Name: string(bytes.Clone(k)),
			})
			return nil
		}); err != nil {
			return err
		}
		return nil
	}); err != nil {
		return nil, err
	}
	return targets, nil
}