// Package linkpearl represents the library itself
package linkpearl

import (
	"database/sql"

	lru "github.com/hashicorp/golang-lru/v2"
	"github.com/rs/zerolog"
	"go.mau.fi/util/dbutil"
	"maunium.net/go/mautrix"
	"maunium.net/go/mautrix/crypto"
	"maunium.net/go/mautrix/crypto/cryptohelper"
	"maunium.net/go/mautrix/event"
)

const (
	// DefaultMaxRetries for operations like autojoin
	DefaultMaxRetries = 10
	// DefaultAccountDataCache size
	DefaultAccountDataCache = 1000
)

// Linkpearl object
type Linkpearl struct {
	db  *sql.DB
	ch  *cryptohelper.CryptoHelper
	acc *lru.Cache[string, map[string]string]
	acr *Crypter
	log zerolog.Logger
	api *mautrix.Client

	joinPermit func(*event.Event) bool
	autoleave  bool
	maxretries int
}

type ReqPresence struct {
	Presence  event.Presence `json:"presence"`
	StatusMsg string         `json:"status_msg,omitempty"`
}

func setDefaults(cfg *Config) {
	if cfg.MaxRetries == 0 {
		cfg.MaxRetries = DefaultMaxRetries
	}
	if cfg.AccountDataCache == 0 {
		cfg.AccountDataCache = DefaultAccountDataCache
	}
	if cfg.JoinPermit == nil {
		// By default, we approve all join requests
		cfg.JoinPermit = func(*event.Event) bool { return true }
	}
}

func initCrypter(secret string) (*Crypter, error) {
	if secret == "" {
		return nil, nil
	}

	return NewCrypter(secret)
}

// New linkpearl
func New(cfg *Config) (*Linkpearl, error) {
	setDefaults(cfg)
	api, err := mautrix.NewClient(cfg.Homeserver, "", "")
	if err != nil {
		return nil, err
	}
	api.Log = cfg.Logger

	acc, _ := lru.New[string, map[string]string](cfg.AccountDataCache) //nolint:errcheck // addressed in setDefaults()
	acr, err := initCrypter(cfg.AccountDataSecret)
	if err != nil {
		return nil, err
	}

	lp := &Linkpearl{
		db:         cfg.DB,
		acc:        acc,
		acr:        acr,
		api:        api,
		log:        cfg.Logger,
		joinPermit: cfg.JoinPermit,
		autoleave:  cfg.AutoLeave,
		maxretries: cfg.MaxRetries,
	}

	db, err := dbutil.NewWithDB(cfg.DB, cfg.Dialect)
	if err != nil {
		return nil, err
	}
	db.Log = dbutil.ZeroLogger(cfg.Logger)
	lp.ch, err = cryptohelper.NewCryptoHelper(lp.api, []byte(cfg.Login), db)
	if err != nil {
		return nil, err
	}
	lp.ch.LoginAs = cfg.LoginAs()
	if err = lp.ch.Init(); err != nil {
		return nil, err
	}
	lp.api.Crypto = lp.ch
	return lp, nil
}

// GetClient returns underlying API client
func (l *Linkpearl) GetClient() *mautrix.Client {
	return l.api
}

// GetDB returns underlying DB object
func (l *Linkpearl) GetDB() *sql.DB {
	return l.db
}

// GetMachine returns underlying OLM machine
func (l *Linkpearl) GetMachine() *crypto.OlmMachine {
	return l.ch.Machine()
}

// GetAccountDataCrypter returns crypter used for account data (if any)
func (l *Linkpearl) GetAccountDataCrypter() *Crypter {
	return l.acr
}

// SetPresence (own). See https://spec.matrix.org/v1.3/client-server-api/#put_matrixclientv3presenceuseridstatus
func (l *Linkpearl) SetPresence(presence event.Presence, message string) error {
	req := ReqPresence{Presence: presence, StatusMsg: message}
	u := l.GetClient().BuildClientURL("v3", "presence", l.GetClient().UserID, "status")
	_, err := l.GetClient().MakeRequest("PUT", u, req, nil)

	return err
}

// SetJoinPermit sets the the join permit callback function
func (l *Linkpearl) SetJoinPermit(value func(*event.Event) bool) {
	l.joinPermit = value
}

// Start performs matrix /sync
func (l *Linkpearl) Start(optionalStatusMsg ...string) error {
	l.initSync()
	var statusMsg string
	if len(optionalStatusMsg) > 0 {
		statusMsg = optionalStatusMsg[0]
	}

	err := l.SetPresence(event.PresenceOnline, statusMsg)
	if err != nil {
		l.log.Error().Err(err).Msg("cannot set presence")
	}
	defer l.Stop()

	l.log.Info().Msg("client has been started")
	return l.api.Sync()
}

// Stop the client
func (l *Linkpearl) Stop() {
	l.log.Debug().Msg("stopping the client")
	if err := l.api.SetPresence(event.PresenceOffline); err != nil {
		l.log.Error().Err(err).Msg("cannot set presence")
	}
	l.api.StopSync()
	if err := l.ch.Close(); err != nil {
		l.log.Error().Err(err).Msg("cannot close crypto helper")
	}
	l.log.Info().Msg("client has been stopped")
}
