package migration1

import (
	"bytes"
	"context"
	"database/sql"
	"fmt"
	"reflect"
	"sort"
	"time"

	"github.com/davecgh/go-spew/spew"
	"github.com/lightningnetwork/lnd/kvdb"
	"github.com/lightningnetwork/lnd/lntypes"
	"github.com/lightningnetwork/lnd/payments/db/migration1/lnwire"
	"github.com/lightningnetwork/lnd/payments/db/migration1/record"
	"github.com/lightningnetwork/lnd/payments/db/migration1/sqlc"
	"github.com/pmezard/go-difflib/difflib"
)

// migratedPaymentRef is a reference to a migrated payment.
type migratedPaymentRef struct {
	Hash      lntypes.Hash
	PaymentID int64
}

// validateMigratedPaymentBatch performs a structural validation pass by
// comparing key fields (hash, amount, fail reason, HTLC count) of KV payments
// with their SQL counterparts. If a structural mismatch is detected, a full
// deep comparison is performed to produce a detailed diff for debugging.
func validateMigratedPaymentBatch(ctx context.Context,
	kvBackend kvdb.Backend, sqlDB SQLMigrationQueries,
	cfg *SQLStoreConfig, batch []migratedPaymentRef) error {

	if len(batch) == 0 {
		return nil
	}

	if cfg == nil || cfg.QueryCfg == nil {
		return fmt.Errorf("missing SQL store config for validation")
	}

	paymentIDs := make([]int64, 0, len(batch))
	for _, item := range batch {
		paymentIDs = append(paymentIDs, item.PaymentID)
	}

	rows, err := sqlDB.FetchPaymentsByIDsMig(ctx, paymentIDs)
	if err != nil {
		return fmt.Errorf("fetch SQL payments: %w", err)
	}
	if len(rows) != len(paymentIDs) {
		return fmt.Errorf("SQL payment batch mismatch: got=%d want=%d",
			len(rows), len(paymentIDs))
	}

	// Perform the structural check by comparing key fields from the KV
	// store with the SQL store.
	err = kvBackend.View(func(kvTx kvdb.RTx) error {
		paymentsBucket := kvTx.ReadBucket(paymentsRootBucket)
		if paymentsBucket == nil {
			return fmt.Errorf("no payments bucket")
		}

		for _, row := range rows {
			hash := row.PaymentIdentifier
			var paymentHash lntypes.Hash
			copy(paymentHash[:], hash)

			paymentBucket := paymentsBucket.NestedReadBucket(hash)
			if paymentBucket == nil {
				return fmt.Errorf("missing payment bucket %x",
					hash[:8])
			}

			kvPayment, err := fetchPayment(paymentBucket)
			if err != nil {
				return fmt.Errorf("fetch KV payment %x: %w",
					hash[:8], err)
			}

			if kvPayment.Status == StatusInFlight {
				// Mirror the migration's legacy terminalization
				// before comparing KV with SQL.
				//
				//nolint:ll
				_, err = terminalizeUnresolvedLegacyZeroAttempts(
					kvPayment,
				)
				if err != nil {
					return fmt.Errorf("normalize KV "+
						"payment %x: %w", hash[:8], err)
				}
			}

			err = structuralCompare(kvPayment, row)
			if err != nil {
				// On structural mismatch, perform a deep
				// comparison to produce a detailed diff.
				deepErr := deepComparePayment(
					ctx, cfg, sqlDB, row.ID,
					paymentHash, kvPayment,
				)
				if deepErr != nil {
					return deepErr
				}

				// If deep comparison passes but structural
				// failed, report the structural error as it
				// indicates an unexpected inconsistency.
				return err
			}

			err = compareDuplicatePayments(
				ctx, paymentBucket, sqlDB, row.ID,
				paymentHash,
			)
			if err != nil {
				return err
			}
		}

		return nil
	}, func() {})
	if err != nil {
		return err
	}

	return nil
}

// structuralCompare performs a fast structural comparison between a KV payment
// and a SQL payment row, checking key fields: payment identifier, amount,
// failure reason, and HTLC attempt count.
func structuralCompare(kvPayment *MPPayment,
	sqlRow sqlc.FetchPaymentsByIDsMigRow) error {

	// Compare payment identifier.
	kvHash := kvPayment.Info.PaymentIdentifier[:]
	if !bytes.Equal(kvHash, sqlRow.PaymentIdentifier) {
		return fmt.Errorf("payment identifier mismatch: kv=%x sql=%x",
			kvHash[:8], sqlRow.PaymentIdentifier[:8])
	}

	// Compare amount.
	kvAmount := int64(kvPayment.Info.Value)
	if kvAmount != sqlRow.AmountMsat {
		return fmt.Errorf("amount mismatch for %x: kv=%d sql=%d",
			sqlRow.PaymentIdentifier[:8], kvAmount,
			sqlRow.AmountMsat)
	}

	// Compare failure reason.
	var kvFailReason sql.NullInt32
	if kvPayment.FailureReason != nil {
		kvFailReason = sql.NullInt32{
			Int32: int32(*kvPayment.FailureReason),
			Valid: true,
		}
	}
	if kvFailReason != sqlRow.FailReason {
		return fmt.Errorf("fail reason mismatch for %x: kv=%v sql=%v",
			sqlRow.PaymentIdentifier[:8], kvFailReason,
			sqlRow.FailReason)
	}

	// Compare HTLC attempt count.
	kvHTLCCount := int64(len(kvPayment.HTLCs))
	if kvHTLCCount != sqlRow.HtlcAttemptCount {
		return fmt.Errorf("HTLC count mismatch for %x: kv=%d sql=%d",
			sqlRow.PaymentIdentifier[:8], kvHTLCCount,
			sqlRow.HtlcAttemptCount)
	}

	return nil
}

// deepComparePayment performs a full deep comparison between a KV payment and
// its SQL counterpart, producing a detailed diff on mismatch.
func deepComparePayment(ctx context.Context, cfg *SQLStoreConfig,
	sqlDB SQLQueries, paymentID int64, paymentHash lntypes.Hash,
	kvPayment *MPPayment) error {

	batchData, err := batchLoadPaymentDetailsData(
		ctx, cfg.QueryCfg, sqlDB, []int64{paymentID},
	)
	if err != nil {
		return fmt.Errorf("load payment data for deep compare %x: %w",
			paymentHash[:8], err)
	}

	byIDRows, err := sqlDB.FetchPaymentsByIDs(ctx, []int64{paymentID})
	if err != nil {
		return fmt.Errorf("fetch payment by ID for deep compare "+
			"%x: %w", paymentHash[:8], err)
	}
	if len(byIDRows) != 1 {
		return fmt.Errorf("expected 1 payment for deep compare, "+
			"got %d", len(byIDRows))
	}

	sqlPayment, err := buildPaymentFromBatchData(byIDRows[0], batchData)
	if err != nil {
		return fmt.Errorf("build SQL payment %x: %w",
			paymentHash[:8], err)
	}

	if kvPayment.Status == StatusInFlight {
		// Mirror the migration's legacy terminalization before
		// comparing KV with SQL.
		_, err = terminalizeUnresolvedLegacyZeroAttempts(kvPayment)
		if err != nil {
			return fmt.Errorf("normalize KV payment %x: %w",
				paymentHash[:8], err)
		}
	}
	normalizeLegacyZeroAttemptIDsForCompare(kvPayment, sqlPayment)
	normalizePaymentForCompare(kvPayment)
	normalizePaymentForCompare(sqlPayment)

	if !reflect.DeepEqual(kvPayment, sqlPayment) {
		dumpCfg := spew.ConfigState{
			DisablePointerAddresses: true,
			DisableCapacities:       true,
			DisableMethods:          true,
			SortKeys:                true,
		}
		diff := difflib.UnifiedDiff{
			A: difflib.SplitLines(
				dumpCfg.Sdump(kvPayment),
			),
			B: difflib.SplitLines(
				dumpCfg.Sdump(sqlPayment),
			),
			FromFile: "kv",
			ToFile:   "sql",
			Context:  3,
		}
		diffText, _ := difflib.GetUnifiedDiffString(diff)

		return fmt.Errorf("payment mismatch %x\n%s",
			paymentHash[:8], diffText)
	}

	return nil
}

// normalizeLegacyZeroAttemptIDsForCompare aligns expected legacy attempt ID
// remaps before deep comparison.
//
// Legacy KV payments can use attempt ID zero to represent an unknown ID. During
// migration those attempts are assigned synthetic SQL attempt indexes, while
// the source KV payment is left unchanged. Match by session key and copy the
// SQL attempt ID into the KV object so fallback deep comparison still reports
// real data mismatches instead of this expected migration repair.
func normalizeLegacyZeroAttemptIDsForCompare(kvPayment,
	sqlPayment *MPPayment) {

	if kvPayment == nil || sqlPayment == nil {
		return
	}

	sqlAttemptsBySessionKey := make(map[string]uint64)
	for i := range sqlPayment.HTLCs {
		htlc := &sqlPayment.HTLCs[i]
		sessionKey := htlc.SessionKey()
		if sessionKey == nil {
			// Leave normalization incomplete so the deep comparison
			// reports the malformed attempt instead of panicking.
			continue
		}

		sessionKeyBytes := sessionKey.Serialize()
		sessionKeyStr := string(sessionKeyBytes)
		sqlAttemptsBySessionKey[sessionKeyStr] = htlc.AttemptID
	}

	for i := range kvPayment.HTLCs {
		htlc := &kvPayment.HTLCs[i]
		if htlc.AttemptID != 0 {
			continue
		}

		sessionKey := htlc.SessionKey()
		if sessionKey == nil {
			// Leave normalization incomplete so the deep comparison
			// reports the malformed attempt instead of panicking.
			continue
		}

		sessionKeyBytes := sessionKey.Serialize()
		sessionKeyStr := string(sessionKeyBytes)
		attemptID, ok := sqlAttemptsBySessionKey[sessionKeyStr]
		if !ok {
			continue
		}

		htlc.AttemptID = attemptID
	}
}

// normalizePaymentForCompare normalizes fields that are expected to differ
// between KV and SQL representations before deep comparison.
func normalizePaymentForCompare(payment *MPPayment) {
	if payment == nil {
		return
	}

	// SequenceNum will not be equal because the kv db can have already
	// payments deleted during its lifetime.
	payment.SequenceNum = 0

	// We normalize timestamps before deep-comparing KV vs SQL objects.
	//
	// - **Microseconds**: SQL backends typically persist timestamps at
	//   microsecond precision (e.g. Postgres), while KV (Go `time.Time`)
	//   can contain nanoseconds. Truncating avoids false mismatches caused
	//   solely by differing storage precision.
	//
	// - **Local timezone**: when reading from SQL, timestamps are typically
	//   materialized in the local timezone by the SQL layer (and/or
	//   converters). Converting both sides to `time.Local` ensures the
	//   comparison is consistent across KV and SQL representations.
	trunc := func(t time.Time) time.Time {
		if t.IsZero() {
			return t
		}

		return time.Unix(0, t.UnixNano()).
			In(time.Local).
			Truncate(time.Microsecond)
	}

	// Normalize PaymentCreationInfo fields.
	if payment.Info != nil {
		payment.Info.CreationTime = trunc(
			payment.Info.CreationTime,
		)
		if len(payment.Info.PaymentRequest) == 0 {
			payment.Info.PaymentRequest = []byte{}
		}
		if len(payment.Info.FirstHopCustomRecords) == 0 {
			payment.Info.FirstHopCustomRecords = lnwire.
				CustomRecords{}
		}
	}

	// Normalize HTLCAttemptInfo so nil is converted to an empty slice.
	if len(payment.HTLCs) == 0 {
		payment.HTLCs = []HTLCAttempt{}
	}

	// Normalize HTLC attempt ordering; SQL/KV may return attempts
	// in different orders.
	sort.SliceStable(payment.HTLCs, func(i, j int) bool {
		return payment.HTLCs[i].AttemptID < payment.HTLCs[j].AttemptID
	})

	// Normalize HTLCAttemptInfo fields.
	for i := range payment.HTLCs {
		htlc := &payment.HTLCs[i]

		htlc.AttemptTime = trunc(htlc.AttemptTime)
		if htlc.Settle != nil {
			htlc.Settle.SettleTime = trunc(
				htlc.Settle.SettleTime,
			)
		}
		if htlc.Failure != nil {
			htlc.Failure.FailTime = trunc(
				htlc.Failure.FailTime,
			)
		}

		// Clear cached fields not persisted in storage.
		htlc.onionBlob = [1366]byte{}
		htlc.circuit = nil
		htlc.cachedSessionKey = nil

		// For legacy payments, the HTLC Hash field may be nil in the
		// bbolt backend. During migration, the SQL code uses the
		// parent payment hash as fallback. To ensure the comparison
		// between bbolt and SQL data succeeds, we apply the same
		// fallback here.
		//
		// See also: patchLegacyPaymentHash in payment_lifecycle.go.
		if htlc.Hash == nil && payment.Info != nil {
			htlc.Hash = &payment.Info.PaymentIdentifier
		}

		if len(htlc.Route.FirstHopWireCustomRecords) == 0 {
			htlc.Route.FirstHopWireCustomRecords =
				lnwire.CustomRecords{}
		}

		for j := range htlc.Route.Hops {
			hop := htlc.Route.Hops[j]
			if len(hop.CustomRecords) == 0 {
				hop.CustomRecords =
					record.CustomSet{}
			}

			// The migration treats nil and empty encrypted data as
			// absent, so it omits the blinded child row. SQL reads
			// the hop back with nil encrypted data and a zero
			// total. Apply the same transformation to the KV
			// copy before comparing the payments. A blinding point
			// without data is rejected during migration, so it
			// cannot reach this comparison.
			if len(hop.EncryptedData) == 0 &&
				hop.BlindingPoint == nil {

				hop.EncryptedData = nil
				hop.TotalAmtMsat = 0
			}

			// LegacyPayload was a hint used by the KV store to
			// determine how to serialize and deserialize the hop
			// payload (i.e. whether to use the legacy format or
			// TLV). The SQL store does not serialize hop data at
			// all — each field is stored natively in its own
			// column — so this flag has no meaning there and is
			// never persisted.
			hop.LegacyPayload = false
		}
	}
}

// duplicateRecord is a record that represents a duplicate payment.
type duplicateRecord struct {
	AmountMsat     int64
	CreatedAt      time.Time
	FailReason     sql.NullInt32
	SettlePreimage []byte
	SettleTime     sql.NullTime
}

// compareDuplicatePayments validates migrated duplicate rows against KV data.
func compareDuplicatePayments(ctx context.Context, paymentBucket kvdb.RBucket,
	sqlDB SQLQueries, paymentID int64, hash lntypes.Hash) error {

	// Fetch the duplicate payments from the KV store.
	kvDuplicates, err := fetchDuplicateRecords(paymentBucket)
	if err != nil {
		return fmt.Errorf("fetch KV duplicates %x: %w",
			hash[:8], err)
	}

	// Fetch the duplicate payments from the SQL store.
	sqlDuplicates, err := sqlDB.FetchPaymentDuplicates(ctx, paymentID)
	if err != nil {
		return fmt.Errorf("fetch SQL duplicates %x: %w",
			hash[:8], err)
	}

	if len(kvDuplicates) != len(sqlDuplicates) {
		return fmt.Errorf("duplicate count mismatch %x: kv=%d "+
			"sql=%d", hash[:8], len(kvDuplicates),
			len(sqlDuplicates))
	}

	kvNormalized := normalizeDuplicateRecords(kvDuplicates)
	sqlNormalized := normalizeDuplicateRecords(
		dbDuplicatesToDuplicateRecords(sqlDuplicates),
	)

	sortDuplicates(kvNormalized)
	sortDuplicates(sqlNormalized)

	if !reflect.DeepEqual(kvNormalized, sqlNormalized) {
		dumpCfg := spew.ConfigState{
			DisablePointerAddresses: true,
			DisableCapacities:       true,
			DisableMethods:          true,
			SortKeys:                true,
		}
		diff := difflib.UnifiedDiff{
			A: difflib.SplitLines(
				dumpCfg.Sdump(kvNormalized),
			),
			B: difflib.SplitLines(
				dumpCfg.Sdump(sqlNormalized),
			),
			FromFile: "kv",
			ToFile:   "sql",
			Context:  3,
		}
		diffText, _ := difflib.GetUnifiedDiffString(diff)

		return fmt.Errorf("duplicate mismatch %x\n%s",
			hash[:8], diffText)
	}

	return nil
}

// fetchDuplicateRecords reads duplicate payment records from the KV bucket.
func fetchDuplicateRecords(paymentBucket kvdb.RBucket) ([]duplicateRecord,
	error) {

	dupBucket := paymentBucket.NestedReadBucket(duplicatePaymentsBucket)
	if dupBucket == nil {
		return nil, nil
	}

	var duplicates []duplicateRecord
	err := dupBucket.ForEach(func(seqBytes, _ []byte) error {
		if len(seqBytes) != 8 {
			return nil
		}

		subBucket := dupBucket.NestedReadBucket(seqBytes)
		if subBucket == nil {
			return nil
		}

		creationData := subBucket.Get(duplicatePaymentCreationInfoKey)
		if creationData == nil {
			return fmt.Errorf("missing duplicate creation info")
		}

		creationInfo, err := deserializeDuplicatePaymentCreationInfo(
			bytes.NewReader(creationData),
		)
		if err != nil {
			return fmt.Errorf("deserialize duplicate creation "+
				"info: %w", err)
		}

		settleData := subBucket.Get(duplicatePaymentSettleInfoKey)
		failReasonData := subBucket.Get(duplicatePaymentFailInfoKey)

		if settleData != nil && len(failReasonData) > 0 {
			return fmt.Errorf("duplicate has both settle and " +
				"fail info")
		}

		var (
			failReason     sql.NullInt32
			settlePreimage []byte
			settleTime     sql.NullTime
		)

		switch {
		case settleData != nil:
			settlePreimage, settleTime, err =
				parseDuplicateSettleData(settleData)
			if err != nil {
				return err
			}
		case len(failReasonData) > 0:
			failReason = sql.NullInt32{
				Int32: int32(failReasonData[0]),
				Valid: true,
			}
		default:
			// If the duplicate has no settle or fail info, it is
			// considered failed. Every duplicate payment must have
			// either a settle or fail info in the sql database. So
			// we set the fail reason to error to mimic the behavior
			// for the kv store.
			failReason = sql.NullInt32{
				Int32: int32(FailureReasonError),
				Valid: true,
			}
		}

		duplicates = append(duplicates, duplicateRecord{
			AmountMsat: int64(creationInfo.Value),
			CreatedAt: normalizeTimeForSQL(
				creationInfo.CreationTime,
			),
			FailReason:     failReason,
			SettlePreimage: settlePreimage,
			SettleTime:     settleTime,
		})

		return nil
	})
	if err != nil {
		return nil, err
	}

	return duplicates, nil
}

// dbDuplicatesToDuplicateRecords maps SQL duplicate rows into comparable
// duplicate records.
func dbDuplicatesToDuplicateRecords(
	rows []sqlc.PaymentDuplicate) []duplicateRecord {

	duplicates := make([]duplicateRecord, 0, len(rows))
	for _, row := range rows {
		duplicates = append(duplicates, duplicateRecord{
			AmountMsat:     row.AmountMsat,
			CreatedAt:      row.CreatedAt,
			FailReason:     row.FailReason,
			SettlePreimage: row.SettlePreimage,
			SettleTime:     row.SettleTime,
		})
	}

	return duplicates
}

// normalizeDuplicateRecords normalizes time precision and empty fields.
func normalizeDuplicateRecords(records []duplicateRecord) []duplicateRecord {
	if len(records) == 0 {
		return []duplicateRecord{}
	}

	trunc := func(t time.Time) time.Time {
		if t.IsZero() {
			return t
		}

		return t.In(time.Local).Truncate(time.Microsecond)
	}

	for i := range records {
		records[i].CreatedAt = trunc(records[i].CreatedAt)
		if records[i].SettleTime.Valid {
			records[i].SettleTime.Time = trunc(
				records[i].SettleTime.Time,
			)
		}

		if len(records[i].SettlePreimage) == 0 {
			records[i].SettlePreimage = []byte{}
		}
	}

	return records
}

// sortDuplicates orders records deterministically for deep comparison.
func sortDuplicates(records []duplicateRecord) {
	sort.SliceStable(records, func(i, j int) bool {
		ai := records[i]
		aj := records[j]

		// Duplicates are "duplicates" because they share the same
		// payment identifier. So ordering can be stable using
		// timestamp + amount.
		if !ai.CreatedAt.Equal(aj.CreatedAt) {
			return ai.CreatedAt.Before(aj.CreatedAt)
		}

		return ai.AmountMsat < aj.AmountMsat
	})
}

// validatePaymentCounts compares the number of migrated payments with the SQL
// payment count to catch missing rows.
func validatePaymentCounts(ctx context.Context, sqlDB SQLQueries,
	expectedCount int64) error {

	sqlCount, err := sqlDB.CountPayments(ctx)
	if err != nil {
		return fmt.Errorf("count SQL payments: %w", err)
	}
	if expectedCount != sqlCount {
		return fmt.Errorf("payment count mismatch: kv=%d sql=%d",
			expectedCount, sqlCount)
	}

	return nil
}
