package paymentsdb

import (
	"bytes"
	"fmt"
	"strconv"
	"time"

	"github.com/btcsuite/btcd/btcec/v2"
	"github.com/lightningnetwork/lnd/lntypes"
	"github.com/lightningnetwork/lnd/lnwire"
	"github.com/lightningnetwork/lnd/record"
	"github.com/lightningnetwork/lnd/routing/route"
	"github.com/lightningnetwork/lnd/sqldb/sqlc"
	"github.com/lightningnetwork/lnd/tlv"
)

// dbPaymentToCreationInfo converts database payment data to the
// PaymentCreationInfo struct.
func dbPaymentToCreationInfo(paymentIdentifier []byte, amountMsat int64,
	createdAt time.Time, intentPayload []byte,
	firstHopCustomRecords lnwire.CustomRecords) *PaymentCreationInfo {

	// This is the payment hash for non-AMP payments and the SetID for AMP
	// payments.
	var identifier lntypes.Hash
	copy(identifier[:], paymentIdentifier)

	return &PaymentCreationInfo{
		PaymentIdentifier: identifier,
		Value:             lnwire.MilliSatoshi(amountMsat),
		// The creation time is stored in the database as UTC but here
		// we convert it to local time.
		CreationTime:          createdAt.Local(),
		PaymentRequest:        intentPayload,
		FirstHopCustomRecords: firstHopCustomRecords,
	}
}

// dbAttemptToHTLCAttempt converts a database HTLC attempt to an HTLCAttempt.
func dbAttemptToHTLCAttempt(dbAttempt sqlc.FetchHtlcAttemptsForPaymentsRow,
	hops []sqlc.FetchHopsForAttemptsRow,
	hopCustomRecords map[int64][]sqlc.PaymentHopCustomRecord,
	routeCustomRecords []sqlc.PaymentAttemptFirstHopCustomRecord,
	includeHops bool) (
	*HTLCAttempt, error) {

	// Convert route-level first hop custom records to CustomRecords map.
	var firstHopWireCustomRecords lnwire.CustomRecords
	if includeHops && len(routeCustomRecords) > 0 {
		firstHopWireCustomRecords = make(lnwire.CustomRecords)
		for _, record := range routeCustomRecords {
			firstHopWireCustomRecords[uint64(record.Key)] =
				record.Value
		}
	}

	// Build the route from the database data.
	route, err := dbDataToRoute(
		hops, hopCustomRecords, dbAttempt.FirstHopAmountMsat,
		dbAttempt.RouteTotalTimeLock, dbAttempt.RouteTotalAmount,
		dbAttempt.RouteSourceKey, firstHopWireCustomRecords,
		!includeHops,
	)
	if err != nil {
		return nil, fmt.Errorf("failed to convert to route: %w",
			err)
	}

	hash, err := lntypes.MakeHash(dbAttempt.PaymentHash)
	if err != nil {
		return nil, fmt.Errorf("failed to parse payment "+
			"hash: %w", err)
	}

	// Create the attempt info.
	var sessionKey [32]byte
	copy(sessionKey[:], dbAttempt.SessionKey)

	info := HTLCAttemptInfo{
		AttemptID:   uint64(dbAttempt.AttemptIndex),
		sessionKey:  sessionKey,
		Route:       *route,
		AttemptTime: dbAttempt.AttemptTime,
		Hash:        &hash,
	}

	attempt := &HTLCAttempt{
		HTLCAttemptInfo: info,
	}

	// If there's no resolution type, the attempt is still in-flight.
	// Return early without processing settlement or failure info.
	if !dbAttempt.ResolutionType.Valid {
		return attempt, nil
	}

	// Add settlement info if present.
	if HTLCAttemptResolutionType(dbAttempt.ResolutionType.Int32) ==
		HTLCAttemptResolutionSettled {

		var preimage lntypes.Preimage
		copy(preimage[:], dbAttempt.SettlePreimage)

		attempt.Settle = &HTLCSettleInfo{
			Preimage:   preimage,
			SettleTime: dbAttempt.ResolutionTime.Time,
		}
	}

	// Add failure info if present.
	if HTLCAttemptResolutionType(dbAttempt.ResolutionType.Int32) ==
		HTLCAttemptResolutionFailed {

		failure := &HTLCFailInfo{
			FailTime: dbAttempt.ResolutionTime.Time,
		}

		if dbAttempt.HtlcFailReason.Valid {
			failure.Reason = HTLCFailReason(
				dbAttempt.HtlcFailReason.Int32,
			)
		}

		if dbAttempt.FailureSourceIndex.Valid {
			failure.FailureSourceIndex = uint32(
				dbAttempt.FailureSourceIndex.Int32,
			)
		}

		// Decode the failure message if present.
		if len(dbAttempt.FailureMsg) > 0 {
			msg, err := lnwire.DecodeFailureMessage(
				bytes.NewReader(dbAttempt.FailureMsg), 0,
			)
			if err != nil {
				return nil, fmt.Errorf("failed to decode "+
					"failure message: %w", err)
			}
			failure.Message = msg
		}

		attempt.Failure = failure
	}

	return attempt, nil
}

// dbDataToRoute converts database route data to a route.Route.
func dbDataToRoute(hops []sqlc.FetchHopsForAttemptsRow,
	hopCustomRecords map[int64][]sqlc.PaymentHopCustomRecord,
	firstHopAmountMsat int64, totalTimeLock int32, totalAmount int64,
	sourceKey []byte, firstHopWireCustomRecords lnwire.CustomRecords,
	allowEmpty bool) (
	*route.Route, error) {

	if len(hops) == 0 {
		if !allowEmpty {
			return nil, fmt.Errorf("no hops provided")
		}

		var sourceNode route.Vertex
		copy(sourceNode[:], sourceKey)

		route := &route.Route{
			TotalTimeLock: uint32(totalTimeLock),
			TotalAmount: lnwire.MilliSatoshi(
				totalAmount,
			),
			SourcePubKey:              sourceNode,
			Hops:                      nil,
			FirstHopWireCustomRecords: firstHopWireCustomRecords,
		}

		if firstHopAmountMsat != 0 {
			route.FirstHopAmount = tlv.NewRecordT[tlv.TlvType0](
				tlv.NewBigSizeT(lnwire.MilliSatoshi(
					firstHopAmountMsat,
				)),
			)
		}

		return route, nil
	}

	// Hops are already sorted by hop_index from the SQL query.
	routeHops := make([]*route.Hop, len(hops))

	for i, hop := range hops {
		pubKey, err := route.NewVertexFromBytes(hop.PubKey)
		if err != nil {
			return nil, fmt.Errorf("failed to parse pub key: %w",
				err)
		}

		var channelID uint64
		if hop.Scid != "" {
			// The SCID is stored as a string representation
			// of the uint64.
			var err error
			channelID, err = strconv.ParseUint(hop.Scid, 10, 64)
			if err != nil {
				return nil, fmt.Errorf("failed to parse "+
					"scid: %w", err)
			}
		}

		routeHop := &route.Hop{
			PubKeyBytes:      pubKey,
			ChannelID:        channelID,
			OutgoingTimeLock: uint32(hop.OutgoingTimeLock),
			AmtToForward:     lnwire.MilliSatoshi(hop.AmtToForward),
		}

		// Add MPP record if present.
		if len(hop.MppPaymentAddr) > 0 {
			var paymentAddr [32]byte
			copy(paymentAddr[:], hop.MppPaymentAddr)
			routeHop.MPP = record.NewMPP(
				lnwire.MilliSatoshi(hop.MppTotalMsat.Int64),
				paymentAddr,
			)
		}

		// Add AMP record if present.
		if len(hop.AmpRootShare) > 0 {
			var rootShare [32]byte
			copy(rootShare[:], hop.AmpRootShare)
			var setID [32]byte
			copy(setID[:], hop.AmpSetID)

			routeHop.AMP = record.NewAMP(
				rootShare, setID,
				uint32(hop.AmpChildIndex.Int32),
			)
		}

		// Add blinding point if present (only for introduction node
		// in blinded route).
		if len(hop.BlindingPoint) > 0 {
			pubKey, err := btcec.ParsePubKey(hop.BlindingPoint)
			if err != nil {
				return nil, fmt.Errorf("failed to parse "+
					"blinding point: %w", err)
			}
			routeHop.BlindingPoint = pubKey
		}

		// Add encrypted data if present (for all blinded hops).
		if len(hop.EncryptedData) > 0 {
			routeHop.EncryptedData = hop.EncryptedData
		}

		// Add total amount if present (only for final hop in blinded
		// route).
		if hop.BlindedPathTotalAmt.Valid {
			routeHop.TotalAmtMsat = lnwire.MilliSatoshi(
				hop.BlindedPathTotalAmt.Int64,
			)
		}

		// Add hop-level custom records.
		if records, ok := hopCustomRecords[hop.ID]; ok {
			routeHop.CustomRecords = make(
				record.CustomSet,
			)
			for _, rec := range records {
				routeHop.CustomRecords[uint64(rec.Key)] =
					rec.Value
			}
		}

		// Add metadata if present.
		if len(hop.MetaData) > 0 {
			routeHop.Metadata = hop.MetaData
		}

		routeHops[i] = routeHop
	}

	// Parse the source node public key.
	var sourceNode route.Vertex
	copy(sourceNode[:], sourceKey)

	route := &route.Route{
		TotalTimeLock:             uint32(totalTimeLock),
		TotalAmount:               lnwire.MilliSatoshi(totalAmount),
		SourcePubKey:              sourceNode,
		Hops:                      routeHops,
		FirstHopWireCustomRecords: firstHopWireCustomRecords,
	}

	// Set the first hop amount if it is set.
	if firstHopAmountMsat != 0 {
		route.FirstHopAmount = tlv.NewRecordT[tlv.TlvType0](
			tlv.NewBigSizeT(lnwire.MilliSatoshi(
				firstHopAmountMsat,
			)),
		)
	}

	return route, nil
}
