// Copyright 2021 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 downloader

import (
	"bytes"
	"context"
	"crypto/sha1"
	//nolint:gosec
	"errors"
	"fmt"
	"io"
	"os"
	"path"
	"path/filepath"
	"runtime"
	"strings"
	"sync/atomic"
	"time"

	"golang.org/x/sync/errgroup"

	"github.com/anacrolix/torrent"
	"github.com/anacrolix/torrent/bencode"
	"github.com/anacrolix/torrent/metainfo"

	"github.com/erigontech/erigon/common"
	"github.com/erigontech/erigon/common/dbg"
	dir2 "github.com/erigontech/erigon/common/dir"
	"github.com/erigontech/erigon/common/log/v3"
	"github.com/erigontech/erigon/db/datadir"
	"github.com/erigontech/erigon/db/downloader/downloadercfg"
	"github.com/erigontech/erigon/db/kv"
	"github.com/erigontech/erigon/db/snapcfg"
	"github.com/erigontech/erigon/db/snaptype"
	"github.com/erigontech/erigon/db/state"
)

// TODO: Update this list, or pull from common location (central manifest or canonical multi-file torrent).
// udpOrHttpTrackers - torrent library spawning several goroutines and producing many requests for each tracker. So we limit amout of trackers by 8
var udpOrHttpTrackers = []string{
	"udp://tracker.opentrackr.org:1337/announce",
	"udp://tracker.torrent.eu.org:451/announce",
	"udp://open.stealth.si:80/announce",
}

// nolint
var websocketTrackers = []string{
	"wss://tracker.btorrent.xyz",
}

// Trackers - break down by priority tier
var Trackers = [][]string{
	udpOrHttpTrackers,
	//websocketTrackers // TODO: Ws protocol producing too many errors and flooding logs. But it's also very fast and reactive.
}

func seedableSegmentFiles(dir string, chainName string, skipSeedableCheck bool) ([]string, error) {
	extensions := snaptype.SeedableV2Extensions()
	if skipSeedableCheck {
		extensions = snaptype.AllV2Extensions()
	}
	files, err := dir2.ListFiles(dir, extensions...)
	if err != nil {
		return nil, err
	}

	segConfig := snapcfg.KnownCfgOrDevnet(chainName)

	res := make([]string, 0, len(files))
	for _, fPath := range files {
		_, name := filepath.Split(fPath)
		// A bit hacky but whatever... basically caplin is incompatible with enums.
		if strings.HasSuffix(fPath, path.Join("caplin", name)) {
			res = append(res, path.Join("caplin", name))
			continue
		}
		if strings.HasPrefix(name, "salt") && strings.HasSuffix(name, "txt") {
			res = append(res, name)
			continue
		}
		if name == state.ERIGONDB_SETTINGS_FILE {
			res = append(res, name)
			continue
		}
		// Skip any other .toml files
		if strings.HasSuffix(name, ".toml") {
			continue
		}
		if !skipSeedableCheck && !snaptype.IsCorrectFileName(name) {
			continue
		}
		ff, isStateFile, ok := snaptype.ParseFileName(dir, name)
		if !skipSeedableCheck && (!ok || isStateFile) {
			continue
		}
		if !skipSeedableCheck && !segConfig.Seedable(ff) {
			continue
		}
		res = append(res, name)
	}
	return res, nil
}

func seedableStateFilesBySubDir(dir, subDir string, skipSeedableCheck bool) ([]string, error) {
	historyDir := filepath.Join(dir, subDir)
	dir2.MustExist(historyDir)
	extensions := snaptype.SeedableV3Extensions()
	if skipSeedableCheck {
		extensions = snaptype.AllV3Extensions()
	}
	files, err := dir2.ListFiles(historyDir, extensions...)
	if err != nil {
		return nil, err
	}
	res := make([]string, 0, len(files))
	for _, fPath := range files {
		_, name := filepath.Split(fPath)
		if !skipSeedableCheck && !snaptype.IsStateFileSeedable(name) {
			continue
		}
		res = append(res, filepath.Join(subDir, name))
	}
	return res, nil
}

func ensureCantLeaveDir(fName, root string) (string, error) {
	if filepath.IsAbs(fName) {
		newFName, err := filepath.Rel(root, fName)
		if err != nil {
			return fName, err
		}
		if !filepath.IsLocal(newFName) {
			return fName, fmt.Errorf("file=%s, is outside of snapshots dir", fName)
		}
		fName = newFName
	}
	if !filepath.IsLocal(fName) {
		return fName, fmt.Errorf("relative paths are not allowed: %s", fName)
	}
	return fName, nil
}

func BuildTorrentIfNeed(ctx context.Context, fName, root string, torrentFiles *AtomicTorrentFS) (ok bool, err error) {
	select {
	case <-ctx.Done():
		return false, context.Cause(ctx)
	default:
	}
	fName, err = ensureCantLeaveDir(fName, root)
	if err != nil {
		return false, err
	}

	exists, err := torrentFiles.Exists(fName)
	if err != nil {
		return false, err
	}
	if exists {
		return false, nil
	}

	fPath := filepath.Join(root, fName)

	// TODO: Consider using the auto-piece sizing?
	info := &metainfo.Info{PieceLength: downloadercfg.DefaultPieceSize, Name: fName}
	if err := info.BuildFromFilePath(fPath); err != nil {
		return false, fmt.Errorf("createTorrentFileFromSegment: %w", err)
	}
	// Really need to check this is "slash"-style. I suspect it will do the wrong thing on Windows.
	info.Name = fName

	return torrentFiles.CreateWithMetaInfo(info, nil)
}

// BuildTorrentFilesIfNeed - create .torrent files from .seg files (big IO) - if .seg files were added manually
func BuildTorrentFilesIfNeed(ctx context.Context, dirs datadir.Dirs, torrentFiles *AtomicTorrentFS, chain string, ignore snapcfg.PreverifiedItems, all bool) (int, error) {
	logEvery := time.NewTicker(20 * time.Second)
	defer logEvery.Stop()

	files, err := SeedableFiles(dirs, chain, all)
	if err != nil {
		return 0, err
	}

	g, ctx := errgroup.WithContext(ctx)
	g.SetLimit(runtime.GOMAXPROCS(-1) * 16)
	var i atomic.Int32
	var createdAmount atomic.Int32

	for _, file := range files {

		if ignore.Contains(file) {
			i.Add(1)
			continue
		}

		g.Go(func() error {
			defer i.Add(1)
			ok, err := BuildTorrentIfNeed(ctx, file, dirs.Snap, torrentFiles)
			if err != nil {
				return err
			}
			if ok {
				createdAmount.Add(1)
			}
			return nil
		})
	}

Loop:
	for int(i.Load()) < len(files) {
		select {
		case <-ctx.Done():
			break Loop // g.Wait() will return right error
		case <-logEvery.C:
			if int(i.Load()) == len(files) {
				break Loop
			}
			log.Info("[snapshots] Creating .torrent files", "progress", fmt.Sprintf("%d/%d", i.Load(), len(files)))
		}
	}
	if err := g.Wait(); err != nil {
		return int(createdAmount.Load()), err
	}
	return int(createdAmount.Load()), nil
}

func CreateMetaInfo(info *metainfo.Info, mi *metainfo.MetaInfo) (*metainfo.MetaInfo, error) {
	if mi == nil {
		infoBytes, err := bencode.Marshal(info)
		if err != nil {
			return nil, err
		}
		mi = &metainfo.MetaInfo{
			CreationDate: time.Now().Unix(),
			// TODO: Differentiate direct web source vs generated locally.
			CreatedBy:    "erigon",
			InfoBytes:    infoBytes,
			AnnounceList: Trackers,
		}
	} else {
		mi.AnnounceList = Trackers
	}
	return mi, nil
}

func AllTorrentPaths(dirs datadir.Dirs) ([]string, error) {
	files, err := dir2.ListFiles(dirs.Snap, ".torrent")
	if err != nil {
		return nil, err
	}
	if dbg.DownloaderOnlyBlocks {
		return files, nil
	}
	l1, err := dir2.ListFiles(dirs.SnapIdx, ".torrent")
	if err != nil {
		return nil, err
	}
	l2, err := dir2.ListFiles(dirs.SnapHistory, ".torrent")
	if err != nil {
		return nil, err
	}
	l3, err := dir2.ListFiles(dirs.SnapDomain, ".torrent")
	if err != nil {
		return nil, err
	}
	l4, err := dir2.ListFiles(dirs.SnapAccessors, ".torrent")
	if err != nil {
		return nil, err
	}
	l5, err := dir2.ListFiles(dirs.SnapCaplin, ".torrent")
	if err != nil {
		return nil, err
	}
	files = append(append(append(append(append(files, l1...), l2...), l3...), l4...), l5...)
	return files, nil
}

func AllTorrentSpecs(dirs datadir.Dirs, torrentFiles *AtomicTorrentFS) (res []*torrent.TorrentSpec, err error) {
	files, err := AllTorrentPaths(dirs)
	if err != nil {
		return nil, err
	}
	for _, fPath := range files {
		if len(fPath) == 0 {
			continue
		}
		a, err := torrentFiles.LoadByPath(fPath)
		if err != nil {
			return nil, fmt.Errorf("AllTorrentSpecs: %w", err)
		}
		res = append(res, a)
	}
	return res, nil
}

// if $DOWNLOADER_ONLY_BLOCKS!="" filters out all non-v1 snapshots. TODO: This does not belong here.
// Downloader should be stupid.
func IsSnapNameAllowed(name string) bool {
	if dbg.DownloaderOnlyBlocks {
		for _, p := range []string{"domain", "history", "idx", "accessor"} {
			if strings.HasPrefix(name, p) {
				return false
			}
		}
	}
	return true
}

func savePeerID(db kv.RwDB, peerID torrent.PeerID) error {
	return db.Update(context.Background(), func(tx kv.RwTx) error {
		return tx.Put(kv.BittorrentInfo, []byte(kv.BittorrentPeerID), peerID[:])
	})
}

func readPeerID(db kv.RoDB) (peerID []byte, err error) {
	if err = db.View(context.Background(), func(tx kv.Tx) error {
		peerIDFromDB, err := tx.GetOne(kv.BittorrentInfo, []byte(kv.BittorrentPeerID))
		if err != nil {
			return fmt.Errorf("get peer id: %w", err)
		}
		peerID = common.Copy(peerIDFromDB)
		return nil
	}); err != nil {
		return nil, err
	}
	return peerID, nil
}

// Trigger all pieces to be verified with the given concurrency primitives. It's an error for a
// piece to not be complete or have an unknown state after verification.
func verifyTorrentComplete(
	ctx context.Context,
	eg *errgroup.Group,
	t *torrent.Torrent,
	verifiedBytes *atomic.Int64,
) {
	eg.Go(func() (err error) {
		// Wrap error for errgroup.Group return.
		defer func() {
			if err != nil {
				err = fmt.Errorf("verifying %v: %w", t.Name(), err)
			}
		}()
		err = t.VerifyDataContext(ctx)
		if err != nil {
			return
		}
		// This is a relatively new API feature, so please report if it does not work as expected.
		if t.Complete().Bool() {
			verifiedBytes.Add(t.Length())
		} else {
			err = errors.New("torrent not complete")
		}
		return
	})
}

func VerifyFileFailFast(ctx context.Context, t *torrent.Torrent, root string, completeBytes *atomic.Uint64) error {
	info := t.Info()
	file := info.UpvertedFiles()[0]
	fPath := filepath.Join(append([]string{root, info.Name}, file.Path...)...)
	f, err := os.Open(fPath)
	if err != nil {
		return err
	}
	defer func() {
		if err != nil {
			f.Close()
		}
	}()

	hasher := sha1.New()
	for i := 0; i < info.NumPieces(); i++ {
		p := info.Piece(i)
		hasher.Reset()
		_, err := io.Copy(hasher, io.NewSectionReader(f, p.Offset(), p.Length()))
		if err != nil {
			return err
		}
		good := bytes.Equal(hasher.Sum(nil), p.V1Hash().Value.Bytes())
		if !good {
			err := fmt.Errorf("hash mismatch at piece %d, file: %s", i, t.Name())
			log.Warn("[verify.failfast] ", "err", err)
			return err
		}

		completeBytes.Add(uint64(p.V1Length()))
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
		}
	}
	return nil
}
