package manager

import (
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/lbryio/lbry.go/v2/extras/errors"
	"github.com/lbryio/lbry.go/v2/extras/jsonrpc"
	"github.com/lbryio/lbry.go/v2/extras/stop"
	"github.com/lbryio/lbry.go/v2/extras/util"
	"github.com/lbryio/ytsync/v5/sdk"
	"github.com/lbryio/ytsync/v5/timing"

	log "github.com/sirupsen/logrus"
)

func waitConfirmations(s *Sync) error {
	start := time.Now()
	defer func(start time.Time) {
		timing.TimedComponent("waitConfirmations").Add(time.Since(start))
	}(start)
	defaultAccount, err := s.getDefaultAccount()
	if err != nil {
		return err
	}
	allConfirmed := false
	waitCount := 0
waiting:
	for !allConfirmed && waitCount < 2 {
		utxolist, err := s.daemon.UTXOList(&defaultAccount, 1, 10000)
		if err != nil {
			return err
		} else if utxolist == nil {
			return errors.Err("no response")
		}

		for _, utxo := range utxolist.Items {
			if utxo.Confirmations <= 0 {
				err = s.waitForNewBlock()
				if err != nil {
					return err
				}
				waitCount++
				continue waiting
			}
		}
		allConfirmed = true
	}
	return nil
}

type abandonResponse struct {
	ClaimID string
	Error   error
	Amount  float64
}

func abandonSupports(s *Sync) (float64, error) {
	start := time.Now()
	defer func(start time.Time) {
		timing.TimedComponent("abandonSupports").Add(time.Since(start))
	}(start)
	totalPages := uint64(1)
	var allSupports []jsonrpc.Claim
	defaultAccount, err := s.getDefaultAccount()
	if err != nil {
		return 0, err
	}
	for page := uint64(1); page <= totalPages; page++ {
		supports, err := s.daemon.SupportList(&defaultAccount, page, 50)
		if err != nil {
			return 0, errors.Prefix("cannot list claims", err)
		}
		allSupports = append(allSupports, (*supports).Items...)
		totalPages = (*supports).TotalPages
	}
	producerWG := &stop.Group{}

	claimIDChan := make(chan string, len(allSupports))
	abandonRspChan := make(chan abandonResponse, len(allSupports))
	alreadyAbandoned := make(map[string]bool, len(allSupports))
	producerWG.Add(1)
	go func() {
		defer producerWG.Done()
		for _, support := range allSupports {
			_, ok := alreadyAbandoned[support.ClaimID]
			if ok {
				continue
			}
			alreadyAbandoned[support.ClaimID] = true
			claimIDChan <- support.ClaimID
		}
	}()
	consumerWG := &stop.Group{}
	//TODO: remove this once the SDK team fixes their RPC bugs....
	s.daemon.SetRPCTimeout(60 * time.Second)
	defer s.daemon.SetRPCTimeout(5 * time.Minute)
	for i := 0; i < s.ConcurrentVideos; i++ {
		consumerWG.Add(1)
		go func() {
			defer consumerWG.Done()
		outer:
			for {
				claimID, more := <-claimIDChan
				if !more {
					return
				} else {
					summary, err := s.daemon.TxoSpend(util.PtrToString("support"), &claimID, nil, nil, nil, &defaultAccount)
					if err != nil {
						if strings.Contains(err.Error(), "Client.Timeout exceeded while awaiting headers") {
							log.Errorf("Support abandon for %s timed out, retrying...", claimID)
							summary, err = s.daemon.TxoSpend(util.PtrToString("support"), &claimID, nil, nil, nil, &defaultAccount)
							if err != nil {
								//TODO GUESS HOW MUCH LBC WAS RELEASED THAT WE DON'T KNOW ABOUT, because screw you SDK
								abandonRspChan <- abandonResponse{
									ClaimID: claimID,
									Error:   err,
									Amount:  0, // this is likely wrong, but oh well... there is literally nothing I can do about it
								}
								continue
							}
						} else {
							abandonRspChan <- abandonResponse{
								ClaimID: claimID,
								Error:   err,
								Amount:  0,
							}
							continue
						}
					}
					if summary == nil || len(*summary) < 1 {
						abandonRspChan <- abandonResponse{
							ClaimID: claimID,
							Error:   errors.Err("error abandoning supports: no outputs while abandoning %s", claimID),
							Amount:  0,
						}
						continue
					}
					var outputAmount float64
					for _, tx := range *summary {
						amount, err := strconv.ParseFloat(tx.Outputs[0].Amount, 64)
						if err != nil {
							abandonRspChan <- abandonResponse{
								ClaimID: claimID,
								Error:   errors.Err(err),
								Amount:  0,
							}
							continue outer
						}
						outputAmount += amount
					}
					if err != nil {
						abandonRspChan <- abandonResponse{
							ClaimID: claimID,
							Error:   errors.Err(err),
							Amount:  0,
						}
						continue
					}
					log.Infof("Abandoned supports of %.4f LBC for claim %s", outputAmount, claimID)
					abandonRspChan <- abandonResponse{
						ClaimID: claimID,
						Error:   nil,
						Amount:  outputAmount,
					}
					continue
				}
			}
		}()
	}
	producerWG.Wait()
	close(claimIDChan)
	consumerWG.Wait()
	close(abandonRspChan)

	totalAbandoned := 0.0
	for r := range abandonRspChan {
		if r.Error != nil {
			log.Errorf("Failed abandoning supports for %s: %s", r.ClaimID, r.Error.Error())
			continue
		}
		totalAbandoned += r.Amount
	}
	return totalAbandoned, nil
}

type updateInfo struct {
	ClaimID             string
	streamUpdateOptions *jsonrpc.StreamUpdateOptions
	videoStatus         *sdk.VideoStatus
}

func transferVideos(s *Sync) error {
	start := time.Now()
	defer func(start time.Time) {
		timing.TimedComponent("transferVideos").Add(time.Since(start))
	}(start)
	cleanTransfer := true

	streamChan := make(chan updateInfo, s.ConcurrentVideos)
	account, err := s.getDefaultAccount()
	if err != nil {
		return err
	}
	streams, err := s.daemon.StreamList(&account, 1, 30000)
	if err != nil {
		return errors.Err(err)
	}
	producerWG := &stop.Group{}
	producerWG.Add(1)
	go func() {
		defer producerWG.Done()
		for _, video := range s.syncedVideos {
			if !video.Published || video.Transferred || video.MetadataVersion != LatestMetadataVersion {
				continue
			}

			var stream *jsonrpc.Claim = nil
			for _, c := range streams.Items {
				if c.ClaimID != video.ClaimID || (c.SigningChannel != nil && c.SigningChannel.ClaimID != s.lbryChannelID) {
					continue
				}
				stream = &c
				break
			}
			if stream == nil {
				return
			}

			streamUpdateOptions := jsonrpc.StreamUpdateOptions{
				StreamCreateOptions: &jsonrpc.StreamCreateOptions{
					ClaimCreateOptions: jsonrpc.ClaimCreateOptions{
						ClaimAddress: &s.clientPublishAddress,
						FundingAccountIDs: []string{
							account,
						},
					},
				},
				Bid: util.PtrToString("0.005"), // Todo - Dont hardcode
			}
			videoStatus := sdk.VideoStatus{
				ChannelID:     s.YoutubeChannelID,
				VideoID:       video.VideoID,
				ClaimID:       video.ClaimID,
				ClaimName:     video.ClaimName,
				Status:        VideoStatusPublished,
				IsTransferred: util.PtrToBool(true),
			}
			streamChan <- updateInfo{
				ClaimID:             video.ClaimID,
				streamUpdateOptions: &streamUpdateOptions,
				videoStatus:         &videoStatus,
			}
		}
	}()

	consumerWG := &stop.Group{}
	for i := 0; i < s.ConcurrentVideos; i++ {
		consumerWG.Add(1)
		go func(worker int) {
			defer consumerWG.Done()
			for {
				ui, more := <-streamChan
				if !more {
					return
				} else {
					err := s.streamUpdate(&ui)
					if err != nil {
						cleanTransfer = false
					}
				}
			}
		}(i)
	}
	producerWG.Wait()
	close(streamChan)
	consumerWG.Wait()

	if !cleanTransfer {
		return errors.Err("A video has failed to transfer for the channel...skipping channel transfer")
	}
	return nil
}

func (s *Sync) streamUpdate(ui *updateInfo) error {
	start := time.Now()
	result, updateError := s.daemon.StreamUpdate(ui.ClaimID, *ui.streamUpdateOptions)
	timing.TimedComponent("transferStreamUpdate").Add(time.Since(start))
	if updateError != nil {
		ui.videoStatus.FailureReason = updateError.Error()
		ui.videoStatus.Status = VideoStatusTranferFailed
		ui.videoStatus.IsTransferred = util.PtrToBool(false)
	} else {
		ui.videoStatus.IsTransferred = util.PtrToBool(len(result.Outputs) != 0)
	}
	log.Infof("TRANSFERRED %t", *ui.videoStatus.IsTransferred)
	statusErr := s.APIConfig.MarkVideoStatus(*ui.videoStatus)
	if statusErr != nil {
		return errors.Prefix(statusErr.Error(), updateError)
	}
	return errors.Err(updateError)
}

func transferChannel(s *Sync) error {
	start := time.Now()
	defer func(start time.Time) {
		timing.TimedComponent("transferChannel").Add(time.Since(start))
	}(start)
	account, err := s.getDefaultAccount()
	if err != nil {
		return err
	}
	channelClaims, err := s.daemon.ChannelList(&account, 1, 50, nil)
	if err != nil {
		return errors.Err(err)
	}
	var channelClaim *jsonrpc.Transaction = nil
	for _, c := range channelClaims.Items {
		if c.ClaimID != s.lbryChannelID {
			continue
		}
		channelClaim = &c
		break
	}
	if channelClaim == nil {
		return nil
	}

	updateOptions := jsonrpc.ChannelUpdateOptions{
		Bid: util.PtrToString(fmt.Sprintf("%.6f", channelClaimAmount-0.005)),
		ChannelCreateOptions: jsonrpc.ChannelCreateOptions{
			ClaimCreateOptions: jsonrpc.ClaimCreateOptions{
				ClaimAddress: &s.clientPublishAddress,
			},
		},
	}
	result, err := s.daemon.ChannelUpdate(s.lbryChannelID, updateOptions)
	if err != nil {
		return errors.Err(err)
	}
	log.Infof("TRANSFERRED %t", len(result.Outputs) != 0)

	return nil
}