#include "screens/launchpad/LaunchpadScreen.h"

#include "app/TerminalShell.h"
#include "app/WindowFrame.h"
#include "core/config/ProfileManager.h"
#include "core/keys/WindowCycler.h"
#include "core/layout/LayoutCatalog.h"
#include "core/layout/LayoutTemplates.h"
#include "core/layout/WorkspaceShell.h"
#include "core/logging/Logger.h"
#include "core/profile/ProfilePaths.h"
#include "core/window/WindowRegistry.h"
#include "screens/launchpad/OnboardingTour.h"
#include "storage/repositories/SettingsRepository.h"
#include "storage/workspace/WorkspaceDb.h"
#include "storage/workspace/WorkspaceSnapshotRing.h"
#include "ui/theme/Theme.h"

#include <QApplication>
#include <QCloseEvent>
#include <QEvent>
#include <QFileInfo>
#include <QFrame>
#include <QGridLayout>
#include <QGuiApplication>
#include <QHBoxLayout>
#include <QIcon>
#include <QInputDialog>
#include <QJsonDocument>
#include <QJsonParseError>
#include <QKeyEvent>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QListWidgetItem>
#include <QMessageBox>
#include <QProcess>
#include <QPushButton>
#include <QRegularExpression>
#include <QScreen>
#include <QVBoxLayout>
#include <QWidget>

namespace fincept::screens {

namespace {
constexpr const char* kLaunchpadTag = "Launchpad";

// The Launchpad was styled with a hand-rolled Tailwind-ish palette (#111827,
// #374151, #e5e7eb, #9ca3af …) that appears nowhere else in the terminal, plus
// 4px radii that DESIGN_SYSTEM.md forbids outright. Everything below reads the
// live Obsidian tokens instead so the portal window matches the product.
QString lp_greeting_ss() {
    return QString("color:%1;font-weight:700;letter-spacing:0.5px;background:transparent;").arg(ui::colors::AMBER());
}
QString lp_muted_ss() {
    return QString("color:%1;background:transparent;").arg(ui::colors::TEXT_SECONDARY());
}
QString lp_banner_ss() {
    return QString("QLabel#launchpadCrashBanner{background:%1;color:%2;border:1px solid %3;"
                   "padding:8px 10px;font-weight:600;}")
        .arg(ui::colors::BG_RAISED(), ui::colors::WARNING(), ui::colors::AMBER_DIM());
}
QString lp_primary_btn_ss() {
    return QString("QPushButton#launchpadContinueBtn{background:%1;color:%2;font-weight:700;border:none;}"
                   "QPushButton#launchpadContinueBtn:hover{background:%3;}")
        .arg(ui::colors::AMBER(), ui::colors::BG_BASE(), ui::colors::AMBER_DIM());
}
QString lp_input_ss() {
    return QString("QLineEdit{background:%1;border:1px solid %2;color:%3;padding:4px 6px;}"
                   "QLineEdit:focus{border-color:%4;}")
        .arg(ui::colors::BG_SURFACE(), ui::colors::BORDER_MED(), ui::colors::TEXT_PRIMARY(), ui::colors::AMBER());
}
QString lp_list_ss() {
    return QString("QListWidget{background:%1;border:1px solid %2;color:%3;}"
                   "QListWidget::item{padding:6px 8px;}"
                   "QListWidget::item:hover{background:%4;}"
                   "QListWidget::item:selected{background:%4;color:%5;}")
        .arg(ui::colors::BG_SURFACE(), ui::colors::BORDER_MED(), ui::colors::TEXT_PRIMARY(), ui::colors::BG_HOVER(),
             ui::colors::AMBER());
}
QString lp_card_ss() {
    return QString("QPushButton{background:%1;border:1px solid %2;color:%3;padding:10px;text-align:left;"
                   "font-weight:600;}"
                   "QPushButton:hover{border-color:%4;color:%4;}")
        .arg(ui::colors::BG_SURFACE(), ui::colors::BORDER_MED(), ui::colors::TEXT_PRIMARY(), ui::colors::AMBER());
}
} // namespace

LaunchpadScreen* LaunchpadScreen::instance() {
    // Lazy singleton; lives until process exit. closeEvent calls
    // QCoreApplication::quit() so we don't need WA_DeleteOnClose.
    static LaunchpadScreen* s = new LaunchpadScreen(nullptr);
    return s;
}

LaunchpadScreen::LaunchpadScreen(QWidget* parent) : QMainWindow(parent) {
    setWindowTitle(tr("Fincept Launchpad"));
    setMinimumSize(480, 320);
    resize(480, 320);

    auto* central = new QWidget(this);
    auto* vl = new QVBoxLayout(central);
    vl->setContentsMargins(24, 24, 24, 24);
    vl->setSpacing(16);

    // Window chrome only — deliberately not a blanket `QWidget{}` rule, which
    // would flatten every child that currently relies on its own style.
    central->setObjectName(QStringLiteral("launchpadCentral"));
    central->setStyleSheet(QString("QWidget#launchpadCentral{background:%1;}").arg(ui::colors::BG_BASE()));

    // Greeting text is set by retranslateUi() — it interpolates the active
    // profile name, which can also change at runtime via surface().
    greeting_ = new QLabel;
    greeting_->setStyleSheet(lp_greeting_ss());
    vl->addWidget(greeting_);

    sub_label_ = new QLabel;
    sub_label_->setStyleSheet(lp_muted_ss());
    sub_label_->setWordWrap(true);
    vl->addWidget(sub_label_);

    // L8: crash recovery banner. Shown only when TerminalShell latched
    // started_after_crash at boot. Encourages the user to use Continue (which
    // restores the most recent auto-snapshot from the moments before the
    // crash). Auto-hides on the first navigation away from the Launchpad.
    crash_banner_ = new QLabel;
    crash_banner_->setVisible(false);
    crash_banner_->setWordWrap(true);
    crash_banner_->setObjectName("launchpadCrashBanner");
    crash_banner_->setStyleSheet(lp_banner_ss());
    vl->addWidget(crash_banner_);

    // "Continue from last session" — the most-common user intent on relaunch.
    // One click restores the most-recent saved or auto-snapshot layout.
    // Hidden when nothing restorable exists (first run / fresh profile).
    btn_continue_ = new QPushButton;
    btn_continue_->setMinimumHeight(48);
    btn_continue_->setObjectName("launchpadContinueBtn");
    btn_continue_->setStyleSheet(lp_primary_btn_ss());
    connect(btn_continue_, &QPushButton::clicked, this, &LaunchpadScreen::on_continue);
    vl->addWidget(btn_continue_);

    // Secondary buttons were left unstyled (native chrome on a dark window).
    const QString secondary_ss =
        QString("QPushButton{background:%1;color:%2;border:1px solid %3;font-weight:600;}"
                "QPushButton:hover{background:%4;color:%5;}"
                "QPushButton:disabled{color:%6;border-color:%3;}")
            .arg(ui::colors::BG_RAISED(), ui::colors::TEXT_SECONDARY(), ui::colors::BORDER_MED(),
                 ui::colors::BG_HOVER(), ui::colors::TEXT_PRIMARY(), ui::colors::TEXT_DIM());

    btn_new_window_ = new QPushButton;
    btn_new_window_->setMinimumHeight(36);
    btn_new_window_->setStyleSheet(secondary_ss);
    connect(btn_new_window_, &QPushButton::clicked, this, &LaunchpadScreen::on_new_window);
    vl->addWidget(btn_new_window_);

    btn_open_layout_ = new QPushButton;
    btn_open_layout_->setMinimumHeight(36);
    btn_open_layout_->setStyleSheet(secondary_ss);
    // Enabled state is driven by refresh_recent_layouts() based on whether
    // any saved layouts exist.
    btn_open_layout_->setEnabled(false);
    connect(btn_open_layout_, &QPushButton::clicked, this, &LaunchpadScreen::on_open_layout);
    vl->addWidget(btn_open_layout_);

    btn_switch_profile_ = new QPushButton;
    btn_switch_profile_->setMinimumHeight(36);
    btn_switch_profile_->setStyleSheet(secondary_ss);
    connect(btn_switch_profile_, &QPushButton::clicked, this, &LaunchpadScreen::on_switch_profile);
    vl->addWidget(btn_switch_profile_);

    recent_label_ = new QLabel;
    recent_label_->setStyleSheet(lp_muted_ss() + "margin-top:8px;");
    vl->addWidget(recent_label_);

    // Phase L6: keyboard-first filter. Up/Down/Enter/Esc are intercepted
    // by eventFilter() and forwarded to recent_layouts_ / on_open_layout /
    // close so the user can keep their hands on the keyboard.
    filter_edit_ = new QLineEdit;
    filter_edit_->setPlaceholderText(tr("Type to filter layouts…"));
    filter_edit_->setClearButtonEnabled(true);
    filter_edit_->setStyleSheet(lp_input_ss());
    filter_edit_->setAccessibleName(tr("Filter saved layouts"));
    filter_edit_->installEventFilter(this);
    connect(filter_edit_, &QLineEdit::textChanged, this, [this](const QString& q) {
        if (!recent_layouts_)
            return;
        for (int i = 0; i < recent_layouts_->count(); ++i) {
            auto* it = recent_layouts_->item(i);
            const bool match = q.isEmpty() || it->text().contains(q, Qt::CaseInsensitive);
            it->setHidden(!match);
        }
        // Auto-select the first visible match so Enter Just Works.
        for (int i = 0; i < recent_layouts_->count(); ++i) {
            if (!recent_layouts_->item(i)->isHidden()) {
                recent_layouts_->setCurrentRow(i);
                break;
            }
        }
    });
    vl->addWidget(filter_edit_);

    recent_layouts_ = new QListWidget;
    recent_layouts_->setStyleSheet(lp_list_ss());
    recent_layouts_->setAccessibleName(tr("Recent layouts"));
    vl->addWidget(recent_layouts_, /*stretch=*/1);
    refresh_recent_layouts();

    // First-run template picker — 5 persona cards in a 3×2 grid (last cell empty).
    // Hidden by default; refresh_first_run_picker() shows it when no layouts
    // and no auto snapshot exist.
    template_picker_ = new QWidget;
    template_picker_->setVisible(false);
    auto* picker_root = new QVBoxLayout(template_picker_);
    picker_root->setContentsMargins(0, 0, 0, 0);
    picker_root->setSpacing(8);

    template_picker_label_ = new QLabel;
    template_picker_label_->setStyleSheet(lp_muted_ss() + "margin-top:8px;");
    picker_root->addWidget(template_picker_label_);

    auto* grid = new QGridLayout;
    grid->setHorizontalSpacing(8);
    grid->setVerticalSpacing(8);

    const auto personas = layout::LayoutTemplates::personas();
    for (int i = 0; i < personas.size(); ++i) {
        const auto& p = personas[i];
        auto* card = new QPushButton;
        card->setText(QString("%1\n\n%2").arg(p.display_name, p.description));
        card->setMinimumHeight(80);
        card->setStyleSheet(lp_card_ss());
        card->setAccessibleName(p.display_name);
        card->setAccessibleDescription(p.description);
        const QString persona_id = p.id;
        connect(card, &QPushButton::clicked, this, [this, persona_id]() { on_template_picked(persona_id); });
        grid->addWidget(card, i / 2, i % 2);
    }
    picker_root->addLayout(grid);
    picker_root->addStretch();

    vl->addWidget(template_picker_, /*stretch=*/1);

    setCentralWidget(central);

    retranslateUi();

    // Centre on the primary screen at construction so the user always
    // sees the launchpad in the middle of their main display.
    if (auto* primary = QGuiApplication::primaryScreen()) {
        const QRect avail = primary->availableGeometry();
        move(avail.center() - QPoint(width() / 2, height() / 2));
    }
}

// ── Re-translation ────────────────────────────────────────────────────────────
// The Launchpad is a singleton that persists for the whole session, so it can
// outlive a language change made from inside a WindowFrame. retranslateUi()
// sets every cached static string; surface() invokes refresh_recent_layouts()
// which rebuilds the list rows, so dynamic content picks up new text too.

void LaunchpadScreen::changeEvent(QEvent* event) {
    if (event->type() == QEvent::LanguageChange) {
        retranslateUi();
        // Rebuild the recent-layouts list so its "no saved layouts yet"
        // placeholder (and any future translated parts) re-render.
        refresh_recent_layouts();
    }
    QMainWindow::changeEvent(event);
}

void LaunchpadScreen::retranslateUi() {
    setWindowTitle(tr("Fincept Launchpad"));

    if (greeting_) {
        const QString profile = ProfileManager::instance().active();
        greeting_->setText(tr("Profile: %1").arg(profile));
    }
    if (sub_label_)
        sub_label_->setText(tr("All windows closed. Open a new window or pick a layout below."));
    if (crash_banner_)
        crash_banner_->setText(tr("Last session ended unexpectedly — your work was auto-saved. "
                                  "Click \"Continue from last session\" to restore."));

    if (btn_continue_)
        btn_continue_->setText(tr("Continue from last session"));
    if (btn_new_window_)
        btn_new_window_->setText(tr("New Window"));
    if (btn_open_layout_)
        btn_open_layout_->setText(tr("Open Saved Layout…"));
    if (btn_switch_profile_)
        btn_switch_profile_->setText(tr("Switch Profile…"));

    // Accessible names track the visible label, so refresh them here rather
    // than only at construction.
    if (btn_continue_)
        btn_continue_->setAccessibleName(btn_continue_->text());
    if (btn_new_window_)
        btn_new_window_->setAccessibleName(btn_new_window_->text());
    if (btn_open_layout_)
        btn_open_layout_->setAccessibleName(btn_open_layout_->text());
    if (btn_switch_profile_)
        btn_switch_profile_->setAccessibleName(btn_switch_profile_->text());
    if (filter_edit_)
        filter_edit_->setAccessibleName(tr("Filter saved layouts"));
    if (recent_layouts_)
        recent_layouts_->setAccessibleName(tr("Recent layouts"));

    if (recent_label_)
        recent_label_->setText(tr("Recent Layouts"));
    if (filter_edit_)
        filter_edit_->setPlaceholderText(tr("Type to filter layouts…"));
    if (template_picker_label_)
        template_picker_label_->setText(tr("Pick a starting template:"));
}

void LaunchpadScreen::surface() {
    // Profile may have changed since construction (Switch Profile relaunches
    // a new process today, but this keeps the label honest if that ever
    // becomes in-process). Cheap to refresh on every surface.
    if (greeting_)
        greeting_->setText(tr("Profile: %1").arg(ProfileManager::instance().active()));

    if (isVisible() && !isMinimized()) {
        raise();
        activateWindow();
        return;
    }
    showNormal();
    raise();
    activateWindow();
    refresh_recent_layouts();
    refresh_continue_visibility();
    refresh_first_run_picker();
    refresh_crash_banner();
    // L6: drop focus into the filter line edit so the user can immediately
    // type / arrow / Enter without reaching for the mouse.
    if (filter_edit_ && filter_edit_->isVisible()) {
        filter_edit_->clear();
        filter_edit_->setFocus(Qt::OtherFocusReason);
    }
    LOG_INFO(kLaunchpadTag, "Surfaced");
}

bool LaunchpadScreen::eventFilter(QObject* obj, QEvent* event) {
    // L6: keyboard-first navigation. The filter runs on filter_edit_, which
    // is the keyboard focus target on surface(). We forward Up/Down to the
    // recent layouts list, Enter to open, Esc to dismiss (when there's
    // somewhere to dismiss to).
    if (obj == filter_edit_ && event->type() == QEvent::KeyPress) {
        auto* ke = static_cast<QKeyEvent*>(event);
        const int key = ke->key();
        if ((key == Qt::Key_Down || key == Qt::Key_Up) && recent_layouts_) {
            // Walk visible rows only — hidden rows are filtered out so
            // arrow keys must skip them.
            const int n = recent_layouts_->count();
            if (n == 0)
                return true;
            int row = recent_layouts_->currentRow();
            const int step = (key == Qt::Key_Down) ? 1 : -1;
            for (int i = 0; i < n; ++i) {
                row = (row + step + n) % n;
                auto* it = recent_layouts_->item(row);
                if (it && !it->isHidden() && (it->flags() & Qt::ItemIsSelectable)) {
                    recent_layouts_->setCurrentRow(row);
                    break;
                }
            }
            return true;
        }
        if (key == Qt::Key_Return || key == Qt::Key_Enter) {
            on_open_layout();
            return true;
        }
        if (key == Qt::Key_Escape) {
            // Only dismiss if there's already a frame to return to —
            // otherwise hide() would leave the user with no UI.
            if (!WindowRegistry::instance().frames().isEmpty()) {
                hide();
                return true;
            }
            // Fall through; no-op rather than triggering a quit accidentally.
        }
    }
    return QMainWindow::eventFilter(obj, event);
}

void LaunchpadScreen::closeEvent(QCloseEvent* event) {
    // Reaching closeEvent always means the OS sent a window-close (X button
    // / Alt-F4 / system menu). Programmatic dismissals from button handlers
    // use hide() and never enter here. Treat every close as a user quit so
    // the app actually exits — otherwise lastWindowClosed re-surfaces this
    // window in an unkillable loop.
    LOG_INFO(kLaunchpadTag, "User closed Launchpad — quitting");
    event->accept();
    QCoreApplication::quit();
}

void LaunchpadScreen::on_new_window() {
    // Spawn a fresh WindowFrame. Reuses WindowCycler's smart-monitor
    // placement so the window lands somewhere visible.
    LOG_INFO(kLaunchpadTag, "New Window button clicked");
    hide();
    WindowCycler::instance().new_window_on_next_monitor();
}

void LaunchpadScreen::on_switch_profile() {
    // Show known profiles + a "Create new…" entry. Picking an existing
    // profile relaunches with --profile <name>. Picking the create entry
    // falls through to a text prompt.
    LOG_INFO(kLaunchpadTag, "Switch Profile button clicked");

    // "Create new" entry is identified by data role rather than string
    // comparison so the sentinel works regardless of UI language.
    const QString create_new_label = tr("+ Create new profile…");
    QStringList items = ProfileManager::instance().list_profiles();
    items.removeDuplicates();
    items.append(create_new_label);

    const QString current = ProfileManager::instance().active();
    int current_idx = items.indexOf(current);
    if (current_idx < 0)
        current_idx = 0;

    bool ok = false;
    const QString picked = QInputDialog::getItem(this, tr("Switch Profile"), tr("Pick a profile or create one:"), items,
                                                 current_idx, /*editable=*/false, &ok);
    if (!ok)
        return;

    QString target = picked;
    if (picked == create_new_label) {
        bool name_ok = false;
        target = QInputDialog::getText(this, tr("Create Profile"), tr("New profile name:"), QLineEdit::Normal,
                                       QString(), &name_ok)
                     .trimmed()
                     .toLower();
        if (!name_ok || target.isEmpty())
            return;
        // ProfileManager sanitises the name into a directory name, so an
        // unchecked value silently lands the user in a differently-named
        // profile than the one they typed.
        static const QRegularExpression kValidName(QStringLiteral("^[a-z0-9_-]{1,32}$"));
        if (!kValidName.match(target).hasMatch()) {
            QMessageBox::warning(this, tr("Invalid Profile Name"),
                                 tr("Use 1-32 characters: lowercase letters, digits, hyphen or underscore."));
            return;
        }
    }

    if (target == current) {
        LOG_INFO(kLaunchpadTag, "Switch Profile: target equals current — no-op");
        return;
    }

    // Relaunching quits this process. Confirm first — it used to happen the
    // instant the picker was dismissed with OK.
    const auto reply = QMessageBox::question(
        this, tr("Switch Profile"),
        tr("Switch to profile \"%1\"?\n\nFincept Terminal will restart.").arg(target),
        QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
    if (reply != QMessageBox::Yes)
        return;

    // Process-level switch today (set_active + relaunch). In-process switch
    // lands with Phase 1b's auth lift.
    const QString exe = QCoreApplication::applicationFilePath();
    QProcess::startDetached(exe, {"--profile", target});
    QCoreApplication::quit();
}

void LaunchpadScreen::on_open_layout() {
    if (!recent_layouts_)
        return;
    auto* item = recent_layouts_->currentItem();
    if (!item) {
        LOG_INFO(kLaunchpadTag, "Open Layout: no item selected");
        return;
    }
    const QString id_str = item->data(Qt::UserRole).toString();
    if (id_str.isEmpty())
        return;

    LayoutId id = LayoutId::from_string(id_str);
    auto r = LayoutCatalog::instance().load_workspace(id);
    if (r.is_err()) {
        LOG_WARN(kLaunchpadTag, QString("Open Layout failed: %1").arg(QString::fromStdString(r.error())));
        // Was log-only: the row simply did nothing when double-clicked.
        QMessageBox::warning(this, tr("Cannot open layout"),
                             tr("That layout could not be loaded:\n%1").arg(QString::fromStdString(r.error())));
        return;
    }
    const layout::Workspace ws = r.value();
    LOG_INFO(kLaunchpadTag, QString("Opening layout '%1'").arg(ws.name));

    hide();
    // Defer apply via queued connection so the spawned WindowFrame is fully
    // materialised before WorkspaceShell::apply walks WindowRegistry. If
    // there are no frames yet (Launchpad-only state), spawn one first.
    QMetaObject::invokeMethod(
        this,
        [ws]() {
            if (fincept::WindowRegistry::instance().frames().isEmpty()) {
                auto* fr = new fincept::WindowFrame(fincept::WindowFrame::next_window_id());
                fr->setAttribute(Qt::WA_DeleteOnClose);
                fr->show();
            }
            layout::WorkspaceShell::apply(ws);
        },
        Qt::QueuedConnection);
}

void LaunchpadScreen::refresh_recent_layouts() {
    if (!recent_layouts_)
        return;
    recent_layouts_->clear();

    // Phase 6: pull from LayoutCatalog. include_auto=false so the
    // continuously-saved "auto" workspace doesn't clutter the list — the
    // user wants to see saved/named layouts here, not the snapshot.
    auto r = LayoutCatalog::instance().recent_layouts(/*limit=*/8, /*include_auto=*/false);
    if (r.is_err() || r.value().isEmpty()) {
        auto* item =
            new QListWidgetItem(tr("(No saved layouts yet — use 'layout save \"<name>\"' to save the current state)"));
        item->setFlags(Qt::NoItemFlags);
        recent_layouts_->addItem(item);
        // Enable the "Open Saved Layout" button if there are any layouts
        // (it's just empty); disabled when none exist.
        if (btn_open_layout_)
            btn_open_layout_->setEnabled(false);
        return;
    }

    // Phase L5: show thumbnail icons next to each layout name.
    recent_layouts_->setIconSize(QSize(64, 36));
    const QString layouts_dir = ProfilePaths::layouts_dir();
    for (const auto& e : r.value()) {
        auto* item = new QListWidgetItem(QString("%1  (%2)").arg(e.name, e.kind));
        item->setData(Qt::UserRole, e.id.to_string());
        if (!e.thumbnail_path.isEmpty()) {
            const QString abs = layouts_dir + "/" + e.thumbnail_path;
            if (QFileInfo::exists(abs))
                item->setIcon(QIcon(abs));
        }
        recent_layouts_->addItem(item);
    }
    if (btn_open_layout_)
        btn_open_layout_->setEnabled(!r.value().isEmpty());
}

void LaunchpadScreen::on_continue() {
    LOG_INFO(kLaunchpadTag, "Continue from last session clicked");
    hide();
    // Defer the actual restore so the Launchpad's hide animation completes
    // first and so any frame WorkspaceShell::apply spawns lands cleanly on
    // the event loop.
    QMetaObject::invokeMethod(
        this,
        []() {
            const int n = layout::WorkspaceShell::load_last_or_default();
            if (n == 0) {
                // Fallback: nothing was restored (bad state, deleted files).
                // Spawn an empty frame so the user isn't left with no window.
                auto* w = new fincept::WindowFrame(fincept::WindowFrame::next_window_id());
                w->setAttribute(Qt::WA_DeleteOnClose);
                w->show();
            }
        },
        Qt::QueuedConnection);
}

void LaunchpadScreen::on_template_picked(const QString& persona_id) {
    LOG_INFO(kLaunchpadTag, QString("Template picked: %1").arg(persona_id));
    layout::Workspace ws = layout::LayoutTemplates::make(persona_id);
    auto sr = LayoutCatalog::instance().save_workspace(ws);
    if (sr.is_err()) {
        LOG_WARN(kLaunchpadTag,
                 QString("Failed to save template '%1': %2").arg(persona_id, QString::fromStdString(sr.error())));
        // First-run path: a silent return here left the user clicking a card
        // that appeared to do nothing at all.
        QMessageBox::warning(this, tr("Cannot start from this template"),
                             tr("The template could not be saved:\n%1").arg(QString::fromStdString(sr.error())));
        return;
    }
    // save_workspace mints a fresh id if one wasn't set; pull the saved id
    // so the WorkspaceShell::apply path pins the right layout.
    ws.id = sr.value();

    hide();
    const bool first_run_tour = !OnboardingTour::has_been_seen();
    QMetaObject::invokeMethod(
        this,
        [ws, first_run_tour]() {
            fincept::WindowFrame* anchor = nullptr;
            if (fincept::WindowRegistry::instance().frames().isEmpty()) {
                anchor = new fincept::WindowFrame(fincept::WindowFrame::next_window_id());
                anchor->setAttribute(Qt::WA_DeleteOnClose);
                anchor->show();
            } else {
                anchor = fincept::WindowRegistry::instance().frames().first();
            }
            layout::WorkspaceShell::apply(ws);

            // Phase 9 / decision 10.10: kick off the tour after a fresh
            // template pick on first run. Defer one tick so the frame's
            // panels have laid out first — the tour parents itself to
            // `anchor` so it appears centred over the frame, not behind it.
            if (first_run_tour && anchor) {
                QPointer<fincept::WindowFrame> guard(anchor);
                QMetaObject::invokeMethod(
                    anchor,
                    [guard]() {
                        if (!guard)
                            return;
                        OnboardingTour::show_for(guard.data());
                    },
                    Qt::QueuedConnection);
            }
        },
        Qt::QueuedConnection);
}

void LaunchpadScreen::refresh_first_run_picker() {
    if (!template_picker_)
        return;

    // Show the picker iff the user has nothing to come back to:
    //   - no saved/builtin layouts in the catalog, AND
    //   - no auto snapshot from a prior session
    bool has_anything = false;
    auto layouts = LayoutCatalog::instance().recent_layouts(/*limit=*/1, /*include_auto=*/false);
    if (layouts.is_ok() && !layouts.value().isEmpty())
        has_anything = true;

    if (!has_anything) {
        if (auto* ring = TerminalShell::instance().snapshot_ring()) {
            auto snaps = ring->latest_auto(/*limit=*/1);
            if (snaps.is_ok() && !snaps.value().isEmpty())
                has_anything = true;
        }
    }

    const bool first_run = !has_anything;
    template_picker_->setVisible(first_run);
    if (recent_layouts_)
        recent_layouts_->setVisible(!first_run);
    if (recent_label_)
        recent_label_->setVisible(!first_run);
}

void LaunchpadScreen::refresh_crash_banner() {
    if (!crash_banner_)
        return;
    // The shell latches the answer at boot — using needs_recovery() directly
    // would false-positive within the first minute (latest auto-snapshot
    // catches up to the marker).
    crash_banner_->setVisible(TerminalShell::instance().started_after_crash());
}

void LaunchpadScreen::refresh_continue_visibility() {
    if (!btn_continue_)
        return;

    // Show the button iff something restorable exists. Two sources of truth:
    //   1. last_loaded_uuid pin (set by every successful WorkspaceShell::apply
    //      of a non-auto layout) — the explicit "what was open last time"
    //   2. most recent kind='auto' snapshot from the ring — the "auto-saved
    //      state from a session that ended without a named save"
    bool has_restorable = false;

    auto last = LayoutCatalog::instance().last_loaded_id();
    if (last.is_ok() && !last.value().is_null())
        has_restorable = true;

    if (!has_restorable) {
        if (auto* ring = TerminalShell::instance().snapshot_ring()) {
            auto snaps = ring->latest_auto(/*limit=*/1);
            if (snaps.is_ok() && !snaps.value().isEmpty())
                has_restorable = true;
        }
    }

    btn_continue_->setVisible(has_restorable);
}

} // namespace fincept::screens
