/*
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_blocked_peers.h"
#include "api/api_common.h"
#include "api/api_sending.h"
#include "apiwrap.h"
#include "base/call_delayed.h"
#include "base/base_file_utilities.h"
#include "base/qthelp_url.h"
#include "base/random.h"
#include "base/timer_rpl.h"
#include "base/unixtime.h"
#include "boxes/peers/choose_peer_box.h"
#include "boxes/peers/create_managed_bot_box.h"
#include "boxes/peer_list_controllers.h"
#include "boxes/premium_preview_box.h"
#include "boxes/share_box.h"
#include "chat_helpers/stickers_lottie.h"
#include "chat_helpers/tabbed_panel.h"
#include "core/application.h"
#include "core/click_handler_types.h"
#include "core/local_url_handlers.h"
#include "core/shortcuts.h"
#include "core/ui_integration.h" // TextContext
#include "data/components/location_pickers.h"
#include "data/data_bot_app.h"
#include "data/data_changes.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_emoji_statuses.h"
#include "data/data_file_origin.h"
#include "data/data_peer_bot_command.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "data/data_web_page.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/stickers/data_stickers.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_helpers.h"
#include "history/history_item_reply_markup.h"
#include "info/bot/starref/info_bot_starref_common.h" // MakePeerBubbleButton
#include "info/profile/info_profile_values.h"
#include "inline_bots/inline_bot_result.h"
#include "inline_bots/inline_bot_confirm_prepared.h"
#include "inline_bots/inline_bot_downloads.h"
#include "inline_bots/inline_bot_storage.h"
#ifdef TDESKTOP_IV_EDITOR
#include "iv/editor/iv_editor_session.h"
#endif // TDESKTOP_IV_EDITOR
#include "iv/iv_instance.h"
#include "lang/lang_keys.h"
#include "main/main_app_config.h"
#include "main/main_domain.h"
#include "main/main_session.h"
#include "mainwidget.h"
#include "payments/payments_checkout_process.h"
#include "payments/payments_non_panel_process.h"
#include "settings/sections/settings_premium.h"
#include "storage/storage_account.h"
#include "storage/storage_domain.h"
#include "ui/basic_click_handlers.h"
#include "ui/boxes/confirm_box.h"
#include "ui/chat/attach/attach_bot_webview.h"
#include "ui/controls/location_picker.h"
#include "ui/controls/userpic_button.h"
#include "ui/effects/ripple_animation.h"
#include "ui/painter.h"
#include "ui/text/text_custom_emoji.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/vertical_list.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/dropdown_menu.h"
#include "ui/widgets/menu/menu_item_base.h"
#include "ui/widgets/popup_menu.h"
#include "webview/webview_interface.h"
#include "window/themes/window_theme.h"
#include "window/window_controller.h"
#include "window/window_peer_menu.h"
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
#include "styles/style_channel_earn.h"
#include "styles/style_chat.h"
#include "styles/style_info.h" // infoVerifiedStar.
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
#include "styles/style_window.h"

#include <QSvgRenderer>

namespace InlineBots {
namespace {

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

[[nodiscard]] QImage PaintButtonEmojiFrame(
		Ui::Text::CustomEmoji &emoji,
		const QColor &textColor,
		int size) {
	const auto ratio = style::DevicePixelRatio();
	auto image = QImage(
		QSize(size, size) * ratio,
		QImage::Format_ARGB32_Premultiplied);
	image.setDevicePixelRatio(ratio);
	image.fill(Qt::transparent);
	auto painter = Painter(&image);
	emoji.paint(painter, Ui::Text::CustomEmoji::Context{
		.textColor = textColor,
		.size = QSize(size, size),
		.now = crl::now(),
		.position = QPoint(0, 0),
	});
	return image;
}

[[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 = (data.vpeer_types()
					? ResolvePeerTypes(data.vpeer_types()->v)
					: PeerTypes()),
				.inactive = data.is_inactive(),
				.inMainMenu = data.is_show_in_side_menu(),
				.inAttachMenu = data.is_show_in_attach_menu(),
				.disclaimerRequired = data.is_side_menu_disclaimer_needed(),
				.requestWriteAccess = data.is_request_write_access(),
			} : std::optional<AttachWebViewBot>();
	});
	if (const auto icon = result ? result->icon : nullptr) {
		icon->forceToCache(true);

		result->media = icon->createMediaView();
		icon->save(Data::FileOrigin(), {});
	}
	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;
}

[[nodiscard]] QString JoinChatDecisionToast(
		not_null<Main::Session*> session,
		PeerId peerId,
		const MTPJoinChatBotResult &result) {
	const auto isChannel = [&] {
		if (const auto peer = session->data().peerLoaded(peerId)) {
			return peer->isBroadcast();
		}
		return false;
	}();
	return result.match([&](const MTPDjoinChatBotResultApproved &) {
		return isChannel
			? tr::lng_action_you_joined_by_request_channel(tr::now)
			: tr::lng_action_you_joined_by_request(tr::now);
	}, [&](const MTPDjoinChatBotResultDeclined &) {
		return isChannel
			? tr::lng_group_request_declined_channel(tr::now)
			: tr::lng_group_request_declined(tr::now);
	}, [&](const MTPDjoinChatBotResultQueued &) {
		return isChannel
			? tr::lng_group_request_sent_channel(tr::now)
			: tr::lng_group_request_sent(tr::now);
	}, [&](const MTPDjoinChatBotResultWebView &) {
		return QString();
	});
}

[[nodiscard]] Ui::LocationPickerConfig ResolveMapsConfig(
		not_null<Main::Session*> session) {
	//const auto &appConfig = session->appConfig();
	//auto map = appConfig.get<base::flat_map<QString, QString>>(
	//	u"tdesktop_config_map"_q,
	//	base::flat_map<QString, QString>());
	return {
		.mapsToken = u"pk.eyJ1Ijoiam9obi1wcmVzdG9uIiwiYSI6ImNseTVod2Y3MDBiczMyanM3d3E3NXloM3kifQ.K12vn1eHYqFAoqVWvnMXiA"_q,
		.geoToken = u"pk.eyJ1Ijoiam9obi1wcmVzdG9uIiwiYSI6ImNseWg0OWpvNTAwa3AycnF5ZDM3a2dkYmUifQ.n12eA4c3AygrB9yinAp2Ww"_q,
	};
}

[[nodiscard]] Window::SessionController *WindowForThread(
		base::weak_ptr<Window::SessionController> weak,
		not_null<Data::Thread*> thread) {
	if (const auto separate = Core::App().separateWindowFor(thread)) {
		return separate->sessionController();
	}
	const auto strong = weak.get();
	if (strong && strong->windowId().hasChatsList()) {
		strong->showThread(thread);
		return strong;
	}
	const auto window = Core::App().ensureSeparateWindowFor(thread);
	return window ? window->sessionController() : nullptr;
}

void ShowChooseBox(
		std::shared_ptr<Ui::Show> show,
		not_null<Main::Session*> session,
		PeerTypes types,
		Fn<void(not_null<Data::Thread*>)> callback,
		rpl::producer<QString> titleOverride = nullptr) {
	const auto weak = std::make_shared<base::weak_qptr<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 = show->show(Box<PeerListBox>(
		std::make_unique<ChooseRecipientBoxController>(
			session,
			std::move(done),
			std::move(filter)),
		std::move(initBox)));
}

void ShowChooseBox(
		not_null<Window::SessionController*> controller,
		PeerTypes types,
		Fn<void(not_null<Data::Thread*>)> callback,
		rpl::producer<QString> titleOverride = nullptr) {
	ShowChooseBox(
		controller->uiShow(),
		&controller->session(),
		types,
		std::move(callback),
		std::move(titleOverride));
}

void FillDisclaimerBox(
		not_null<Ui::GenericBox*> box,
		Fn<void(bool accepted)> done) {
	const auto updateCheck = std::make_shared<Fn<void()>>();
	const auto validateCheck = std::make_shared<Fn<bool()>>();

	const auto callback = [=](Fn<void()> close) {
		if (validateCheck && (*validateCheck)()) {
			done(true);
			close();
		}
	};

	const auto padding = st::boxRowPadding;
	Ui::ConfirmBox(box, {
		.text = tr::lng_mini_apps_disclaimer_text(
			tr::now,
			tr::rich),
		.confirmed = callback,
		.cancelled = [=](Fn<void()> close) { done(false); close(); },
		.confirmText = tr::lng_box_ok(),
		.labelPadding = QMargins(padding.left(), 0, padding.right(), 0),
		.title = tr::lng_mini_apps_disclaimer_title(),
	});

	auto checkView = std::make_unique<Ui::CheckView>(
		st::defaultCheck,
		false,
		[=] { if (*updateCheck) { (*updateCheck)(); } });
	const auto check = checkView.get();
	const auto row = box->addRow(
		object_ptr<Ui::Checkbox>(
			box.get(),
			tr::lng_mini_apps_disclaimer_button(
				lt_link,
				rpl::single(tr::link(
					tr::lng_mini_apps_disclaimer_link(tr::now),
					tr::lng_mini_apps_tos_url(tr::now))),
				tr::marked),
			st::urlAuthCheckbox,
			std::move(checkView)),
		{
			st::boxRowPadding.left(),
			st::boxRowPadding.left(),
			st::boxRowPadding.right(),
			0,
		});
	row->setAllowTextLines();
	row->setClickHandlerFilter([=](
			const ClickHandlerPtr &link,
			Qt::MouseButton button) {
		ActivateClickHandler(row, link, ClickContext{
			.button = button,
			.other = QVariant::fromValue(ClickHandlerContext{
				.show = box->uiShow(),
			})
		});
		return false;
	});

	(*updateCheck) = [=] { row->update(); };

	const auto showError = Ui::CheckView::PrepareNonToggledError(
		check,
		box->lifetime());

	(*validateCheck) = [=] {
		if (check->checked()) {
			return true;
		}
		showError();
		return false;
	};
}

WebViewContext ResolveContext(
		not_null<UserData*> bot,
		WebViewContext context) {
	if (!context.dialogsEntryState.key) {
		if (const auto strong = context.controller.get()) {
			context.dialogsEntryState = strong->dialogsEntryStateCurrent();
		}
	}
	if (!context.action) {
		const auto &state = context.dialogsEntryState;
		if (const auto thread = state.key.thread()) {
			context.action = Api::SendAction(thread);
			context.action->replyTo = state.currentReplyTo;
			context.action->options.suggest = state.currentSuggest;
		} else {
			context.action = Api::SendAction(bot->owner().history(bot));
		}
	}
	if (!context.dialogsEntryState.key) {
		using namespace Dialogs;
		using Section = EntryState::Section;
		const auto history = context.action->history;
		const auto topicId = context.action->replyTo.topicRootId;
		const auto topic = history->peer->forumTopicFor(topicId);
		context.dialogsEntryState = EntryState{
			.key = (topic ? Key{ topic } : Key{ history }),
			.section = (topic ? Section::Replies : Section::History),
			.currentReplyTo = context.action->replyTo,
			.currentSuggest = context.action->options.suggest,
		};
	}
	return context;
}

void FillBotUsepic(
		not_null<Ui::GenericBox*> box,
		not_null<PeerData*> bot,
		base::weak_ptr<Window::SessionController> weak) {
	auto aboutLabel = object_ptr<Ui::FlatLabel>(
		box->verticalLayout(),
		tr::lng_allow_bot_webview_details(
			lt_emoji,
			rpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)),
			tr::rich
		) | rpl::map([](TextWithEntities text) {
			return tr::link(std::move(text), u"internal:"_q);
		}),
		st::defaultFlatLabel);
	const auto userpic = Ui::CreateChild<Ui::UserpicButton>(
		box->verticalLayout(),
		bot,
		st::infoPersonalChannelUserpic);
	Ui::AddSkip(box->verticalLayout());
	aboutLabel->setClickHandlerFilter([=](auto &&...) {
		if (const auto strong = weak.get()) {
			strong->showPeerHistory(
				bot->id,
				Window::SectionShow::Way::Forward);
			return true;
		}
		return false;
	});
	const auto title = Ui::CreateChild<Ui::RpWidget>(box->verticalLayout());
	const auto titleLabel = Ui::CreateChild<Ui::FlatLabel>(
		title,
		rpl::single(bot->name()),
		box->getDelegate()->style().title);
	const auto icon = bot->isVerified() ? &st::infoVerifiedStar : nullptr;
	const auto iconCheck = icon
		? &st::infoPeerBadge.verifiedCheck
		: nullptr;
	title->resize(
		titleLabel->width() + (icon ? icon->width() : 0),
		titleLabel->height());
	title->widthValue(
	) | rpl::distinct_until_changed() | rpl::on_next([=](int w) {
		titleLabel->resizeToWidth(w
			- (icon ? icon->width() + st::lineWidth : 0));
	}, title->lifetime());
	if (icon) {
		title->paintRequest() | rpl::on_next([=] {
			auto p = Painter(title);
			p.fillRect(title->rect(), Qt::transparent);
			const auto x = std::min(
				titleLabel->textMaxWidth() + st::lineWidth,
				title->width() - st::lineWidth - icon->width());
			const auto y = (title->height() - icon->height()) / 2;
			const auto w = title->width();
			icon->paint(p, x, y, w);
			iconCheck->paint(p, x, y, w);
		}, title->lifetime());
	}

	Ui::IconWithTitle(box->verticalLayout(), userpic, title, aboutLabel);
}

std::unique_ptr<Ui::RpWidget> MakeEmojiSetStatusPreview(
		not_null<QWidget*> parent,
		not_null<PeerData*> peer,
		not_null<DocumentData*> document) {
	const auto emoji = Ui::CreateChild<Ui::PaddingWrap<Ui::FlatLabel>>(
		parent,
		object_ptr<Ui::FlatLabel>(
			parent,
			rpl::single(
				Ui::Text::SingleCustomEmoji(
					Data::SerializeCustomEmojiId(document->id),
					(document->sticker()
						? document->sticker()->alt
						: QString()))),
			st::botEmojiStatusEmoji,
			st::defaultPopupMenu,
			Core::TextContext({ .session = &peer->session() })),
		style::margins(st::normalFont->spacew, 0, 0, 0));
	emoji->entity()->resizeToWidth(emoji->entity()->textMaxWidth());

	auto result = Info::BotStarRef::MakePeerBubbleButton(
		parent,
		peer,
		emoji);
	result->setAttribute(Qt::WA_TransparentForMouseEvents);
	return result;
}

bool CheckEmojiStatusPremium(not_null<UserData*> bot) {
	if (bot->session().premium()) {
		return true;
	}
	const auto window = ChatHelpers::ResolveWindowDefault()(
		&bot->session());
	if (window) {
		ShowPremiumPreviewBox(window, PremiumFeature::EmojiStatus);
		window->window().activate();
	}
	return false;
}

void ConfirmEmojiStatusAccessBox(
		not_null<Ui::GenericBox*> box,
		not_null<UserData*> bot,
		Fn<void(bool)> done) {
	box->setNoContentMargin(true);

	const auto set = box->lifetime().make_state<bool>();

	box->addTopButton(st::boxTitleClose, [=] {
		box->closeBox();
	});

	AddSkip(box->verticalLayout(), 4 * st::defaultVerticalListSkip);

	const auto statusIcon = ChatHelpers::GenerateLocalTgsSticker(
		&bot->session(),
		u"hello_status"_q,
		true);

	auto ownedSet = MakeEmojiSetStatusPreview(
		box,
		bot->session().user(),
		statusIcon);
	box->addRow(
		object_ptr<Ui::RpWidget>::fromRaw(ownedSet.release()));

	AddSkip(box->verticalLayout(), 2 * st::defaultVerticalListSkip);

	auto name = tr::bold(bot->name());
	box->addRow(
		object_ptr<Ui::FlatLabel>(
			box,
			tr::lng_bot_emoji_status_access_text(
				lt_bot,
				rpl::single(name),
				lt_name,
				rpl::single(name),
				tr::rich),
			st::botEmojiStatusText),
		style::al_top);

	box->addButton(tr::lng_bot_emoji_status_access_allow(), [=] {
		if (!CheckEmojiStatusPremium(bot)) {
			return;
		}
		*set = true;
		box->closeBox();
		done(true);
	});
	box->addButton(tr::lng_cancel(), [=] {
		const auto was = *set;
		box->closeBox();
		if (!was) {
			done(false);
		}
	});
}

void ConfirmEmojiStatusBox(
		not_null<Ui::GenericBox*> box,
		not_null<UserData*> bot,
		not_null<DocumentData*> document,
		TimeId duration,
		Fn<void(bool)> done) {
	box->setNoContentMargin(true);

	auto owned = Settings::MakeEmojiStatusPreview(box, document);
	const auto preview = box->addRow(
		object_ptr<Ui::RpWidget>::fromRaw(owned.release()));
	preview->resize(preview->width(), st::botEmojiStatusPreviewHeight);

	const auto set = box->lifetime().make_state<bool>();

	box->addTopButton(st::boxTitleClose, [=] {
		box->closeBox();
	});

	box->addRow(
		object_ptr<Ui::FlatLabel>(
			box,
			tr::lng_bot_emoji_status_title(),
			st::botEmojiStatusTitle),
		style::al_top);
	AddSkip(box->verticalLayout());

	box->addRow(
		object_ptr<Ui::FlatLabel>(
			box,
			tr::lng_bot_emoji_status_text(
				lt_bot,
				rpl::single(tr::bold(bot->name())),
				tr::rich),
			st::botEmojiStatusText),
		style::al_top);

	AddSkip(box->verticalLayout(), 2 * st::defaultVerticalListSkip);

	auto ownedSet = MakeEmojiSetStatusPreview(
		box,
		document->session().user(),
		document);
	box->addRow(
		object_ptr<Ui::RpWidget>::fromRaw(ownedSet.release()));

	box->addButton(tr::lng_bot_emoji_status_confirm(), [=] {
		if (!CheckEmojiStatusPremium(bot)) {
			return;
		}
		document->owner().emojiStatuses().set(
			{ document->id },
			duration ? (base::unixtime::now() + duration) : 0);
		*set = true;
		box->closeBox();
		done(true);
	});
	box->addButton(tr::lng_cancel(), [=] {
		box->closeBox();
	});
	box->boxClosing() | rpl::on_next([=] {
		if (!*set) {
			done(false);
		}
	}, box->lifetime());
}

class BotAction final : public Ui::Menu::ItemBase {
public:
	BotAction(
		not_null<Ui::Menu::Menu*> parent,
		std::shared_ptr<Ui::Show> show,
		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 paint(Painter &p);

	const std::shared_ptr<Ui::Show> _show;
	const not_null<QAction*> _dummyAction;
	const style::Menu &_st;
	const AttachWebViewBot _bot;

	MenuBotIcon _icon;

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

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

};

BotAction::BotAction(
	not_null<Ui::Menu::Menu*> parent,
	std::shared_ptr<Ui::Show> show,
	const style::Menu &st,
	const AttachWebViewBot &bot,
	Fn<void()> callback)
: ItemBase(parent, st)
, _show(std::move(show))
, _dummyAction(Ui::CreateChild<QAction>(parent))
, _st(st)
, _bot(bot)
, _icon(this, _bot.media)
, _height(_st.itemPadding.top()
		+ _st.itemStyle.font->height
		+ _st.itemPadding.bottom()) {
	setAcceptBoth(false);
	fitToMenuWidth();
	setActionTriggered(std::move(callback));

	_icon.move(_st.itemIconPosition);

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

	enableMouseSelecting();
	prepare();
}

void BotAction::paint(Painter &p) {
	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);
	}

	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), [=] {
		const auto bot = _bot.user;
		bot->session().attachWebView().removeFromMenu(_show, bot);
	}, &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

WebViewResultData ParseWebViewResult(const MTPWebViewResult &result) {
	const auto &data = result.data();
	return {
		.url = qs(data.vurl()),
		.queryId = data.vquery_id().value_or_empty(),
		.fullscreen = data.is_fullscreen(),
		.fullsize = data.is_fullsize(),
		.sameOrigin = data.is_same_origin(),
	};
}

base::weak_ptr<WebViewInstance> WebViewInstance::PendingActivation;

MenuBotIcon::MenuBotIcon(
	QWidget *parent,
	std::shared_ptr<Data::DocumentMedia> media)
: RpWidget(parent)
, _media(std::move(media)) {
	style::PaletteChanged(
	) | rpl::on_next([=] {
		_image = QImage();
		update();
	}, lifetime());

	setAttribute(Qt::WA_TransparentForMouseEvents);
	resize(st::menuIconAdmin.size());
	show();
}

void MenuBotIcon::paintEvent(QPaintEvent *e) {
	validate();
	if (!_image.isNull()) {
		QPainter(this).drawImage(0, 0, _image);
	}
}

void MenuBotIcon::validate() {
	const auto ratio = style::DevicePixelRatio();
	const auto wanted = size() * ratio;
	if (_mask.size() != wanted) {
		if (!_media || !_media->loaded()) {
			return;
		}
		auto icon = QSvgRenderer(_media->bytes());
		_mask = QImage(wanted, QImage::Format_ARGB32_Premultiplied);
		_mask.setDevicePixelRatio(style::DevicePixelRatio());
		_mask.fill(Qt::transparent);
		if (icon.isValid()) {
			auto p = QPainter(&_mask);
			icon.render(&p, rect());
			p.end();

			_mask = Images::Colored(std::move(_mask), Qt::white);
		}
	}
	if (_image.isNull()) {
		_image = style::colorizeImage(_mask, st::menuIconColor);
	}
}

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;
}

WebViewInstance::WebViewInstance(WebViewDescriptor &&descriptor)
: _parentShow(descriptor.parentShow
	? std::move(descriptor.parentShow)
	: descriptor.context.controller
	? descriptor.context.controller.get()->uiShow()
	: nullptr)
, _session(&descriptor.bot->session())
, _bot(descriptor.bot)
, _context(ResolveContext(_bot, std::move(descriptor.context)))
, _button(std::move(descriptor.button))
, _source(std::move(descriptor.source)) {
	Expects(_parentShow != nullptr);

	resolve();
}

WebViewInstance::~WebViewInstance() {
	_session->api().request(base::take(_requestId)).cancel();
	_session->api().request(base::take(_prolongId)).cancel();
	base::take(_panel);
}

Main::Session &WebViewInstance::session() const {
	return *_session;
}

not_null<UserData*> WebViewInstance::bot() const {
	return _bot;
}

WebViewSource WebViewInstance::source() const {
	return _source;
}

void WebViewInstance::activate() {
	if (_panel) {
		_panel->requestActivate();
	} else {
		PendingActivation = this;
	}
}

void WebViewInstance::requestFullBot() {
	if (_bot->isFullLoaded()) {
		return;
	}
	_bot->updateFull();
	_bot->session().changes().peerUpdates(
		_bot,
		Data::PeerUpdate::Flag::FullInfo
	) | rpl::on_next([=] {
		if (_botFullWaitingArgs.has_value()) {
			auto args = *base::take(_botFullWaitingArgs);
			if (args.result.url.isEmpty()) {
				showGame();
			} else {
				show(std::move(args));
			}
		}
	}, _lifetime);
}

void WebViewInstance::resolve() {
	requestFullBot();
	v::match(_source, [&](WebViewSourceButton data) {
		confirmOpen([=] {
			if (data.simple) {
				requestSimple();
			} else {
				requestButton();
			}
		});
	}, [&](WebViewSourceSwitch) {
		confirmOpen([=] {
			requestSimple();
		});
	}, [&](WebViewSourceLinkApp data) {
		resolveApp(
			data.appname,
			data.token,
			(_context.maySkipConfirmation
				? ConfirmType::None
				: ConfirmType::Always));
	}, [&](WebViewSourceLinkBotProfile) {
		confirmOpen([=] {
			requestMain();
		}, !_context.maySkipConfirmation);
	}, [&](WebViewSourceLinkAttachMenu data) {
		requestWithMenuAdd();
	}, [&](WebViewSourceMainMenu) {
		requestWithMainMenuDisclaimer();
	}, [&](WebViewSourceAttachMenu) {
		requestWithMenuAdd();
	}, [&](WebViewSourceBotMenu) {
		if (!openAppFromBotMenuLink()) {
			confirmOpen([=] {
				requestButton();
			});
		}
	}, [&](WebViewSourceGame game) {
		showGame();
	}, [&](WebViewSourceBotProfile) {
		if (_context.maySkipConfirmation) {
			requestMain();
		} else {
			confirmOpen([=] {
				requestMain();
			});
		}
	}, [&](WebViewSourceAgeVerification) {
		requestMain();
	}, [&](WebViewSourceJoinChat data) {
		confirmOpen([=] {
			show({
				.result = data.result,
			});
		}, true);
	});
}

bool WebViewInstance::openAppFromBotMenuLink() {
	const auto url = QString::fromUtf8(_button.url);
	const auto local = Core::TryConvertUrlToLocal(url);
	const auto prefix = u"tg://resolve?"_q;
	if (!local.startsWith(prefix)) {
		return false;
	}
	const auto params = qthelp::url_parse_params(
		local.mid(prefix.size()),
		qthelp::UrlParamNameTransform::ToLower);
	const auto domainParam = params.value(u"domain"_q);
	const auto appnameParam = params.value(u"appname"_q);
	const auto webChannelPreviewLink = (domainParam == u"s"_q)
		&& !appnameParam.isEmpty();
	const auto appname = webChannelPreviewLink ? QString() : appnameParam;
	if (appname.isEmpty()) {
		return false;
	}
	resolveApp(appname, params.value(u"startapp"_q), ConfirmType::Once);
	return true;
}

void WebViewInstance::resolveApp(
		const QString &appname,
		const QString &startparam,
		ConfirmType confirmType) {
	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;
		const auto &data = result.data();
		const auto received = _session->data().processBotApp(
			_bot->id,
			data.vapp());
		_app = received ? received : already;
		_appStartParam = startparam;
		if (!_app) {
			_parentShow->showToast(tr::lng_username_app_not_found(tr::now));
			close();
			return;
		}
		const auto confirm = data.is_inactive()
			|| (confirmType != ConfirmType::None);
		const auto writeAccess = result.data().is_request_write_access();
		const auto forceConfirmation = data.is_inactive()
			|| (confirmType == ConfirmType::Always);

		// Check if this app can be added to main menu.
		// On fail it'll still be opened.
		using Result = AttachWebView::AddToMenuResult;
		const auto done = crl::guard(this, [=](Result value, auto) {
			if (value == Result::Cancelled) {
				close();
			} else if (value != Result::Unsupported) {
				requestApp(true);
			} else if (confirm) {
				confirmAppOpen(writeAccess, [=](bool allowWrite) {
					requestApp(allowWrite);
				}, forceConfirmation);
			} else {
				requestApp(false);
			}
		});
		_session->attachWebView().requestAddToMenu(_bot, done);
	}).fail([=] {
		_parentShow->showToast(tr::lng_username_app_not_found(tr::now));
		close();
	}).send();
}

void WebViewInstance::confirmOpen(Fn<void()> done, bool forceConfirmation) {
	if (!forceConfirmation
		&& (_bot->isVerified()
			|| _session->local().isPeerTrustedOpenWebView(_bot->id))) {
		done();
		return;
	}
	const auto callback = [=](Fn<void()> close) {
		_session->local().markPeerTrustedOpenWebView(_bot->id);
		close();
		done();
	};
	const auto cancel = [=](Fn<void()> close) {
		botClose();
		close();
	};

	_parentShow->show(Box([=](not_null<Ui::GenericBox*> box) {
		FillBotUsepic(box, _bot, _context.controller);
		Ui::ConfirmBox(box, {
			.text = tr::lng_profile_open_app_about(
				tr::now,
				lt_terms,
				tr::link(
					tr::lng_profile_open_app_terms(tr::now),
					tr::lng_mini_apps_tos_url(tr::now)),
				tr::rich),
			.confirmed = crl::guard(this, callback),
			.cancelled = crl::guard(this, cancel),
			.confirmText = tr::lng_view_button_bot_app(),
		});
	}));
}

void WebViewInstance::confirmAppOpen(
		bool writeAccess,
		Fn<void(bool allowWrite)> done,
		bool forceConfirmation) {
	if (!forceConfirmation
		&& (_bot->isVerified()
			|| _session->local().isPeerTrustedOpenWebView(_bot->id))) {
		done(writeAccess);
		return;
	}
	_parentShow->show(Box([=](not_null<Ui::GenericBox*> box) {
		const auto allowed = std::make_shared<Ui::Checkbox*>();
		const auto callback = [=](Fn<void()> close) {
			_session->local().markPeerTrustedOpenWebView(_bot->id);
			done((*allowed) && (*allowed)->checked());
			close();
		};
		const auto cancelled = [=](Fn<void()> close) {
			botClose();
			close();
		};
		FillBotUsepic(box, _bot, _context.controller);
		Ui::ConfirmBox(box, {
			tr::lng_profile_open_app_about(
				tr::now,
				lt_terms,
				tr::link(
					tr::lng_profile_open_app_terms(tr::now),
					tr::lng_mini_apps_tos_url(tr::now)),
				tr::rich),
			crl::guard(this, callback),
			crl::guard(this, cancelled),
			tr::lng_view_button_bot_app(),
		});
		if (writeAccess) {
			(*allowed) = box->addRow(
				object_ptr<Ui::Checkbox>(
					box,
					tr::lng_url_auth_allow_messages(
						tr::now,
						lt_bot,
						tr::bold(_bot->name()),
						tr::marked),
					true,
					st::urlAuthCheckbox),
				style::margins(
					st::boxRowPadding.left(),
					st::boxPhotoCaptionSkip,
					st::boxRowPadding.right(),
					st::boxPhotoCaptionSkip));
			(*allowed)->setAllowTextLines();
		}
	}));
}

void WebViewInstance::requestButton() {
	Expects(_context.action.has_value());

	const auto &action = *_context.action;
	using Flag = MTPmessages_RequestWebView::Flag;
	_requestId = _session->api().request(MTPmessages_RequestWebView(
		MTP_flags(Flag::f_theme_params
			| (_context.fullscreen ? Flag::f_fullscreen : Flag(0))
			| (_button.url.isEmpty() ? Flag(0) : Flag::f_url)
			| (_button.startCommand.isEmpty()
				? Flag(0)
				: Flag::f_start_param)
			| (v::is<WebViewSourceBotMenu>(_source)
				? Flag::f_from_bot_menu
				: Flag(0))
			| (action.replyTo ? Flag::f_reply_to : 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_bytes(_button.url),
		MTP_string(_button.startCommand),
		MTP_dataJSON(MTP_bytes(botThemeParams().json)),
		MTP_string("tdesktop"),
		action.mtpReplyTo(),
		(action.options.sendAs
			? action.options.sendAs->input()
			: MTP_inputPeerEmpty())
	)).done([=](const MTPWebViewResult &result) {
		show({
			.result = ParseWebViewResult(result),
		});
	}).fail([=](const MTP::Error &error) {
		_parentShow->showToast(error.type());
		if (error.type() == u"BOT_INVALID"_q) {
			_session->attachWebView().requestBots();
		}
		close();
	}).send();
}

void WebViewInstance::requestSimple() {
	using Flag = MTPmessages_RequestSimpleWebView::Flag;
	_requestId = _session->api().request(MTPmessages_RequestSimpleWebView(
		MTP_flags(Flag::f_theme_params
			| (_context.fullscreen ? Flag::f_fullscreen : Flag(0))
			| (v::is<WebViewSourceSwitch>(_source)
				? (Flag::f_url | Flag::f_from_switch_webview)
				: v::is<WebViewSourceMainMenu>(_source)
				? (Flag::f_from_side_menu
					| (_button.startCommand.isEmpty() // from LinkMainMenu
						? Flag()
						: Flag::f_start_param))
				: Flag::f_url)),
		_bot->inputUser(),
		MTP_bytes(_button.url),
		MTP_string(_button.startCommand),
		MTP_dataJSON(MTP_bytes(botThemeParams().json)),
		MTP_string("tdesktop")
	)).done([=](const MTPWebViewResult &result) {
		show({
			.result = ParseWebViewResult(result),
		});
	}).fail([=](const MTP::Error &error) {
		_parentShow->showToast(error.type());
		close();
	}).send();
}

void WebViewInstance::requestMain() {
	using Flag = MTPmessages_RequestMainWebView::Flag;
	_requestId = _session->api().request(MTPmessages_RequestMainWebView(
		MTP_flags(Flag::f_theme_params
			| (_context.fullscreen ? Flag::f_fullscreen : Flag(0))
			| (_button.startCommand.isEmpty()
						? Flag()
						: Flag::f_start_param)
			| (v::is<WebViewSourceLinkBotProfile>(_source)
				? (v::get<WebViewSourceLinkBotProfile>(_source).compact
					? Flag::f_compact
					: Flag(0))
				: Flag(0))),
		_context.action->history->peer->input(),
		_bot->inputUser(),
		MTP_string(_button.startCommand),
		MTP_dataJSON(MTP_bytes(botThemeParams().json)),
		MTP_string("tdesktop")
	)).done([=](const MTPWebViewResult &result) {
		show({
			.result = ParseWebViewResult(result),
		});
	}).fail([=](const MTP::Error &error) {
		_parentShow->showToast(error.type());
		close();
	}).send();
}

void WebViewInstance::requestApp(bool allowWrite) {
	Expects(_app != nullptr);
	Expects(_context.action.has_value());

	using Flag = MTPmessages_RequestAppWebView::Flag;
	const auto app = _app;
	const auto title = app->title;
	const auto flags = Flag::f_theme_params
		| (_context.fullscreen ? Flag::f_fullscreen : Flag(0))
		| (_appStartParam.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(_appStartParam),
		MTP_dataJSON(MTP_bytes(botThemeParams().json)),
		MTP_string("tdesktop")
	)).done([=](const MTPWebViewResult &result) {
		_requestId = 0;
		show({
			.result = ParseWebViewResult(result),
			.title = title,
		});
	}).fail([=](const MTP::Error &error) {
		_requestId = 0;
		if (error.type() == u"BOT_INVALID"_q) {
			_session->attachWebView().requestBots();
		}
		close();
	}).send();
}

void WebViewInstance::requestWithMainMenuDisclaimer() {
	using Result = AttachWebView::AddToMenuResult;
	const auto done = crl::guard(this, [=](Result value, auto) {
		if (value == Result::Cancelled) {
			close();
		} else if (value == Result::Unsupported) {
			_parentShow->showToast(tr::lng_bot_menu_not_supported(tr::now));
			close();
		} else {
			requestSimple();
		}
	});
	_session->attachWebView().acceptMainMenuDisclaimer(
		_parentShow,
		_bot,
		done);
}

void WebViewInstance::requestWithMenuAdd() {
	using Result = AttachWebView::AddToMenuResult;
	const auto done = crl::guard(this, [=](Result value, PeerTypes types) {
		if (value == Result::Cancelled) {
			close();
		} else if (value == Result::Unsupported) {
			_parentShow->showToast(tr::lng_bot_menu_not_supported(tr::now));
			close();
		} else if (v::is<WebViewSourceLinkAttachMenu>(_source)) {
			maybeChooseAndRequestButton(types);
		} else if (v::is<WebViewSourceAttachMenu>(_source)) {
			requestButton();
		} else {
			requestSimple();
		}
	});
	_session->attachWebView().requestAddToMenu(_bot, done);
}

void WebViewInstance::maybeChooseAndRequestButton(PeerTypes supported) {
	Expects(v::is<WebViewSourceLinkAttachMenu>(_source));

	const auto link = v::get<WebViewSourceLinkAttachMenu>(_source);
	const auto chooseFrom = (link.choose & supported);
	if (!chooseFrom) {
		requestButton();
		return;
	}
	const auto bot = _bot;
	const auto button = _button;
	const auto weak = _context.controller;
	const auto done = [=](not_null<Data::Thread*> thread) {
		if (const auto controller = WindowForThread(weak, thread)) {
			thread->session().attachWebView().open({
				.bot = bot,
				.context = {
					.controller = controller,
					.action = Api::SendAction(thread),
				},
				.button = button,
				.source = InlineBots::WebViewSourceLinkAttachMenu{
					.thread = thread,
					.token = button.startCommand,
				},
			});
		}
	};
	ShowChooseBox(_parentShow, _session, chooseFrom, done);
	close();
}

void WebViewInstance::show(ShowArgs &&args) {
	if (!_bot->isFullLoaded()) {
		_botFullWaitingArgs.emplace(std::move(args));
		return;
	}
	auto title = args.title.isEmpty()
		? Info::Profile::NameValue(_bot)
		: rpl::single(args.title);
	auto titleBadge = Ui::TitleBadgeDescriptor();
	if (_bot->isVerified()) {
		titleBadge.size = st::infoVerifiedStar.size()
			+ QSize(0, st::lineWidth);
		titleBadge.paint = [](QPainter &p, QSize size) {
			const auto w = size.width();
			st::infoVerifiedStar.paint(p, st::lineWidth, 0, w);
			st::infoPeerBadge.verifiedCheck.paint(p, st::lineWidth, 0, w);
		};
	}

	const auto &bots = _session->attachWebView().attachBots();

	using Button = Ui::BotWebView::MenuButton;
	const auto attached = ranges::find(
		bots,
		not_null{ _bot },
		&AttachWebViewBot::user);
	const auto hasOpenBot = v::is<WebViewSourceMainMenu>(_source)
		|| (_context.action && _context.action->history->peer != _bot);
	const auto hasRemoveFromMenu = (attached != end(bots))
		&& (!attached->inactive || attached->inMainMenu)
		&& (v::is<WebViewSourceMainMenu>(_source)
			|| v::is<WebViewSourceAttachMenu>(_source)
			|| v::is<WebViewSourceLinkAttachMenu>(_source));
	const auto buttons = (hasOpenBot ? Button::OpenBot : Button::None)
		| (!hasRemoveFromMenu
			? Button::None
			: attached->inMainMenu
			? Button::RemoveFromMainMenu
			: Button::RemoveFromMenu);
	const auto allowClipboardRead = v::is<WebViewSourceMainMenu>(_source)
		|| v::is<WebViewSourceAttachMenu>(_source)
		|| (attached != end(bots)
			&& (attached->inAttachMenu || attached->inMainMenu));
	const auto downloads = &_session->attachWebView().downloads();
	_panelUrl = args.result.url;
	_panel = Ui::BotWebView::Show({
		.url = args.result.url,
		.storageId = _session->local().resolveStorageIdBots(),
		.title = std::move(title),
		.titleBadge = std::move(titleBadge),
		.bottom = rpl::single('@' + _bot->username()),
		.delegate = static_cast<Ui::BotWebView::Delegate*>(this),
		.menuButtons = buttons,
		.fullscreen = args.result.fullscreen,
		.sameOrigin = args.result.sameOrigin,
		.allowClipboardRead = allowClipboardRead,
		.downloadsProgress = downloads->progress(_bot),
	});
	started(args.result.queryId);

	if (const auto strong = PendingActivation.get()) {
		if (strong == this) {
			PendingActivation = nullptr;
			_panel->requestActivate();
		}
	}
}

void WebViewInstance::showGame() {
	Expects(v::is<WebViewSourceGame>(_source));

	if (!_bot->isFullLoaded()) {
		_botFullWaitingArgs = ShowArgs{};
		return;
	}
	const auto game = v::get<WebViewSourceGame>(_source);
	_panelUrl = QString::fromUtf8(_button.url);
	_panel = Ui::BotWebView::Show({
		.url = _panelUrl,
		.storageId = _session->local().resolveStorageIdBots(),
		.title = rpl::single(game.title),
		.bottom = rpl::single('@' + _bot->username()),
		.delegate = static_cast<Ui::BotWebView::Delegate*>(this),
		.menuButtons = Ui::BotWebView::MenuButton::ShareGame,
	});
}

void WebViewInstance::close() {
	_session->attachWebView().close(this);
}

void WebViewInstance::started(uint64 queryId) {
	if (v::is<WebViewSourceJoinChat>(_source)) {
		if (!queryId) {
			return;
		}
		_session->attachWebView().watchJoinChatWebView(
			queryId,
			_parentShow,
			_context.controller,
			this);
		return;
	}

	Expects(_context.action.has_value());

	if (!queryId) {
		return;
	}

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

	const auto action = *_context.action;
	base::timer_each(
		kProlongTimeout
	) | rpl::on_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 : 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),
			action.mtpReplyTo(),
			(action.options.sendAs
				? action.options.sendAs->input()
				: MTP_inputPeerEmpty())
		)).done([=] {
			_prolongId = 0;
		}).send();
	}, _panel->lifetime());
}

Webview::ThemeParams WebViewInstance::botThemeParams() {
	auto result = Window::Theme::WebViewParams();
	if (const auto info = _bot->botInfo.get()) {
		const auto night = Window::Theme::IsNightMode();
		const auto &title = night
			? info->botAppColorTitleNight
			: info->botAppColorTitleDay;
		const auto &body = night
			? info->botAppColorBodyNight
			: info->botAppColorBodyDay;
		if (title.alpha() == 255) {
			result.titleBg = title;
		}
		if (body.alpha() == 255) {
			result.bodyBg = body;
		}
	}
	return result;
}

Ui::Text::MarkedContext WebViewInstance::botTextContext() {
	return Core::TextContext({ .session = _session });
}

auto WebViewInstance::botDownloads(bool forceCheck)
-> const std::vector<Ui::BotWebView::DownloadsEntry> & {
	return _session->attachWebView().downloads().list(_bot, forceCheck);
}

void WebViewInstance::botDownloadsAction(
		uint32 id,
		Ui::BotWebView::DownloadsAction type) {
	_session->attachWebView().downloads().action(_bot, id, type);
}

bool WebViewInstance::botHandleLocalUri(QString uri, bool keepOpen) {
	const auto local = Core::TryConvertUrlToLocal(uri);
	if (Core::InternalPassportOrOAuthLink(local)) {
		return true;
	} else if (!local.startsWith(u"tg://"_q, Qt::CaseInsensitive)
		&& !local.startsWith(u"tonsite://"_q, Qt::CaseInsensitive)
		&& !local.startsWith(u"ton://"_q, Qt::CaseInsensitive)) {
		return false;
	}
	const auto bot = _bot;
	const auto context = std::make_shared<WebViewContext>(_context);
	if (!keepOpen) {
		botClose();
	}
	crl::on_main([=] {
		if (bot->session().windows().empty()) {
			Core::App().domain().activate(&bot->session().account());
		}
		const auto window = !bot->session().windows().empty()
			? bot->session().windows().front().get()
			: nullptr;
		context->controller = window;
		const auto variant = QVariant::fromValue(ClickHandlerContext{
			.sessionWindow = window,
			.botWebviewContext = context,
		});
		UrlClickHandler::Open(local, variant);
	});
	return true;
}

void WebViewInstance::botHandleInvoice(QString slug) {
	Expects(_panel != nullptr);

	using Result = Payments::CheckoutResult;
	const auto weak = base::make_weak(_panel.get());
	const auto reactivate = [=](Result result) {
		if (const auto strong = weak.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.");
			}());
		}
	};
	Payments::CheckoutProcess::Start(
		_session,
		slug,
		reactivate,
		nonPanelPaymentFormFactory(reactivate));
}

auto WebViewInstance::nonPanelPaymentFormFactory(
	Fn<void(Payments::CheckoutResult)> reactivate)
-> Fn<void(Payments::NonPanelPaymentForm)> {
	using namespace Payments;
	const auto panel = base::make_weak(_panel.get());
	const auto weak = _context.controller;
	return [=](Payments::NonPanelPaymentForm form) {
		using CreditsFormDataPtr = std::shared_ptr<CreditsFormData>;
		using CreditsReceiptPtr = std::shared_ptr<CreditsReceiptData>;
		v::match(form, [&](const CreditsFormDataPtr &form) {
			if (const auto strong = panel.get()) {
				ProcessCreditsPayment(
					uiShow(),
					strong->toastParent().get(),
					form,
					reactivate);
			}
		}, [&](const CreditsReceiptPtr &receipt) {
			if (const auto controller = weak.get()) {
				ProcessCreditsReceipt(controller, receipt, reactivate);
			}
		}, [&](RealFormPresentedNotification) {
			_panel->hideForPayment();
		});
	};
}

void WebViewInstance::botHandleMenuButton(
		Ui::BotWebView::MenuButton button) {
	Expects(_panel != nullptr);

	using Button = Ui::BotWebView::MenuButton;
	const auto bot = _bot;
	switch (button) {
	case Button::OpenBot:
		botClose();
		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:
	case Button::RemoveFromMainMenu: {
		const auto &bots = _session->attachWebView().attachBots();
		const auto attached = ranges::find(
			bots,
			_bot,
			&AttachWebViewBot::user);
		const auto name = (attached != end(bots))
			? attached->name
			: _bot->name();
		const auto done = crl::guard(this, [=] {
			const auto session = _session;
			const auto was = _parentShow;
			botClose();

			const auto active = Core::App().activeWindow();
			const auto show = active ? active->uiShow() : was;
			session->attachWebView().removeFromMenu(show, bot);
			if (active) {
				active->activate();
			}
		});
		const auto main = (button == Button::RemoveFromMainMenu);
		_panel->showBox(Ui::MakeConfirmBox({
			(main
				? tr::lng_bot_remove_from_side_menu_sure
				: tr::lng_bot_remove_from_menu_sure)(
					tr::now,
					lt_bot,
					tr::bold(name),
					tr::marked),
			done,
		}));
	} break;
	case Button::ShareGame: {
		const auto itemId = v::is<WebViewSourceGame>(_source)
			? v::get<WebViewSourceGame>(_source).messageId
			: FullMsgId();
		if (!_panel || !itemId) {
			return;
		} else if (const auto item = _session->data().message(itemId)) {
			FastShareMessage(uiShow(), item);
		} else {
			_panel->showToast({ tr::lng_message_not_found(tr::now) });
		}
	} break;
	}
}

bool WebViewInstance::botValidateExternalLink(QString uri) {
	const auto lower = uri.toLower();
	const auto allowed = _session->appConfig().get<std::vector<QString>>(
		"web_app_allowed_protocols",
		std::vector{ u"http"_q, u"https"_q });
	for (const auto &protocol : allowed) {
		if (lower.startsWith(protocol + u"://"_q)) {
			return true;
		}
	}
	return false;
}

void WebViewInstance::botOpenIvLink(QString uri) {
	const auto window = _context.controller.get();
	if (window) {
		Core::App().iv().openWithIvPreferred(window, uri);
	} else {
		Core::App().iv().openWithIvPreferred(_session, uri);
	}
}

void WebViewInstance::botSendData(QByteArray data) {
	Expects(_context.action.has_value());

	const auto button = std::get_if<WebViewSourceButton>(&_source);
	if (!button
		|| !button->simple
		|| _context.action->history->peer != _bot
		|| _dataSent) {
		return;
	}
	_dataSent = true;
	_session->api().request(MTPmessages_SendWebViewData(
		_bot->inputUser(),
		MTP_long(base::RandomValue<uint64>()),
		MTP_string(_button.text),
		MTP_bytes(data)
	)).done([session = _session](const MTPUpdates &result) {
		session->api().applyUpdates(result);
	}).send();
	botClose();
}

void WebViewInstance::botSwitchInlineQuery(
		std::vector<QString> chatTypes,
		QString query) {
	const auto controller = _context.controller.get();
	const auto types = PeerTypesFromNames(chatTypes);
	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());
	}
	botClose();
}

void WebViewInstance::botCheckWriteAccess(Fn<void(bool allowed)> callback) {
	_session->api().request(MTPbots_CanSendMessage(
		_bot->inputUser()
	)).done([=](const MTPBool &result) {
		callback(mtpIsTrue(result));
	}).fail([=] {
		callback(false);
	}).send();
}

void WebViewInstance::botAllowWriteAccess(Fn<void(bool allowed)> callback) {
	_session->api().request(MTPbots_AllowSendMessage(
		_bot->inputUser()
	)).done([session = _session, callback](const MTPUpdates &result) {
		session->api().applyUpdates(result);
		callback(true);
	}).fail([=] {
		callback(false);
	}).send();
}

bool WebViewInstance::botStorageWrite(
		QString key,
		std::optional<QString> value) {
	return _session->attachWebView().storage().write(_bot->id, key, value);
}

std::optional<QString> WebViewInstance::botStorageRead(QString key) {
	return _session->attachWebView().storage().read(_bot->id, key);
}

void WebViewInstance::botStorageClear() {
	_session->attachWebView().storage().clear(_bot->id);
}

void WebViewInstance::botRequestEmojiStatusAccess(
		Fn<void(bool allowed)> callback) {
	if (_bot->botInfo->canManageEmojiStatus) {
		callback(true);
	} else if (const auto panel = _panel.get()) {
		const auto bot = _bot;
		panel->showBox(Box(ConfirmEmojiStatusAccessBox, bot, [=](bool ok) {
			if (!ok) {
				callback(false);
				return;
			}
			const auto session = &bot->session();
			bot->botInfo->canManageEmojiStatus = true;
			session->api().request(MTPbots_ToggleUserEmojiStatusPermission(
				bot->inputUser(),
				MTP_bool(true)
			)).done([=] {
				callback(true);
			}).fail([=] {
				callback(false);
			}).send();
		}));
	} else {
		callback(false);
	}
}

void WebViewInstance::botSharePhone(Fn<void(bool shared)> callback) {
	const auto history = _bot->owner().history(_bot);
	if (_bot->isBlocked()) {
		const auto done = crl::guard(this, [=](bool success) {
			if (success) {
				botSharePhone(callback);
			} else {
				callback(false);
			}
		});
		_session->api().blockedPeers().unblock(_bot, done);
		return;
	}
	auto action = Api::SendAction(history);
	action.clearDraft = false;
	_session->api().shareContact(
		_session->user(),
		action,
		std::move(callback));
}

void WebViewInstance::botInvokeCustomMethod(
		Ui::BotWebView::CustomMethodRequest request) {
	const auto callback = request.callback;
	_session->api().request(MTPbots_InvokeWebViewCustomMethod(
		_bot->inputUser(),
		MTP_string(request.method),
		MTP_dataJSON(MTP_bytes(request.params))
	)).done([=](const MTPDataJSON &result) {
		callback(result.data().vdata().v);
	}).fail([=](const MTP::Error &error) {
		callback(base::make_unexpected(error.type()));
	}).send();
}

void WebViewInstance::botSendPreparedMessage(
		Ui::BotWebView::SendPreparedMessageRequest request) {
	const auto bot = _bot;
	const auto id = request.id;
	const auto panel = _panel.get();
	const auto weak = base::make_weak(panel);
	const auto callback = request.callback;
	if (!panel) {
		callback(u"UNKNOWN_ERROR"_q);
		return;
	}
	_session->api().request(MTPmessages_GetPreparedInlineMessage(
		bot->inputUser(),
		MTP_string(request.id)
	)).done([=](const MTPmessages_PreparedInlineMessage &result) {
		const auto panel = weak.get();
		const auto &data = result.data();
		bot->owner().processUsers(data.vusers());
		const auto parsed = std::shared_ptr<Result>(Result::Create(
			&bot->session(),
			data.vquery_id().v,
			data.vresult()));
		if (!parsed || !panel) {
			callback(u"UNKNOWN_ERROR"_q);
			return;
		}
		const auto types = PeerTypesFromMTP(data.vpeer_types());
		const auto history = bot->owner().history(bot->session().user());
		const auto item = parsed->makeMessage(history, {
			.id = bot->owner().nextNonHistoryEntryId(),
			.flags = MessageFlag::FakeHistoryItem,
			.from = bot->session().userPeerId(),
			.date = base::unixtime::now(),
			.viaBotId = peerToUser(bot->id),
		});
		struct State {
			base::weak_qptr<Ui::BoxContent> preview;
			base::weak_qptr<Ui::BoxContent> choose;
			rpl::event_stream<not_null<Data::Thread*>> recipient;
			Fn<void(Api::SendOptions)> send;
			SendPaymentHelper sendPayment;
			bool sent = false;
		};
		const auto state = std::make_shared<State>();
		auto recipient = state->recipient.events();
		const auto send = [=](
				std::vector<not_null<Data::Thread*>> list,
				Api::SendOptions options) {
			if (state->sent) {
				return;
			}
			state->sent = true;
			const auto failed = std::make_shared<int>();
			const auto count = int(list.size());
			const auto weak1 = state->preview;
			const auto weak2 = state->choose;
			const auto close = [=] {
				if (const auto strong = weak1.get()) {
					strong->closeBox();
				}
				if (const auto strong = weak2.get()) {
					strong->closeBox();
				}
			};
			const auto done = [=](bool success) {
				if (*failed < 0) {
					return;
				}
				if (success) {
					*failed = -1;
					if (const auto strong2 = weak2.get()) {
						strong2->showToast({ tr::lng_share_done(tr::now) });
					} else if (const auto strong1 = weak1.get()) {
						strong1->showToast({ tr::lng_share_done(tr::now) });
					}
					base::call_delayed(Ui::Toast::kDefaultDuration, close);
					callback(QString());
				} else if (++*failed == count) {
					close();
					callback(u"MESSAGE_SEND_FAILED"_q);
				}
			};
			for (const auto &thread : list) {
				bot->session().api().sendInlineResult(
					bot,
					parsed.get(),
					Api::SendAction(thread, options),
					std::nullopt,
					done);
			}
		};
		auto box = Box(PreparedPreviewBox, item, std::move(recipient), [=] {
			if (state->sent) {
				return;
			}
			const auto chosen = [=](not_null<Data::Thread*> thread) {
				if (!Data::CanSend(thread, ChatRestriction::SendInline)) {
					panel->showToast({
						tr::lng_restricted_send_inline_all(tr::now),
					});
					return false;
				}
				state->recipient.fire_copy(thread);
				return true;
			};
			auto box = Window::PrepareChooseRecipientBox(
				&bot->session(),
				chosen,
				tr::lng_inline_switch_choose(),
				nullptr,
				types,
				send);
			state->choose = box.data();
			panel->showBox(std::move(box));
		}, [=](not_null<Data::Thread*> thread) {
			const auto weak = base::make_weak(thread);
			state->send = [=](Api::SendOptions options) {
				const auto strong = weak.get();
				if (!strong) {
					state->send = nullptr;
					return;
				}
				const auto withPaymentApproved = [=](int stars) {
					if (const auto onstack = state->send) {
						auto copy = options;
						copy.starsApproved = stars;
						onstack(copy);
					}
				};
				const auto checked = state->sendPayment.check(
					uiShow(),
					strong->peer(),
					options,
					1,
					withPaymentApproved);
				if (!checked) {
					return;
				}
				[[maybe_unused]] const auto ongoing = base::take(state->send);
				send({ strong }, options);
			};
			state->send({});
		});
		box->boxClosing() | rpl::on_next([=] {
			if (!state->sent) {
				callback("USER_DECLINED");
			}
		}, box->lifetime());
		state->preview = box.data();
		panel->showBox(std::move(box));
	}).fail([=] {
		callback(u"MESSAGE_EXPIRED"_q);
	}).send();
}

void WebViewInstance::botRequestChat(
		Ui::BotWebView::RequestChatRequest request) {
	const auto bot = _bot;
	const auto callback = request.callback;
	const auto requestId = request.requestId;
	if (!_panel) {
		callback(u"UNKNOWN_ERROR"_q);
		return;
	}
	const auto show = uiShow();
	bot->session().api().request(MTPbots_GetRequestedWebViewButton(
		bot->inputUser(),
		MTP_string(requestId)
	)).done([show, bot, callback, requestId](
			const MTPKeyboardButton &result) {
		result.match([&](const MTPDkeyboardButtonRequestPeer &data) {
			if (!*show) {
				callback(u"UNKNOWN_ERROR"_q);
				return;
			}
			const auto buttonId = data.vbutton_id();
			const auto sendPeers = [=](
					std::vector<not_null<PeerData*>> peers) {
				using Flag = MTPmessages_SendBotRequestedPeer::Flag;
				bot->session().api().request(
					MTPmessages_SendBotRequestedPeer(
						MTP_flags(Flag::f_webapp_req_id),
						bot->input(),
						MTPint(),
						MTP_string(requestId),
						buttonId,
						MTP_vector_from_range(
							peers | ranges::views::transform([](
									not_null<PeerData*> peer) {
								return MTPInputPeer(peer->input());
							})))
				).done([=](const MTPUpdates &result) {
					bot->session().api().applyUpdates(result);
					callback(QString());
				}).fail([callback](const MTP::Error &error) {
					callback(error.type());
				}).send();
			};
			data.vpeer_type().match([&](
					const MTPDrequestPeerTypeCreateBot &createData) {
				ShowCreateManagedBotBox({
					.show = show,
					.manager = bot,
					.suggestedName = qs(
						createData.vsuggested_name().value_or_empty()),
					.suggestedUsername = qs(
						createData.vsuggested_username()
							.value_or_empty()),
					.done = [=](not_null<UserData*> createdBot) {
						sendPeers({ createdBot });
						show->showBox(Ui::MakeInformBox({
							.text = tr::lng_managed_bot_created_text(
								tr::now,
								lt_parent_name,
								bot->name()),
							.title = tr::lng_managed_bot_created_title(
								tr::now,
								lt_name,
								createdBot->name()),
						}));
					},
					.cancelled = [=] {
						callback(u"USER_DECLINED"_q);
					},
				});
			}, [&](const auto &) {
				const auto query = RequestPeerQueryFromTL(data);
				ShowChoosePeerBox(show, bot, query, sendPeers, [=] {
					callback(u"USER_DECLINED"_q);
				});
			});
		}, [&](const auto &) {
			callback(u"UNSUPPORTED_BUTTON_TYPE"_q);
		});
	}).fail([callback](const MTP::Error &error) {
		callback(error.type());
	}).send();
}

void WebViewInstance::botSetEmojiStatus(
		Ui::BotWebView::SetEmojiStatusRequest request) {
	const auto bot = _bot;
	const auto panel = _panel.get();
	const auto callback = request.callback;
	const auto duration = request.duration;
	if (!panel) {
		callback(u"UNKNOWN_ERROR"_q);
		return;
	}
	_session->data().customEmojiManager().resolve(
		request.customEmojiId
	) | rpl::on_next_error([=](not_null<DocumentData*> document) {
		const auto sticker = document->sticker();
		if (!sticker || sticker->setType != Data::StickersType::Emoji) {
			callback(u"SUGGESTED_EMOJI_INVALID"_q);
			return;
		}
		const auto done = [=](bool success) {
			callback(success ? QString() : u"USER_DECLINED"_q);
		};
		panel->showBox(
			Box(ConfirmEmojiStatusBox, bot, document, duration, done));
	}, [=] { callback(u"SUGGESTED_EMOJI_INVALID"_q); }, panel->lifetime());
}

void WebViewInstance::botDownloadFile(
		Ui::BotWebView::DownloadFileRequest request) {
	const auto callback = request.callback;
	if (_confirmingDownload || !_panel) {
		callback(false);
		return;
	}
	_confirmingDownload = true;
	const auto done = [=](QString path) {
		_confirmingDownload = false;
		if (path.isEmpty()) {
			callback(false);
			return;
		}
		_session->attachWebView().downloads().start({
			.bot = _bot,
			.url = request.url,
			.path = path,
		});
		callback(true);
	};
	_session->api().request(MTPbots_CheckDownloadFileParams(
		_bot->inputUser(),
		MTP_string(request.name),
		MTP_string(request.url)
	)).done([=] {
		_panel->showBox(Box(DownloadFileBox, DownloadBoxArgs{
			.session = _session,
			.bot = _bot->name(),
			.name = base::FileNameFromUserString(request.name),
			.url = request.url,
			.done = done,
		}));
	}).fail([=] {
		done(QString());
	}).send();
}

void WebViewInstance::botResolveButtonEmoji(
		Ui::BotWebView::ResolveButtonEmojiRequest request) {
	const auto panel = _panel.get();
	if (!panel || !request.customEmojiId || request.size <= 0) {
		request.callback(QImage());
		return;
	}
	struct State {
		Fn<void(QImage)> callback;
		std::unique_ptr<Ui::Text::CustomEmoji> emoji;
		QColor textColor;
		int size = 0;
		bool sent = false;
	};
	const auto state = std::make_shared<State>();
	state->callback = std::move(request.callback);
	state->textColor = request.textColor.isValid()
		? request.textColor
		: QColor(255, 255, 255);
	state->size = request.size;
	const auto weak = base::make_weak(panel);
	const auto attempt = std::make_shared<Fn<void()>>();
	const auto weakAttempt = std::weak_ptr<Fn<void()>>(attempt);
	const auto weakState = std::weak_ptr<State>(state);
	*attempt = [weak, weakState] {
		const auto state = weakState.lock();
		const auto panel = weak.get();
		if (!state
			|| state->sent
			|| !panel
			|| !state->emoji
			|| !state->emoji->ready()) {
			return;
		}
		state->sent = true;
		state->callback(PaintButtonEmojiFrame(
			*state->emoji,
			state->textColor,
			state->size));
	};
	const auto fail = [state] {
		if (state->sent) {
			return;
		}
		state->sent = true;
		state->callback(QImage());
	};
	_session->data().customEmojiManager().resolve(
		request.customEmojiId
	) | rpl::on_next_error([=](not_null<DocumentData*> document) {
		state->emoji = MakeWrappedEmoji<Ui::Text::FirstFrameEmoji>(
			_session->data().customEmojiManager().create(
				document,
				[weakAttempt] {
					if (const auto attempt = weakAttempt.lock()) {
						(*attempt)();
					}
				},
				Data::CustomEmojiManager::SizeTag::Normal,
				state->size));
		(*attempt)();
	}, fail, panel->lifetime());
}

void WebViewInstance::botVerifyAge(int age) {
	if (v::is<WebViewSourceAgeVerification>(_source)) {
		v::get<WebViewSourceAgeVerification>(_source).done(age);
	}
}

void WebViewInstance::botOpenPrivacyPolicy() {
	const auto bot = _bot;
	const auto weak = _context.controller;
	const auto command = u"privacy"_q;
	const auto findCommand = [=] {
		if (!bot->isBot()) {
			return QString();
		}
		for (const auto &data : bot->botInfo->commands) {
			const auto isSame = !data.command.compare(
				command,
				Qt::CaseInsensitive);
			if (isSame) {
				return data.command;
			}
		}
		return QString();
	};
	const auto makeOtherContext = [=](bool forceWindow) {
		return QVariant::fromValue(ClickHandlerContext{
			.sessionWindow = (forceWindow
				? WindowForThread(weak, bot->owner().history(bot))
				: weak),
			.peer = bot,
		});
	};
	const auto sendCommand = [=] {
		const auto original = findCommand();
		if (original.isEmpty()) {
			return false;
		}
		BotCommandClickHandler('/' + original).onClick(ClickContext{
			Qt::LeftButton,
			makeOtherContext(true)
		});
		return true;
	};
	const auto openUrl = [=](const QString &url) {
		Core::App().iv().openWithIvPreferred(
			_session,
			url,
			makeOtherContext(false));
	};
	if (const auto info = _bot->botInfo.get()) {
		if (!info->privacyPolicyUrl.isEmpty()) {
			openUrl(info->privacyPolicyUrl);
			return;
		}
	}
	if (!sendCommand()) {
		openUrl(tr::lng_profile_bot_privacy_url(tr::now));
	}
}

void WebViewInstance::botClose() {
	crl::on_main(this, [=] { close(); });
}

std::shared_ptr<Main::SessionShow> WebViewInstance::uiShow() {
	class Show final : public Main::SessionShow {
	public:
		explicit Show(not_null<WebViewInstance*> that) : _that(that) {
		}

		void showOrHideBoxOrLayer(
				std::variant<
				v::null_t,
				object_ptr<Ui::BoxContent>,
				std::unique_ptr<Ui::LayerWidget>> &&layer,
				Ui::LayerOptions options,
				anim::type animated) const override {
			using UniqueLayer = std::unique_ptr<Ui::LayerWidget>;
			using ObjectBox = object_ptr<Ui::BoxContent>;
			const auto panel = _that ? _that->_panel.get() : nullptr;
			if (v::is<UniqueLayer>(layer)) {
				Unexpected("Layers in WebView are not implemented.");
			} else if (auto box = std::get_if<ObjectBox>(&layer)) {
				if (panel) {
					panel->showBox(std::move(*box), options, animated);
				}
			} else if (panel) {
				panel->hideLayer(animated);
			}
		}
		[[nodiscard]] not_null<QWidget*> toastParent() const override {
			const auto panel = _that ? _that->_panel.get() : nullptr;

			Ensures(panel != nullptr);
			return panel->toastParent();
		}
		[[nodiscard]] bool valid() const override {
			return _that && (_that->_panel != nullptr);
		}
		operator bool() const override {
			return valid();
		}

		[[nodiscard]] Main::Session &session() const override {
			Expects(_that.get() != nullptr);

			return *_that->_session;
		}

	private:
		const base::weak_ptr<WebViewInstance> _that;

	};
	return std::make_shared<Show>(this);
}

AttachWebView::AttachWebView(not_null<Main::Session*> session)
: _session(session)
, _downloads(std::make_unique<Downloads>(session))
, _storage(std::make_unique<Storage>(session))
, _refreshTimer([=] { requestBots(); }) {
	_refreshTimer.callEach(kRefreshBotsTimeout);
	_session->data().joinChatWebViewDecision(
	) | rpl::on_next([=](
			const Data::Session::JoinChatWebViewDecision &decision) {
		const auto i = _joinChatWebViews.find(decision.queryId);
		if (i == end(_joinChatWebViews)) {
			return;
		}
		auto data = std::move(i->second);
		_joinChatWebViews.erase(i);

		const auto text = JoinChatDecisionToast(
			_session,
			decision.peerId,
			decision.result);
		const auto approved = (decision.result.type()
			== mtpc_joinChatBotResultApproved);
		const auto instance = data.instance.get();
		if (instance) {
			close(instance);
			if (approved) {
				if (const auto peer = _session->data().peerLoaded(
						decision.peerId)) {
					if (const auto window = WindowForThread(
							data.controller,
							peer->owner().history(peer))) {
						window->showPeerHistory(
							peer,
							Window::SectionShow::Way::Forward);
					}
				}
			}
		}
		if (data.show && !text.isEmpty()) {
			data.show->showToast(text);
		}
	}, _lifetime);
}

AttachWebView::~AttachWebView() {
	closeAll();
	_session->api().request(_popularAppBotsRequestId).cancel();
}

void AttachWebView::openByUsername(
		not_null<Window::SessionController*> controller,
		const Api::SendAction &action,
		const QString &botUsername,
		const QString &startCommand,
		bool fullscreen) {
	if (botUsername.isEmpty()
		|| (_botUsername == botUsername
			&& _startCommand == startCommand
			&& _fullScreenRequested == fullscreen)) {
		return;
	}
	cancel();

	_botUsername = botUsername;
	_startCommand = startCommand;
	_fullScreenRequested = fullscreen;
	const auto weak = base::make_weak(controller);
	const auto show = controller->uiShow();
	resolveUsername(show, crl::guard(weak, [=](not_null<PeerData*> peer) {
		_botUsername = QString();
		const auto token = base::take(_startCommand);
		const auto fullscreen = base::take(_fullScreenRequested);

		const auto bot = peer->asUser();
		if (!bot || !bot->isBot()) {
			if (const auto strong = weak.get()) {
				strong->showToast(tr::lng_bot_menu_not_supported(tr::now));
			}
			return;
		}

		open({
			.bot = bot,
			.context = {
				.controller = controller,
				.action = action,
				.fullscreen = fullscreen,
			},
			.button = { .startCommand = token },
			.source = InlineBots::WebViewSourceLinkAttachMenu{},
		});
	}));
}

void AttachWebView::watchJoinChatWebView(
		uint64 queryId,
		std::shared_ptr<Ui::Show> show,
		base::weak_ptr<Window::SessionController> controller,
		base::weak_ptr<WebViewInstance> instance) {
	_joinChatWebViews[queryId] = {
		.show = std::move(show),
		.controller = controller,
		.instance = instance,
	};
}

void AttachWebView::close(not_null<WebViewInstance*> instance) {
	const auto i = ranges::find(
		_instances,
		instance.get(),
		&std::unique_ptr<WebViewInstance>::get);
	if (i != end(_instances)) {
		const auto taken = base::take(*i);
		_instances.erase(i);
	}
}

void AttachWebView::closeAll() {
	cancel();
	base::take(_instances);
}

void AttachWebView::loadPopularAppBots() {
	if (_popularAppBotsLoaded.current() || _popularAppBotsRequestId) {
		return;
	}
	_popularAppBotsRequestId = _session->api().request(
		MTPbots_GetPopularAppBots(
			MTP_string(),
			MTP_int(kPopularAppBotsLimit))
	).done([=](const MTPbots_PopularAppBots &result) {
		_popularAppBotsRequestId = 0;

		const auto &list = result.data().vusers().v;
		auto parsed = std::vector<not_null<UserData*>>();
		parsed.reserve(list.size());
		for (const auto &user : list) {
			const auto bot = _session->data().processUser(user);
			if (bot->isBot()) {
				parsed.push_back(bot);
			}
		}
		_popularAppBots = std::move(parsed);
		_popularAppBotsLoaded = true;
	}).send();
}

auto AttachWebView::popularAppBots() const
-> const std::vector<not_null<UserData*>> & {
	return _popularAppBots;
}

rpl::producer<> AttachWebView::popularAppBotsLoaded() const {
	return _popularAppBotsLoaded.changes() | rpl::to_empty;
}

void AttachWebView::cancel() {
	_session->api().request(base::take(_requestId)).cancel();
	_botUsername = QString();
	_startCommand = QString();
}

void AttachWebView::requestBots(Fn<void()> callback) {
	if (callback) {
		_botsRequestCallbacks.push_back(std::move(callback));
	}
	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)) {
					_attachBots.push_back(std::move(*parsed));
				}
			}
			_attachBotsUpdates.fire({});
		});
		for (const auto &callback : base::take(_botsRequestCallbacks)) {
			callback();
		}
	}).fail([=] {
		_botsRequestId = 0;
		for (const auto &callback : base::take(_botsRequestCallbacks)) {
			callback();
		}
	}).send();
}

bool AttachWebView::disclaimerAccepted(const AttachWebViewBot &bot) const {
	return _disclaimerAccepted.contains(bot.user);
}

bool AttachWebView::showMainMenuNewBadge(
		const AttachWebViewBot &bot) const {
	return bot.inMainMenu
		&& bot.disclaimerRequired
		&& !disclaimerAccepted(bot);
}

void AttachWebView::requestAddToMenu(
		not_null<UserData*> bot,
		Fn<void(AddToMenuResult, PeerTypes supported)> done) {
	auto &process = _addToMenu[bot];
	if (done) {
		process.done.push_back(std::move(done));
	}
	if (process.requestId) {
		return;
	}

	const auto finish = [=](AddToMenuResult result, PeerTypes supported) {
		if (auto process = _addToMenu.take(bot)) {
			for (const auto &done : process->done) {
				done(result, supported);
			}
		}
	};
	if (!bot->isBot() || !bot->botInfo->supportsAttachMenu) {
		finish(AddToMenuResult::Unsupported, {});
		return;
	}

	process.requestId = _session->api().request(
		MTPmessages_GetAttachMenuBot(bot->inputUser())
	).done([=](const MTPAttachMenuBotsBot &result) {
		_addToMenu[bot].requestId = 0;
		const auto &data = result.data();
		_session->data().processUsers(data.vusers());
		const auto parsed = ParseAttachBot(_session, data.vbot());
		if (!parsed || bot != parsed->user) {
			finish(AddToMenuResult::Unsupported, {});
			return;
		}
		const auto i = ranges::find(
			_attachBots,
			not_null(bot),
			&AttachWebViewBot::user);
		if (i != end(_attachBots)) {
			// Save flags in our list, like 'inactive'.
			*i = *parsed;
		}
		const auto types = parsed->types;
		if (parsed->inactive) {
			confirmAddToMenu(*parsed, [=](bool added) {
				const auto result = added
					? AddToMenuResult::Added
					: AddToMenuResult::Cancelled;
				finish(result, types);
			});
		} else {
			requestBots();
			finish(AddToMenuResult::AlreadyInMenu, types);
		}
	}).fail([=] {
		finish(AddToMenuResult::Unsupported, {});
	}).send();
}

void AttachWebView::removeFromMenu(
		std::shared_ptr<Ui::Show> show,
		not_null<UserData*> bot) {
	toggleInMenu(bot, ToggledState::Removed, [=](bool success) {
		if (success) {
			show->showToast(tr::lng_bot_remove_from_menu_done(tr::now));
		}
	});
}

void AttachWebView::resolveUsername(
		std::shared_ptr<Ui::Show> show,
		Fn<void(not_null<PeerData*>)> done) {
	if (const auto peer = _session->data().peerByUsername(_botUsername)) {
		done(peer);
		return;
	}
	_session->api().request(base::take(_requestId)).cancel();
	_requestId = _session->api().request(MTPcontacts_ResolveUsername(
		MTP_flags(0),
		MTP_string(_botUsername),
		MTP_string()
	)).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) {
			show->showToast(
				tr::lng_username_not_found(tr::now, lt_user, _botUsername));
		}
	}).send();
}

void AttachWebView::open(WebViewDescriptor &&descriptor) {
	for (const auto &instance : _instances) {
		if (instance->bot() == descriptor.bot
			&& instance->source() == descriptor.source) {
			instance->activate();
			return;
		}
	}
	_instances.push_back(
		std::make_unique<WebViewInstance>(std::move(descriptor)));
	_instances.back()->activate();
}

void AttachWebView::acceptMainMenuDisclaimer(
		std::shared_ptr<Ui::Show> show,
		not_null<UserData*> bot,
		Fn<void(AddToMenuResult, PeerTypes supported)> done) {
	const auto i = ranges::find(_attachBots, bot, &AttachWebViewBot::user);
	if (i == end(_attachBots)) {
		_attachBotsUpdates.fire({});
		return;
	} else if (i->inactive) {
		requestAddToMenu(bot, std::move(done));
		return;
	} else if (!i->disclaimerRequired || disclaimerAccepted(*i)) {
		done(AddToMenuResult::AlreadyInMenu, i->types);
		return;
	}
	const auto types = i->types;
	show->show(Box(FillDisclaimerBox, crl::guard(this, [=](bool accepted) {
		if (accepted) {
			_disclaimerAccepted.emplace(bot);
			_attachBotsUpdates.fire({});
			done(AddToMenuResult::AlreadyInMenu, types);
		} else {
			done(AddToMenuResult::Cancelled, {});
		}
	})));
}

void AttachWebView::confirmAddToMenu(
		AttachWebViewBot bot,
		Fn<void(bool added)> callback) {
	const auto active = Core::App().activeWindow();
	if (!active) {
		if (callback) {
			callback(false);
		}
		return;
	}
	const auto weak = base::make_weak(active);
	active->show(Box([=](not_null<Ui::GenericBox*> box) {
		const auto allowed = std::make_shared<Ui::Checkbox*>();
		const auto disclaimer = !disclaimerAccepted(bot);
		const auto done = [=](Fn<void()> close) {
			const auto state = (disclaimer
				|| ((*allowed) && (*allowed)->checked()))
				? ToggledState::AllowedToWrite
				: ToggledState::Added;
			toggleInMenu(bot.user, state, [=](bool success) {
				if (callback) {
					callback(success);
				}
				if (const auto strong = weak.get()) {
					strong->showToast((bot.inMainMenu
						? tr::lng_bot_add_to_side_menu_done
						: tr::lng_bot_add_to_menu_done)(tr::now));
				}
			});
			close();
		};
		if (disclaimer) {
			FillDisclaimerBox(box, [=](bool accepted) {
				if (accepted) {
					_disclaimerAccepted.emplace(bot.user);
					_attachBotsUpdates.fire({});
					done([] {});
				} else if (callback) {
					callback(false);
				}
			});
			box->addRow(object_ptr<Ui::FixedHeightWidget>(
				box,
				st::boxRowPadding.left()));
			box->addRow(object_ptr<Ui::FlatLabel>(
				box,
				tr::lng_bot_will_be_added(
					lt_bot,
					rpl::single(tr::bold(bot.name)),
					tr::marked),
				st::boxLabel));
		} else {
			Ui::ConfirmBox(box, {
				(bot.inMainMenu
					? tr::lng_bot_add_to_side_menu
					: tr::lng_bot_add_to_menu)(
						tr::now,
						lt_bot,
						tr::bold(bot.name),
						tr::marked),
				done,
				(callback
					? [=](Fn<void()> close) { callback(false); close(); }
					: Fn<void(Fn<void()>)>()),
			});
			if (bot.requestWriteAccess) {
				(*allowed) = box->addRow(
					object_ptr<Ui::Checkbox>(
						box,
						tr::lng_url_auth_allow_messages(
							tr::now,
							lt_bot,
							tr::bold(bot.name),
							tr::marked),
						true,
						st::urlAuthCheckbox),
					style::margins(
						st::boxRowPadding.left(),
						(disclaimer
							? st::boxPhotoCaptionSkip
							: st::boxRowPadding.left()),
						st::boxRowPadding.right(),
						st::boxRowPadding.left()));
				(*allowed)->setAllowTextLines();
			}
		}
	}));
}

void AttachWebView::toggleInMenu(
		not_null<UserData*> bot,
		ToggledState state,
		Fn<void(bool success)> 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;
		_session->api().request(base::take(_botsRequestId)).cancel();
		requestBots(callback ? [=] { callback(true); } : Fn<void()>());
	}).fail([=] {
		cancel();
		if (callback) {
			callback(false);
		}
	}).send();
}

void ChooseAndSendLocation(
		not_null<Window::SessionController*> controller,
		const Ui::LocationPickerConfig &config,
		Api::SendAction action) {
	const auto weak = base::make_weak(controller);
	const auto session = &controller->session();
	if (const auto picker = session->locationPickers().lookup(action)) {
		picker->activate();
		return;
	}
	struct State {
		SendPaymentHelper sendPayment;
		Fn<void(Data::InputVenue, Api::SendAction)> send;
	};
	const auto state = std::make_shared<State>();
	state->send = [=](Data::InputVenue venue, Api::SendAction action) {
		if (const auto strong = weak.get()) {
			const auto withPaymentApproved = [=](int stars) {
				if (const auto onstack = state->send) {
					auto copy = action;
					copy.options.starsApproved = stars;
					onstack(venue, copy);
				}
			};
			const auto checked = state->sendPayment.check(
				strong,
				action.history->peer,
				action.options,
				1,
				withPaymentApproved);
			if (!checked) {
				return;
			}
		}
		state->send = nullptr;
		if (venue.justLocation()) {
			Api::SendLocation(action, venue.lat, venue.lon);
		} else {
			Api::SendVenue(action, venue);
		}
	};
	const auto callback = [=](Data::InputVenue venue) {
		state->send(venue, action);
	};
	const auto picker = Ui::LocationPicker::Show({
		.parent = controller->widget(),
		.config = config,
		.chooseLabel = tr::lng_maps_point_send(),
		.recipient = action.history->peer,
		.session = session,
		.callback = crl::guard(session, callback),
		.quit = [] { Shortcuts::Launch(Shortcuts::Command::Quit); },
		.storageId = session->local().resolveStorageIdBots(),
		.closeRequests = controller->content()->death(),
	});
	session->locationPickers().emplace(action, picker);
}

std::unique_ptr<Ui::DropdownMenu> MakeAttachBotsMenu(
		not_null<QWidget*> parent,
		not_null<Window::SessionController*> controller,
		not_null<PeerData*> peer,
		Fn<Api::SendAction()> actionFactory,
		Fn<SendMenu::Details()> sendMenuDetails,
		Fn<void(bool)> attach) {
	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, false)) {
		++minimal;
		raw->addAction(tr::lng_attach_document(tr::now), [=] {
			attach(false);
		}, &st::menuIconFile);
	}
	if (peer->canCreatePolls(false)) {
		++minimal;
		raw->addAction(tr::lng_polls_menu_item(tr::now), [=] {
			const auto action = actionFactory();
			const auto source = action.options.scheduled
				? Api::SendType::Scheduled
				: Api::SendType::Normal;
			const auto chosen = kDefaultPollCreateFlags;
			Window::PeerMenuCreatePoll(
				controller,
				peer,
				action.replyTo,
				action.options.suggest,
				chosen,
				PollData::Flags(),
				source,
				sendMenuDetails());
		}, &st::menuIconCreatePoll);
	}
	if (peer->canCreateTodoLists(false)) {
		++minimal;
		raw->addAction(tr::lng_todo_menu_item(tr::now), [=] {
			const auto action = actionFactory();
			const auto source = action.options.scheduled
				? Api::SendType::Scheduled
				: Api::SendType::Normal;
			Window::PeerMenuCreateTodoList(
				controller,
				peer,
				action.replyTo,
				action.options.suggest,
				source,
				sendMenuDetails());
		}, &st::menuIconCreateTodoList);
	}
#ifdef TDESKTOP_IV_EDITOR
	if (Data::CanSendAnyOf(peer, ChatRestriction::SendOther, false)) {
		raw->addAction(tr::lng_article_menu_item(tr::now), [=] {
			Iv::Editor::ShowComposeBox(
				controller,
				peer,
				actionFactory(),
				sendMenuDetails);
		}, &st::menuIconArticle);
	}
#endif // TDESKTOP_IV_EDITOR
	const auto session = &controller->session();
	const auto locationType = ChatRestriction::SendOther;
	const auto config = ResolveMapsConfig(session);
	if (Data::CanSendAnyOf(peer, locationType, false)
		&& Ui::LocationPicker::Available(config)) {
		raw->addAction(tr::lng_maps_point(tr::now), [=] {
			ChooseAndSendLocation(controller, config, actionFactory());
		}, &st::menuIconAddress);
	}
	const auto addBots = Data::CanSend(peer, ChatRestriction::SendInline, false)
		&& !peer->starsPerMessageChecked();
	for (const auto &bot : bots->attachBots()) {
		if (!addBots
			|| !bot.inAttachMenu
			|| !PeerMatchesTypes(peer, bot.user, bot.types)) {
			continue;
		}
		const auto callback = [=] {
			bots->open({
				.bot = bot.user,
				.context = {
					.controller = controller,
					.action = actionFactory(),
				},
				.source = InlineBots::WebViewSourceAttachMenu(),
			});
		};
		auto action = base::make_unique_q<BotAction>(
			raw->menu(),
			controller->uiShow(),
			raw->menu()->st(),
			bot,
			callback);
		action->forceShown(
		) | rpl::on_next([=](bool shown) {
			if (shown) {
				raw->setAutoHiding(false);
			} else {
				raw->hideAnimated();
				raw->setAutoHiding(true);
			}
		}, action->lifetime());
		raw->addAction(std::move(action));
	}
	const auto actions = raw->actions().size();
	const auto onclick = ChatHelpers::ShowPanelOnClick();
	if (!actions) {
		return nullptr;
	} else if (actions <= minimal && !onclick) {
		return nullptr;
	}
	return result;
}

} // namespace InlineBots
