#include "macro-schedule-tab.hpp"
#include "calendar/calendar-event.hpp"
#include "macro-schedule-entry-dialog.hpp"

#include "macro-condition.hpp"
#include "macro-helpers.hpp"
#include "macro-signals.hpp"
#include "obs-module-helper.hpp"
#include "sync-helpers.hpp"
#include "tab-helpers.hpp"
#include "ui-helpers.hpp"

#include <QKeyEvent>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QMenu>
#include <QVBoxLayout>

namespace advss {

// ---------------------------------------------------------------------------
// Tab registration
// ---------------------------------------------------------------------------

static bool registerTab();
static bool registerTabDone = registerTab();

static MacroScheduleTab *tabWidget = nullptr;

static constexpr int MACRO_COUNT_THRESHOLD = 5;

static void setTabVisible(QTabWidget *tab, bool visible)
{
	SetTabVisibleByName(
		tab, visible,
		obs_module_text("AdvSceneSwitcher.macroScheduleTab.title"));
}

static bool enoughMacros()
{
	return (int)GetAllMacros().size() >= MACRO_COUNT_THRESHOLD;
}

static const QStringList schedulingConditionIds = {"date", "timer"};

static bool anyMacroHasSchedulingCondition()
{
	for (const auto &macro : GetAllMacros()) {
		auto conditions = GetMacroConditions(macro.get());
		if (!conditions) {
			continue;
		}
		for (const auto &condition : *conditions) {
			if (schedulingConditionIds.contains(
				    QString::fromStdString(
					    condition->GetId()))) {
				return true;
			}
		}
	}
	return false;
}

static void setupTab(QTabWidget *tab)
{
	if (!GetScheduleEntries().empty() || anyMacroHasSchedulingCondition()) {
		setTabVisible(tab, true);
		return;
	} else {
		setTabVisible(tab, false);
	}

	QWidget::connect(MacroSignalManager::Instance(),
			 &MacroSignalManager::ConditionTypeCreated, tab,
			 [tab](const QString &typeId) {
				 if (!schedulingConditionIds.contains(typeId)) {
					 return;
				 }
				 setTabVisible(tab, true);
			 });
}

static bool registerTab()
{
	AddSetupTabCallback("macroScheduleTab", MacroScheduleTab::Create,
			    setupTab);
	return true;
}

// ---------------------------------------------------------------------------
// Entry -> CalendarEvent conversion
// ---------------------------------------------------------------------------

static QColor ColorForEntry(const MacroScheduleEntry &entry)
{
	if (!entry.enabled || entry.IsExpired()) {
		return QColor(150, 150, 150);
	}
	return entry.color.isValid() ? entry.color : QColor(70, 130, 180);
}

// Returns one CalendarEvent per occurrence of entry within [rangeStart, rangeEnd].
// For non-repeating entries this is at most one event.
// A hard cap of 500 occurrences prevents infinite loops for misconfigured entries.
static QList<CalendarEvent>
EntryToCalendarEvents(const MacroScheduleEntry &entry, const QDate &rangeStart,
		      const QDate &rangeEnd)
{
	QList<CalendarEvent> result;

	const QDateTime windowStart(rangeStart, QTime(0, 0));
	const QDateTime windowEnd(rangeEnd, QTime(23, 59, 59));

	// Walk occurrences starting from the entry's first trigger time.
	// We use a copy to advance lastTriggered without modifying the real entry.
	MacroScheduleEntry cursor = entry;
	cursor.lastTriggered =
		QDateTime(); // start from the very first occurrence
	cursor.timesTriggered = 0;

	static constexpr int MAX_OCCURRENCES = 500;

	for (int i = 0; i < MAX_OCCURRENCES; ++i) {
		const QDateTime next = cursor.NextTriggerTime();
		if (!next.isValid() || next > windowEnd) {
			break;
		}

		if (next >= windowStart) {
			CalendarEvent ev;
			// Use id + occurrence index to make each event unique
			// while still allowing the tab to look up the parent entry.
			ev.id = QString::fromStdString(entry.id);
			ev.title = entry.GetSummary();
			ev.color = ColorForEntry(entry);
			ev.userData = ev.id;
			ev.start = next;
			if (entry.hasEndDate && entry.endDate.isValid()) {
				ev.end = entry.endDate;
			}
			result.append(ev);
		}

		if (!entry.doesRepeat) {
			break; // one-shot — no more occurrences
		}

		// Advance cursor past this occurrence
		cursor.lastTriggered = next;
		++cursor.timesTriggered;

		if (cursor.IsExpired()) {
			break;
		}
	}

	return result;
}

// ---------------------------------------------------------------------------
// Table columns
// ---------------------------------------------------------------------------

static constexpr int COL_NAME = 0;
static constexpr int COL_MACRO = 1;
static constexpr int COL_SCHEDULE = 2;
static constexpr int COL_NEXT_TRIGGER = 3;
static constexpr int COL_STATUS = 4;
static constexpr int COL_COUNT = 5;

// ---------------------------------------------------------------------------
// MacroScheduleTab
// ---------------------------------------------------------------------------

MacroScheduleTab *MacroScheduleTab::Create()
{
	tabWidget = new MacroScheduleTab();
	return tabWidget;
}

MacroScheduleTab::MacroScheduleTab(QWidget *parent)
	: QWidget(parent),
	  _calendar(new CalendarWidget(this)),
	  _table(new QTableWidget(0, COL_COUNT, this)),
	  _helpLabel(new QLabel(
		  obs_module_text("AdvSceneSwitcher.macroScheduleTab.help"),
		  this)),
	  _addBtn(new QToolButton(this)),
	  _editBtn(new QPushButton(
		  obs_module_text("AdvSceneSwitcher.macroScheduleTab.edit"),
		  this)),
	  _removeBtn(new QToolButton(this)),
	  _toggleBtn(new QPushButton(
		  obs_module_text("AdvSceneSwitcher.macroScheduleTab.enable"),
		  this)),
	  _refreshTimer(new QTimer(this))
{
	_addBtn->setProperty("themeID",
			     QVariant(QString::fromUtf8("addIconSmall")));
	_addBtn->setProperty("class", QVariant(QString::fromUtf8("icon-plus")));
	_addBtn->setToolTip(obs_module_text(
		"AdvSceneSwitcher.macroScheduleTab.add.tooltip"));

	_removeBtn->setProperty("themeID",
				QVariant(QString::fromUtf8("removeIconSmall")));
	_removeBtn->setProperty("class",
				QVariant(QString::fromUtf8("icon-trash")));
	_removeBtn->setToolTip(obs_module_text(
		"AdvSceneSwitcher.macroScheduleTab.remove.tooltip"));

	if (GetScheduleEntries().empty()) {
		_addButtonHighlight =
			HighlightWidget(_addBtn, QColor(Qt::green));
	}

	// --- Table ---
	_table->setSelectionBehavior(QAbstractItemView::SelectRows);
	_table->setSelectionMode(QAbstractItemView::ExtendedSelection);
	_table->setContextMenuPolicy(Qt::CustomContextMenu);
	_table->installEventFilter(this);
	_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
	_table->setAlternatingRowColors(true);
	_table->horizontalHeader()->setSectionResizeMode(
		QHeaderView::ResizeToContents);
	_table->horizontalHeader()->setStretchLastSection(true);
	_table->verticalHeader()->setVisible(false);
	_table->setHorizontalHeaderLabels(
		{obs_module_text(
			 "AdvSceneSwitcher.macroScheduleTab.column.name"),
		 obs_module_text(
			 "AdvSceneSwitcher.macroScheduleTab.column.macro"),
		 obs_module_text(
			 "AdvSceneSwitcher.macroScheduleTab.column.schedule"),
		 obs_module_text(
			 "AdvSceneSwitcher.macroScheduleTab.column.nextTrigger"),
		 obs_module_text(
			 "AdvSceneSwitcher.macroScheduleTab.column.status")});

	// --- Help label ---
	_helpLabel->setAlignment(Qt::AlignCenter);
	_helpLabel->setWordWrap(true);

	// --- Button row below the table ---
	auto buttonRow = new QHBoxLayout();
	buttonRow->setContentsMargins(0, 0, 0, 0);
	buttonRow->addWidget(_addBtn);
	buttonRow->addWidget(_removeBtn);
	buttonRow->addStretch();
	buttonRow->addWidget(_editBtn);
	buttonRow->addWidget(_toggleBtn);

	// --- Right panel: help + table + buttons ---
	auto rightPanel = new QWidget(this);
	auto rightLayout = new QVBoxLayout(rightPanel);
	rightLayout->setContentsMargins(0, 0, 0, 0);
	rightLayout->addWidget(_helpLabel);
	rightLayout->addWidget(_table, 1);
	rightLayout->addLayout(buttonRow);
	rightPanel->setLayout(rightLayout);

	// --- Splitter ---
	auto splitter = new QSplitter(Qt::Horizontal, this);
	splitter->addWidget(_calendar);
	splitter->addWidget(rightPanel);
	splitter->setStretchFactor(0, 1);
	splitter->setStretchFactor(1, 1);

	// --- Main layout ---
	auto mainLayout = new QVBoxLayout(this);
	mainLayout->setContentsMargins(4, 4, 4, 4);
	mainLayout->addWidget(splitter, 1);
	setLayout(mainLayout);

	// --- Connections: toolbar buttons ---
	connect(_addBtn, &QPushButton::clicked, this, &MacroScheduleTab::Add);
	connect(_editBtn, &QPushButton::clicked, this, &MacroScheduleTab::Edit);
	connect(_removeBtn, &QPushButton::clicked, this,
		&MacroScheduleTab::Remove);
	connect(_toggleBtn, &QPushButton::clicked, this,
		&MacroScheduleTab::ToggleEnabled);
	connect(_table, &QWidget::customContextMenuRequested, this,
		&MacroScheduleTab::ShowContextMenu);

	// --- Connections: table ---
	connect(_table, &QTableWidget::doubleClicked, this,
		[this]() { Edit(); });
	connect(_table->selectionModel(),
		&QItemSelectionModel::selectionChanged, this,
		&MacroScheduleTab::OnSelectionChanged);

	// --- Connections: calendar ---
	connect(_calendar, &CalendarWidget::SlotClicked, this,
		&MacroScheduleTab::AddAtTime);
	connect(_calendar, &CalendarWidget::EventClicked, this,
		&MacroScheduleTab::SelectTableRowById);
	connect(_calendar, &CalendarWidget::EventDoubleClicked, this,
		&MacroScheduleTab::EditById);
	connect(_calendar, &CalendarWidget::VisibleRangeChanged, this,
		[this](const QDate &, const QDate &) {
			std::deque<MacroScheduleEntry> snapshot;
			{
				auto lock = LockContext();
				snapshot = GetScheduleEntries();
			}
			RefreshCalendar(snapshot);
		});

	_refreshTimer->setInterval(30000);
	connect(_refreshTimer, &QTimer::timeout, this,
		&MacroScheduleTab::Refresh);
	_refreshTimer->start();

	// Defer the initial refresh: Create() is called while the context lock
	// is already held, so we cannot call LockContext() here directly.
	QTimer::singleShot(0, this, &MacroScheduleTab::Refresh);
}

// ---------------------------------------------------------------------------
// Refresh
// ---------------------------------------------------------------------------

void MacroScheduleTab::Refresh()
{
	std::deque<MacroScheduleEntry> snapshot;
	{
		auto lock = LockContext();
		snapshot = GetScheduleEntries();
	}

	PopulateTable();
	RefreshCalendar(snapshot);
	UpdateButtonStates();
}

void MacroScheduleTab::RefreshCalendar(
	const std::deque<MacroScheduleEntry> &entries)
{
	const QDate rangeStart = _calendar->VisibleRangeStart();
	const QDate rangeEnd = _calendar->VisibleRangeEnd();

	QList<CalendarEvent> events;
	for (const auto &entry : entries) {
		events.append(
			EntryToCalendarEvents(entry, rangeStart, rangeEnd));
	}
	_calendar->SetEvents(events);
}

void MacroScheduleTab::PopulateTable()
{
	// Remember which entry was selected so we can restore it after repopulating
	const int prevIdx = SelectedEntryIndex();
	QString prevId;
	std::deque<MacroScheduleEntry> snapshot;
	{
		auto lock = LockContext();
		const auto &entries = GetScheduleEntries();
		if (prevIdx >= 0 && prevIdx < (int)entries.size()) {
			prevId = QString::fromStdString(entries[prevIdx].id);
		}
		snapshot = entries;
	}

	_table->setRowCount(0);

	for (const auto &entry : snapshot) {
		const int row = _table->rowCount();
		_table->insertRow(row);

		auto makeItem = [](const QString &text) {
			auto item = new QTableWidgetItem(text);
			item->setFlags(item->flags() & ~Qt::ItemIsEditable);
			return item;
		};

		_table->setItem(row, COL_NAME, makeItem(entry.GetSummary()));
		_table->setItem(
			row, COL_MACRO,
			makeItem(QString::fromStdString(entry.macro.Name())));
		_table->setItem(row, COL_SCHEDULE,
				makeItem(entry.GetRepeatDescription()));
		_table->setItem(row, COL_NEXT_TRIGGER,
				makeItem(entry.GetNextTriggerString()));

		QString status;
		if (!entry.enabled) {
			status = obs_module_text(
				"AdvSceneSwitcher.macroScheduleTab.status.disabled");
		} else if (entry.IsExpired()) {
			status = obs_module_text(
				"AdvSceneSwitcher.macroScheduleTab.status.expired");
		} else {
			status = obs_module_text(
				"AdvSceneSwitcher.macroScheduleTab.status.active");
		}
		_table->setItem(row, COL_STATUS, makeItem(status));

		_table->item(row, COL_NAME)
			->setData(Qt::UserRole,
				  QString::fromStdString(entry.id));
	}

	SetHelpVisible(_table->rowCount() == 0);

	// Restore selection
	if (!prevId.isEmpty()) {
		SelectTableRowById(prevId);
	}
}

void MacroScheduleTab::SetHelpVisible(bool visible)
{
	_helpLabel->setVisible(visible);
	_table->setVisible(!visible);
}

// ---------------------------------------------------------------------------
// Selection helpers
// ---------------------------------------------------------------------------

int MacroScheduleTab::SelectedEntryIndex() const
{
	const auto selected = _table->selectionModel()->selectedRows();
	if (selected.isEmpty()) {
		return -1;
	}
	const QString id = _table->item(selected.first().row(), COL_NAME)
				   ->data(Qt::UserRole)
				   .toString();
	const auto &entries = GetScheduleEntries();
	for (int i = 0; i < (int)entries.size(); ++i) {
		if (QString::fromStdString(entries[i].id) == id) {
			return i;
		}
	}
	return -1;
}

void MacroScheduleTab::SelectTableRowById(const QString &entryId)
{
	for (int row = 0; row < _table->rowCount(); ++row) {
		auto item = _table->item(row, COL_NAME);
		if (item && item->data(Qt::UserRole).toString() == entryId) {
			_table->selectRow(row);
			_table->scrollToItem(item);
			return;
		}
	}
}

QStringList MacroScheduleTab::SelectedEntryIds() const
{
	QStringList ids;
	for (const auto &index : _table->selectionModel()->selectedRows()) {
		auto item = _table->item(index.row(), COL_NAME);
		if (item) {
			ids.append(item->data(Qt::UserRole).toString());
		}
	}
	return ids;
}

bool MacroScheduleTab::eventFilter(QObject *obj, QEvent *event)
{
	if (obj == _table && event->type() == QEvent::KeyPress) {
		const auto keyEvent = static_cast<QKeyEvent *>(event);
		if (keyEvent->key() == Qt::Key_Delete) {
			Remove();
			return true;
		}
	}
	return QWidget::eventFilter(obj, event);
}

void MacroScheduleTab::UpdateButtonStates()
{
	const QStringList ids = SelectedEntryIds();
	const bool hasSelection = !ids.isEmpty();
	const bool singleSelection = ids.size() == 1;
	_editBtn->setEnabled(singleSelection);
	_removeBtn->setEnabled(hasSelection);
	_toggleBtn->setEnabled(hasSelection);

	if (hasSelection) {
		// Label reflects whether the action will enable or disable:
		// disable if all selected are enabled, otherwise enable.
		bool allEnabled = true;
		{
			auto lock = LockContext();
			for (const auto &entry : GetScheduleEntries()) {
				if (ids.contains(
					    QString::fromStdString(entry.id)) &&
				    !entry.enabled) {
					allEnabled = false;
					break;
				}
			}
		}
		_toggleBtn->setText(obs_module_text(
			allEnabled
				? "AdvSceneSwitcher.macroScheduleTab.disable"
				: "AdvSceneSwitcher.macroScheduleTab.enable"));
	}
}

void MacroScheduleTab::OnSelectionChanged()
{
	UpdateButtonStates();

	// Highlight the corresponding calendar event
	const int idx = SelectedEntryIndex();
	if (idx >= 0) {
		QDate triggerDate;
		{
			auto lock = LockContext();
			const auto &entries = GetScheduleEntries();
			if (idx < (int)entries.size()) {
				const QDateTime next =
					entries[idx].NextTriggerTime();
				if (next.isValid()) {
					triggerDate = next.date();
				}
			}
		}
		if (triggerDate.isValid()) {
			_calendar->GoToDate(triggerDate);
		}
	}
}

// ---------------------------------------------------------------------------
// CRUD actions
// ---------------------------------------------------------------------------

void MacroScheduleTab::Add()
{
	AddAtTime(QDateTime::currentDateTime());
}

void MacroScheduleTab::AddAtTime(const QDateTime &startTime)
{
	MacroScheduleEntry entry;
	entry.startDateTime = startTime;

	if (!MacroScheduleEntryDialog::AskForSettings(this, entry, true)) {
		return;
	}
	const QString newId = QString::fromStdString(entry.id);
	{
		auto lock = LockContext();
		GetScheduleEntries().emplace_back(std::move(entry));
	}
	Refresh();
	SelectTableRowById(newId);

	if (_addButtonHighlight) {
		_addButtonHighlight->deleteLater();
		_addButtonHighlight = nullptr;
	}
}

void MacroScheduleTab::Edit()
{
	const int idx = SelectedEntryIndex();
	if (idx < 0) {
		return;
	}
	MacroScheduleEntry copy;
	{
		auto lock = LockContext();
		copy = GetScheduleEntries()[idx];
	}
	const QString id = QString::fromStdString(copy.id);
	if (!MacroScheduleEntryDialog::AskForSettings(this, copy, false)) {
		return;
	}

	{
		auto lock = LockContext();
		GetScheduleEntries()[idx] = std::move(copy);
	}
	Refresh();
	SelectTableRowById(id);
}

void MacroScheduleTab::EditById(const QString &entryId)
{
	SelectTableRowById(entryId);
	Edit();
}

void MacroScheduleTab::Remove()
{
	const QStringList ids = SelectedEntryIds();
	if (ids.isEmpty()) {
		return;
	}

	// Build a confirmation message
	QString msg;
	if (ids.size() == 1) {
		QString name;
		{
			auto lock = LockContext();
			const int idx = SelectedEntryIndex();
			if (idx >= 0) {
				name = GetScheduleEntries()[idx].GetSummary();
			}
		}
		msg = QString(obs_module_text(
				      "AdvSceneSwitcher.macroScheduleTab.remove.confirm"))
			      .arg(name);
	} else {
		msg = QString(obs_module_text(
				      "AdvSceneSwitcher.macroScheduleTab.remove.confirmMultiple"))
			      .arg(ids.size());
	}

	if (!DisplayMessage(msg, true)) {
		return;
	}

	{
		auto lock = LockContext();
		auto &entries = GetScheduleEntries();
		entries.erase(
			std::remove_if(entries.begin(), entries.end(),
				       [&ids](const MacroScheduleEntry &e) {
					       return ids.contains(
						       QString::fromStdString(
							       e.id));
				       }),
			entries.end());
	}
	Refresh();
}

void MacroScheduleTab::ToggleEnabled()
{
	const QStringList ids = SelectedEntryIds();
	if (ids.isEmpty()) {
		return;
	}

	// If all selected entries are enabled, disable them; otherwise enable all.
	{
		auto lock = LockContext();
		bool allEnabled = true;
		for (const auto &entry : GetScheduleEntries()) {
			if (ids.contains(QString::fromStdString(entry.id)) &&
			    !entry.enabled) {
				allEnabled = false;
				break;
			}
		}
		const bool newState = !allEnabled;
		for (auto &entry : GetScheduleEntries()) {
			if (ids.contains(QString::fromStdString(entry.id))) {
				entry.enabled = newState;
			}
		}
	}
	Refresh();
}

// ---------------------------------------------------------------------------
// Context menu
// ---------------------------------------------------------------------------

void MacroScheduleTab::ShowContextMenu(const QPoint &pos)
{
	const QStringList ids = SelectedEntryIds();
	const bool hasSelection = !ids.isEmpty();
	const bool singleSelection = ids.size() == 1;

	QMenu menu(this);

	auto addAction = menu.addAction(
		obs_module_text("AdvSceneSwitcher.macroScheduleTab.add"), this,
		&MacroScheduleTab::Add);
	(void)addAction;

	menu.addSeparator();

	auto editAction = menu.addAction(
		obs_module_text("AdvSceneSwitcher.macroScheduleTab.edit"), this,
		&MacroScheduleTab::Edit);
	editAction->setEnabled(singleSelection);

	auto removeAction = menu.addAction(
		obs_module_text("AdvSceneSwitcher.macroScheduleTab.remove"),
		this, &MacroScheduleTab::Remove);
	removeAction->setEnabled(hasSelection);

	bool allEnabled = true;
	if (hasSelection) {
		auto lock = LockContext();
		for (const auto &entry : GetScheduleEntries()) {
			if (ids.contains(QString::fromStdString(entry.id)) &&
			    !entry.enabled) {
				allEnabled = false;
				break;
			}
		}
	}
	auto toggleAction = menu.addAction(
		obs_module_text(
			allEnabled
				? "AdvSceneSwitcher.macroScheduleTab.disable"
				: "AdvSceneSwitcher.macroScheduleTab.enable"),
		this, &MacroScheduleTab::ToggleEnabled);
	toggleAction->setEnabled(hasSelection);

	menu.exec(_table->viewport()->mapToGlobal(pos));
}

} // namespace advss
