/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.

For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "inline_bots/bot_attach_web_view.h"

#include "api/api_common.h"
#include "data/data_bot_app.h"
#include "data/data_user.h"
#include "data/data_file_origin.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_session.h"
#include "main/main_session.h"
#include "main/main_domain.h"
#include "storage/storage_domain.h"
#include "info/profile/info_profile_values.h"
#include "ui/boxes/confirm_box.h"
#include "ui/toasts/common_toasts.h"
#include "ui/chat/attach/attach_bot_webview.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/dropdown_menu.h"
#include "ui/widgets/popup_menu.h"
#include "ui/widgets/menu/menu_item_base.h"
#include "ui/text/text_utilities.h"
#include "ui/effects/ripple_animation.h"
#include "ui/painter.h"
#include "window/themes/window_theme.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "webview/webview_interface.h"
#include "core/application.h"
#include "core/local_url_handlers.h"
#include "ui/basic_click_handlers.h"
#include "history/history.h"
#include "history/history_item.h"
#include "payments/payments_checkout_process.h"
#include "storage/storage_account.h"
#include "boxes/peer_list_controllers.h"
#include "lang/lang_keys.h"
#include "base/random.h"
#include "base/timer_rpl.h"
#include "apiwrap.h"
#include "mainwidget.h"
#include "styles/style_boxes.h"
#include "styles/style_menu_icons.h"

#include <QSvgRenderer>

namespace InlineBots {
namespace {

constexpr auto kProlongTimeout = 60 * crl::time(1000);

struct ParsedBot {
	UserData *bot = nullptr;
	bool inactive = false;
};

[[nodiscard]] DocumentData *ResolveIcon(
		not_null<Main::Session*> session,
		const MTPDattachMenuBot &data) {
	for (const auto &icon : data.vicons().v) {
		const auto document = icon.match([&](
			const MTPDattachMenuBotIcon &data
		) -> DocumentData* {
			if (data.vname().v == "default_static") {
				return session->data().processDocument(data.vicon()).get();
			}
			return nullptr;
		});
		if (document) {
			return document;
		}
	}
	return nullptr;
}

[[nodiscard]] PeerTypes ResolvePeerTypes(
		const QVector<MTPAttachMenuPeerType> &types) {
	auto result = PeerTypes();
	for (const auto &type : types) {
		result |= type.match([&](const MTPDattachMenuPeerTypeSameBotPM &) {
			return PeerType::SameBot;
		}, [&](const MTPDattachMenuPeerTypeBotPM &) {
			return PeerType::Bot;
		}, [&](const MTPDattachMenuPeerTypePM &) {
			return PeerType::User;
		}, [&](const MTPDattachMenuPeerTypeChat &) {
			return PeerType::Group;
		}, [&](const MTPDattachMenuPeerTypeBroadcast &) {
			return PeerType::Broadcast;
		});
	}
	return result;
}

[[nodiscard]] std::optional<AttachWebViewBot> ParseAttachBot(
		not_null<Main::Session*> session,
		const MTPAttachMenuBot &bot) {
	auto result = bot.match([&](const MTPDattachMenuBot &data) {
		const auto user = session->data().userLoaded(UserId(data.vbot_id()));
		const auto good = user
			&& user->isBot()
			&& user->botInfo->supportsAttachMenu;
		return good
			? AttachWebViewBot{
				.user = user,
				.icon = ResolveIcon(session, data),
				.name = qs(data.vshort_name()),
				.types = ResolvePeerTypes(data.vpeer_types().v),
				.inactive = data.is_inactive(),
				.hasSettings = data.is_has_settings(),
				.requestWriteAccess = data.is_request_write_access(),
			} : std::optional<AttachWebViewBot>();
	});
	if (result && result->icon) {
		result->icon->forceToCache(true);
	}
	return result;
}

[[nodiscard]] PeerTypes PeerTypesFromNames(
		const std::vector<QString> &names) {
	auto result = PeerTypes();
	for (const auto &name : names) {
		//, bots, groups, channels
		result |= (name == u"users"_q)
			? PeerType::User
			: name == u"bots"_q
			? PeerType::Bot
			: name == u"groups"_q
			? PeerType::Group
			: name == u"channels"_q
			? PeerType::Broadcast
			: PeerType(0);
	}
	return result;
}

void ShowChooseBox(
		not_null<Window::SessionController*> controller,
		PeerTypes types,
		Fn<void(not_null<Data::Thread*>)> callback,
		rpl::producer<QString> titleOverride = nullptr) {
	const auto weak = std::make_shared<QPointer<Ui::BoxContent>>();
	auto done = [=](not_null<Data::Thread*> thread) mutable {
		if (const auto strong = *weak) {
			strong->closeBox();
		}
		callback(thread);
	};
	auto filter = [=](not_null<Data::Thread*> thread) -> bool {
		const auto peer = thread->peer();
		if (!Data::CanSend(thread, ChatRestriction::SendInline, false)) {
			return false;
		} else if (const auto user = peer->asUser()) {
			if (user->isBot()) {
				return (types & PeerType::Bot);
			} else {
				return (types & PeerType::User);
			}
		} else if (peer->isBroadcast()) {
			return (types & PeerType::Broadcast);
		} else {
			return (types & PeerType::Group);
		}
	};
	auto initBox = [=](not_null<PeerListBox*> box) {
		if (titleOverride) {
			box->setTitle(std::move(titleOverride));
		}
		box->addButton(tr::lng_cancel(), [box] {
			box->closeBox();
		});
	};
	*weak = controller->show(Box<PeerListBox>(
		std::make_unique<ChooseRecipientBoxController>(
			&controller->session(),
			std::move(done),
			std::move(filter)),
		std::move(initBox)), Ui::LayerOption::KeepOther);
}

[[nodiscard]] base::flat_set<not_null<AttachWebView*>> &ActiveWebViews() {
	static auto result = base::flat_set<not_null<AttachWebView*>>();
	return result;
}

class BotAction final : public Ui::Menu::ItemBase {
public:
	BotAction(
		not_null<Ui::RpWidget*> parent,
		const style::Menu &st,
		const AttachWebViewBot &bot,
		Fn<void()> callback);

	bool isEnabled() const override;
	not_null<QAction*> action() const override;

	[[nodiscard]] rpl::producer<bool> forceShown() const;

	void handleKeyPress(not_null<QKeyEvent*> e) override;

private:
	void contextMenuEvent(QContextMenuEvent *e) override;

	QPoint prepareRippleStartPosition() const override;
	QImage prepareRippleMask() const override;

	int contentHeight() const override;

	void prepare();
	void validateIcon();
	void paint(Painter &p);

	const not_null<QAction*> _dummyAction;
	const style::Menu &_st;
	const AttachWebViewBot _bot;

	base::unique_qptr<Ui::PopupMenu> _menu;
	rpl::event_stream<bool> _forceShown;

	Ui::Text::String _text;
	QImage _mask;
	QImage _icon;
	int _textWidth = 0;
	const int _height;

};

BotAction::BotAction(
	not_null<Ui::RpWidget*> parent,
	const style::Menu &st,
	const AttachWebViewBot &bot,
	Fn<void()> callback)
: ItemBase(parent, st)
, _dummyAction(new QAction(parent))
, _st(st)
, _bot(bot)
, _height(_st.itemPadding.top()
		+ _st.itemStyle.font->height
		+ _st.itemPadding.bottom()) {
	setAcceptBoth(false);
	initResizeHook(parent->sizeValue());
	setClickedCallback(std::move(callback));

	paintRequest(
	) | rpl::start_with_next([=] {
		Painter p(this);
		paint(p);
	}, lifetime());

	style::PaletteChanged(
	) | rpl::start_with_next([=] {
		_icon = QImage();
		update();
	}, lifetime());

	enableMouseSelecting();
	prepare();
}

void BotAction::validateIcon() {
	if (_mask.isNull()) {
		if (!_bot.media || !_bot.media->loaded()) {
			return;
		}
		auto icon = QSvgRenderer(_bot.media->bytes());
		if (!icon.isValid()) {
			_mask = QImage(
				QSize(1, 1) * style::DevicePixelRatio(),
				QImage::Format_ARGB32_Premultiplied);
			_mask.fill(Qt::transparent);
		} else {
			const auto size = style::ConvertScale(icon.defaultSize());
			_mask = QImage(
				size * style::DevicePixelRatio(),
				QImage::Format_ARGB32_Premultiplied);
			_mask.setDevicePixelRatio(style::DevicePixelRatio());
			_mask.fill(Qt::transparent);
			{
				auto p = QPainter(&_mask);
				icon.render(&p, QRect(QPoint(), size));
			}
			_mask = Images::Colored(std::move(_mask), QColor(255, 255, 255));
		}
	}
	if (_icon.isNull()) {
		_icon = style::colorizeImage(_mask, st::menuIconColor);
	}
}

void BotAction::paint(Painter &p) {
	validateIcon();

	const auto selected = isSelected();
	if (selected && _st.itemBgOver->c.alpha() < 255) {
		p.fillRect(0, 0, width(), _height, _st.itemBg);
	}
	p.fillRect(0, 0, width(), _height, selected ? _st.itemBgOver : _st.itemBg);
	if (isEnabled()) {
		paintRipple(p, 0, 0);
	}

	if (!_icon.isNull()) {
		p.drawImage(_st.itemIconPosition, _icon);
	}

	p.setPen(selected ? _st.itemFgOver : _st.itemFg);
	_text.drawLeftElided(
		p,
		_st.itemPadding.left(),
		_st.itemPadding.top(),
		_textWidth,
		width());
}

void BotAction::prepare() {
	_text.setMarkedText(_st.itemStyle, { _bot.name });
	const auto textWidth = _text.maxWidth();
	const auto &padding = _st.itemPadding;

	const auto goodWidth = padding.left()
		+ textWidth
		+ padding.right();

	const auto w = std::clamp(goodWidth, _st.widthMin, _st.widthMax);
	_textWidth = w - (goodWidth - textWidth);
	setMinWidth(w);
	update();
}

bool BotAction::isEnabled() const {
	return true;
}

not_null<QAction*> BotAction::action() const {
	return _dummyAction;
}

void BotAction::contextMenuEvent(QContextMenuEvent *e) {
	_menu = nullptr;
	_menu = base::make_unique_q<Ui::PopupMenu>(
		this,
		st::popupMenuWithIcons);
	_menu->addAction(tr::lng_bot_remove_from_menu(tr::now), [=] {
		_bot.user->session().attachWebView().removeFromMenu(_bot.user);
	}, &st::menuIconDelete);

	QObject::connect(_menu, &QObject::destroyed, [=] {
		_forceShown.fire(false);
	});

	_forceShown.fire(true);
	_menu->popup(e->globalPos());
	e->accept();
}

QPoint BotAction::prepareRippleStartPosition() const {
	return mapFromGlobal(QCursor::pos());
}

QImage BotAction::prepareRippleMask() const {
	return Ui::RippleAnimation::RectMask(size());
}

int BotAction::contentHeight() const {
	return _height;
}

rpl::producer<bool> BotAction::forceShown() const {
	return _forceShown.events();
}

void BotAction::handleKeyPress(not_null<QKeyEvent*> e) {
	if (!isSelected()) {
		return;
	}
	const auto key = e->key();
	if (key == Qt::Key_Enter || key == Qt::Key_Return) {
		setClicked(Ui::Menu::TriggeredSource::Keyboard);
	}
}

} // namespace

bool PeerMatchesTypes(
		not_null<PeerData*> peer,
		not_null<UserData*> bot,
		PeerTypes types) {
	if (const auto user = peer->asUser()) {
		return (user == bot)
			? (types & PeerType::SameBot)
			: user->isBot()
			? (types & PeerType::Bot)
			: (types & PeerType::User);
	} else if (peer->isBroadcast()) {
		return (types & PeerType::Broadcast);
	}
	return (types & PeerType::Group);
}

PeerTypes ParseChooseTypes(QStringView choose) {
	auto result = PeerTypes();
	for (const auto &entry : choose.split(QChar(' '))) {
		if (entry == u"users"_q) {
			result |= PeerType::User;
		} else if (entry == u"bots"_q) {
			result |= PeerType::Bot;
		} else if (entry == u"groups"_q) {
			result |= PeerType::Group;
		} else if (entry == u"channels"_q) {
			result |= PeerType::Broadcast;
		}
	}
	return result;
}

struct AttachWebView::Context {
	base::weak_ptr<Window::SessionController> controller;
	Dialogs::EntryState dialogsEntryState;
	Api::SendAction action;
	bool fromSwitch = false;
	bool fromBotApp = false;
};

AttachWebView::AttachWebView(not_null<Main::Session*> session)
: _session(session) {
}

AttachWebView::~AttachWebView() {
	ActiveWebViews().remove(this);
}

void AttachWebView::request(
		not_null<Window::SessionController*> controller,
		const Api::SendAction &action,
		const QString &botUsername,
		const QString &startCommand) {
	if (botUsername.isEmpty()) {
		return;
	}
	const auto username = _bot ? _bot->username() : _botUsername;
	const auto context = LookupContext(controller, action);
	if (IsSame(_context, context)
		&& username.toLower() == botUsername.toLower()
		&& _startCommand == startCommand) {
		if (_panel) {
			_panel->requestActivate();
		}
		return;
	}
	cancel();

	_context = std::make_unique<Context>(context);
	_botUsername = botUsername;
	_startCommand = startCommand;
	resolve();
}

AttachWebView::Context AttachWebView::LookupContext(
		not_null<Window::SessionController*> controller,
		const Api::SendAction &action) {
	return {
		.controller = controller,
		.dialogsEntryState = controller->currentDialogsEntryState(),
		.action = action,
	};
}

bool AttachWebView::IsSame(
		const std::unique_ptr<Context> &a,
		const Context &b) {
	// Check fields that are sent to API in bot attach webview requests.
	return a
		&& (a->controller == b.controller)
		&& (a->dialogsEntryState == b.dialogsEntryState)
		&& (a->fromSwitch == b.fromSwitch)
		&& (a->action.history == b.action.history)
		&& (a->action.replyTo == b.action.replyTo)
		&& (a->action.topicRootId == b.action.topicRootId)
		&& (a->action.options.sendAs == b.action.options.sendAs)
		&& (a->action.options.silent == b.action.options.silent);
}

void AttachWebView::request(
		not_null<Window::SessionController*> controller,
		const Api::SendAction &action,
		not_null<UserData*> bot,
		const WebViewButton &button) {
	requestWithOptionalConfirm(
		bot,
		button,
		LookupContext(controller, action),
		button.fromMenu ? nullptr : controller.get());
}

void AttachWebView::requestWithOptionalConfirm(
		not_null<UserData*> bot,
		const WebViewButton &button,
		const Context &context,
		Window::SessionController *controllerForConfirm) {
	if (IsSame(_context, context) && _bot == bot) {
		if (_panel) {
			_panel->requestActivate();
		} else if (_requestId) {
			return;
		}
	}
	cancel();

	_bot = bot;
	_context = std::make_unique<Context>(context);
	if (controllerForConfirm) {
		confirmOpen(controllerForConfirm, [=] {
			request(button);
		});
	} else {
		request(button);
	}
}

void AttachWebView::request(const WebViewButton &button) {
	Expects(_context != nullptr && _bot != nullptr);

	_startCommand = button.startCommand;
	const auto &action = _context->action;

	using Flag = MTPmessages_RequestWebView::Flag;
	const auto flags = Flag::f_theme_params
		| (button.url.isEmpty() ? Flag(0) : Flag::f_url)
		| (_startCommand.isEmpty() ? Flag(0) : Flag::f_start_param)
		| (action.replyTo ? Flag::f_reply_to_msg_id : Flag(0))
		| (action.topicRootId ? Flag::f_top_msg_id : Flag(0))
		| (action.options.sendAs ? Flag::f_send_as : Flag(0))
		| (action.options.silent ? Flag::f_silent : Flag(0));
	_requestId = _session->api().request(MTPmessages_RequestWebView(
		MTP_flags(flags),
		action.history->peer->input,
		_bot->inputUser,
		MTP_bytes(button.url),
		MTP_string(_startCommand),
		MTP_dataJSON(MTP_bytes(Window::Theme::WebViewParams().json)),
		MTP_string("tdesktop"),
		MTP_int(action.replyTo.bare),
		MTP_int(action.topicRootId.bare),
		(action.options.sendAs
			? action.options.sendAs->input
			: MTP_inputPeerEmpty())
	)).done([=](const MTPWebViewResult &result) {
		_requestId = 0;
		const auto &data = result.data();
		show(
			data.vquery_id().v,
			qs(data.vurl()),
			button.text,
			button.fromMenu || button.url.isEmpty());
	}).fail([=](const MTP::Error &error) {
		_requestId = 0;
		if (error.type() == u"BOT_INVALID"_q) {
			requestBots();
		}
	}).send();
}

void AttachWebView::cancel() {
	ActiveWebViews().remove(this);
	_session->api().request(base::take(_requestId)).cancel();
	_session->api().request(base::take(_prolongId)).cancel();
	_panel = nullptr;
	_context = nullptr;
	_bot = nullptr;
	_app = nullptr;
	_botUsername = QString();
	_botAppName = QString();
	_startCommand = QString();
}

void AttachWebView::requestBots() {
	if (_botsRequestId) {
		return;
	}
	_botsRequestId = _session->api().request(MTPmessages_GetAttachMenuBots(
		MTP_long(_botsHash)
	)).done([=](const MTPAttachMenuBots &result) {
		_botsRequestId = 0;
		result.match([&](const MTPDattachMenuBotsNotModified &) {
		}, [&](const MTPDattachMenuBots &data) {
			_session->data().processUsers(data.vusers());
			_botsHash = data.vhash().v;
			_attachBots.clear();
			_attachBots.reserve(data.vbots().v.size());
			for (const auto &bot : data.vbots().v) {
				if (auto parsed = ParseAttachBot(_session, bot)) {
					if (!parsed->inactive) {
						if (const auto icon = parsed->icon) {
							parsed->media = icon->createMediaView();
							icon->save(Data::FileOrigin(), {});
						}
						_attachBots.push_back(std::move(*parsed));
					}
				}
			}
			_attachBotsUpdates.fire({});
		});
	}).fail([=] {
		_botsRequestId = 0;
	}).send();
}

void AttachWebView::requestAddToMenu(
		not_null<UserData*> bot,
		const QString &startCommand) {
	requestAddToMenu(bot, startCommand, nullptr, std::nullopt, PeerTypes());
}

void AttachWebView::requestAddToMenu(
		not_null<UserData*> bot,
		const QString &startCommand,
		Window::SessionController *controller,
		std::optional<Api::SendAction> action,
		PeerTypes chooseTypes) {
	Expects(controller != nullptr || _context != nullptr);

	if (!bot->isBot() || !bot->botInfo->supportsAttachMenu) {
		showToast(tr::lng_bot_menu_not_supported(tr::now), controller);
		return;
	}
	const auto wasController = (controller != nullptr);
	_addToMenuChooseController = base::make_weak(controller);
	_addToMenuStartCommand = startCommand;
	_addToMenuChooseTypes = chooseTypes;
	if (!controller) {
		_addToMenuContext = base::take(_context);
	} else if (action) {
		_addToMenuContext = std::make_unique<Context>(
			LookupContext(controller, *action));
	}
	if (_addToMenuId) {
		if (_addToMenuBot == bot) {
			return;
		}
		_session->api().request(base::take(_addToMenuId)).cancel();
	}
	_addToMenuBot = bot;
	_addToMenuId = _session->api().request(MTPmessages_GetAttachMenuBot(
		bot->inputUser
	)).done([=](const MTPAttachMenuBotsBot &result) {
		_addToMenuId = 0;
		const auto bot = base::take(_addToMenuBot);
		const auto context = std::shared_ptr(base::take(_addToMenuContext));
		const auto chooseTypes = base::take(_addToMenuChooseTypes);
		const auto startCommand = base::take(_addToMenuStartCommand);
		const auto chooseController = base::take(_addToMenuChooseController);
		const auto open = [=](PeerTypes types) {
			const auto strong = chooseController.get();
			if (!strong) {
				if (wasController) {
					// Just ignore the click if controller was destroyed.
					return true;
				}
			} else if (const auto useTypes = chooseTypes & types) {
				const auto done = [=](not_null<Data::Thread*> thread) {
					strong->showThread(thread);
					requestWithOptionalConfirm(
						bot,
						{ .startCommand = startCommand },
						LookupContext(strong, Api::SendAction(thread)));
				};
				ShowChooseBox(strong, useTypes, done);
				return true;
			}
			if (!context) {
				return false;
			}
			requestWithOptionalConfirm(
				bot,
				{ .startCommand = startCommand },
				*context);
			return true;
		};
		result.match([&](const MTPDattachMenuBotsBot &data) {
			_session->data().processUsers(data.vusers());
			if (const auto parsed = ParseAttachBot(_session, data.vbot())) {
				if (bot == parsed->user) {
					const auto types = parsed->types;
					if (parsed->inactive) {
						confirmAddToMenu(*parsed, [=] {
							open(types);
						});
					} else {
						requestBots();
						if (!open(types)) {
							showToast(
								tr::lng_bot_menu_already_added(tr::now));
						}
					}
				}
			}
		});
	}).fail([=] {
		_addToMenuId = 0;
		_addToMenuBot = nullptr;
		_addToMenuContext = nullptr;
		_addToMenuStartCommand = QString();
		showToast(tr::lng_bot_menu_not_supported(tr::now));
	}).send();
}

void AttachWebView::removeFromMenu(not_null<UserData*> bot) {
	toggleInMenu(bot, ToggledState::Removed, [=] {
		showToast(tr::lng_bot_remove_from_menu_done(tr::now));
	});
}

void AttachWebView::resolve() {
	resolveUsername(_botUsername, [=](not_null<PeerData*> bot) {
		if (!_context) {
			return;
		}
		_bot = bot->asUser();
		if (!_bot) {
			showToast(tr::lng_bot_menu_not_supported(tr::now));
			return;
		}
		requestAddToMenu(_bot, _startCommand);
	});
}

void AttachWebView::resolveUsername(
		const QString &username,
		Fn<void(not_null<PeerData*>)> done) {
	if (const auto peer = _session->data().peerByUsername(username)) {
		done(peer);
		return;
	}
	_session->api().request(base::take(_requestId)).cancel();
	_requestId = _session->api().request(MTPcontacts_ResolveUsername(
		MTP_string(username)
	)).done([=](const MTPcontacts_ResolvedPeer &result) {
		_requestId = 0;
		result.match([&](const MTPDcontacts_resolvedPeer &data) {
			_session->data().processUsers(data.vusers());
			_session->data().processChats(data.vchats());
			if (const auto peerId = peerFromMTP(data.vpeer())) {
				done(_session->data().peer(peerId));
			}
		});
	}).fail([=](const MTP::Error &error) {
		_requestId = 0;
		if (error.code() == 400) {
			showToast(
				tr::lng_username_not_found(tr::now, lt_user, username));
		}
	}).send();
}

void AttachWebView::requestSimple(
		not_null<Window::SessionController*> controller,
		not_null<UserData*> bot,
		const WebViewButton &button) {
	cancel();
	_bot = bot;
	_context = std::make_unique<Context>(LookupContext(
		controller,
		Api::SendAction(bot->owner().history(bot))));
	_context->fromSwitch = button.fromSwitch;
	confirmOpen(controller, [=] {
		requestSimple(button);
	});
}

void AttachWebView::requestSimple(const WebViewButton &button) {
	using Flag = MTPmessages_RequestSimpleWebView::Flag;
	_requestId = _session->api().request(MTPmessages_RequestSimpleWebView(
		MTP_flags(Flag::f_theme_params
			| (button.fromSwitch ? Flag::f_from_switch_webview : Flag())),
		_bot->inputUser,
		MTP_bytes(button.url),
		MTP_dataJSON(MTP_bytes(Window::Theme::WebViewParams().json)),
		MTP_string("tdesktop")
	)).done([=](const MTPSimpleWebViewResult &result) {
		_requestId = 0;
		result.match([&](const MTPDsimpleWebViewResultUrl &data) {
			const auto queryId = uint64();
			show(queryId, qs(data.vurl()), button.text);
		});
	}).fail([=](const MTP::Error &error) {
		_requestId = 0;
	}).send();
}

void AttachWebView::requestMenu(
		not_null<Window::SessionController*> controller,
		not_null<UserData*> bot) {
	cancel();
	_bot = bot;
	_context = std::make_unique<Context>(LookupContext(
		controller,
		Api::SendAction(bot->owner().history(bot))));
	const auto url = bot->botInfo->botMenuButtonUrl;
	const auto text = bot->botInfo->botMenuButtonText;
	confirmOpen(controller, [=] {
		const auto &action = _context->action;
		using Flag = MTPmessages_RequestWebView::Flag;
		_requestId = _session->api().request(MTPmessages_RequestWebView(
			MTP_flags(Flag::f_theme_params
				| Flag::f_url
				| Flag::f_from_bot_menu
				| (action.replyTo? Flag::f_reply_to_msg_id : Flag(0))
				| (action.topicRootId ? Flag::f_top_msg_id : Flag(0))
				| (action.options.sendAs ? Flag::f_send_as : Flag(0))
				| (action.options.silent ? Flag::f_silent : Flag(0))),
			action.history->peer->input,
			_bot->inputUser,
			MTP_string(url),
			MTPstring(), // start_param
			MTP_dataJSON(MTP_bytes(Window::Theme::WebViewParams().json)),
			MTP_string("tdesktop"),
			MTP_int(action.replyTo.bare),
			MTP_int(action.topicRootId.bare),
			(action.options.sendAs
				? action.options.sendAs->input
				: MTP_inputPeerEmpty())
		)).done([=](const MTPWebViewResult &result) {
			_requestId = 0;
			const auto &data = result.data();
			show(data.vquery_id().v, qs(data.vurl()), text);
		}).fail([=](const MTP::Error &error) {
			_requestId = 0;
			if (error.type() == u"BOT_INVALID"_q) {
				requestBots();
			}
		}).send();
	});
}

void AttachWebView::requestApp(
		not_null<Window::SessionController*> controller,
		const Api::SendAction &action,
		not_null<UserData*> bot,
		const QString &appName,
		const QString &startParam,
		bool forceConfirmation) {
	const auto context = LookupContext(controller, action);
	if (_requestId
		&& _bot == bot
		&& _startCommand == startParam
		&& _botAppName == appName
		&& IsSame(_context, context)) {
		return;
	}
	cancel();
	_bot = bot;
	_startCommand = startParam;
	_botAppName = appName;
	_context = std::make_unique<Context>(context);
	_context->fromBotApp = true;
	const auto already = _session->data().findBotApp(_bot->id, appName);
	_requestId = _session->api().request(MTPmessages_GetBotApp(
		MTP_inputBotAppShortName(
			bot->inputUser,
			MTP_string(appName)),
		MTP_long(already ? already->hash : 0)
	)).done([=](const MTPmessages_BotApp &result) {
		_requestId = 0;
		if (!_bot || !_context) {
			return;
		}
		const auto &data = result.data();
		const auto firstTime = data.is_inactive();
		const auto received = _session->data().processBotApp(
			_bot->id,
			data.vapp());
		_app = received ? received : already;
		if (!_app) {
			cancel();
			showToast(tr::lng_username_app_not_found(tr::now));
			return;
		}
		const auto confirm = firstTime || forceConfirmation;
		if (confirm) {
			confirmAppOpen(result.data().is_request_write_access());
		} else {
			requestAppView(false);
		}
	}).fail([=] {
		cancel();
		showToast(tr::lng_username_app_not_found(tr::now));
	}).send();
}

void AttachWebView::confirmAppOpen(bool requestWriteAccess) {
	const auto controller = _context ? _context->controller.get() : nullptr;
	if (!controller || !_bot) {
		return;
	}
	controller->show(Box([=](not_null<Ui::GenericBox*> box) {
		const auto allowed = std::make_shared<Ui::Checkbox*>();
		const auto done = [=](Fn<void()> close) {
			requestAppView((*allowed) && (*allowed)->checked());
			close();
		};
		Ui::ConfirmBox(box, {
			tr::lng_allow_bot_webview(
				tr::now,
				lt_bot_name,
				Ui::Text::Bold(_bot->name()),
				Ui::Text::RichLangValue),
			done,
		});
		if (requestWriteAccess) {
			(*allowed) = box->addRow(
				object_ptr<Ui::Checkbox>(
					box,
					tr::lng_url_auth_allow_messages(
						tr::now,
						lt_bot,
						Ui::Text::Bold(_bot->name()),
						Ui::Text::WithEntities),
					true,
					st::urlAuthCheckbox),
				style::margins(
					st::boxRowPadding.left(),
					st::boxPhotoCaptionSkip,
					st::boxRowPadding.right(),
					st::boxPhotoCaptionSkip));
			(*allowed)->setAllowTextLines();
		}
	}));
}

void AttachWebView::requestAppView(bool allowWrite) {
	if (!_context || !_app) {
		return;
	}
	using Flag = MTPmessages_RequestAppWebView::Flag;
	const auto flags = Flag::f_theme_params
		| (_startCommand.isEmpty() ? Flag(0) : Flag::f_start_param)
		| (allowWrite ? Flag::f_write_allowed : Flag(0));
	_requestId = _session->api().request(MTPmessages_RequestAppWebView(
		MTP_flags(flags),
		_context->action.history->peer->input,
		MTP_inputBotAppID(MTP_long(_app->id), MTP_long(_app->accessHash)),
		MTP_string(_startCommand),
		MTP_dataJSON(MTP_bytes(Window::Theme::WebViewParams().json)),
		MTP_string("tdesktop")
	)).done([=](const MTPAppWebViewResult &result) {
		_requestId = 0;
		const auto &data = result.data();
		const auto queryId = uint64();
		show(queryId, qs(data.vurl()));
	}).fail([=](const MTP::Error &error) {
		_requestId = 0;
		if (error.type() == u"BOT_INVALID"_q) {
			requestBots();
		}
	}).send();
}

void AttachWebView::confirmOpen(
		not_null<Window::SessionController*> controller,
		Fn<void()> done) {
	if (!_bot) {
		return;
	} else if (_bot->isVerified()
		|| _bot->session().local().isBotTrustedOpenWebView(_bot->id)) {
		done();
		return;
	}
	const auto callback = [=] {
		_bot->session().local().markBotTrustedOpenWebView(_bot->id);
		controller->hideLayer();
		done();
	};
	controller->show(Ui::MakeConfirmBox({
		.text = tr::lng_allow_bot_webview(
			tr::now,
			lt_bot_name,
			Ui::Text::Bold(_bot->name()),
			Ui::Text::RichLangValue),
		.confirmed = callback,
		.confirmText = tr::lng_box_ok(),
	}));
}

void AttachWebView::ClearAll() {
	while (!ActiveWebViews().empty()) {
		ActiveWebViews().front()->cancel();
	}
}

void AttachWebView::show(
		uint64 queryId,
		const QString &url,
		const QString &buttonText,
		bool allowClipboardRead) {
	Expects(_bot != nullptr && _context != nullptr);

	const auto close = crl::guard(this, [=] {
		crl::on_main(this, [=] { cancel(); });
	});
	const auto sendData = crl::guard(this, [=](QByteArray data) {
		if (!_context
			|| _context->fromSwitch
			|| _context->fromBotApp
			|| _context->action.history->peer != _bot
			|| queryId) {
			return;
		}
		const auto randomId = base::RandomValue<uint64>();
		_session->api().request(MTPmessages_SendWebViewData(
			_bot->inputUser,
			MTP_long(randomId),
			MTP_string(buttonText),
			MTP_bytes(data)
		)).done([=](const MTPUpdates &result) {
			_session->api().applyUpdates(result);
		}).send();
		crl::on_main(this, [=] { cancel(); });
	});
	const auto switchInlineQuery = crl::guard(this, [=](
			std::vector<QString> typeNames,
			QString query) {
		const auto controller = _context
			? _context->controller.get()
			: nullptr;
		const auto types = PeerTypesFromNames(typeNames);
		if (!_bot
			|| !_bot->isBot()
			|| _bot->botInfo->inlinePlaceholder.isEmpty()
			|| !controller) {
			return;
		} else if (!types) {
			if (_context->dialogsEntryState.key.owningHistory()) {
				controller->switchInlineQuery(
					_context->dialogsEntryState,
					_bot,
					query);
			}
		} else {
			const auto bot = _bot;
			const auto done = [=](not_null<Data::Thread*> thread) {
				controller->switchInlineQuery(thread, bot, query);
			};
			ShowChooseBox(
				controller,
				types,
				done,
				tr::lng_inline_switch_choose());
		}
		crl::on_main(this, [=] { cancel(); });
	});
	const auto handleLocalUri = [close](QString uri) {
		const auto local = Core::TryConvertUrlToLocal(uri);
		if (uri == local || Core::InternalPassportLink(local)) {
			return local.startsWith(u"tg://"_q);
		} else if (!local.startsWith(u"tg://"_q, Qt::CaseInsensitive)) {
			return false;
		}
		UrlClickHandler::Open(local, {});
		close();
		return true;
	};
	const auto panel = std::make_shared<
		base::weak_ptr<Ui::BotWebView::Panel>>(nullptr);
	const auto handleInvoice = [=, session = _session](QString slug) {
		using Result = Payments::CheckoutResult;
		const auto reactivate = [=](Result result) {
			if (const auto strong = panel->get()) {
				strong->invoiceClosed(slug, [&] {
					switch (result) {
					case Result::Paid: return "paid";
					case Result::Failed: return "failed";
					case Result::Pending: return "pending";
					case Result::Cancelled: return "cancelled";
					}
					Unexpected("Payments::CheckoutResult value.");
				}());
			}
		};
		if (const auto strong = panel->get()) {
			strong->hideForPayment();
		}
		Payments::CheckoutProcess::Start(session, slug, reactivate);
	};
	auto title = Info::Profile::NameValue(_bot);
	ActiveWebViews().emplace(this);

	using Button = Ui::BotWebView::MenuButton;
	const auto attached = ranges::find(
		_attachBots,
		not_null{ _bot },
		&AttachWebViewBot::user);
	const auto name = (attached != end(_attachBots))
		? attached->name
		: _bot->name();
	const auto hasSettings = (attached != end(_attachBots))
		&& !attached->inactive
		&& attached->hasSettings;
	const auto hasOpenBot = !_context
		|| (_bot != _context->action.history->peer);
	const auto hasRemoveFromMenu = (attached != end(_attachBots))
		&& !attached->inactive;
	const auto buttons = (hasSettings ? Button::Settings : Button::None)
		| (hasOpenBot ? Button::OpenBot : Button::None)
		| (hasRemoveFromMenu ? Button::RemoveFromMenu : Button::None);
	const auto bot = _bot;

	const auto handleMenuButton = crl::guard(this, [=](Button button) {
		switch (button) {
		case Button::OpenBot:
			close();
			if (bot->session().windows().empty()) {
				Core::App().domain().activate(&bot->session().account());
			}
			if (!bot->session().windows().empty()) {
				const auto window = bot->session().windows().front();
				window->showPeerHistory(bot);
				window->window().activate();
			}
			break;
		case Button::RemoveFromMenu:
			if (const auto strong = panel->get()) {
				const auto done = crl::guard(this, [=] {
					removeFromMenu(bot);
					close();
					if (const auto active = Core::App().activeWindow()) {
						active->activate();
					}
				});
				strong->showBox(Ui::MakeConfirmBox({
					tr::lng_bot_remove_from_menu_sure(
						tr::now,
						lt_bot,
						Ui::Text::Bold(name),
						Ui::Text::WithEntities),
					done,
				}));
			}
			break;
		}
	});

	_panel = Ui::BotWebView::Show({
		.url = url,
		.userDataPath = _session->domain().local().webviewDataPath(),
		.title = std::move(title),
		.bottom = rpl::single('@' + _bot->username()),
		.handleLocalUri = handleLocalUri,
		.handleInvoice = handleInvoice,
		.sendData = sendData,
		.switchInlineQuery = switchInlineQuery,
		.close = close,
		.phone = _session->user()->phone(),
		.menuButtons = buttons,
		.handleMenuButton = handleMenuButton,
		.themeParams = [] { return Window::Theme::WebViewParams(); },
		.allowClipboardRead = allowClipboardRead,
	});
	*panel = _panel.get();
	started(queryId);
}

void AttachWebView::started(uint64 queryId) {
	Expects(_bot != nullptr && _context != nullptr);

	if (_context->fromSwitch || !queryId) {
		return;
	}

	_session->data().webViewResultSent(
	) | rpl::filter([=](const Data::Session::WebViewResultSent &sent) {
		return (sent.queryId == queryId);
	}) | rpl::start_with_next([=] {
		cancel();
	}, _panel->lifetime());

	const auto action = _context->action;
	base::timer_each(
		kProlongTimeout
	) | rpl::start_with_next([=] {
		using Flag = MTPmessages_ProlongWebView::Flag;
		_session->api().request(base::take(_prolongId)).cancel();
		_prolongId = _session->api().request(MTPmessages_ProlongWebView(
			MTP_flags(Flag(0)
				| (action.replyTo ? Flag::f_reply_to_msg_id : Flag(0))
				| (action.topicRootId ? Flag::f_top_msg_id : Flag(0))
				| (action.options.sendAs ? Flag::f_send_as : Flag(0))
				| (action.options.silent ? Flag::f_silent : Flag(0))),
			action.history->peer->input,
			_bot->inputUser,
			MTP_long(queryId),
			MTP_int(action.replyTo.bare),
			MTP_int(action.topicRootId.bare),
			(action.options.sendAs
				? action.options.sendAs->input
				: MTP_inputPeerEmpty())
		)).done([=] {
			_prolongId = 0;
		}).send();
	}, _panel->lifetime());
}

void AttachWebView::showToast(
		const QString &text,
		Window::SessionController *controller) {
	const auto strong = controller
		? controller
		: _context
		? _context->controller.get()
		: _addToMenuContext
		? _addToMenuContext->controller.get()
		: nullptr;
	Ui::ShowMultilineToast({
		.parentOverride = (strong
			? Window::Show(strong).toastParent().get()
			: nullptr),
		.text = { text },
	});
}

void AttachWebView::confirmAddToMenu(
		AttachWebViewBot bot,
		Fn<void()> callback) {
	const auto active = Core::App().activeWindow();
	if (!active) {
		return;
	}
	_confirmAddBox = active->show(Box([=](not_null<Ui::GenericBox*> box) {
		const auto allowed = std::make_shared<Ui::Checkbox*>();
		const auto done = [=](Fn<void()> close) {
			const auto state = ((*allowed) && (*allowed)->checked())
				? ToggledState::AllowedToWrite
				: ToggledState::Added;
			toggleInMenu(bot.user, state, [=] {
				if (callback) {
					callback();
				}
				showToast(tr::lng_bot_add_to_menu_done(tr::now));
			});
			close();
		};
		Ui::ConfirmBox(box, {
			tr::lng_bot_add_to_menu(
				tr::now,
				lt_bot,
				Ui::Text::Bold(bot.name),
				Ui::Text::WithEntities),
			done,
		});
		if (bot.requestWriteAccess) {
			(*allowed) = box->addRow(
				object_ptr<Ui::Checkbox>(
					box,
					tr::lng_url_auth_allow_messages(
						tr::now,
						lt_bot,
						Ui::Text::Bold(bot.name),
						Ui::Text::WithEntities),
					true,
					st::urlAuthCheckbox),
				style::margins(
					st::boxRowPadding.left(),
					st::boxPhotoCaptionSkip,
					st::boxRowPadding.right(),
					st::boxPhotoCaptionSkip));
			(*allowed)->setAllowTextLines();
		}
	}));
}

void AttachWebView::toggleInMenu(
		not_null<UserData*> bot,
		ToggledState state,
		Fn<void()> callback) {
	using Flag = MTPmessages_ToggleBotInAttachMenu::Flag;
	_session->api().request(MTPmessages_ToggleBotInAttachMenu(
		MTP_flags((state == ToggledState::AllowedToWrite)
			? Flag::f_write_allowed
			: Flag()),
		bot->inputUser,
		MTP_bool(state != ToggledState::Removed)
	)).done([=] {
		_requestId = 0;
		requestBots();
		if (callback) {
			callback();
		}
	}).fail([=] {
		cancel();
	}).send();
}

std::unique_ptr<Ui::DropdownMenu> MakeAttachBotsMenu(
		not_null<QWidget*> parent,
		not_null<Window::SessionController*> controller,
		not_null<PeerData*> peer,
		Fn<Api::SendAction()> actionFactory,
		Fn<void(bool)> attach) {
	if (!Data::CanSend(peer, ChatRestriction::SendInline)) {
		return nullptr;
	}
	auto result = std::make_unique<Ui::DropdownMenu>(
		parent,
		st::dropdownMenuWithIcons);
	const auto bots = &peer->session().attachWebView();
	const auto raw = result.get();
	auto minimal = 0;
	if (Data::CanSend(peer, ChatRestriction::SendPhotos, false)) {
		++minimal;
		raw->addAction(tr::lng_attach_photo_or_video(tr::now), [=] {
			attach(true);
		}, &st::menuIconPhoto);
	}
	const auto fileTypes = ChatRestriction::SendVideos
		| ChatRestriction::SendGifs
		| ChatRestriction::SendStickers
		| ChatRestriction::SendMusic
		| ChatRestriction::SendFiles;
	if (Data::CanSendAnyOf(peer, fileTypes)) {
		++minimal;
		raw->addAction(tr::lng_attach_document(tr::now), [=] {
			attach(false);
		}, &st::menuIconFile);
	}
	for (const auto &bot : bots->attachBots()) {
		if (!PeerMatchesTypes(peer, bot.user, bot.types)) {
			continue;
		}
		const auto callback = [=] {
			bots->request(
				controller,
				actionFactory(),
				bot.user,
				{ .fromMenu = true });
		};
		auto action = base::make_unique_q<BotAction>(
			raw,
			raw->menu()->st(),
			bot,
			callback);
		action->forceShown(
		) | rpl::start_with_next([=](bool shown) {
			if (shown) {
				raw->setAutoHiding(false);
			} else {
				raw->hideAnimated();
				raw->setAutoHiding(true);
			}
		}, action->lifetime());
		raw->addAction(std::move(action));
	}
	if (raw->actions().size() <= minimal) {
		return nullptr;
	}
	return result;
}

} // namespace InlineBots
