package dcs

import (
	"context"
	"io"
	"net"

	"github.com/go-faster/errors"
	"go.uber.org/multierr"

	"github.com/gotd/td/internal/crypto"
	"github.com/gotd/td/internal/mtproxy"
	"github.com/gotd/td/internal/mtproxy/obfuscator"
	"github.com/gotd/td/internal/proto/codec"
	"github.com/gotd/td/transport"
)

var _ Resolver = mtProxy{}

type mtProxy struct {
	dial          DialFunc
	protocol      protocol
	addr, network string

	secret mtproxy.Secret
	tag    [4]byte
	rand   io.Reader
}

func (m mtProxy) Primary(ctx context.Context, dc int, _ List) (transport.Conn, error) {
	return m.resolve(ctx, dc)
}

func (m mtProxy) MediaOnly(ctx context.Context, dc int, _ List) (transport.Conn, error) {
	if dc > 0 {
		dc *= -1
	}
	return m.resolve(ctx, dc)
}

func (m mtProxy) CDN(ctx context.Context, dc int, _ List) (transport.Conn, error) {
	return m.resolve(ctx, dc)
}

func (m mtProxy) resolve(ctx context.Context, dc int) (transport.Conn, error) {
	c, err := m.dial(ctx, m.network, m.addr)
	if err != nil {
		return nil, errors.Wrapf(err, "connect to the MTProxy %q", m.addr)
	}

	conn, err := m.handshakeConn(c, dc)
	if err != nil {
		err = errors.Wrap(err, "handshake")
		return nil, multierr.Combine(err, c.Close())
	}

	return conn, nil
}

// handshakeConn inits given net.Conn as MTProto connection.
func (m mtProxy) handshakeConn(c net.Conn, dc int) (transport.Conn, error) {
	var obsConn *obfuscator.Conn
	switch m.secret.Type {
	case mtproxy.Simple, mtproxy.Secured:
		obsConn = obfuscator.Obfuscated2(m.rand, c)
	case mtproxy.TLS:
		obsConn = obfuscator.FakeTLS(m.rand, c)
	default:
		return nil, errors.Errorf("unknown MTProxy secret type: %d", m.secret.Type)
	}

	secret := m.secret
	if err := obsConn.Handshake(m.tag, dc, secret); err != nil {
		return nil, errors.Wrap(err, "MTProxy handshake")
	}

	transportConn, err := m.protocol.Handshake(obsConn)
	if err != nil {
		return nil, errors.Wrap(err, "transport handshake")
	}

	return transportConn, nil
}

// MTProxyOptions is MTProxy resolver creation options.
type MTProxyOptions struct {
	// Dial specifies the dial function for creating unencrypted TCP connections.
	// If Dial is nil, then the resolver dials using package net.
	Dial DialFunc
	// Network to use. Defaults to "tcp"
	Network string
	// Random source for MTProxy obfuscator.
	Rand io.Reader
}

func (m *MTProxyOptions) setDefaults() {
	if m.Dial == nil {
		var d net.Dialer
		m.Dial = d.DialContext
	}
	if m.Network == "" {
		m.Network = "tcp"
	}
	if m.Rand == nil {
		m.Rand = crypto.DefaultRand()
	}
}

// MTProxy creates MTProxy obfuscated DC resolver.
//
// See https://core.telegram.org/mtproto/mtproto-transports#transport-obfuscation.
func MTProxy(addr string, secret []byte, opts MTProxyOptions) (Resolver, error) {
	s, err := mtproxy.ParseSecret(secret)
	if err != nil {
		return nil, err
	}

	var cdc codec.Codec = codec.PaddedIntermediate{}
	tag := codec.PaddedIntermediateClientStart

	// FIXME(tdakkota): some proxies forces to use Padded (Secure) Intermediate
	// 	even if secret denotes to use another transport type.
	if s.Type != mtproxy.TLS {
		if c, ok := s.ExpectedCodec(); ok {
			cdc = c
			tag = [4]byte{s.Tag, s.Tag, s.Tag, s.Tag}
		}
	}

	opts.setDefaults()
	return mtProxy{
		dial:     opts.Dial,
		addr:     addr,
		network:  opts.Network,
		protocol: transport.NewProtocol(func() transport.Codec { return codec.NoHeader{Codec: cdc} }),
		secret:   s,
		tag:      tag,
		rand:     opts.Rand,
	}, nil
}
