// Copyright 2024 The Erigon Authors
// This file is part of Erigon.
//
// Erigon is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Erigon is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with Erigon. If not, see <http://www.gnu.org/licenses/>.

package snaptype2

import (
	"context"
	"encoding/binary"
	"errors"
	"fmt"
	"path/filepath"

	"github.com/erigontech/erigon/common"
	"github.com/erigontech/erigon/common/background"
	"github.com/erigontech/erigon/common/crypto"
	"github.com/erigontech/erigon/common/dbg"
	"github.com/erigontech/erigon/common/log/v3"
	"github.com/erigontech/erigon/db/recsplit"
	"github.com/erigontech/erigon/db/seg"
	"github.com/erigontech/erigon/db/snapcfg"
	"github.com/erigontech/erigon/db/snaptype"
	"github.com/erigontech/erigon/db/state/statecfg"
	"github.com/erigontech/erigon/db/version"
	"github.com/erigontech/erigon/execution/chain"
	"github.com/erigontech/erigon/execution/chain/networkname"
	"github.com/erigontech/erigon/execution/rlp"
	"github.com/erigontech/erigon/execution/types"
)

func init() {
	ethereumTypes := append(BlockSnapshotTypes, snaptype.CaplinSnapshotTypes...)

	snapcfg.RegisterKnownTypes(networkname.Mainnet, ethereumTypes)
	snapcfg.RegisterKnownTypes(networkname.Sepolia, ethereumTypes)
	snapcfg.RegisterKnownTypes(networkname.Gnosis, ethereumTypes)
	snapcfg.RegisterKnownTypes(networkname.Chiado, ethereumTypes)
	snapcfg.RegisterKnownTypes(networkname.Hoodi, ethereumTypes)
	snapcfg.RegisterKnownTypes(networkname.Bloatnet, ethereumTypes)
}

var Enums = struct {
	snaptype.Enums
	Salt,
	Headers,
	Bodies,
	Transactions,
	Domains,
	Histories,
	InvertedIndicies,
	Accessor,
	Txt snaptype.Enum
}{
	Enums:            snaptype.Enums{},
	Salt:             snaptype.MinCoreEnum,
	Headers:          snaptype.MinCoreEnum + 1,
	Bodies:           snaptype.MinCoreEnum + 2,
	Transactions:     snaptype.MinCoreEnum + 3,
	Domains:          snaptype.MinCoreEnum + 4,
	Histories:        snaptype.MinCoreEnum + 5,
	InvertedIndicies: snaptype.MinCoreEnum + 6,
	Accessor:         snaptype.MinCoreEnum + 7,
	Txt:              snaptype.MinCoreEnum + 8,
}

var Indexes = struct {
	HeaderHash,
	BodyHash,
	TxnHash,
	TxnHash2BlockNum snaptype.Index
}{
	HeaderHash:       snaptype.Index{Name: statecfg.HeadersIdx, Version: statecfg.Schema.HeadersBlock.FileVersion.AccessorIdx},
	BodyHash:         snaptype.Index{Name: statecfg.BodiesIdx, Version: statecfg.Schema.BodiesBlock.FileVersion.AccessorIdx},
	TxnHash:          snaptype.Index{Name: statecfg.TransactionsIdx, Version: statecfg.Schema.TransactionsBlock.FileVersion.AccessorIdx},
	TxnHash2BlockNum: snaptype.Index{Name: statecfg.TransactionsToBlockIdx, Version: statecfg.Schema.TxnHash2BlockNumBlock.FileVersion.AccessorIdx, Offset: 1},
}

var (
	Salt = snaptype.RegisterType(
		Enums.Salt,
		"salt",
		snaptype.Versions{
			Current:      version.ZeroVersion, //2,
			MinSupported: version.ZeroVersion,
		},
		nil,
		nil,
		nil,
	)
	Headers = snaptype.RegisterType(
		Enums.Headers,
		statecfg.Headers,
		statecfg.Schema.HeadersBlock.FileVersion.DataSeg,
		nil,
		[]snaptype.Index{Indexes.HeaderHash},
		snaptype.IndexBuilderFunc(
			func(ctx context.Context, info snaptype.FileInfo, salt uint32, _ *chain.Config, tmpDir string, p *background.Progress, lvl log.Lvl, logger log.Logger) (err error) {
				hasher := crypto.NewKeccakState()
				defer crypto.ReturnToPool(hasher)
				var h common.Hash

				cfg := recsplit.RecSplitArgs{
					Enums:              true,
					BucketSize:         recsplit.DefaultBucketSize,
					LeafSize:           recsplit.DefaultLeafSize,
					TmpDir:             tmpDir,
					Salt:               &salt,
					BaseDataID:         info.From,
					LessFalsePositives: true,
				}
				if err := snaptype.BuildIndex(ctx, info, Indexes.HeaderHash.Version, cfg, log.LvlDebug, p, func(idx *recsplit.RecSplit, i, offset uint64, word []byte) error {
					if p != nil {
						p.Processed.Add(1)
					}

					headerRlp := word[1:]
					hasher.Reset()
					hasher.Write(headerRlp)
					hasher.Read(h[:])
					if err := idx.AddKey(h[:], offset); err != nil {
						return err
					}
					return nil
				}, logger); err != nil {
					return fmt.Errorf("HeadersIdx: %w", err)
				}
				return nil
			}),
	)

	Bodies = snaptype.RegisterType(
		Enums.Bodies,
		statecfg.Bodies,
		statecfg.Schema.BodiesBlock.FileVersion.DataSeg,
		nil,
		[]snaptype.Index{Indexes.BodyHash},
		snaptype.IndexBuilderFunc(
			func(ctx context.Context, info snaptype.FileInfo, salt uint32, _ *chain.Config, tmpDir string, p *background.Progress, lvl log.Lvl, logger log.Logger) (err error) {
				num := make([]byte, binary.MaxVarintLen64)

				cfg := recsplit.RecSplitArgs{
					Enums:      true,
					BucketSize: recsplit.DefaultBucketSize,
					LeafSize:   recsplit.DefaultLeafSize,
					TmpDir:     tmpDir,
					Salt:       &salt,
					BaseDataID: info.From,
				}
				if err := snaptype.BuildIndex(ctx, info, Indexes.BodyHash.Version, cfg, log.LvlDebug, p, func(idx *recsplit.RecSplit, i, offset uint64, _ []byte) error {
					if p != nil {
						p.Processed.Add(1)
					}
					n := binary.PutUvarint(num, i)
					if err := idx.AddKey(num[:n], offset); err != nil {
						return err
					}
					return nil
				}, logger); err != nil {
					return fmt.Errorf("can't index %s: %w", info.Name(), err)
				}
				return nil
			}),
	)

	Transactions = snaptype.RegisterType(
		Enums.Transactions,
		statecfg.Transactions,
		statecfg.Schema.TransactionsBlock.FileVersion.DataSeg,
		nil,
		[]snaptype.Index{Indexes.TxnHash, Indexes.TxnHash2BlockNum},
		snaptype.IndexBuilderFunc(
			func(ctx context.Context, sn snaptype.FileInfo, salt uint32, chainConfig *chain.Config, tmpDir string, p *background.Progress, lvl log.Lvl, logger log.Logger) (err error) {
				defer func() {
					if rec := recover(); rec != nil {
						err = fmt.Errorf("index panic: at=%s, %v, %s", sn.Name(), rec, dbg.Stack())
					}
				}()
				firstBlockNum := sn.From
				bodiesPathPattern, err := version.ReplaceVersionWithMask(sn.As(Bodies).Path)
				if err != nil {
					return fmt.Errorf("can't replace path with mask %s for indexing in bodies: %w", sn.As(Bodies).Path, err)
				}
				bodiesPath, bVer, ok, err := version.FindFilesWithVersionsByPattern(bodiesPathPattern)
				if err != nil {
					return fmt.Errorf("can't find files with vers by pattern due to err %s for indexing in bodies: %w", bodiesPathPattern, err)
				}
				if !ok {
					return fmt.Errorf("can't find files with vers by pattern %s for indexing in bodies", bodiesPathPattern)
				}
				statecfg.Schema.BodiesBlock.FileVersion.DataSeg.MustSupport(bVer, filepath.Base(bodiesPath))
				bodiesSegment, err := seg.NewDecompressor(bodiesPath)
				if err != nil {
					return fmt.Errorf("can't open %s for indexing in bodies: %w", sn.As(Bodies).Path, err)
				}
				defer bodiesSegment.Close()

				baseTxnID, expectedCount, err := TxsAmountBasedOnBodiesSnapshots(bodiesSegment, sn.Len()-1)
				if err != nil {
					return err
				}

				txPathPattern, err := version.ReplaceVersionWithMask(sn.Path)
				if err != nil {
					return fmt.Errorf("can't replace path with mask %s for indexing in txs: %w", sn.Path, err)
				}
				txPath, tVer, ok, err := version.FindFilesWithVersionsByPattern(txPathPattern)
				if err != nil {
					return fmt.Errorf("can't find files with vers by pattern due to err %s for indexing in txs: %w", txPathPattern, err)
				}
				if !ok {
					return fmt.Errorf("can't find files with vers by pattern %s for indexing in txs", txPathPattern)
				}
				statecfg.Schema.TransactionsBlock.FileVersion.DataSeg.MustSupport(tVer, filepath.Base(txPath))
				d, err := seg.NewDecompressor(txPath)
				if err != nil {
					return fmt.Errorf("can't open %s for indexing in transactions: %w", sn.Path, err)
				}
				defer d.Close()
				if d.Count() != expectedCount {
					return fmt.Errorf("TransactionsIdx: at=%d-%d, pre index building, expect: %d, got %d", sn.From, sn.To, expectedCount, d.Count())
				}

				txnHashIdx, err := recsplit.NewRecSplit(recsplit.RecSplitArgs{
					KeyCount: d.Count(),

					Enums:              true,
					LessFalsePositives: true,

					BucketSize: recsplit.DefaultBucketSize,
					LeafSize:   recsplit.DefaultLeafSize,
					TmpDir:     tmpDir,
					IndexFile:  filepath.Join(sn.Dir(), sn.Type.IdxFileName(Indexes.TxnHash.Version.Current, sn.From, sn.To)),
					BaseDataID: baseTxnID.U64(),
				}, logger)
				if err != nil {
					return err
				}
				defer txnHashIdx.Close()

				txnHash2BlockNumIdx, err := recsplit.NewRecSplit(recsplit.RecSplitArgs{
					KeyCount:   d.Count(),
					Enums:      false,
					BucketSize: recsplit.DefaultBucketSize,
					LeafSize:   recsplit.DefaultLeafSize,
					TmpDir:     tmpDir,
					IndexFile: filepath.Join(sn.Dir(),
						sn.Type.IdxFileName(Indexes.TxnHash2BlockNum.Version.Current,
							sn.From, sn.To, Indexes.TxnHash2BlockNum)),
					BaseDataID: firstBlockNum,
				}, logger)
				if err != nil {
					return err
				}
				defer txnHash2BlockNumIdx.Close()
				txnHashIdx.LogLvl(log.LvlDebug)
				txnHash2BlockNumIdx.LogLvl(log.LvlDebug)

				bodyBuf, word := make([]byte, 0, 4096), make([]byte, 0, 4096)

				defer d.MadvSequential().DisableReadAhead()
				defer bodiesSegment.MadvSequential().DisableReadAhead()

				for {
					txnHashIdx.SetProgress(p)
					g, bodyGetter := d.MakeGetter(), bodiesSegment.MakeGetter()
					var ti, offset, nextPos uint64
					blockNum := firstBlockNum
					body := &types.BodyForStorage{}

					bodyBuf, _ = bodyGetter.Next(bodyBuf[:0])
					if err := rlp.DecodeBytes(bodyBuf, body); err != nil {
						return err
					}

					for g.HasNext() {
						word, nextPos = g.Next(word[:0])
						select {
						case <-ctx.Done():
							return ctx.Err()
						default:
						}

						// TODO review this code, test pass with lhs+1 <= baseTxnID.U64()+ti
						for body.BaseTxnID.LastSystemTx(body.TxCount) < baseTxnID.U64()+ti { // skip empty blocks; ti here is not transaction index in one block, but total transaction index counter
							if !bodyGetter.HasNext() {
								return errors.New("not enough bodies")
							}

							bodyBuf, _ = bodyGetter.Next(bodyBuf[:0])
							if err := rlp.DecodeBytes(bodyBuf, body); err != nil {
								return err
							}

							blockNum++
						}

						firstTxByteAndlengthOfAddress := 21
						isSystemTx := len(word) == 0
						var txnHash common.Hash
						if isSystemTx { // system-txs hash:pad32(txnID)
							binary.BigEndian.PutUint64(txnHash[:], baseTxnID.U64()+ti)
						} else {
							txn, err := types.DecodeTransaction(word[firstTxByteAndlengthOfAddress:])
							if err != nil {
								return fmt.Errorf("ParseTransaction: %w, blockNum: %d, i: %d", err, blockNum, ti)
							}
							txnHash = txn.Hash()
						}

						if err := txnHashIdx.AddKey(txnHash[:], offset); err != nil {
							return err
						}
						if err := txnHash2BlockNumIdx.AddKey(txnHash[:], blockNum); err != nil {
							return err
						}

						ti++
						offset = nextPos
					}

					if int(ti) != expectedCount {
						return fmt.Errorf("TransactionsIdx: at=%d-%d, post index building, expect: %d, got %d", sn.From, sn.To, expectedCount, ti)
					}

					if err := txnHashIdx.Build(ctx); err != nil {
						if errors.Is(err, recsplit.ErrCollision) {
							logger.Warn("Building recsplit. Collision happened. It's ok. Restarting with another salt...", "err", err)
							txnHashIdx.ResetNextSalt()
							txnHash2BlockNumIdx.ResetNextSalt()
							continue
						}
						return fmt.Errorf("txnHashIdx: %w", err)
					}
					if err := txnHash2BlockNumIdx.Build(ctx); err != nil {
						if errors.Is(err, recsplit.ErrCollision) {
							logger.Warn("Building recsplit. Collision happened. It's ok. Restarting with another salt...", "err", err)
							txnHashIdx.ResetNextSalt()
							txnHash2BlockNumIdx.ResetNextSalt()
							continue
						}
						return fmt.Errorf("txnHash2BlockNumIdx: %w", err)
					}

					return nil
				}
			}),
	)
	Domains = snaptype.RegisterType(
		Enums.Domains,
		"domain",
		version.V1_1_standart,
		nil,
		nil,
		nil,
	)
	Histories = snaptype.RegisterType(
		Enums.Histories,
		"history",
		version.V1_1_standart,
		nil,
		nil,
		nil,
	)
	InvertedIndicies = snaptype.RegisterType(
		Enums.InvertedIndicies,
		"idx",
		version.V1_1_standart,
		nil,
		nil,
		nil,
	)

	Accessors = snaptype.RegisterType(
		Enums.Accessor,
		"accessor",
		version.V1_1_standart,
		nil,
		nil,
		nil,
	)

	Txt = snaptype.RegisterType(
		Enums.Txt,
		"txt",
		version.V1_1_standart,
		nil,
		nil,
		nil,
	)
	BlockSnapshotTypes = []snaptype.Type{Headers, Bodies, Transactions}
	E3StateTypes       = []snaptype.Type{Domains, Histories, InvertedIndicies, Accessors, Txt}
)

func TxsAmountBasedOnBodiesSnapshots(bodiesSegment *seg.Decompressor, len uint64) (baseTxID types.BaseTxnID, expectedCount int, err error) {
	gg := bodiesSegment.MakeGetter()
	buf, _ := gg.Next(nil)
	firstBody := &types.BodyForStorage{}
	if err = rlp.DecodeBytes(buf, firstBody); err != nil {
		return
	}
	baseTxID = firstBody.BaseTxnID

	lastBody := new(types.BodyForStorage)
	i := uint64(0)
	for gg.HasNext() {
		i++
		if i == len {
			buf, _ = gg.Next(buf[:0])
			if err = rlp.DecodeBytes(buf, lastBody); err != nil {
				return
			}
			if gg.HasNext() {
				panic(1)
			}
		} else {
			gg.Skip()
		}
	}

	if lastBody.BaseTxnID < firstBody.BaseTxnID {
		return 0, 0, fmt.Errorf("negative txs count %s: lastBody.BaseTxId=%d < firstBody.BaseTxId=%d", bodiesSegment.FileName(), lastBody.BaseTxnID, firstBody.BaseTxnID)
	}

	// TODO: check if it is correct
	magic := uint64(1)
	expectedCount = int(lastBody.BaseTxnID.LastSystemTx(lastBody.TxCount) + magic - firstBody.BaseTxnID.U64())
	return
}
