// ============================================================================
//  qt_student_manager —— Day5 综合案例参考实现：学员信息管理器
//  对应教案：OUTLINE_4DAY.md §3（Day5 综合项目案例讲解）——把 Day1–4 作业线
//  收束成一个完整小软件：QMainWindow 骨架 + QTableWidget 列表 + 增改对话框 +
//  JSON 落盘读回 + QThread 后台导入不卡界面。
//  配套文档：docs/teaching/examples/student_manager/README.md
//
//  离屏验证（自测模式）：QT_QPA_PLATFORM=offscreen tools/run_qt.sh qt_student_manager
//  自测流程：添加 2 条记录 → 保存 JSON → 清空 → 读回校验 → 后台导入 3 条 →
//  总数校验，全部通过退出码 0，任一步失败退出码 1。
// ============================================================================

#include <QAction>
#include <QApplication>
#include <QDebug>
#include <QDialog>
#include <QDialogButtonBox>
#include <QFile>
#include <QFormLayout>
#include <QHeaderView>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QLineEdit>
#include <QMainWindow>
#include <QMenuBar>
#include <QMessageBox>
#include <QStatusBar>
#include <QTableWidget>
#include <QThread>
#include <QTimer>
#include <QToolBar>

// ---------------------------------------------------------------- 数据层 ----

struct Student {
    QString name;
    double score = 0.0;
    QString tag;
};

// 序列化/反序列化是一对互逆操作（Day4）：正确性标准 = 写出去再读回来数据一致。
static QJsonObject toJson(const Student& s) {
    return QJsonObject{{"name", s.name}, {"score", s.score}, {"tag", s.tag}};
}

static Student fromJson(const QJsonObject& obj) {
    // 类型不匹配时 toString()/toDouble() 不报错只给默认值（Day4 坑卡）——
    // 教学示例里字段固定，生产代码应先 contains()+isDouble() 逐字段校验。
    return Student{obj["name"].toString(), obj["score"].toDouble(), obj["tag"].toString()};
}

// ---------------------------------------------------------- 添加记录对话框 ----

class AddStudentDialog : public QDialog {
    Q_OBJECT
public:
    explicit AddStudentDialog(QWidget* parent = nullptr) : QDialog(parent) {
        setWindowTitle(QStringLiteral("添加学员"));
        m_name = new QLineEdit(this);
        m_score = new QLineEdit(this);
        m_tag = new QLineEdit(this);

        auto* buttons = new QDialogButtonBox(
            QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
        connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
        connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);

        auto* form = new QFormLayout(this);
        form->addRow(QStringLiteral("姓名"), m_name);
        form->addRow(QStringLiteral("成绩"), m_score);
        form->addRow(QStringLiteral("标签"), m_tag);
        form->addRow(buttons);
    }

    Student student() const {
        return Student{m_name->text(), m_score->text().toDouble(), m_tag->text()};
    }

private:
    QLineEdit* m_name;
    QLineEdit* m_score;
    QLineEdit* m_tag;
};

// ------------------------------------------------------ 后台导入 worker ----

// worker-object 模式（而非继承 QThread）：耗时活儿放进 worker，把 worker
// moveToThread 到工作线程；worker 与界面之间只通过信号槽通信——跨线程连接
// 自动变 Queued，槽在接收者所属线程执行，天然不碰 GUI 线程以外的控件。
class ImportWorker : public QObject {
    Q_OBJECT
public slots:
    void doImport(int rows) {
        for (int i = 1; i <= rows; ++i) {
            QThread::msleep(20);   // 模拟耗时 IO；放主线程就是界面冻结（Day4 埋点）
            emit rowReady(QStringLiteral("导入学员%1").arg(i), 60.0 + i, QStringLiteral("导入"));
        }
        emit finished();
    }
signals:
    void rowReady(const QString& name, double score, const QString& tag);
    void finished();
};

// -------------------------------------------------------------- 主窗口 ----

class MainWindow : public QMainWindow {
    Q_OBJECT
public:
    explicit MainWindow(QWidget* parent = nullptr) : QMainWindow(parent) {
        setWindowTitle(QStringLiteral("学员信息管理器"));

        // QMainWindow 不是大号 QWidget：中心部件必须显式安放（Day3 坑卡）
        m_table = new QTableWidget(0, 3, this);
        m_table->setHorizontalHeaderLabels(
            {QStringLiteral("姓名"), QStringLiteral("成绩"), QStringLiteral("标签")});
        m_table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
        setCentralWidget(m_table);

        auto* actAdd = new QAction(QStringLiteral("添加记录"), this);
        auto* actSave = new QAction(QStringLiteral("保存"), this);
        auto* actLoad = new QAction(QStringLiteral("读取"), this);
        m_actImport = new QAction(QStringLiteral("后台导入"), this);
        auto* actAbout = new QAction(QStringLiteral("关于"), this);

        QMenu* fileMenu = menuBar()->addMenu(QStringLiteral("文件(&F)"));
        fileMenu->addAction(actSave);
        fileMenu->addAction(actLoad);
        QMenu* recordMenu = menuBar()->addMenu(QStringLiteral("记录(&R)"));
        recordMenu->addAction(actAdd);
        recordMenu->addAction(m_actImport);
        menuBar()->addMenu(QStringLiteral("帮助(&H)"))->addAction(actAbout);

        QToolBar* bar = addToolBar(QStringLiteral("main"));
        bar->addAction(actAdd);
        bar->addAction(actSave);
        bar->addAction(actLoad);
        bar->addAction(m_actImport);

        // QAction 显示出来 ≠ 生效：忘 connect 就是「菜单点了没反应」（Day3 坑卡）
        connect(actAdd, &QAction::triggered, this, &MainWindow::onAdd);
        connect(actSave, &QAction::triggered, this, [this] { saveTo(m_dataPath); });
        connect(actLoad, &QAction::triggered, this, [this] { loadFrom(m_dataPath); });
        connect(m_actImport, &QAction::triggered, this, &MainWindow::onImport);
        connect(actAbout, &QAction::triggered, this, [this] {
            QMessageBox::about(this, QStringLiteral("关于"),
                               QStringLiteral("Day5 综合案例参考实现：Day1–4 全部知识点的收束。"));
        });

        // 相对路径随工作目录漂移（Day4 坑卡）——一律基于 applicationDirPath 拼路径
        m_dataPath = QCoreApplication::applicationDirPath() + "/students.json";
        statusBar()->showMessage(QStringLiteral("数据文件：") + m_dataPath);
    }

    int rowCount() const { return m_table->rowCount(); }
    void clearAll() { m_table->setRowCount(0); }

    void addRow(const Student& s) {
        const int row = m_table->rowCount();
        m_table->insertRow(row);
        m_table->setItem(row, 0, new QTableWidgetItem(s.name));
        m_table->setItem(row, 1, new QTableWidgetItem(QString::number(s.score)));
        m_table->setItem(row, 2, new QTableWidgetItem(s.tag));
    }

    bool saveTo(const QString& path) {
        QJsonArray arr;
        for (int row = 0; row < m_table->rowCount(); ++row) {
            arr.append(toJson(Student{m_table->item(row, 0)->text(),
                                      m_table->item(row, 1)->text().toDouble(),
                                      m_table->item(row, 2)->text()}));
        }
        QFile file(path);
        if (!file.open(QIODevice::WriteOnly)) {   // open() 必须检查（Day4 坑卡）
            statusBar()->showMessage(QStringLiteral("保存失败：打不开 ") + path);
            return false;
        }
        file.write(QJsonDocument(arr).toJson(QJsonDocument::Indented));
        statusBar()->showMessage(QStringLiteral("已保存 %1 条记录").arg(arr.size()));
        return true;
    }

    bool loadFrom(const QString& path) {
        QFile file(path);
        if (!file.open(QIODevice::ReadOnly)) {
            statusBar()->showMessage(QStringLiteral("读取失败：打不开 ") + path);
            return false;
        }
        QJsonParseError err;   // 不检查 ParseError = 坏文件静默变空表（Day4 坑卡）
        const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &err);
        if (err.error != QJsonParseError::NoError || !doc.isArray()) {
            statusBar()->showMessage(QStringLiteral("JSON 解析失败：") + err.errorString());
            return false;
        }
        clearAll();
        const QJsonArray arr = doc.array();
        for (const QJsonValue& v : arr) addRow(fromJson(v.toObject()));
        statusBar()->showMessage(QStringLiteral("已读取 %1 条记录").arg(arr.size()));
        return true;
    }

    void startImport(int rows) {
        m_actImport->setEnabled(false);   // 防重复启动（也防重复 connect，Day2 坑卡）
        auto* thread = new QThread(this);
        auto* worker = new ImportWorker;   // 不能给 parent：有 parent 的对象禁止 moveToThread
        worker->moveToThread(thread);      // 必须在 start() 之前（Day5 讲授点）

        connect(thread, &QThread::started, worker, [worker, rows] { worker->doImport(rows); });
        // 跨线程信号 → 主线程槽：自动 Queued，addRow 在 GUI 线程安全执行
        connect(worker, &ImportWorker::rowReady, this,
                [this](const QString& name, double score, const QString& tag) {
                    addRow(Student{name, score, tag});
                });
        connect(worker, &ImportWorker::finished, this, [this] {
            m_actImport->setEnabled(true);
            statusBar()->showMessage(QStringLiteral("后台导入完成"));
            emit importFinished();
        });
        connect(worker, &ImportWorker::finished, thread, &QThread::quit);
        connect(thread, &QThread::finished, worker, &QObject::deleteLater);
        connect(thread, &QThread::finished, thread, &QObject::deleteLater);
        thread->start();
    }

signals:
    void importFinished();

private slots:
    void onAdd() {
        AddStudentDialog dlg(this);        // 模态：exec() 阻塞等结果（Day3）
        if (dlg.exec() == QDialog::Accepted) addRow(dlg.student());
    }
    void onImport() { startImport(10); }

private:
    QTableWidget* m_table = nullptr;
    QAction* m_actImport = nullptr;
    QString m_dataPath;
};

// ------------------------------------------------------------ 自测入口 ----

// 离屏自测：走一遍「添加→保存→清空→读回→后台导入」完整闭环并校验。
static void runSelfTest(MainWindow* win) {
    const QString path = QCoreApplication::applicationDirPath() + "/students_selftest.json";
    win->addRow({QStringLiteral("张三"), 91.5, QStringLiteral("组长")});
    win->addRow({QStringLiteral("李四"), 88.0, QStringLiteral("美工")});
    if (!win->saveTo(path)) { QCoreApplication::exit(1); return; }
    win->clearAll();
    if (!win->loadFrom(path) || win->rowCount() != 2) {
        qCritical() << "自测失败：JSON 读回记录数 =" << win->rowCount() << "预期 2";
        QCoreApplication::exit(1);
        return;
    }
    qInfo() << "自测 1/2 通过：JSON 写入→清空→读回，2 条记录完整";

    QObject::connect(win, &MainWindow::importFinished, win, [win, path] {
        QFile::remove(path);
        if (win->rowCount() != 5) {
            qCritical() << "自测失败：导入后总数 =" << win->rowCount() << "预期 5";
            QCoreApplication::exit(1);
            return;
        }
        qInfo() << "自测 2/2 通过：后台线程导入 3 条，总计 5 条，界面未阻塞";
        QCoreApplication::exit(0);
    });
    win->startImport(3);
}

int main(int argc, char* argv[]) {
    QApplication app(argc, argv);
    MainWindow win;
    win.resize(560, 360);
    win.show();

    if (qgetenv("QT_QPA_PLATFORM") == "offscreen") {
        QTimer::singleShot(0, &win, [&win] { runSelfTest(&win); });
    }
    return app.exec();
}

#include "qt_student_manager.moc"
