package dcs

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"crypto/rsa"
	"crypto/sha256"
	"encoding/base64"
	"math/big"
	"net"
	"sort"
	"sync"

	"github.com/go-faster/errors"

	"github.com/gotd/td/bin"
	"github.com/gotd/td/internal/crypto"
	"github.com/gotd/td/tg"
)

var dnsKey struct {
	rsa.PublicKey
	once sync.Once
	eBig *big.Int
}

//nolint:gochecknoinits
func init() {
	dnsKey.once.Do(func() {
		k, err := crypto.ParseRSAPublicKeys([]byte(`-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAyr+18Rex2ohtVy8sroGPBwXD3DOoKCSpjDqYoXgCqB7ioln4eDCF
fOBUlfXUEvM/fnKCpF46VkAftlb4VuPDeQSS/ZxZYEGqHaywlroVnXHIjgqoxiAd
192xRGreuXIaUKmkwlM9JID9WS2jUsTpzQ91L8MEPLJ/4zrBwZua8W5fECwCCh2c
9G5IzzBm+otMS/YKwmR1olzRCyEkyAEjXWqBI9Ftv5eG8m0VkBzOG655WIYdyV0H
fDK/NWcvGqa0w/nriMD6mDjKOryamw0OP9QuYgMN0C9xMW9y8SmP4h92OAWodTYg
Y1hZCxdv6cs5UnW9+PWvS+WIbkh+GaWYxwIDAQAB
-----END RSA PUBLIC KEY-----`))
		if err != nil {
			panic(err)
		}

		dnsKey.PublicKey = *k[0]
		dnsKey.eBig = big.NewInt(int64(dnsKey.E))
	})
}

// parseDNSList decodes raw encrypted simple config.
//
// Notice that parseDNSList does not decode base64, user should do it manually.
func parseDNSList(input [256]byte) (tg.HelpConfigSimple, error) {
	// See https://github.com/tdlib/td/blob/master/td/telegram/ConfigManager.cpp#L148.
	x := new(big.Int).SetBytes(input[:])
	y := new(big.Int).Exp(x, dnsKey.eBig, dnsKey.N)

	dataRSA := make([]byte, 256)
	if !crypto.FillBytes(y, dataRSA) {
		return tg.HelpConfigSimple{}, errors.New("dataRSA has invalid size")
	}

	block, err := aes.NewCipher(dataRSA[:32])
	if err != nil {
		return tg.HelpConfigSimple{}, err
	}
	d := cipher.NewCBCDecrypter(block, dataRSA[16:32])
	dataCBC := dataRSA[32:]
	d.CryptBlocks(dataCBC, dataCBC)

	decrypted := dataCBC[:len(dataCBC)-16]
	decryptedHash := sha256.Sum256(decrypted)
	hash := dataCBC[len(dataCBC)-16:]

	if !bytes.Equal(decryptedHash[:16], hash) {
		return tg.HelpConfigSimple{}, errors.New("hash mismatch")
	}

	var cfg tg.HelpConfigSimple
	if err := cfg.Decode(&bin.Buffer{Buf: decrypted[4:]}); err != nil {
		return tg.HelpConfigSimple{}, err
	}
	return cfg, nil
}

type sortByLen []string

func (s sortByLen) Len() int {
	return len(s)
}

func (s sortByLen) Less(i, j int) bool {
	return len(s[i]) > len(s[j])
}

func (s sortByLen) Swap(i, j int) {
	s[i], s[j] = s[j], s[i]
}

// DNSConfig is DC connection config obtained from DNS.
type DNSConfig struct {
	// Date field of HelpConfigSimple.
	Date int
	// Expires field of HelpConfigSimple.
	Expires int
	// Rules field of HelpConfigSimple.
	Rules []tg.AccessPointRule
}

// Options returns DC options from this config.
func (d DNSConfig) Options() (r []tg.DCOption) {
	convertIP := func(ip int) string {
		return net.IPv4(
			byte(ip),
			byte(ip>>8),
			byte(ip>>16),
			byte(ip>>24),
		).String()
	}
	for _, rule := range d.Rules {
		for _, ip := range rule.IPs {
			switch ip := ip.(type) {
			case *tg.IPPort:
				r = append(r, tg.DCOption{
					ID:        rule.DCID,
					IPAddress: convertIP(ip.Ipv4),
					Port:      ip.Port,
				})
			case *tg.IPPortSecret:
				r = append(r, tg.DCOption{
					TCPObfuscatedOnly: true,
					ID:                rule.DCID,
					IPAddress:         convertIP(ip.Ipv4),
					Port:              ip.Port,
					Secret:            ip.Secret,
				})
			}
		}
	}
	return r
}

// ParseDNSConfig parses tg.HelpConfigSimple from TXT response.
func ParseDNSConfig(txt []string) (DNSConfig, error) {
	encoding := base64.StdEncoding
	const (
		decodedLen = 256
		encodedLen = 344
	)
	sort.Sort(sortByLen(txt))

	var totalLength int
	for i := range txt {
		totalLength += len(txt[i])
	}
	if totalLength != encodedLen {
		return DNSConfig{}, errors.Errorf("invalid input length %d", totalLength)
	}

	var (
		encoded [encodedLen]byte
		decoded [decodedLen]byte
	)
	n := 0
	for i := range txt {
		n += copy(encoded[n:], txt[i])
	}

	if _, err := encoding.Decode(decoded[:], encoded[:]); err != nil {
		return DNSConfig{}, errors.Wrap(err, "decode")
	}

	cfg, err := parseDNSList(decoded)
	if err != nil {
		return DNSConfig{}, errors.Wrap(err, "decrypt config")
	}

	return DNSConfig{
		Date:    cfg.Date,
		Expires: cfg.Expires,
		Rules:   cfg.Rules,
	}, nil
}
