package lightning

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"sync"
	"time"
)

// FakeNode is a Lightning node that can be told to misbehave.
//
// Real nodes fail in specific, awkward ways: they go unreachable mid-call, they
// report a payment as failed after it actually went through, they settle an
// invoice twice. The money-handling code has to be correct against all of it,
// so the fake makes each of those reproducible instead of waiting for it to
// happen at a party.
type FakeNode struct {
	mu sync.Mutex

	balanceMsat int64
	invoices    map[string]*fakeInvoice
	payments    []Payment

	// Failure switches.
	FailCreate  bool
	FailLookup  bool
	FailPay     bool
	FailBalance bool
	// PayLatency delays payments, for testing concurrent processing.
	PayLatency time.Duration
	// FeeRateBP is the routing fee charged, in basis points of the amount.
	// Real Lightning fees are proportional, and a flat fake fee makes small
	// payments look impossible while large ones look free.
	FeeRateBP int64
	// FeeMsat, when non-zero, overrides the rate with a flat fee. Used to
	// test the fee cap.
	FeeMsat int64
}

// maxRateBP mirrors Limits.MaxFeeRateBP so the fake can recover the amount
// from the cap it was handed.
const maxRateBP = 100

type fakeInvoice struct {
	hash    string
	amount  int64
	settled bool
	paidAt  time.Time
}

func NewFakeNode(balanceMsat int64) *FakeNode {
	return &FakeNode{
		balanceMsat: balanceMsat,
		invoices:    make(map[string]*fakeInvoice),
		FeeRateBP:   10, // 0.1%, a realistic routing fee
	}
}

func (f *FakeNode) CreateInvoice(ctx context.Context, amountMsat int64, memo string) (Invoice, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	if f.FailCreate {
		return Invoice{}, fmt.Errorf("fake: node refused to create an invoice")
	}
	var raw [16]byte
	if _, err := rand.Read(raw[:]); err != nil {
		return Invoice{}, err
	}
	hash := hex.EncodeToString(raw[:])
	f.invoices[hash] = &fakeInvoice{hash: hash, amount: amountMsat}
	return Invoice{
		PaymentHash: hash,
		Bolt11:      "lnbcrt" + hash,
		AmountMsat:  amountMsat,
		ExpiresAt:   time.Now().Add(time.Hour),
	}, nil
}

func (f *FakeNode) LookupInvoice(ctx context.Context, paymentHash string) (bool, int64, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	if f.FailLookup {
		return false, 0, fmt.Errorf("fake: node unreachable")
	}
	inv, ok := f.invoices[paymentHash]
	if !ok {
		return false, 0, fmt.Errorf("fake: unknown invoice")
	}
	return inv.settled, inv.amount, nil
}

// feeFor computes what this node would charge to route an amount.
func (f *FakeNode) feeFor(amountMsat int64) int64 {
	if f.FeeMsat > 0 {
		return f.FeeMsat
	}
	return amountMsat * f.FeeRateBP / 10000
}

func (f *FakeNode) PayInvoice(ctx context.Context, bolt11 string, maxFeeMsat int64) (Payment, error) {
	if f.PayLatency > 0 {
		select {
		case <-time.After(f.PayLatency):
		case <-ctx.Done():
			return Payment{}, ctx.Err()
		}
	}
	f.mu.Lock()
	defer f.mu.Unlock()
	if f.FailPay {
		return Payment{}, fmt.Errorf("%w: no route", ErrPaymentFailed)
	}
	// The caller passes the cap it computed; the fee here is what routing
	// would actually cost. A real node refuses when the cap is too tight.
	fee := f.feeFor(maxFeeMsat * 10000 / maxRateBP)
	if f.FeeMsat > 0 {
		fee = f.FeeMsat
	}
	if fee > maxFeeMsat {
		return Payment{}, fmt.Errorf("%w: fee %d exceeds cap %d",
			ErrPaymentFailed, fee, maxFeeMsat)
	}
	var raw [16]byte
	if _, err := rand.Read(raw[:]); err != nil {
		return Payment{}, err
	}
	p := Payment{
		PaymentHash: hex.EncodeToString(raw[:]),
		Preimage:    hex.EncodeToString(raw[:]),
		FeeMsat:     fee,
	}
	f.payments = append(f.payments, p)
	return p, nil
}

func (f *FakeNode) Balance(ctx context.Context) (int64, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	if f.FailBalance {
		return 0, fmt.Errorf("fake: node unreachable")
	}
	return f.balanceMsat, nil
}

// --- test controls ---

// MarkPaid simulates someone paying an invoice.
func (f *FakeNode) MarkPaid(paymentHash string) {
	f.mu.Lock()
	defer f.mu.Unlock()
	if inv, ok := f.invoices[paymentHash]; ok {
		inv.settled = true
		inv.paidAt = time.Now()
		f.balanceMsat += inv.amount
	}
}

// SetBalance overrides the node's reported balance, for solvency tests.
func (f *FakeNode) SetBalance(msat int64) {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.balanceMsat = msat
}

// PaymentCount reports how many outbound payments were actually sent, which is
// how a test detects a double-spend that the ledger alone would not reveal.
func (f *FakeNode) PaymentCount() int {
	f.mu.Lock()
	defer f.mu.Unlock()
	return len(f.payments)
}
