package peers

import (
	"context"
	"strings"

	"github.com/go-faster/errors"

	"go.mau.fi/mautrix-telegram/pkg/gotd/ascii"
	"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/internal/deeplink"
	"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)

// Resolve uses given string to create new peer promise.
//
// Input examples:
//
//	@telegram
//	telegram
//	t.me/telegram
//	https://t.me/telegram
//	tg:resolve?domain=telegram
//	tg://resolve?domain=telegram
//	+13115552368
//	+1 (311) 555-0123
//	+1 311 555-6162
//	13115556162
func (m *Manager) Resolve(ctx context.Context, from string) (Peer, error) {
	from = strings.TrimSpace(from)

	if deeplink.IsDeeplinkLike(from) {
		return m.ResolveDeeplink(ctx, from)
	}
	if isPhoneNumber(from) {
		return m.ResolvePhone(ctx, from)
	}
	return m.ResolveDomain(ctx, from)
}

func isPhoneNumber(s string) bool {
	if s == "" {
		return false
	}
	r := rune(s[0])
	return r == '+' || ascii.IsDigit(r)
}

func cleanupPhone(phone string) string {
	var needClean bool
	for _, ch := range phone {
		if !ascii.IsDigit(ch) {
			needClean = true
			break
		}
	}
	if !needClean {
		return phone
	}

	clean := strings.Builder{}
	clean.Grow(len(phone) + 1)

	for _, ch := range phone {
		if ascii.IsDigit(ch) {
			clean.WriteRune(ch)
		}
	}

	return clean.String()
}

// ResolvePhone uses given phone to resolve User.
//
// Input example:
//
//	+13115552368
//	+1 (311) 555-0123
//	+1 311 555-6162
//	13115556162
//
// Note that Telegram represents phone numbers according to the E.164 standard
// without the plus sign (”+”) prefix. The resolver therefore takes an easy
// route and just deletes any non-digit symbols from phone number string.
func (m *Manager) ResolvePhone(ctx context.Context, phone string) (User, error) {
	tried := false

	phone = cleanupPhone(phone)
	for {
		if tried {
			return User{}, &PhoneNotFoundError{Phone: phone}
		}

		key, v, found, err := m.storage.FindPhone(ctx, phone)
		if err != nil {
			return User{}, errors.Wrap(err, "find by phone")
		}
		if found {
			return m.GetUser(ctx, &tg.InputUser{
				UserID:     key.ID,
				AccessHash: v.AccessHash,
			})
		}
		if m.selfIsBot() {
			return User{}, &PhoneNotFoundError{Phone: phone}
		}
		tried = true

		users, err := m.updateContacts(ctx)
		if err != nil {
			return User{}, errors.Wrap(err, "update contacts")
		}

		for _, user := range users {
			if u, ok := user.AsNotEmpty(); ok && u.Phone == phone {
				return m.User(u), nil
			}
		}
	}
}

func validateDomain(domain string) error {
	return deeplink.ValidateDomain(domain)
}

func (m *Manager) findPeerClass(p tg.PeerClass, users []tg.UserClass, chats []tg.ChatClass) (Peer, bool) {
	switch p := p.(type) {
	case *tg.PeerUser:
		for _, user := range users {
			u, ok := user.AsNotEmpty()
			if ok && u.ID == p.UserID {
				return m.User(u), true
			}
		}
	case *tg.PeerChat:
		for _, chat := range chats {
			c, ok := chat.(*tg.Chat)
			if ok && c.ID == p.ChatID {
				return m.Chat(c), true
			}
		}
	case *tg.PeerChannel:
		for _, chat := range chats {
			c, ok := chat.(*tg.Channel)
			if ok && c.ID == p.ChannelID {
				return m.Channel(c), true
			}
		}
	}
	return nil, false
}

// ResolveDomain uses given domain to create new peer promise.
//
// May be prefixed with @ or not.
//
// Input examples:
//
//	@telegram
//	telegram
func (m *Manager) ResolveDomain(ctx context.Context, domain string) (Peer, error) {
	domain = strings.TrimPrefix(domain, "@")

	if err := validateDomain(domain); err != nil {
		return nil, errors.Wrap(err, "validate domain")
	}

	ch := m.sg.DoChan(domain, func() (interface{}, error) {
		result, err := m.api.ContactsResolveUsername(ctx, &tg.ContactsResolveUsernameRequest{
			Username: domain,
		})
		if err != nil {
			return nil, errors.Wrap(err, "resolve")
		}
		return result, nil
	})

	var result *tg.ContactsResolvedPeer
	select {
	case r := <-ch:
		if err := r.Err; err != nil {
			return nil, r.Err
		}
		result = r.Val.(*tg.ContactsResolvedPeer)
	case <-ctx.Done():
		return nil, ctx.Err()
	}

	if err := m.applyEntities(ctx, result.Users, result.Chats); err != nil {
		return nil, err
	}

	p, ok := m.findPeerClass(result.Peer, result.Users, result.Chats)
	if !ok {
		return nil, &PeerNotFoundError{Peer: result.Peer}
	}

	return p, nil
}

// ResolveDeeplink uses given deeplink to create new peer promise.
//
// Input examples:
//
//	t.me/telegram
//	https://t.me/telegram
//	tg:resolve?domain=telegram
//	tg://resolve?domain=telegram
func (m *Manager) ResolveDeeplink(ctx context.Context, u string) (Peer, error) {
	link, err := deeplink.Expect(u, deeplink.Resolve)
	if err != nil {
		return nil, err
	}
	domain := link.Args.Get("domain")

	if err := validateDomain(domain); err != nil {
		return nil, errors.Wrap(err, "validate domain")
	}

	return m.ResolveDomain(ctx, domain)
}

func (m *Manager) ResolveDeeplinkJoin(ctx context.Context, u string) (tg.ChatInviteClass, error) {
	link, err := deeplink.Expect(u, deeplink.Join)
	if err != nil {
		return nil, err
	}
	domain := link.Args.Get("domain")

	if err := validateDomain(domain); err != nil {
		return nil, errors.Wrap(err, "validate domain")
	}

	inviteInfo, err := m.api.MessagesCheckChatInvite(ctx, link.Args.Get("invite"))
	if err != nil {
		return nil, errors.Wrap(err, "check invite")
	}

	return inviteInfo, nil
}
