package kubo

import (
	"context"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"

	"github.com/ipfs/boxo/files"
	"github.com/ipfs/boxo/path"
	"github.com/ipfs/kubo/core"
	"github.com/ipfs/kubo/core/coreapi"
	coreiface "github.com/ipfs/kubo/core/coreiface"
	"github.com/ipfs/kubo/core/coreiface/options"
	"github.com/ipfs/kubo/repo/fsrepo/migrations"
	"github.com/ipfs/kubo/repo/fsrepo/migrations/ipfsfetcher"
	"github.com/libp2p/go-libp2p/core/peer"
)

// addMigrations adds any migration downloaded by the fetcher to the IPFS node.
func addMigrations(ctx context.Context, node *core.IpfsNode, fetcher migrations.Fetcher, pin bool) error {
	var fetchers []migrations.Fetcher
	if mf, ok := fetcher.(*migrations.MultiFetcher); ok {
		fetchers = mf.Fetchers()
	} else {
		fetchers = []migrations.Fetcher{fetcher}
	}

	for _, fetcher := range fetchers {
		switch f := fetcher.(type) {
		case *ipfsfetcher.IpfsFetcher:
			// Add migrations by connecting to temp node and getting from IPFS
			err := addMigrationPaths(ctx, node, f.AddrInfo(), f.FetchedPaths(), pin)
			if err != nil {
				return err
			}
		case *migrations.HttpFetcher, *migrations.RetryFetcher: // https://github.com/ipfs/kubo/issues/8780
			// Add the downloaded migration files directly
			if migrations.DownloadDirectory != "" {
				var paths []string
				err := filepath.Walk(migrations.DownloadDirectory, func(filePath string, info os.FileInfo, err error) error {
					if info.IsDir() {
						return nil
					}
					paths = append(paths, filePath)
					return nil
				})
				if err != nil {
					return err
				}
				err = addMigrationFiles(ctx, node, paths, pin)
				if err != nil {
					return err
				}
			}
		default:
			return errors.New("cannot get migrations from unknown fetcher type")
		}
	}

	return nil
}

// addMigrationFiles adds the files at paths to IPFS, optionally pinning them.
func addMigrationFiles(ctx context.Context, node *core.IpfsNode, paths []string, pin bool) error {
	if len(paths) == 0 {
		return nil
	}
	ifaceCore, err := coreapi.NewCoreAPI(node)
	if err != nil {
		return err
	}
	ufs := ifaceCore.Unixfs()

	// Add migration files
	for _, filePath := range paths {
		f, err := os.Open(filePath)
		if err != nil {
			return err
		}

		fi, err := f.Stat()
		if err != nil {
			f.Close()
			return err
		}

		ipfsPath, err := ufs.Add(ctx, files.NewReaderStatFile(f, fi), options.Unixfs.Pin(pin, ""))
		f.Close()
		if err != nil {
			return err
		}
		fmt.Printf("Added migration file %q: %s\n", filepath.Base(filePath), ipfsPath)
	}

	return nil
}

// addMigrationPaths adds the files at paths to IPFS, optionally pinning
// them. This is done after connecting to the peer.
func addMigrationPaths(ctx context.Context, node *core.IpfsNode, peerInfo peer.AddrInfo, paths []path.Path, pin bool) error {
	if len(paths) == 0 {
		return errors.New("nothing downloaded by ipfs fetcher")
	}
	if len(peerInfo.Addrs) == 0 {
		return errors.New("no local swarm address for migration node")
	}

	ipfs, err := coreapi.NewCoreAPI(node)
	if err != nil {
		return err
	}

	// Connect to temp node
	if err := ipfs.Swarm().Connect(ctx, peerInfo); err != nil {
		return fmt.Errorf("could not connect to migration peer %q: %s", peerInfo.ID, err)
	}
	fmt.Printf("connected to migration peer %q\n", peerInfo)

	if pin {
		pinAPI := ipfs.Pin()
		for _, ipfsPath := range paths {
			err := pinAPI.Add(ctx, ipfsPath)
			if err != nil {
				return err
			}
			fmt.Printf("Added and pinned migration file: %q\n", ipfsPath)
		}
		return nil
	}

	ufs := ipfs.Unixfs()

	// Add migration files
	for _, ipfsPath := range paths {
		err = ipfsGet(ctx, ufs, ipfsPath)
		if err != nil {
			return err
		}
	}

	return nil
}

func ipfsGet(ctx context.Context, ufs coreiface.UnixfsAPI, ipfsPath path.Path) error {
	nd, err := ufs.Get(ctx, ipfsPath)
	if err != nil {
		return err
	}
	defer nd.Close()

	fnd, ok := nd.(files.File)
	if !ok {
		return fmt.Errorf("not a file node: %q", ipfsPath)
	}
	_, err = io.Copy(io.Discard, fnd)
	if err != nil {
		return fmt.Errorf("cannot read migration: %w", err)
	}
	fmt.Printf("Added migration file: %q\n", ipfsPath)
	return nil
}
