// mautrix-telegram - A Matrix-Telegram puppeting bridge.
// Copyright (C) 2025 Sumner Evans
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

package connector

import (
	"context"
	"errors"
	"fmt"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/rs/zerolog"
	"go.mau.fi/util/ptr"
	"maunium.net/go/mautrix/bridgev2"

	"go.mau.fi/mautrix-telegram/pkg/connector/ids"
	"go.mau.fi/mautrix-telegram/pkg/connector/store"
	"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/query/hasher"
	"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)

var (
	_ bridgev2.IdentifierResolvingNetworkAPI = (*TelegramClient)(nil)
	_ bridgev2.ContactListingNetworkAPI      = (*TelegramClient)(nil)
	_ bridgev2.UserSearchingNetworkAPI       = (*TelegramClient)(nil)
	_ bridgev2.GroupCreatingNetworkAPI       = (*TelegramClient)(nil)
)

func (tc *TelegramClient) resolveUser(ctx context.Context, user tg.UserClass) (*bridgev2.ResolveIdentifierResponse, error) {
	networkUserID := ids.MakeUserID(user.GetID())
	if ghost, err := tc.main.Bridge.GetGhostByID(ctx, networkUserID); err != nil {
		return nil, fmt.Errorf("failed to get ghost: %w", err)
	} else if userInfo, err := tc.wrapUserInfo(ctx, user, ghost); err != nil {
		return nil, fmt.Errorf("failed to get user info: %w", err)
	} else {
		return tc.makeResolveIdentifierResponse(ghost, user, userInfo), nil
	}
}

func (tc *TelegramClient) makeResolveIdentifierResponse(ghost *bridgev2.Ghost, user tg.UserClass, info *bridgev2.UserInfo) *bridgev2.ResolveIdentifierResponse {
	return &bridgev2.ResolveIdentifierResponse{
		Ghost:    ghost,
		UserID:   ids.MakeUserID(user.GetID()),
		UserInfo: info,
		Chat: &bridgev2.CreateChatResponse{
			PortalKey: tc.makePortalKeyFromID(ids.PeerTypeUser, user.GetID(), 0),
		},
	}
}

func (tc *TelegramClient) resolveUserID(ctx context.Context, userID int64) (resp *bridgev2.ResolveIdentifierResponse, err error) {
	_, err = tc.ScopedStore.GetAccessHash(ctx, ids.PeerTypeUser, userID)
	if errors.Is(err, store.ErrNoAccessHash) {
		username, usernameErr := tc.main.Store.Username.Get(ctx, ids.PeerTypeUser, userID)
		if usernameErr != nil {
			return nil, fmt.Errorf("failed to get username after missing access hash: %w", usernameErr)
		} else if username != "" {
			zerolog.Ctx(ctx).Debug().
				Str("target_username", username).
				Int64("target_user_id", userID).
				Msg("Access hash not found for user ID, trying to look up username")
			return tc.resolveUsername(ctx, username, userID)
		}
		return nil, fmt.Errorf("%w: %w", bridgev2.ErrResolveIdentifierTryNext, err)
	} else if err != nil {
		return nil, fmt.Errorf("failed to get access hash from store: %w", err)
	}
	networkUserID := ids.MakeUserID(userID)
	resp = &bridgev2.ResolveIdentifierResponse{
		UserID: networkUserID,
		Chat: &bridgev2.CreateChatResponse{
			PortalKey: tc.makePortalKeyFromID(ids.PeerTypeUser, userID, 0),
		},
	}
	resp.Ghost, err = tc.main.Bridge.GetExistingGhostByID(ctx, networkUserID)
	if err != nil {
		return nil, fmt.Errorf("failed to get ghost: %w", err)
	} else if resp.Ghost == nil || resp.Ghost.Name == "" {
		// Try to fetch the user from Telegram
		if user, err := tc.getSingleUser(ctx, userID); err != nil {
			return nil, fmt.Errorf("failed to get user with ID %d: %w", userID, err)
		} else if user.TypeID() != tg.UserTypeID {
			return nil, fmt.Errorf("unexpected user type: %T", user)
		} else if userInfo, err := tc.updateGhost(ctx, userID, user.(*tg.User)); err != nil {
			return nil, fmt.Errorf("failed to update ghost: %w", err)
		} else {
			if resp.Ghost == nil {
				resp.Ghost, _ = tc.main.Bridge.GetExistingGhostByID(ctx, networkUserID)
			}
			return tc.makeResolveIdentifierResponse(resp.Ghost, user, userInfo), nil
		}
	}
	return
}

func (tc *TelegramClient) resolveUsername(ctx context.Context, username string, expectedID int64) (*bridgev2.ResolveIdentifierResponse, error) {
	resolved, err := APICallWithUpdates(ctx, tc, func() (*tg.ContactsResolvedPeer, error) {
		return tc.client.API().ContactsResolveUsername(ctx, &tg.ContactsResolveUsernameRequest{
			Username: username,
		})
	})
	if tg.IsUsernameNotOccupied(err) {
		if expectedID != 0 {
			err = tc.main.Store.Username.Delete(ctx, username)
			if err != nil {
				zerolog.Ctx(ctx).Warn().Err(err).Str("username", username).
					Msg("Failed to delete stale username mapping")
			}
			return nil, fmt.Errorf("%w: resolving %s didn't return a result (wanted %d)", bridgev2.ErrResolveIdentifierTryNext, username, expectedID)
		}
		return nil, nil
	} else if err != nil {
		return nil, fmt.Errorf("failed to resolve username: %w", err)
	}
	peer, ok := resolved.GetPeer().(*tg.PeerUser)
	if !ok {
		return nil, fmt.Errorf("unexpected peer type: %T", resolved.GetPeer())
	}
	if expectedID != 0 && peer.GetUserID() != expectedID {
		return nil, fmt.Errorf("%w: resolving %s returned %d instead of %d", bridgev2.ErrResolveIdentifierTryNext, username, peer.GetUserID(), expectedID)
	}
	for _, user := range resolved.GetUsers() {
		if user.GetID() == peer.GetUserID() {
			return tc.resolveUser(ctx, user)
		}
	}
	return nil, fmt.Errorf("peer user not found in contact resolved response")
}

// Parses user links or usernames with or without the @ sign in front of the username.
// This verifies the following restrictions:
// - Usernames must be at least 5 characters long
// - Usernames must be at most 32 characters long
// - Usernames must start with a letter
// - Usernames must contain only letters, numbers, and underscores
// - Usernames cannot end with an underscore
// TODO some usernames are shorter, figure out actual limits
// (some bots like @pic and @gif have 3 characters, fragment might allow 4 characters)
var usernameRe = regexp.MustCompile(`^(?:(?:https?://)?t(?:elegram)?\.(?:me|dog)/|tg:/{0,2}resolve\?domain=|@)?([a-zA-Z]\w{3,30}[a-zA-Z\d])$`)

func (tc *TelegramClient) ResolveIdentifier(ctx context.Context, identifier string, createChat bool) (*bridgev2.ResolveIdentifierResponse, error) {
	log := zerolog.Ctx(ctx).With().Str("identifier", identifier).Logger()
	log.Debug().Msg("Resolving identifier")

	if len(identifier) == 0 {
		return nil, fmt.Errorf("empty identifier")
	}

	if identifier[0] == '+' {
		normalized := strings.TrimPrefix(identifier, "+")
		if userID, err := tc.main.Store.PhoneNumber.GetUserID(ctx, normalized); err != nil {
			return nil, fmt.Errorf("failed to get user ID by phone number: %w", err)
		} else if userID == 0 {
			log.Info().Msg("Phone number not found in database")
			return nil, nil
		} else {
			return tc.resolveUserID(ctx, userID)
		}
	} else if userID, err := strconv.ParseInt(identifier, 10, 64); err == nil && userID > 0 {
		// This is an integer, try and parse it as a Telegram User ID
		return tc.resolveUserID(ctx, userID)
	} else if match := usernameRe.FindStringSubmatch(identifier); match != nil && !strings.Contains(identifier, "__") {
		// This is a username
		entityType, userID, err := tc.main.Store.Username.GetEntityID(ctx, match[1])
		if entityType == ids.PeerTypeUser && (err == nil || userID != 0) {
			// We know this username.
			resp, err := tc.resolveUserID(ctx, userID)
			if err == nil || !errors.Is(err, store.ErrNoAccessHash) {
				return resp, err
			}
		}
		return tc.resolveUsername(ctx, match[1], 0)
	}
	return nil, fmt.Errorf("invalid identifier: %q (must be a phone number, username, or Telegram user ID)", identifier)
}

func (tc *TelegramClient) SearchUsers(ctx context.Context, query string) (resp []*bridgev2.ResolveIdentifierResponse, err error) {
	contactsFound, err := APICallWithUpdates(ctx, tc, func() (*tg.ContactsFound, error) {
		return tc.client.API().ContactsSearch(ctx, &tg.ContactsSearchRequest{Q: query})
	})
	if err != nil {
		return nil, err
	}
	users := map[int64]tg.UserClass{}
	for _, user := range contactsFound.GetUsers() {
		users[user.GetID()] = user
	}

	addResult := func(p tg.PeerClass) error {
		if peer, ok := p.(*tg.PeerUser); !ok {
			return nil
		} else if user, ok := users[peer.GetUserID()]; ok {
			if r, err := tc.resolveUser(ctx, user); err != nil {
				return err
			} else {
				resp = append(resp, r)
			}
			return nil
		} else {
			return fmt.Errorf("peer user not found in contact search response")
		}
	}

	for _, p := range contactsFound.MyResults {
		if err := addResult(p); err != nil {
			return nil, err
		}
	}
	for _, p := range contactsFound.Results {
		if err := addResult(p); err != nil {
			return nil, err
		}
	}
	return resp, nil
}

func (tc *TelegramClient) GetContactList(ctx context.Context) (resp []*bridgev2.ResolveIdentifierResponse, err error) {
	tc.contactsLock.Lock()
	defer tc.contactsLock.Unlock()
	var contacts *tg.ContactsContacts
	if time.Since(tc.lastContactReq) > 10*time.Minute {
		contacts, err = APICallWithOnlyUserUpdates(ctx, tc, func() (*tg.ContactsContacts, error) {
			c, err := tc.client.API().ContactsGetContacts(ctx, tc.cachedContactsHash)
			if err != nil {
				return nil, err
			}
			switch typedResp := c.(type) {
			case *tg.ContactsContacts:
				tc.cachedContacts = typedResp
				var h hasher.Hasher
				for _, contact := range tc.cachedContacts.Contacts {
					h.Update(uint32(contact.UserID))
				}
				tc.cachedContactsHash = h.Sum()
			case *tg.ContactsContactsNotModified:
				// No changes
			default:
				return nil, fmt.Errorf("unexpected contacts type: %T", c)
			}
			return tc.cachedContacts, nil
		})
		if err != nil {
			return nil, err
		}
		tc.lastContactReq = time.Now()
	} else {
		contacts = tc.cachedContacts
	}
	users := map[int64]tg.UserClass{}
	for _, user := range contacts.GetUsers() {
		users[user.GetID()] = user
	}

	for _, contact := range contacts.Contacts {
		if user, ok := users[contact.UserID]; ok {
			if r, err := tc.resolveUser(ctx, user); err != nil {
				return nil, err
			} else {
				resp = append(resp, r)
			}
		} else {
			return nil, fmt.Errorf("contact user not found in contact list response")
		}
	}
	return resp, nil
}

// TODO support channels
func (tc *TelegramClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCreateParams) (*bridgev2.CreateChatResponse, error) {
	req := tg.MessagesCreateChatRequest{
		Title: ptr.Val(params.Name).Name,
	}
	for _, networkUserID := range params.Participants {
		if peerType, userID, err := ids.ParseUserID(networkUserID); err != nil {
			return nil, fmt.Errorf("failed to parse user ID: %w", err)
		} else if peerType != ids.PeerTypeUser {
			return nil, fmt.Errorf("unexpected peer type: %s", peerType)
		} else if inputUser, err := tc.getInputUser(ctx, userID); err != nil {
			return nil, fmt.Errorf("failed to get input user: %w", err)
		} else {
			req.Users = append(req.Users, inputUser)
		}
	}
	invitedUsers, err := tc.client.API().MessagesCreateChat(ctx, &req)
	if err != nil {
		return nil, fmt.Errorf("failed to create chat: %w", err)
	}
	invited, ok := invitedUsers.Updates.(interface {
		GetChats() (value []tg.ChatClass)
	})
	if !ok {
		return nil, fmt.Errorf("unexpected response type: %T", invitedUsers.Updates)
	}

	// TODO notify about users that couldn't be invited

	if chats := invited.GetChats(); len(chats) != 1 {
		return nil, fmt.Errorf("unexpected number of chats: %d", len(chats))
	} else if chat, ok := chats[0].(*tg.Chat); !ok {
		return nil, fmt.Errorf("unexpected chat type: %T", chats[0])
	} else {
		portalKey := tc.makePortalKeyFromID(ids.PeerTypeChat, chat.ID, 0)
		if params.RoomID != "" {
			portal, err := tc.main.Bridge.GetPortalByKey(ctx, portalKey)
			if err != nil {
				return nil, err
			}
			err = portal.UpdateMatrixRoomID(ctx, params.RoomID, bridgev2.UpdateMatrixRoomIDParams{
				SyncDBMetadata: func() {
					portal.Name = req.Title
					portal.NameSet = true
				},
				OverwriteOldPortal: true,
				TombstoneOldRoom:   true,
				DeleteOldRoom:      true,
				ChatInfoSource:     tc.userLogin,
			})
			if err != nil {
				return nil, err
			}
		}
		return &bridgev2.CreateChatResponse{
			PortalKey: portalKey,
		}, nil
	}
}
