package walletconnect

import (
	"crypto/ecdsa"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"math/big"
	"strconv"
	"strings"
	"time"

	"github.com/ethereum/go-ethereum/common/hexutil"
	"github.com/ethereum/go-ethereum/log"
	signercore "github.com/ethereum/go-ethereum/signer/core/apitypes"

	"github.com/status-im/status-go/eth-node/types"
	"github.com/status-im/status-go/multiaccounts/accounts"
	"github.com/status-im/status-go/params"
	"github.com/status-im/status-go/services/typeddata"
	"github.com/status-im/status-go/services/wallet/walletevent"
)

const (
	SupportedEip155Namespace = "eip155"

	ProposeUserPairEvent = walletevent.EventType("WalletConnectProposeUserPair")
)

var (
	ErrorInvalidSessionProposal = errors.New("invalid session proposal")
	ErrorNamespaceNotSupported  = errors.New("namespace not supported")
	ErrorChainsNotSupported     = errors.New("chains not supported")
	ErrorInvalidParamsCount     = errors.New("invalid params count")
	ErrorInvalidAddressMsgIndex = errors.New("invalid address and/or msg index (must be 0 or 1)")
	ErrorMethodNotSupported     = errors.New("method not supported")
)

type Topic string

type Namespace struct {
	Methods  []string `json:"methods"`
	Chains   []string `json:"chains"` // CAIP-2 format e.g. ["eip155:1"]
	Events   []string `json:"events"`
	Accounts []string `json:"accounts,omitempty"` // CAIP-10 format e.g. ["eip155:1:0x453...228"]
}

type Metadata struct {
	Description string   `json:"description"`
	URL         string   `json:"url"`
	Icons       []string `json:"icons"`
	Name        string   `json:"name"`
	VerifyURL   string   `json:"verifyUrl"`
}

type Proposer struct {
	PublicKey string   `json:"publicKey"`
	Metadata  Metadata `json:"metadata"`
}

type Verified struct {
	VerifyURL  string `json:"verifyUrl"`
	Validation string `json:"validation"`
	Origin     string `json:"origin"`
	IsScam     bool   `json:"isScam,omitempty"`
}

type VerifyContext struct {
	Verified Verified `json:"verified"`
}

// Params has RequiredNamespaces entries if part of "proposal namespace" and Namespaces entries if part of "session namespace"
// see https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#controller-side-validation-of-incoming-proposal-namespaces-wallet
type Params struct {
	ID                 int64                `json:"id"`
	PairingTopic       Topic                `json:"pairingTopic"`
	Expiry             int64                `json:"expiry"`
	RequiredNamespaces map[string]Namespace `json:"requiredNamespaces"`
	OptionalNamespaces map[string]Namespace `json:"optionalNamespaces"`
	Proposer           Proposer             `json:"proposer"`
	Verify             VerifyContext        `json:"verifyContext"`
}

type SessionProposal struct {
	ID     int64  `json:"id"`
	Params Params `json:"params"`
}

type PairSessionResponse struct {
	SessionProposal     SessionProposal      `json:"sessionProposal"`
	SupportedNamespaces map[string]Namespace `json:"supportedNamespaces"`
}

type RequestParams struct {
	Request struct {
		Method string            `json:"method"`
		Params []json.RawMessage `json:"params"`
	} `json:"request"`
	ChainID string `json:"chainId"`
}

type SessionRequest struct {
	ID     int64         `json:"id"`
	Topic  Topic         `json:"topic"`
	Params RequestParams `json:"params"`
	Verify VerifyContext `json:"verifyContext"`
}

type SessionDelete struct {
	ID    int64 `json:"id"`
	Topic Topic `json:"topic"`
}

type Session struct {
	Acknowledged       bool                 `json:"acknowledged"`
	Controller         string               `json:"controller"`
	Expiry             int64                `json:"expiry"`
	Namespaces         map[string]Namespace `json:"namespaces"`
	OptionalNamespaces map[string]Namespace `json:"optionalNamespaces"`
	PairingTopic       Topic                `json:"pairingTopic"`
	Peer               Proposer             `json:"peer"`
	Relay              json.RawMessage      `json:"relay"`
	RequiredNamespaces map[string]Namespace `json:"requiredNamespaces"`
	Self               Proposer             `json:"self"`
	Topic              Topic                `json:"topic"`
}

// Valid namespace
func (n *Namespace) Valid(namespaceName string, chainID *uint64) bool {
	if chainID == nil {
		if len(n.Chains) == 0 {
			log.Warn("namespace doesn't refer to any chain")
			return false
		}
		for _, caip2Str := range n.Chains {
			resolvedNamespaceName, _, err := parseCaip2ChainID(caip2Str)
			if err != nil {
				log.Warn("namespace chain not in caip2 format", "chain", caip2Str, "error", err)
				return false
			}

			if resolvedNamespaceName != namespaceName {
				log.Warn("namespace name doesn't match", "namespace", namespaceName, "chain", caip2Str)
				return false
			}
		}
	}
	return true
}

// ValidateForProposal validates params part of the Proposal Namespace
func (p *Params) ValidateForProposal() bool {
	for key, ns := range p.RequiredNamespaces {
		var chainID *uint64
		if strings.Contains(key, ":") {
			resolvedNamespaceName, cID, err := parseCaip2ChainID(key)
			if err != nil {
				log.Warn("params validation failed CAIP-2", "str", key, "error", err)
				return false
			}
			key = resolvedNamespaceName
			chainID = &cID
		}

		if !isValidNamespaceName(key) {
			log.Warn("invalid namespace name", "namespace", key)
			return false
		}

		if !ns.Valid(key, chainID) {
			return false
		}
	}

	return true
}

// ValidateProposal validates params part of the Proposal Namespace
// https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#controller-side-validation-of-incoming-proposal-namespaces-wallet
func (p *SessionProposal) ValidateProposal() bool {
	return p.Params.ValidateForProposal()
}

// AddSession adds a new active session to the database
func AddSession(db *sql.DB, networks []params.Network, session_json string) error {
	var session Session
	err := json.Unmarshal([]byte(session_json), &session)
	if err != nil {
		return fmt.Errorf("unmarshal session: %v", err)
	}

	chains := supportedChainsInSession(session)
	testChains, err := areTestChains(networks, chains)
	if err != nil {
		return fmt.Errorf("areTestChains: %v", err)
	}

	rowEntry := DBSession{
		Topic:            session.Topic,
		Disconnected:     false,
		SessionJSON:      session_json,
		Expiry:           session.Expiry,
		CreatedTimestamp: time.Now().Unix(),
		PairingTopic:     session.PairingTopic,
		TestChains:       testChains,
		DBDApp: DBDApp{
			URL:  session.Peer.Metadata.URL,
			Name: session.Peer.Metadata.Name,
		},
	}
	if len(session.Peer.Metadata.Icons) > 0 {
		rowEntry.IconURL = session.Peer.Metadata.Icons[0]
	}

	return UpsertSession(db, rowEntry)
}

// areTestChains assumes chains to tests are all testnets or all mainnets
func areTestChains(networks []params.Network, chainIDs []uint64) (isTest bool, err error) {
	for _, n := range networks {
		for _, chainID := range chainIDs {
			if n.ChainID == chainID {
				return n.IsTest, nil
			}
		}
	}

	return false, fmt.Errorf("no network found for chainIDs %v", chainIDs)
}

func supportedChainsInSession(session Session) []uint64 {
	caipChains := session.Namespaces[SupportedEip155Namespace].Chains
	chains := make([]uint64, 0, len(caipChains))
	for _, caip2Str := range caipChains {
		_, chainID, err := parseCaip2ChainID(caip2Str)
		if err != nil {
			log.Warn("Failed parsing CAIP-2", "str", caip2Str, "error", err)
			continue
		}

		chains = append(chains, chainID)
	}
	return chains
}

func caip10Accounts(accounts []*accounts.Account, chains []uint64) []string {
	addresses := make([]string, 0, len(accounts)*len(chains))
	for _, acc := range accounts {
		for _, chainID := range chains {
			addresses = append(addresses, fmt.Sprintf("%s:%s:%s", SupportedEip155Namespace, strconv.FormatUint(chainID, 10), acc.Address.Hex()))
		}
	}
	return addresses
}

func SafeSignTypedDataForDApps(typedJson string, privateKey *ecdsa.PrivateKey, chainID uint64, legacy bool) (types.HexBytes, error) {
	// Parse the data for both legacy and non-legacy cases to validate the chain
	var typed typeddata.TypedData
	err := json.Unmarshal([]byte(typedJson), &typed)
	if err != nil {
		return types.HexBytes{}, err
	}

	chain := new(big.Int).SetUint64(chainID)

	var sig hexutil.Bytes
	if legacy {
		sig, err = typeddata.Sign(typed, privateKey, chain)
	} else {
		// Validate chainID if part of the typed data
		if _, exist := typed.Domain[typeddata.ChainIDKey]; exist {
			if err := typed.ValidateChainID(chain); err != nil {
				return types.HexBytes{}, err
			}
		}

		var typedV4 signercore.TypedData
		err = json.Unmarshal([]byte(typedJson), &typedV4)
		if err != nil {
			return types.HexBytes{}, err
		}

		sig, err = typeddata.SignTypedDataV4(typedV4, privateKey, chain)
	}
	if err != nil {
		return types.HexBytes{}, err
	}

	return types.HexBytes(sig), err
}