package libwallet

import (
	"io/ioutil"
	"net/http"
	"net/url"
	"strconv"
	"strings"

	"github.com/btcsuite/btcd/txscript"
	"github.com/btcsuite/btcutil"
	"github.com/golang/protobuf/proto"
	"github.com/pkg/errors"
)

type AddressVersion int

const (
	addressV1            AddressVersion = 1
	addressV2            AddressVersion = 2
	addressV3            AddressVersion = 3
	addressSubmarineSwap AddressVersion = 101
)

type muunAddress struct {
	version        AddressVersion
	derivationPath string
	address        string
	redeemScript   []byte
}

func newMuunAddress(version AddressVersion, userPublicKey, muunPublicKey *HDPublicKey) (MuunAddress, error) {
	if userPublicKey.Path != muunPublicKey.Path {
		return nil, errors.Errorf("paths must match for address generation (%v != %v)", userPublicKey.Path, muunPublicKey.Path)
	}

	switch version {
	case addressV1:
		return CreateAddressV1(userPublicKey)
	case addressV2:
		return CreateAddressV2(userPublicKey, muunPublicKey)
	case addressV3:
		return CreateAddressV3(userPublicKey, muunPublicKey)
	case addressSubmarineSwap:
		return CreateAddressSubmarineSwap(userPublicKey)
	}

	return nil, errors.Errorf("unknown version %v", version)
}

func (a *muunAddress) Version() int {
	return int(a.version)
}

func (a *muunAddress) DerivationPath() string {
	return a.derivationPath
}

func (a *muunAddress) Address() string {
	return a.address
}

func (a *muunAddress) RedeemScript() []byte {
	return a.redeemScript
}

// MuunPaymentURI is muun's uri struct
type MuunPaymentURI struct {
	Address      string
	Label        string
	Message      string
	Amount       string
	URI          string
	BIP70Url     string
	CreationTime string
	ExpiresTime  string
	Invoice      *Invoice
}

const (
	bitcoinScheme = "bitcoin:"
	muunScheme    = "muun:"
)

// GetPaymentURI builds a MuunPaymentURI from an address and a network
func GetPaymentURI(address string, network *Network) (*MuunPaymentURI, error) {

	uriAddress := normalizeAddress(address)

	components, err := url.Parse(uriAddress)
	if err != nil {
		return nil, err
	}

	if components.Scheme != "bitcoin" {
		return nil, errors.New("Invalid scheme")
	}

	base58Address := components.Opaque

	// When URIs are bitcoin:// the address comes in host
	// this happens in iOS that mostly ignores bitcoin: format
	if len(base58Address) == 0 {
		base58Address = components.Host
	}

	queryValues, err := url.ParseQuery(components.RawQuery)
	if err != nil {
		return nil, errors.Wrapf(err, "Couldnt parse query")
	}

	var label, message, amount string

	if len(queryValues["label"]) != 0 {
		label = queryValues["label"][0]
	}

	if len(queryValues["message"]) != 0 {
		message = queryValues["message"][0]
	}

	if len(queryValues["amount"]) != 0 {
		amount = queryValues["amount"][0]
	}

	if len(queryValues["lightning"]) != 0 {
		invoice, err := ParseInvoice(queryValues["lightning"][0], network)

		if err == nil {
			return &MuunPaymentURI{Invoice:invoice}, nil
		}
	}

	//BIP70 check
	if len(queryValues["r"]) != 0 {
		if len(base58Address) > 0 {
			return &MuunPaymentURI{
				Address:  base58Address,
				Label:    label,
				Message:  message,
				Amount:   amount,
				URI:      uriAddress,
				BIP70Url: queryValues["r"][0],
			}, nil
		}
		return &MuunPaymentURI{
			Label:    label,
			Message:  message,
			Amount:   amount,
			URI:      uriAddress,
			BIP70Url: queryValues["r"][0],
		}, nil
	}

	// Bech32 check
	validatedBase58Address, err := btcutil.DecodeAddress(base58Address, network.network)
	if err != nil {
		return nil, err
	}

	if !validatedBase58Address.IsForNet(network.network) {
		return nil, errors.Errorf("Network mismatch")
	}

	return &MuunPaymentURI{
		Address: validatedBase58Address.String(),
		Label:   label,
		Message: message,
		Amount:  amount,
		URI:     uriAddress,
	}, nil

}

// DoPaymentRequestCall builds a MuunPaymentUri from a url and a network. Handling BIP70 to 72
func DoPaymentRequestCall(url string, network *Network) (*MuunPaymentURI, error) {
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return nil, errors.Wrapf(err, "Failed to create request to: %s", url)
	}

	req.Header.Set("Accept", "application/bitcoin-paymentrequest")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, errors.Wrapf(err, "Failed to make request to: %s", url)
	}
	defer resp.Body.Close()

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, errors.Wrapf(err, "Failed to read body response")
	}

	payReq := &PaymentRequest{}
	err = proto.Unmarshal(body, payReq)
	if err != nil {
		return nil, errors.Wrapf(err, "Failed to Unmarshall paymentRequest")
	}

	payDetails := &PaymentDetails{}

	err = proto.Unmarshal(payReq.SerializedPaymentDetails, payDetails)
	if err != nil {
		return nil, errors.Wrapf(err, "Failed to Unmarshall paymentDetails")
	}

	if len(payDetails.Outputs) == 0 {
		return nil, errors.New("No outputs provided")
	}

	address, err := getAddressFromScript(payDetails.Outputs[0].Script, network)
	if err != nil {
		errors.Wrapf(err, "Failed to get address")
	}

	return &MuunPaymentURI{
		Address:      address,
		Message:      *payDetails.Memo,
		Amount:       strconv.FormatUint(*payDetails.Outputs[0].Amount, 10),
		BIP70Url:     url,
		CreationTime: strconv.FormatUint(*payDetails.Time, 10),
		ExpiresTime:  strconv.FormatUint(*payDetails.Expires, 10),
	}, nil
}

func getAddressFromScript(script []byte, network *Network) (string, error) {
	pkScript, err := txscript.ParsePkScript(script)
	if err != nil {
		return "", err
	}
	address, err := pkScript.Address(network.network)
	if err != nil {
		return "", err
	}
	return address.String(), nil
}

func normalizeAddress(rawAddress string) string {
	newAddress := rawAddress

	if strings.Contains(newAddress, muunScheme) {
		newAddress = strings.Replace(newAddress, muunScheme, bitcoinScheme, 1)
	}

	if !strings.Contains(newAddress, bitcoinScheme) {
		newAddress = bitcoinScheme + rawAddress
	}

	return newAddress
}
