module api_modules.classroom_compliance.score;

import handy_httpd.components.optional;
import ddbc;
import slf4d;
import std.datetime;

import api_modules.classroom_compliance.util;
import api_modules.classroom_compliance.score_eval;
import db;

enum ScorePeriod : string {
    WEEK = "week"
}

private struct ScoringParameters {
    Expr expr;
    DateRange dateRange;
}

private struct ChecklistItemCategoryStats {
    ulong total;
    ulong checked;

    double getUncheckedRatio() const {
        double unchecked = total - checked;
        return unchecked / total;
    }
}

/**
 * Gets an associative array that maps student ids to their (optional) scores.
 * Scores are calculated based on aggregate statistics from their entries. A
 * student's score will be empty if they do not have any data available, or are
 * absent for all dates within the grading period.
 * Params:
 *   conn = The database connection.
 *   classId = The id of the class to filter by.
 *   date = The date to calculate scores for.
 * Returns: A map of scores.
 */
Optional!double[ulong] getScores(
    Connection conn,
    ulong classId,
    Date date
) {
    Optional!double[ulong] scores;
    auto optScoringParams = getScoringParameters(conn, classId, date);
    if (optScoringParams.isNull) {
        warnF!"Unable to obtain scoring parameters for class %d on %s."(classId, date.toISOExtString());
        return scores;
    }
    const ScoringParameters params = optScoringParams.value;
    debugF!"Calculating scores for class %d using expression (%s) from %s to %s."(
        classId,
        params.expr.toString(),
        params.dateRange.from,
        params.dateRange.to
    );

    const query = import("source/api_modules/classroom_compliance/queries/find_scoring_parameters.sql");
    PreparedStatement ps = conn.prepareStatement(query);
    scope(exit) ps.close();
    ps.setDate(1, params.dateRange.from);
    ps.setDate(2, params.dateRange.to);
    ps.setUlong(3, classId);
    foreach (DataSetReader r; ps.executeQuery()) {
        ulong studentId = r.getUlong(1);
        ulong entryCount = r.getUlong(2);
        ChecklistItemCategoryStats[string] categoryStats;
        categoryStats["classroom_readiness"] = ChecklistItemCategoryStats(
            r.getUlong(3),
            r.getUlong(4)
        );
        categoryStats["behavior"] = ChecklistItemCategoryStats(
            r.getUlong(5),
            r.getUlong(6)
        );
        scores[studentId] = calculateScore(
            params.expr,
            entryCount,
            categoryStats
        );
    }
    return scores;
}

private Optional!ScoringParameters getScoringParameters(Connection conn, ulong classId, in Date date) {
    const q = "SELECT score_expression, score_period FROM classroom_compliance_class WHERE id = ?";
    PreparedStatement ps = conn.prepareStatement(q);
    scope(exit) ps.close();
    ps.setUlong(1, classId);
    ResultSet rs = ps.executeQuery();
    scope(exit) rs.close();
    if (!rs.first()) return Optional!ScoringParameters.empty;
    string expr = rs.getString(1);
    string period = rs.getString(2);
    return Optional!ScoringParameters.of(ScoringParameters(
        parseExpression(expr),
        getDateRangeFromPeriodAndDate(date, period)
    ));
}

private DateRange getDateRangeFromPeriodAndDate(in Date date, in string period) {
    if (period == ScorePeriod.WEEK) {
        Date friday = date;
        if (date.dayOfWeek() == DayOfWeek.sun) {
            friday = date - days(2);
        } else {
            int diff = DayOfWeek.fri - date.dayOfWeek();
            friday = date + days(diff);
        }
        Date monday = friday - days(4);
        return DateRange(monday, friday);
    } else {
        throw new Exception("Unsupported period: " ~ period);
    }
}

/**
 * Calculates the score for a particular student, using the formula defined
 * by the class.
 * Returns: The score, or an empty optional if there isn't enough data.
 */
private Optional!double calculateScore(
    in Expr scoreExpression,
    ulong entryCount,
    in ChecklistItemCategoryStats[string] categoryStats
) {
    if (entryCount == 0) return Optional!double.empty;

    double classroomReadinessScore = 0;
    if ("classroom_readiness" in categoryStats) {
        classroomReadinessScore = categoryStats["classroom_readiness"].getUncheckedRatio();
    }

    double behaviorScore = 0;
    if ("behavior" in categoryStats) {
        behaviorScore = categoryStats["behavior"].getUncheckedRatio();
    }

    try {
        double score = scoreExpression.eval([
            "classroom_readiness": classroomReadinessScore,
            "behavior": behaviorScore,
        ]);
        import std.math : isNaN;
        if (isNaN(score)) return Optional!double.empty;
        return Optional!double.of(score);
    } catch (ExpressionEvaluationException e) {
        import handy_httpd;
        throw new HttpStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to compute score: " ~ e.msg);
    }
    
}
