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);
    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 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 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);
    auto entries = findAll(
        conn,
        "SELECT * FROM classroom_compliance_entry WHERE student_id = ? ORDER BY date DESC",
        &ClassroomComplianceEntry.parse,
        student.id
    );
    JSONValue response = JSONValue.emptyArray;
    foreach (entry; entries) response.array ~= entry.toJsonObj();
    ctx.response.writeBodyString(response.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(id) 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(id) FROM classroom_compliance_entry WHERE student_id = ? AND absent = true",
        student.id
    );
    const ulong phoneNoncomplianceCount = count(
        conn,
        "SELECT COUNT(id) FROM classroom_compliance_entry WHERE phone_compliant = FALSE AND student_id = ?",
        student.id
    );
    const behaviorCountQuery = "
    SELECT COUNT(id)
    FROM classroom_compliance_entry
    WHERE student_id = ? AND behavior_rating = ?
    ";

    const ulong behaviorGoodCount = count(conn, behaviorCountQuery, student.id, 3);
    const ulong behaviorMediocreCount = count(conn, behaviorCountQuery, student.id, 2);
    const ulong behaviorPoorCount = count(conn, behaviorCountQuery, student.id, 1);
    
    // Calculate derived statistics.
    const ulong attendanceCount = entryCount - absenceCount;
    double attendanceRate = attendanceCount / cast(double) entryCount;
    double phoneComplianceRate = (attendanceCount - phoneNoncomplianceCount) / cast(double) attendanceCount;
    double behaviorScore = (
        behaviorGoodCount * 1.0 +
        behaviorMediocreCount * 0.5
    ) / attendanceCount;
    
    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 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.
}