package server

import (
	"bytes"
	"compress/zlib"
	"crypto/sha256"
	"encoding/base64"
	"encoding/binary"
	"encoding/hex"
	"errors"
	"fmt"
	"net/http"
	"strings"

	"github.com/gorilla/rpc"
	"github.com/lbryio/herald.go/db"
	"github.com/lbryio/herald.go/internal"
	"github.com/lbryio/lbcd/chaincfg"
	"github.com/lbryio/lbcd/chaincfg/chainhash"
	"github.com/lbryio/lbcd/txscript"
	"github.com/lbryio/lbcd/wire"
	"github.com/lbryio/lbcutil"
	"golang.org/x/exp/constraints"
)

type BlockchainCodec struct {
	rpc.Codec
}

func (c *BlockchainCodec) NewRequest(r *http.Request) rpc.CodecRequest {
	return &BlockchainCodecRequest{c.Codec.NewRequest(r)}
}

// BlockchainCodecRequest provides ability to rewrite the incoming
// request "method" field. For example:
//     blockchain.block.get_header -> blockchain_block.Get_header
//     blockchain.address.listunspent -> blockchain_address.Listunspent
// This makes the "method" string compatible with Gorilla/RPC
// requirements.
type BlockchainCodecRequest struct {
	rpc.CodecRequest
}

func (cr *BlockchainCodecRequest) Method() (string, error) {
	rawMethod, err := cr.CodecRequest.Method()
	if err != nil {
		return rawMethod, err
	}
	parts := strings.Split(rawMethod, ".")
	if len(parts) < 2 {
		return rawMethod, fmt.Errorf("blockchain rpc: service/method ill-formed: %q", rawMethod)
	}
	service := strings.Join(parts[0:len(parts)-1], "_")
	method := parts[len(parts)-1]
	if len(method) < 1 {
		return rawMethod, fmt.Errorf("blockchain rpc: method ill-formed: %q", method)
	}
	method = strings.ToUpper(string(method[0])) + string(method[1:])
	return service + "." + method, err
}

// BlockchainService methods handle "blockchain.block.*" RPCs
type BlockchainService struct {
	DB    *db.ReadOnlyDBColumnFamily
	Chain *chaincfg.Params
}

// BlockchainAddressService methods handle "blockchain.address.*" RPCs
type BlockchainAddressService struct {
	BlockchainService
}

// BlockchainScripthashService methods handle "blockchain.scripthash.*" RPCs
type BlockchainScripthashService struct {
	BlockchainService
}

const CHUNK_SIZE = 96
const MAX_CHUNK_SIZE = 40960
const HEADER_SIZE = wire.MaxBlockHeaderPayload
const HASHX_LEN = 11

func min[Ord constraints.Ordered](x, y Ord) Ord {
	if x < y {
		return x
	}
	return y
}

type BlockGetServerHeightReq struct{}
type BlockGetServerHeightResp uint32

func (s *BlockchainService) Get_server_height(r *http.Request, req *BlockGetServerHeightReq, resp **BlockGetServerHeightResp) error {
	if s.DB == nil || s.DB.LastState == nil {
		return fmt.Errorf("unknown height")
	}
	result := BlockGetServerHeightResp(s.DB.LastState.Height)
	*resp = &result
	return nil
}

type BlockGetChunkReq uint32
type BlockGetChunkResp string

// 'blockchain.block.get_chunk'
func (s *BlockchainService) Get_chunk(r *http.Request, req *BlockGetChunkReq, resp **BlockGetChunkResp) error {
	index := uint32(*req)
	db_headers, err := s.DB.GetHeaders(index*CHUNK_SIZE, CHUNK_SIZE)
	if err != nil {
		return err
	}
	raw := make([]byte, 0, HEADER_SIZE*len(db_headers))
	for _, h := range db_headers {
		raw = append(raw, h[:]...)
	}
	headers := BlockGetChunkResp(hex.EncodeToString(raw))
	*resp = &headers
	return err
}

type BlockGetHeaderReq uint32
type BlockGetHeaderResp struct {
	Version       uint32 `json:"version"`
	PrevBlockHash string `json:"prev_block_hash"`
	MerkleRoot    string `json:"merkle_root"`
	ClaimTrieRoot string `json:"claim_trie_root"`
	Timestamp     uint32 `json:"timestamp"`
	Bits          uint32 `json:"bits"`
	Nonce         uint32 `json:"nonce"`
	BlockHeight   uint32 `json:"block_height"`
}

// 'blockchain.block.get_header'
func (s *BlockchainService) Get_header(r *http.Request, req *BlockGetHeaderReq, resp **BlockGetHeaderResp) error {
	height := uint32(*req)
	headers, err := s.DB.GetHeaders(height, 1)
	if err != nil {
		return err
	}
	if len(headers) < 1 {
		return errors.New("not found")
	}
	decode := func(header *[HEADER_SIZE]byte, height uint32) *BlockGetHeaderResp {
		var h1, h2, h3 chainhash.Hash
		h1.SetBytes(header[4:36])
		h2.SetBytes(header[36:68])
		h3.SetBytes(header[68:100])
		return &BlockGetHeaderResp{
			Version:       binary.LittleEndian.Uint32(header[0:]),
			PrevBlockHash: h1.String(),
			MerkleRoot:    h2.String(),
			ClaimTrieRoot: h3.String(),
			Timestamp:     binary.LittleEndian.Uint32(header[100:]),
			Bits:          binary.LittleEndian.Uint32(header[104:]),
			Nonce:         binary.LittleEndian.Uint32(header[108:]),
			BlockHeight:   height,
		}
	}
	*resp = decode(&headers[0], height)
	return err
}

type BlockHeadersReq struct {
	StartHeight uint32 `json:"start_height"`
	Count       uint32 `json:"count"`
	CpHeight    uint32 `json:"cp_height"`
	B64         bool   `json:"b64"`
}

type BlockHeadersResp struct {
	Base64 string `json:"base64,omitempty"`
	Hex    string `json:"hex,omitempty"`
	Count  uint32 `json:"count"`
	Max    uint32 `json:"max"`
	Branch string `json:"branch,omitempty"`
	Root   string `json:"root,omitempty"`
}

// 'blockchain.block.headers'
func (s *BlockchainService) Headers(r *http.Request, req *BlockHeadersReq, resp **BlockHeadersResp) error {
	count := min(req.Count, MAX_CHUNK_SIZE)
	db_headers, err := s.DB.GetHeaders(req.StartHeight, count)
	if err != nil {
		return err
	}
	count = uint32(len(db_headers))
	raw := make([]byte, 0, HEADER_SIZE*count)
	for _, h := range db_headers {
		raw = append(raw, h[:]...)
	}
	result := &BlockHeadersResp{
		Count: count,
		Max:   MAX_CHUNK_SIZE,
	}
	if req.B64 {
		zipped := bytes.Buffer{}
		w := zlib.NewWriter(&zipped)
		w.Write(raw)
		w.Close()
		result.Base64 = base64.StdEncoding.EncodeToString(zipped.Bytes())
	} else {
		result.Hex = hex.EncodeToString(raw)
	}
	if count > 0 && req.CpHeight > 0 {
		// TODO
		//last_height := height + count - 1
	}
	*resp = result
	return err
}

func decodeScriptHash(scripthash string) ([]byte, error) {
	sh, err := hex.DecodeString(scripthash)
	if err != nil {
		return nil, err
	}
	if len(sh) != chainhash.HashSize {
		return nil, fmt.Errorf("invalid scripthash: %v (length %v)", scripthash, len(sh))
	}
	internal.ReverseBytesInPlace(sh)
	return sh, nil
}

func hashX(scripthash []byte) []byte {
	return scripthash[:HASHX_LEN]
}

func hashXScript(script []byte, coin *chaincfg.Params) []byte {
	if _, err := txscript.ExtractClaimScript(script); err == nil {
		baseScript := txscript.StripClaimScriptPrefix(script)
		if class, addrs, _, err := txscript.ExtractPkScriptAddrs(baseScript, coin); err == nil {
			switch class {
			case txscript.PubKeyHashTy, txscript.ScriptHashTy, txscript.PubKeyTy:
				script, _ := txscript.PayToAddrScript(addrs[0])
				return hashXScript(script, coin)
			}
		}
	}
	sum := sha256.Sum256(script)
	return sum[:HASHX_LEN]
}

type AddressGetBalanceReq struct {
	Address string `json:"address"`
}
type AddressGetBalanceResp struct {
	Confirmed   uint64 `json:"confirmed"`
	Unconfirmed uint64 `json:"unconfirmed"`
}

// 'blockchain.address.get_balance'
func (s *BlockchainAddressService) Get_balance(r *http.Request, req *AddressGetBalanceReq, resp **AddressGetBalanceResp) error {
	address, err := lbcutil.DecodeAddress(req.Address, s.Chain)
	if err != nil {
		return err
	}
	script, err := txscript.PayToAddrScript(address)
	if err != nil {
		return err
	}
	hashX := hashXScript(script, s.Chain)
	confirmed, unconfirmed, err := s.DB.GetBalance(hashX)
	if err != nil {
		return err
	}
	*resp = &AddressGetBalanceResp{confirmed, unconfirmed}
	return err
}

type scripthashGetBalanceReq struct {
	ScriptHash string `json:"scripthash"`
}
type ScripthashGetBalanceResp struct {
	Confirmed   uint64 `json:"confirmed"`
	Unconfirmed uint64 `json:"unconfirmed"`
}

// 'blockchain.scripthash.get_balance'
func (s *BlockchainScripthashService) Get_balance(r *http.Request, req *scripthashGetBalanceReq, resp **ScripthashGetBalanceResp) error {
	scripthash, err := decodeScriptHash(req.ScriptHash)
	if err != nil {
		return err
	}
	hashX := hashX(scripthash)
	confirmed, unconfirmed, err := s.DB.GetBalance(hashX)
	if err != nil {
		return err
	}
	*resp = &ScripthashGetBalanceResp{confirmed, unconfirmed}
	return err
}

type AddressGetHistoryReq struct {
	Address string `json:"address"`
}
type TxInfo struct {
	TxHash string `json:"tx_hash"`
	Height uint32 `json:"height"`
}
type TxInfoFee struct {
	TxInfo
	Fee uint64 `json:"fee"`
}
type AddressGetHistoryResp struct {
	Confirmed   []TxInfo    `json:"confirmed"`
	Unconfirmed []TxInfoFee `json:"unconfirmed"`
}

// 'blockchain.address.get_history'
func (s *BlockchainAddressService) Get_history(r *http.Request, req *AddressGetHistoryReq, resp **AddressGetHistoryResp) error {
	address, err := lbcutil.DecodeAddress(req.Address, s.Chain)
	if err != nil {
		return err
	}
	script, err := txscript.PayToAddrScript(address)
	if err != nil {
		return err
	}
	hashX := hashXScript(script, s.Chain)
	dbTXs, err := s.DB.GetHistory(hashX)
	if err != nil {
		return err
	}
	confirmed := make([]TxInfo, 0, len(dbTXs))
	for _, tx := range dbTXs {
		confirmed = append(confirmed,
			TxInfo{
				TxHash: tx.TxHash.String(),
				Height: tx.Height,
			})
	}
	result := &AddressGetHistoryResp{
		Confirmed:   confirmed,
		Unconfirmed: []TxInfoFee{}, // TODO
	}
	*resp = result
	return err
}

type ScripthashGetHistoryReq struct {
	ScriptHash string `json:"scripthash"`
}
type ScripthashGetHistoryResp struct {
	Confirmed   []TxInfo    `json:"confirmed"`
	Unconfirmed []TxInfoFee `json:"unconfirmed"`
}

// 'blockchain.scripthash.get_history'
func (s *BlockchainScripthashService) Get_history(r *http.Request, req *ScripthashGetHistoryReq, resp **ScripthashGetHistoryResp) error {
	scripthash, err := decodeScriptHash(req.ScriptHash)
	if err != nil {
		return err
	}
	hashX := hashX(scripthash)
	dbTXs, err := s.DB.GetHistory(hashX)
	if err != nil {
		return err
	}
	confirmed := make([]TxInfo, 0, len(dbTXs))
	for _, tx := range dbTXs {
		confirmed = append(confirmed,
			TxInfo{
				TxHash: tx.TxHash.String(),
				Height: tx.Height,
			})
	}
	result := &ScripthashGetHistoryResp{
		Confirmed:   confirmed,
		Unconfirmed: []TxInfoFee{}, // TODO
	}
	*resp = result
	return err
}

type AddressGetMempoolReq struct {
	Address string `json:"address"`
}
type AddressGetMempoolResp []TxInfoFee

// 'blockchain.address.get_mempool'
func (s *BlockchainAddressService) Get_mempool(r *http.Request, req *AddressGetMempoolReq, resp **AddressGetMempoolResp) error {
	address, err := lbcutil.DecodeAddress(req.Address, s.Chain)
	if err != nil {
		return err
	}
	script, err := txscript.PayToAddrScript(address)
	if err != nil {
		return err
	}
	hashX := hashXScript(script, s.Chain)
	// TODO...
	internal.ReverseBytesInPlace(hashX)
	unconfirmed := make([]TxInfoFee, 0, 100)
	result := AddressGetMempoolResp(unconfirmed)
	*resp = &result
	return err
}

type ScripthashGetMempoolReq struct {
	ScriptHash string `json:"scripthash"`
}
type ScripthashGetMempoolResp []TxInfoFee

// 'blockchain.scripthash.get_mempool'
func (s *BlockchainScripthashService) Get_mempool(r *http.Request, req *ScripthashGetMempoolReq, resp **ScripthashGetMempoolResp) error {
	scripthash, err := decodeScriptHash(req.ScriptHash)
	if err != nil {
		return err
	}
	hashX := hashX(scripthash)
	// TODO...
	internal.ReverseBytesInPlace(hashX)
	unconfirmed := make([]TxInfoFee, 0, 100)
	result := ScripthashGetMempoolResp(unconfirmed)
	*resp = &result
	return err
}

type AddressListUnspentReq struct {
	Address string `json:"address"`
}
type TXOInfo struct {
	TxHash string `json:"tx_hash"`
	TxPos  uint16 `json:"tx_pos"`
	Height uint32 `json:"height"`
	Value  uint64 `json:"value"`
}
type AddressListUnspentResp []TXOInfo

// 'blockchain.address.listunspent'
func (s *BlockchainAddressService) Listunspent(r *http.Request, req *AddressListUnspentReq, resp **AddressListUnspentResp) error {
	address, err := lbcutil.DecodeAddress(req.Address, s.Chain)
	if err != nil {
		return err
	}
	script, err := txscript.PayToAddrScript(address)
	if err != nil {
		return err
	}
	hashX := hashXScript(script, s.Chain)
	dbTXOs, err := s.DB.GetUnspent(hashX)
	unspent := make([]TXOInfo, 0, len(dbTXOs))
	for _, txo := range dbTXOs {
		unspent = append(unspent,
			TXOInfo{
				TxHash: txo.TxHash.String(),
				TxPos:  txo.TxPos,
				Height: txo.Height,
				Value:  txo.Value,
			})
	}
	result := AddressListUnspentResp(unspent)
	*resp = &result
	return err
}

type ScripthashListUnspentReq struct {
	ScriptHash string `json:"scripthash"`
}
type ScripthashListUnspentResp []TXOInfo

// 'blockchain.scripthash.listunspent'
func (s *BlockchainScripthashService) Listunspent(r *http.Request, req *ScripthashListUnspentReq, resp **ScripthashListUnspentResp) error {
	scripthash, err := decodeScriptHash(req.ScriptHash)
	if err != nil {
		return err
	}
	hashX := hashX(scripthash)
	dbTXOs, err := s.DB.GetUnspent(hashX)
	unspent := make([]TXOInfo, 0, len(dbTXOs))
	for _, txo := range dbTXOs {
		unspent = append(unspent,
			TXOInfo{
				TxHash: txo.TxHash.String(),
				TxPos:  txo.TxPos,
				Height: txo.Height,
				Value:  txo.Value,
			})
	}
	result := ScripthashListUnspentResp(unspent)
	*resp = &result
	return err
}
