package util

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/textproto"
	"strings"

	"github.com/application-research/filclient"
	datatransfer "github.com/filecoin-project/go-data-transfer"
	blocks "github.com/ipfs/go-block-format"
	"github.com/ipfs/go-cid"
	blockstore "github.com/ipfs/go-ipfs-blockstore"
	ipld "github.com/ipfs/go-ipld-format"
	"github.com/labstack/echo/v4"
	"github.com/multiformats/go-multiaddr"
	"github.com/multiformats/go-multihash"
	"go.opentelemetry.io/otel/trace"
	"go.uber.org/zap"
)

func CanRestartTransfer(st *filclient.ChannelState) bool {
	switch st.Status {
	case datatransfer.Cancelled,
		datatransfer.Failed,
		datatransfer.Completed:
		return false
	default:
		return true
	}
}

func TransferFailed(st *filclient.ChannelState) (bool, string) {
	msg, _ := datatransfer.Statuses[st.Status]
	switch st.Status {
	case datatransfer.Cancelled, datatransfer.Failed:
		return true, msg
	default:
		return false, msg
	}
}

func ParseDealLabel(s string) (cid.Cid, error) {
	return cid.Decode(s)
}

func FilterUnwalkableLinks(links []*ipld.Link) []*ipld.Link {
	out := make([]*ipld.Link, 0, len(links))

	for _, l := range links {
		if CidIsUnwalkable(l.Cid) {
			continue
		}
		out = append(out, l)
	}

	return out
}

func CidIsUnwalkable(c cid.Cid) bool {
	pref := c.Prefix()
	if pref.MhType == multihash.IDENTITY {
		return true
	}

	if pref.Codec == cid.FilCommitmentSealed || pref.Codec == cid.FilCommitmentUnsealed {
		return true
	}

	return false
}

func ErrorIfContentAddingDisabled(isContentAddingDisabled bool) error {
	if isContentAddingDisabled {
		return &HttpError{
			Code:    http.StatusBadRequest,
			Reason:  ERR_CONTENT_ADDING_DISABLED,
			Details: "uploading content to this node is not allowed at the moment",
		}
	}
	return nil
}

// required for car uploads
func WithContentLengthCheck(f func(echo.Context) error) func(echo.Context) error {
	return func(c echo.Context) error {
		if c.Request().Header.Get("Content-Length") == "" {
			return &HttpError{
				Code:    http.StatusLengthRequired,
				Reason:  ERR_CONTENT_LENGTH_REQUIRED,
				Details: "uploading car content requires Content-Length header value to be set",
			}
		}
		return f(c)
	}
}

type binder struct {
	log *zap.SugaredLogger
}

func NewBinder(log *zap.SugaredLogger) binder {
	return binder{log: log}
}

func (b binder) Bind(i interface{}, c echo.Context) error {
	defer func() {
		if err := c.Request().Body.Close(); err != nil {
			log.Warnf("failed to close request body: %s", err)
		}
	}()

	if err := json.NewDecoder(c.Request().Body).Decode(i); err != nil {
		return &HttpError{
			Code:    http.StatusBadRequest,
			Reason:  ERR_INVALID_INPUT,
			Details: fmt.Sprintf("one or more params has an invalid data type or not supported: %s", err),
		}
	}
	return nil
}

func JSONPayloadMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
	return func(c echo.Context) error {
		if err := checkContentType(c.Request().Header, "application/json"); err != nil {
			return err
		}
		return next(c)
	}
}

func WithMultipartFormDataChecker(next echo.HandlerFunc) echo.HandlerFunc {
	return func(c echo.Context) error {
		if err := checkContentType(c.Request().Header, "multipart/form-data"); err != nil {
			return err
		}
		return next(c)
	}
}

func checkContentType(header http.Header, expectedContentType string) error {
	contentType := textproto.MIMEHeader(header).Get("Content-Type")
	if !strings.HasPrefix(contentType, expectedContentType) {
		return &HttpError{
			Code:    http.StatusUnsupportedMediaType,
			Reason:  ERR_UNSUPPORTED_CONTENT_TYPE,
			Details: fmt.Sprintf("this endpoint only supports %s payloads. Found %s.", expectedContentType, contentType),
		}
	}
	return nil
}

func DumpBlockstoreTo(ctx context.Context, tc trace.Tracer, from, to blockstore.Blockstore) error {
	ctx, span := tc.Start(ctx, "blockstoreCopy")
	defer span.End()

	// TODO: smarter batching... im sure ive written this logic before, just gotta go find it
	keys, err := from.AllKeysChan(ctx)
	if err != nil {
		return err
	}

	var batch []blocks.Block

	for k := range keys {
		blk, err := from.Get(ctx, k)
		if err != nil {
			return err
		}

		batch = append(batch, blk)

		if len(batch) > 500 {
			if err := to.PutMany(ctx, batch); err != nil {
				return err
			}
			batch = batch[:0]
		}
	}

	if len(batch) > 0 {
		if err := to.PutMany(ctx, batch); err != nil {
			return err
		}
	}
	return nil
}

// It takes a slice of strings and returns a slice of multiaddresses
func ToMultiAddresses(addrs []string) ([]multiaddr.Multiaddr, error) {
	var multiAddrs []multiaddr.Multiaddr
	for _, addr := range addrs {
		a, err := ToMultiAddress(addr)
		if err != nil {
			log.Errorf("toMultiAddresses failed: %s", err)
		}
		multiAddrs = append(multiAddrs, a)
	}
	return multiAddrs, nil
}

// Converting the public key to a multiaddress.
func ToMultiAddress(addr string) (multiaddr.Multiaddr, error) {
	a, err := multiaddr.NewMultiaddr(addr)
	if err != nil {
		return nil, fmt.Errorf("failed to parse string multi addr: %w", err)
	}
	return a, nil
}
