module api_modules.classroom_compliance.api_student;

import handy_httpd;
import ddbc;
import std.json;

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

void createStudent(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 StudentPayload {
        string name;
        ushort deskNumber;
        bool removed;
    }
    auto payload = readJsonPayload!(StudentPayload)(ctx);
    bool studentExists = recordExists(
        conn,
        "SELECT id FROM classroom_compliance_student WHERE name = ? AND class_id = ?",
        payload.name, cls.id
    );
    if (studentExists) {
        ctx.response.status = HttpStatus.BAD_REQUEST;
        ctx.response.writeBodyString("A student with that name already exists in this class.");
        return;
    }
    bool deskAlreadyOccupied = payload.deskNumber != 0 && recordExists(
        conn,
        "SELECT id FROM classroom_compliance_student WHERE class_id = ? AND desk_number = ?",
        cls.id, payload.deskNumber
    );
    if (deskAlreadyOccupied) {
        ctx.response.status = HttpStatus.BAD_REQUEST;
        ctx.response.writeBodyString("There is already a student assigned to that desk number.");
        return;
    }
    ulong studentId = insertOne(
        conn,
        "INSERT INTO classroom_compliance_student
        (name, class_id, desk_number, removed)
        VALUES (?, ?, ?, ?) RETURNING id",
        payload.name, cls.id, payload.deskNumber, payload.removed
    );
    auto student = findOne(
        conn,
        "SELECT * FROM classroom_compliance_student WHERE id = ?",
        &ClassroomComplianceStudent.parse,
        studentId
    ).orElseThrow();
    writeJsonBody(ctx, student);
}

void getStudents(ref HttpRequestContext ctx) {
    Connection conn = getDb();
    scope(exit) conn.close();
    User user = getUserOrThrow(ctx, conn);
    auto cls = getClassOrThrow(ctx, conn, user);
    auto students = findAll(
        conn,
        "SELECT * FROM classroom_compliance_student WHERE class_id = ? ORDER BY name ASC",
        &ClassroomComplianceStudent.parse,
        cls.id
    );
    writeJsonBody(ctx, students);
}

void getStudent(ref HttpRequestContext ctx) {
    Connection conn = getDb();
    scope(exit) conn.close();
    User user = getUserOrThrow(ctx, conn);
    auto student = getStudentOrThrow(ctx, conn, user);
    writeJsonBody(ctx, student);
}

void updateStudent(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.");
    auto student = getStudentOrThrow(ctx, conn, user);
    struct StudentUpdatePayload {
        string name;
        ushort deskNumber;
        bool removed;
    }
    auto payload = readJsonPayload!(StudentUpdatePayload)(ctx);
    // If there is nothing to update, quit.
    if (
        payload.name == student.name
        && payload.deskNumber == student.deskNumber
        && payload.removed == student.removed
    ) return;
    // Check that the new name doesn't already exist.
    bool newNameExists = payload.name != student.name && recordExists(
        conn,
        "SELECT id FROM classroom_compliance_student WHERE name = ? AND class_id = ?",
        payload.name, student.classId
    );
    if (newNameExists) {
        ctx.response.status = HttpStatus.BAD_REQUEST;
        ctx.response.writeBodyString("A student with that name already exists in this class.");
        return;
    }
    // Check that if a new desk number is assigned, that it's not already assigned to anyone else.
    bool newDeskOccupied = payload.deskNumber != 0 && payload.deskNumber != student.deskNumber && recordExists(
        conn,
        "SELECT id FROM classroom_compliance_student WHERE class_id = ? AND desk_number = ?",
        student.classId, payload.deskNumber
    );
    if (newDeskOccupied) {
        ctx.response.status = HttpStatus.BAD_REQUEST;
        ctx.response.writeBodyString("That desk is already assigned to another student.");
        return;
    }
    update(
        conn,
        "UPDATE classroom_compliance_student SET name = ?, desk_number = ?, removed = ? WHERE id = ?",
        payload.name,
        payload.deskNumber,
        payload.removed,
        student.id
    );
    auto updatedStudent = findOne(
        conn,
        "SELECT * FROM classroom_compliance_student WHERE id = ?",
        &ClassroomComplianceStudent.parse,
        student.id
    ).orElseThrow();
    writeJsonBody(ctx, updatedStudent);
}

void deleteStudent(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.");
    auto student = getStudentOrThrow(ctx, conn, user);
    update(
        conn,
        "DELETE FROM classroom_compliance_student WHERE id = ? AND class_id = ?",
        student.id, student.classId
    );
}

void getStudentEntries(ref HttpRequestContext ctx) {
    Connection conn = getDb();
    scope(exit) conn.close();
    User user = getUserOrThrow(ctx, conn);
    auto student = getStudentOrThrow(ctx, conn, user);

    const query = import("source/api_modules/classroom_compliance/queries/find_entries_by_student.sql");
    PreparedStatement ps = conn.prepareStatement(query);
    scope(exit) ps.close();
    ps.setUlong(1, student.id);
    ResultSet rs = ps.executeQuery();

    ClassroomComplianceEntry entry;
    ClassroomComplianceEntryChecklistItem[] checklistItems;
    JSONValue responseArray = JSONValue.emptyArray;

    bool hasNextRow = rs.next();
    while (hasNextRow) {
        entry.date = rs.getDate(1);
        entry.classId = rs.getUlong(2);
        entry.createdAt = rs.getUlong(3);
        entry.absent = rs.getBoolean(4);
        entry.comment = rs.getString(5);
        entry.studentId = student.id;
        bool hasChecklistItem = !rs.isNull(6);
        if (hasChecklistItem) {
            checklistItems ~= ClassroomComplianceEntryChecklistItem(
                rs.getString(6),
                rs.getBoolean(7),
                rs.getString(8)
            );
        }

        hasNextRow = rs.next();
        bool shouldSaveEntry = !hasNextRow || rs.getDate(1) != entry.date;
        if (shouldSaveEntry) {
            JSONValue obj = JSONValue.emptyObject;
            obj.object["date"] = JSONValue(entry.date.toISOExtString());
            obj.object["createdAt"] = JSONValue(entry.createdAt);
            obj.object["absent"] = JSONValue(entry.absent);
            obj.object["comment"] = JSONValue(entry.comment);
            obj.object["checklistItems"] = JSONValue.emptyArray;
            foreach (item; checklistItems) {
                JSONValue ckObj = JSONValue.emptyObject;
                ckObj.object["item"] = JSONValue(item.item);
                ckObj.object["checked"] = JSONValue(item.checked);
                ckObj.object["category"] = JSONValue(item.category);
                obj.object["checklistItems"].array ~= ckObj;
            }
            responseArray.array ~= obj;
            checklistItems = [];
        }
    }
    ctx.response.writeBodyString(responseArray.toJSON(), "application/json");
}

void getStudentOverview(ref HttpRequestContext ctx) {
    Connection conn = getDb();
    scope(exit) conn.close();
    User user = getUserOrThrow(ctx, conn);
    auto student = getStudentOrThrow(ctx, conn, user);
    
    const ulong entryCount = count(
        conn,
        "SELECT COUNT(DISTINCT(class_id, date))
        FROM classroom_compliance_entry
        WHERE student_id = ?",
        student.id
    );
    if (entryCount == 0) {
        ctx.response.status = HttpStatus.NOT_FOUND;
        ctx.response.writeBodyString("No entries for this student.");
        return;
    }
    const ulong absenceCount = count(
        conn,
        "SELECT COUNT(DISTINCT(class_id, date))
        FROM classroom_compliance_entry
        WHERE student_id = ? AND absent = true",
        student.id
    );
    
    // Calculate derived statistics.
    const ulong attendanceCount = entryCount - absenceCount;
    double attendanceRate = attendanceCount / cast(double) entryCount;
    
    JSONValue response = JSONValue.emptyObject;
    response.object["attendanceRate"] = JSONValue(attendanceRate);
    // response.object["phoneComplianceRate"] = JSONValue(phoneComplianceRate);
    // response.object["behaviorScore"] = JSONValue(behaviorScore);
    response.object["entryCount"] = JSONValue(entryCount);
    ctx.response.writeBodyString(response.toJSON(), "application/json");
}

void moveStudentToOtherClass(ref HttpRequestContext ctx) {
    Connection conn = getDb();
    scope(exit) conn.close();
    conn.setAutoCommit(false);
    User user = getUserOrThrow(ctx, conn);
    auto cls = getClassOrThrow(ctx, conn, user);
    if (cls.archived) throw new HttpStatusException(HttpStatus.FORBIDDEN, "Class is archived.");
    auto student = getStudentOrThrow(ctx, conn, user);
    struct Payload {
        ulong classId;
    }
    Payload payload = readJsonPayload!(Payload)(ctx);
    if (payload.classId == student.classId) {
        return; // Quit if the student is already in the desired class.
    }
    // Check that the desired class exists, and belongs to the user.
    bool newClassIdValid = recordExists(
        conn,
        "SELECT id FROM classroom_compliance_class WHERE user_id = ? and id = ?",
        user.id, payload.classId
    );
    if (!newClassIdValid) {
        ctx.response.status = HttpStatus.BAD_REQUEST;
        ctx.response.writeBodyString("Invalid class was selected.");
        return;
    }
    // Check that the new class doesn't already have a student with the same name.
    bool studentNameExistsInNewClass = recordExists(
        conn,
        "SELECT id FROM classroom_compliance_student WHERE class_id = ? AND name = ?",
        payload.classId,
        student.name
    );
    if (studentNameExistsInNewClass) {
        ctx.response.status = HttpStatus.BAD_REQUEST;
        ctx.response.writeBodyString("A student in the selected class has the same name as this one.");
        return;
    }
    // All good, so update the student's class to the desired one, and reset their desk.
    update(
        conn,
        "UPDATE classroom_compliance_student SET class_id = ?, desk_number = 0 WHERE id = ?",
        payload.classId, student.id
    );
    conn.commit();
    // We just return 200 OK, no response body.
}

void getStudentLabels(ref HttpRequestContext ctx) {
    Connection conn = getDb();
    scope(exit) conn.close();
    User user = getUserOrThrow(ctx, conn);
    auto student = getStudentOrThrow(ctx, conn, user);
    string[] labels = findAll(
        conn,
        "SELECT label FROM classroom_compliance_student_label WHERE student_id = ? ORDER BY label ASC",
        (r) => r.getString(1),
        student.id
    );
    writeJsonBody(ctx, labels);
}

void updateStudentLabels(ref HttpRequestContext ctx) {
    Connection conn = getDb();
    scope(exit) conn.close();
    User user = getUserOrThrow(ctx, conn);
    auto student = getStudentOrThrow(ctx, conn, user);
    string[] labels = readJsonPayload!(string[])(ctx);

    conn.setAutoCommit(false);
    update(conn, "DELETE FROM classroom_compliance_student_label WHERE student_id = ?", student.id);
    PreparedStatement ps = conn.prepareStatement(
        "INSERT INTO classroom_compliance_student_label (student_id, label) VALUES (?, ?)"
    );
    foreach (label; labels) {
        ps.setUlong(1, student.id);
        ps.setString(2, label);
        ps.executeUpdate();
    }
    conn.commit();
}
