package mtproto

import (
	"io"
	"time"

	"go.opentelemetry.io/otel/trace"
	"go.uber.org/zap"

	"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
	"go.mau.fi/mautrix-telegram/pkg/gotd/clock"
	"go.mau.fi/mautrix-telegram/pkg/gotd/crypto"
	"go.mau.fi/mautrix-telegram/pkg/gotd/exchange"
	"go.mau.fi/mautrix-telegram/pkg/gotd/proto"
	"go.mau.fi/mautrix-telegram/pkg/gotd/rpc"
	"go.mau.fi/mautrix-telegram/pkg/gotd/tmap"
)

// Options of Conn.
type Options struct {
	// DC is datacenter ID for key exchange.
	// Defaults to 2.
	DC int

	// PublicKeys of telegram.
	//
	// If not provided, embedded public keys will be used.
	PublicKeys []exchange.PublicKey

	// Random is random source. Defaults to crypto.
	Random io.Reader
	// Logger is instance of zap.Logger. No logs by default.
	Logger *zap.Logger
	// Handler will be called on received message.
	Handler Handler

	// AckBatchSize is maximum ack-s to buffer.
	AckBatchSize int
	// AckInterval is maximum time to buffer ack.
	AckInterval time.Duration

	// RetryInterval is duration between retries.
	RetryInterval time.Duration
	// MaxRetries is max retry count until rpc request failure.
	MaxRetries int

	// DialTimeout is timeout of creating connection.
	DialTimeout time.Duration
	// ExchangeTimeout is timeout of every key exchange request.
	ExchangeTimeout time.Duration
	// SaltFetchInterval is duration between get_future_salts request.
	SaltFetchInterval time.Duration
	// PingTimeout sets ping_delay_disconnect timeout.
	PingTimeout time.Duration
	// PingInterval is duration between ping_delay_disconnect request.
	PingInterval time.Duration
	// PingCallback is called after a ping is acknowledged by the server.
	PingCallback func()
	// RequestTimeout is function which returns request timeout for given type ID.
	RequestTimeout func(req uint32) time.Duration

	// CompressThreshold is a threshold in bytes to determine that message
	// is large enough to be compressed using GZIP.
	// If < 0, compression will be disabled.
	// If == 0, default value will be used.
	CompressThreshold int
	// MessageID is message id source. Share source between connection to
	// reduce collision probability.
	MessageID MessageIDSource
	// Clock is current time source. Defaults to system time.
	Clock clock.Clock
	// Types map, used in verbose logging of incoming message.
	Types *tmap.Map
	// Key that can be used to restore previous connection.
	Key crypto.AuthKey
	// Salt from server that can be used to restore previous connection.
	Salt int64

	// Tracer for OTEL.
	Tracer trace.Tracer

	// Private options.

	// Cipher defines message crypto.
	Cipher Cipher
	// engine for replacing RPC engine.
	engine *rpc.Engine

	// OnError is a function that is called if there is any error.
	OnError func(err error)
}

type nopHandler struct{}

func (nopHandler) OnMessage(b *bin.Buffer) error   { return nil }
func (nopHandler) OnSession(session Session) error { return nil }

func (opt *Options) setDefaultPublicKeys() {
	// Using public keys that are included with distribution if not
	// provided.
	//
	// This should never fail and keys should be valid for recent
	// library versions.
	opt.PublicKeys = vendoredKeys()
}

func (opt *Options) setDefaults() {
	if opt.DC == 0 {
		opt.DC = 2
	}
	if opt.Random == nil {
		opt.Random = crypto.DefaultRand()
	}
	if opt.Logger == nil {
		opt.Logger = zap.NewNop()
	}
	if opt.AckBatchSize == 0 {
		opt.AckBatchSize = 20
	}
	if opt.AckInterval == 0 {
		opt.AckInterval = 15 * time.Second
	}
	if opt.RetryInterval == 0 {
		opt.RetryInterval = 5 * time.Second
	}
	if opt.MaxRetries == 0 {
		opt.MaxRetries = 5
	}
	if opt.DialTimeout == 0 {
		opt.DialTimeout = 35 * time.Second
	}
	if opt.ExchangeTimeout == 0 {
		opt.ExchangeTimeout = exchange.DefaultTimeout
	}
	if opt.SaltFetchInterval == 0 {
		opt.SaltFetchInterval = 1 * time.Hour
	}
	if opt.PingTimeout == 0 {
		opt.PingTimeout = 15 * time.Second
	}
	if opt.PingInterval == 0 {
		opt.PingInterval = 1 * time.Minute
	}
	if opt.RequestTimeout == nil {
		opt.RequestTimeout = func(req uint32) time.Duration {
			return 15 * time.Second
		}
	}
	if opt.CompressThreshold == 0 {
		opt.CompressThreshold = 1024
	}
	if opt.Clock == nil {
		opt.Clock = clock.System
	}
	if opt.MessageID == nil {
		opt.MessageID = proto.NewMessageIDGen(opt.Clock.Now)
	}
	if len(opt.PublicKeys) == 0 {
		opt.setDefaultPublicKeys()
	}
	if opt.Handler == nil {
		opt.Handler = nopHandler{}
	}
	if opt.Cipher == nil {
		opt.Cipher = crypto.NewClientCipher(opt.Random)
	}
}
