package withdraw

import (
	"bytes"
	"context"
	"database/sql"

	"github.com/btcsuite/btcd/btcutil"
	"github.com/btcsuite/btcd/chaincfg/chainhash"
	"github.com/btcsuite/btcd/wire"
	"github.com/lightninglabs/loop/loopdb"
	"github.com/lightninglabs/loop/loopdb/sqlc"
	"github.com/lightninglabs/loop/staticaddr/deposit"
	"github.com/lightningnetwork/lnd/clock"
)

type Querier interface {
	// CreateWithdrawal inserts a new withdrawal.
	CreateWithdrawal(ctx context.Context,
		arg sqlc.CreateWithdrawalParams) error

	// UpdateWithdrawal updates a withdrawal with confirmation parameters.
	UpdateWithdrawal(ctx context.Context,
		arg sqlc.UpdateWithdrawalParams) error

	// GetWithdrawalIDByDepositID retrieves the withdrawal ID associated
	// with a given deposit ID.
	GetWithdrawalIDByDepositID(ctx context.Context, depositID []byte) (
		[]byte, error)

	// CreateWithdrawalDeposit links withdrawal to deposits.
	CreateWithdrawalDeposit(ctx context.Context,
		arg sqlc.CreateWithdrawalDepositParams) error

	// GetWithdrawalDeposits retrieves the deposit IDs associated with a
	// withdrawal.
	GetWithdrawalDeposits(ctx context.Context, withdrawalID []byte) (
		[][]byte, error)

	// GetAllWithdrawals retrieves all withdrawals from the database.
	GetAllWithdrawals(ctx context.Context) ([]sqlc.Withdrawal, error)
}

// BaseDB is the interface that contains all the queries generated by sqlc for
// the static_address_swaps table and transaction functionality.
type BaseDB interface {
	Querier

	// ExecTx allows for executing a function in the context of a database
	// transaction.
	ExecTx(ctx context.Context, txOptions loopdb.TxOptions,
		txBody func(Querier) error) error
}

// SqlStore is the backing store for static address withdrawals.
type SqlStore struct {
	baseDB       BaseDB
	depositStore deposit.Store
	clock        clock.Clock
}

// NewSqlStore constructs a new SQLStore from a BaseDB. The BaseDB is agnostic
// to the underlying driver which can be postgres or sqlite.
func NewSqlStore(db BaseDB, depositStore deposit.Store) *SqlStore {
	return &SqlStore{
		baseDB:       db,
		depositStore: depositStore,
		clock:        clock.NewDefaultClock(),
	}
}

// CreateWithdrawal creates a static address withdrawal record in the database.
func (s *SqlStore) CreateWithdrawal(ctx context.Context,
	deposits []*deposit.Deposit) error {

	id, err := GetRandomWithdrawalID()
	if err != nil {
		return err
	}

	var totalAmount btcutil.Amount
	for _, deposit := range deposits {
		totalAmount += deposit.Value
	}

	createArgs := sqlc.CreateWithdrawalParams{
		WithdrawalID:       id[:],
		TotalDepositAmount: int64(totalAmount),
		InitiationTime:     s.clock.Now().UTC(),
	}
	return s.baseDB.ExecTx(ctx, &loopdb.SqliteTxOptions{},
		func(q Querier) error {
			err := q.CreateWithdrawal(ctx, createArgs)
			if err != nil {
				return err
			}

			for _, deposit := range deposits {
				err = q.CreateWithdrawalDeposit(
					ctx, sqlc.CreateWithdrawalDepositParams{
						WithdrawalID: id[:],
						DepositID:    deposit.ID[:],
					})
				if err != nil {
					return err
				}
			}

			return nil
		})
}

// UpdateWithdrawal updates a withdrawal record with the transaction
// information, including the withdrawn amount, change amount, and
// confirmation height. It is expected that the withdrawal has already been
// created with CreateWithdrawal, and that the deposits slice contains the
// deposits associated with the withdrawal.
func (s *SqlStore) UpdateWithdrawal(ctx context.Context,
	deposits []*deposit.Deposit, tx *wire.MsgTx, confirmationHeight uint32,
	changePkScript []byte) error {

	// Populate the optional change amount.
	withdrawnAmount, changeAmount := int64(0), int64(0)
	if len(tx.TxOut) == 1 {
		withdrawnAmount = tx.TxOut[0].Value
	} else if len(tx.TxOut) == 2 {
		withdrawnAmount, changeAmount = tx.TxOut[0].Value, tx.TxOut[1].Value
		if bytes.Equal(changePkScript, tx.TxOut[0].PkScript) {
			changeAmount = tx.TxOut[0].Value
			withdrawnAmount = tx.TxOut[1].Value
		}
	}

	updateArgs := sqlc.UpdateWithdrawalParams{
		WithdrawalTxID: sql.NullString{
			String: tx.TxHash().String(),
			Valid:  true,
		},
		WithdrawnAmount: sql.NullInt64{
			Int64: withdrawnAmount,
			Valid: withdrawnAmount > 0,
		},
		ChangeAmount: sql.NullInt64{
			Int64: changeAmount,
			Valid: changeAmount > 0,
		},
		ConfirmationHeight: sql.NullInt64{
			Int64: int64(confirmationHeight),
			Valid: confirmationHeight > 0,
		},
	}

	return s.baseDB.ExecTx(ctx, &loopdb.SqliteTxOptions{},
		func(q Querier) error {
			withdrawalID, err := q.GetWithdrawalIDByDepositID(
				ctx, deposits[0].ID[:],
			)
			if err != nil {
				return err
			}

			updateArgs.WithdrawalID = withdrawalID
			err = q.UpdateWithdrawal(ctx, updateArgs)
			if err != nil {
				return err
			}

			return nil
		})
}

// GetAllWithdrawals retrieves all static address withdrawals from the
// database. It returns a slice of Withdrawal structs, each containing a list
// of associated deposits.
func (s *SqlStore) GetAllWithdrawals(ctx context.Context) ([]Withdrawal,
	error) {

	withdrawals, err := s.baseDB.GetAllWithdrawals(ctx)
	if err != nil {
		return nil, err
	}

	result := make([]Withdrawal, 0, len(withdrawals))
	for _, w := range withdrawals {
		depositIDs, err := s.baseDB.GetWithdrawalDeposits(ctx,
			w.WithdrawalID)

		if err != nil {
			return nil, err
		}

		deposits := make([]*deposit.Deposit, 0, len(depositIDs))
		for _, dID := range depositIDs {
			deposit, err := s.depositStore.GetDeposit(
				ctx, deposit.ID(dID),
			)
			if err != nil {
				return nil, err
			}
			deposits = append(deposits, deposit)
		}

		txID, err := chainhash.NewHashFromStr(w.WithdrawalTxID.String)
		if err != nil {
			return nil, err
		}

		result = append(result, Withdrawal{
			ID:                 ID(w.WithdrawalID),
			TxID:               *txID,
			Deposits:           deposits,
			TotalDepositAmount: btcutil.Amount(w.TotalDepositAmount),
			WithdrawnAmount:    btcutil.Amount(w.WithdrawnAmount.Int64),
			ChangeAmount:       btcutil.Amount(w.ChangeAmount.Int64),
			InitiationTime:     w.InitiationTime,
			ConfirmationHeight: w.ConfirmationHeight.Int64,
		})
	}

	return result, nil
}
