module api_modules.classroom_compliance.api_class;

import handy_httpd;
import ddbc;

import api_modules.classroom_compliance.model;
import api_modules.classroom_compliance.util;
import api_modules.auth : User, getUserOrThrow;
import db;
import data_utils;

void createClass(ref HttpRequestContext ctx) {
    Connection conn = getDb();
    scope(exit) conn.close();
    User user = getUserOrThrow(ctx, conn);
    struct ClassPayload {
        ushort number;
        string schoolYear;
    }
    auto payload = readJsonPayload!(ClassPayload)(ctx);
    const bool classNumberExists = recordExists(
        conn,
        "SELECT id FROM classroom_compliance_class WHERE number = ? AND school_year = ? AND user_id = ?",
        payload.number, payload.schoolYear, user.id
    );
    if (classNumberExists) {
        ctx.response.status = HttpStatus.BAD_REQUEST;
        ctx.response.writeBodyString("There is already a class with this number, for the same school year.");
        return;
    }
    ulong classId = insertOne(
        conn,
        "INSERT INTO classroom_compliance_class (number, school_year, user_id) VALUES (?, ?, ?) RETURNING id",
        payload.number, payload.schoolYear, user.id
    );
    auto newClass = findOne(
        conn,
        "SELECT * FROM classroom_compliance_class WHERE id = ? AND user_id = ?",
        &ClassroomComplianceClass.parse,
        classId, user.id
    ).orElseThrow();
    writeJsonBody(ctx, newClass);
}

void getClasses(ref HttpRequestContext ctx) {
    Connection conn = getDb();
    scope(exit) conn.close();
    User user = getUserOrThrow(ctx, conn);
    const query = "
    SELECT
        c.id, c.number, c.school_year, c.archived,
        COUNT(DISTINCT s.id) AS student_count,
        COUNT(DISTINCT (e.student_id, e.date)) AS entry_count,
        MAX(e.date) AS last_entry_date
    FROM classroom_compliance_class c
    LEFT JOIN classroom_compliance_student s ON c.id = s.class_id
    LEFT JOIN classroom_compliance_entry e ON c.id = e.class_id
    WHERE c.user_id = ?
    GROUP BY c.id
    ORDER BY school_year DESC, number ASC
    ";
    struct ClassResponse {
        ulong id;
        ushort number;
        string schoolYear;
        bool archived;
        uint studentCount;
        uint entryCount;
        string lastEntryDate;
    }
    ClassResponse[] classes = findAll(
        conn,
        query,
        r => ClassResponse(
            r.getUlong(1),
            r.getUshort(2),
            r.getString(3),
            r.getBoolean(4),
            r.getUint(5),
            r.getUint(6),
            r.getDate(7).toISOExtString()
        ),
        user.id
    );
    writeJsonBody(ctx, classes);
}

void getClass(ref HttpRequestContext ctx) {
    Connection conn = getDb();
    scope(exit) conn.close();
    User user = getUserOrThrow(ctx, conn);
    auto cls = getClassOrThrow(ctx, conn, user);
    writeJsonBody(ctx, cls);
}

void deleteClass(ref HttpRequestContext ctx) {
    Connection conn = getDb();
    scope(exit) conn.close();
    User user = getUserOrThrow(ctx, conn);
    auto cls = getClassOrThrow(ctx, conn, user);
    if (cls.archived) throw new HttpStatusException(HttpStatus.FORBIDDEN, "Class is archived.");
    const query = "DELETE FROM classroom_compliance_class WHERE id = ? AND user_id = ?";
    PreparedStatement ps = conn.prepareStatement(query);
    scope(exit) ps.close();
    ps.setUlong(1, cls.id);
    ps.setUlong(2, user.id);
    ps.executeUpdate();
}

void getClassNotes(ref HttpRequestContext ctx) {
    Connection conn = getDb();
    scope(exit) conn.close();
    User user = getUserOrThrow(ctx, conn);
    auto cls = getClassOrThrow(ctx, conn, user);
    const query = "SELECT * FROM classroom_compliance_class_note WHERE class_id = ? ORDER BY created_at DESC";
    const notes = findAll(conn, query, &ClassroomComplianceClassNote.parse, cls.id);
    writeJsonBody(ctx, notes);
}

void createClassNote(ref HttpRequestContext ctx) {
    Connection conn = getDb();
    scope(exit) conn.close();
    User user = getUserOrThrow(ctx, conn);
    auto cls = getClassOrThrow(ctx, conn, user);
    if (cls.archived) throw new HttpStatusException(HttpStatus.FORBIDDEN, "Class is archived.");
    struct Payload {string content;}
    Payload payload = readJsonPayload!(Payload)(ctx);
    const query = "INSERT INTO classroom_compliance_class_note (class_id, content) VALUES (?, ?) RETURNING id";
    ulong id = insertOne(conn, query, cls.id, payload.content);
    const note = findOne(
        conn,
        "SELECT * FROM classroom_compliance_class_note WHERE id = ?",
        &ClassroomComplianceClassNote.parse,
        id
    );
    writeJsonBody(ctx, note);
}

void deleteClassNote(ref HttpRequestContext ctx) {
    Connection conn = getDb();
    scope(exit) conn.close();
    User user = getUserOrThrow(ctx, conn);
    auto cls = getClassOrThrow(ctx, conn, user);
    if (cls.archived) throw new HttpStatusException(HttpStatus.FORBIDDEN, "Class is archived.");
    ulong noteId = ctx.request.getPathParamAs!ulong("noteId");
    const query = "DELETE FROM classroom_compliance_class_note WHERE class_id = ? AND id = ?";
    update(conn, query, cls.id, noteId);
}

void resetStudentDesks(ref HttpRequestContext ctx) {
    Connection conn = getDb();
    scope(exit) conn.close();
    User user = getUserOrThrow(ctx, conn);
    auto cls = getClassOrThrow(ctx, conn, user);
    if (cls.archived) throw new HttpStatusException(HttpStatus.FORBIDDEN, "Class is archived.");
    const query = "UPDATE classroom_compliance_student SET desk_number = 0 WHERE class_id = ?";
    update(conn, query, cls.id);
}

void setArchived(ref HttpRequestContext ctx) {
    Connection conn = getDb();
    scope(exit) conn.close();
    User user = getUserOrThrow(ctx, conn);
    auto cls = getClassOrThrow(ctx, conn, user);
    const query = "UPDATE classroom_compliance_class SET archived = ? WHERE id = ?";
    update(conn, query, !cls.archived, cls.id);
}

void updateScoreParameters(ref HttpRequestContext ctx) {
    import api_modules.classroom_compliance.score_eval : validateExpression;
    import std.algorithm : canFind;
    Connection conn = getDb();
    scope(exit) conn.close();
    User user = getUserOrThrow(ctx, conn);
    auto cls = getClassOrThrow(ctx, conn, user);
    if (cls.archived) throw new HttpStatusException(HttpStatus.FORBIDDEN, "Class is archived.");
    struct Payload {
        string scoreExpression;
        string scorePeriod;
    }
    Payload payload = readJsonPayload!(Payload)(ctx);
    bool scoreExpressionChanged = cls.scoreExpression != payload.scoreExpression;
    bool scorePeriodChanged = cls.scorePeriod != payload.scorePeriod;
    if (scoreExpressionChanged && !validateExpression(payload.scoreExpression)) {
        ctx.response.status = HttpStatus.BAD_REQUEST;
        ctx.response.writeBodyString("Invalid score expression.");
        return;
    }
    const VALID_SCORE_PERIODS = ["week"];
    if (scorePeriodChanged && !canFind(VALID_SCORE_PERIODS, payload.scorePeriod)) {
        ctx.response.status = HttpStatus.BAD_REQUEST;
        ctx.response.writeBodyString("Invalid score period.");
        return;
    }
    conn.setAutoCommit(false);
    if (scoreExpressionChanged) {
        update(
            conn,
            "UPDATE classroom_compliance_class SET score_expression = ? WHERE id = ?",
            payload.scoreExpression,
            cls.id
        );
    }
    if (scorePeriodChanged) {
        update(
            conn,
            "UPDATE classroom_compliance_class SET score_period = ? WHERE id = ?",
            payload.scorePeriod,
            cls.id
        );
    }
    conn.commit();
    // Reload the class and then write it to the output.
    auto updatedClass = getClassOrThrow(ctx, conn, user);
    writeJsonBody(ctx, updatedClass);
}
