<?php

/**
 * Trait: ASTGDRMSYS_Grade_Helper
 * Contains grading logic shared across result and marksheet classes.
 */

if (! defined('ABSPATH')) {
	exit;
}

trait ASTGDRMSYS_Grade_Helper
{
	/**
	 * Fetches the subjects and corresponding marks for a given student result.
	 *
	 * This method retrieves the student's marks from the database, unserializes them,
	 * and then fetches subject details for each subject code. It constructs an array
	 * of subjects with their marks, remarks, grade points, and optionally GPA (excluding fourth subjects).
	 *
	 * @param array $student_result The associative array containing student result data, including 'sid' (student ID) and 'class'.
	 * @return array An array of subjects with their details, marks, remarks, grade points, and GPA (if applicable).
	 */
	protected function fetch_student_subjects($student_result)
	{
		global $wpdb;

		$marks_table = esc_sql($this->prefix . 'astgdrmsys_mark');
		$marks_result = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT * FROM $marks_table WHERE `sid` = %d",
				$student_result['sid']
			),
			ARRAY_A
		);
		
		$marks_data = ! empty($marks_result) ? maybe_unserialize($marks_result[0]['marks']) : array();
		$subjects   = [];
		
		if (is_array($marks_data)) {
			$fourth_subject_code = $marks_data['fourth_subject'] ?? ($marks_data[0]['fourth_subject'] ?? null);
			$marks               = isset($marks_data[0]) ? $marks_data[0] : $marks_data;
			unset($marks['fourth_subject']);
			
                        foreach ($marks as $subcode => $mark) {
                                if ($subcode === 'insub') {
                                        continue;
                                }

                                $subject_table = esc_sql($this->prefix . 'astgdrmsys_subject');
                                $subject = $wpdb->get_row(
                                        $wpdb->prepare(
                                                "SELECT * FROM $subject_table WHERE `subcode` = %s AND `class` = %d",
                                                $subcode,
                                                $student_result['class']
                                        ),
                                        ARRAY_A
                                );

                                if ($subject) {
                                        if (is_array($mark) && ! empty($mark['not_taken'])) {
                                                continue;
                                        }

                                        if (is_array($mark) && !isset($mark['total'])) {
                                                $computed_total = 0;
                                                $has_numeric    = false;

                                                foreach ($mark as $part_key => $part_value) {
                                                        if (in_array($part_key, ['not_taken', 'fourth_subject'], true)) {
                                                                continue;
                                                        }

                                                        if (is_numeric($part_value)) {
                                                                $computed_total += floatval($part_value);
                                                                $has_numeric     = true;
                                                        }
                                                }

                                                if ($has_numeric) {
                                                        $mark['total'] = $computed_total;
                                                }
                                        }
					
					$is_fourth = ($subcode === $fourth_subject_code);
					$subject_data = [
						'subject'     => $subject,
						'mark'        => $mark,
						'remark'      => $this->calculate_subject_remark($subject, $mark),
						'grade_point' => $this->calculate_grade_point($subject, $mark),
						'is_fourth'   => $is_fourth,
					];
					
					if (! $is_fourth) {
						$subject_data['gpa_without_4'] = $this->calculate_gpa_without_4($subject, $mark);
					}
					
					$subjects[] = $subject_data;
				}
			}
		}
		
		return $subjects;
	}
	
	/**
	 * Calculates the percentage obtained for a subject.
	 *
	 * @param array $subject Subject details including total marks.
	 * @param mixed $mark    Obtained mark or array with a total key.
	 * @return float Percentage value between 0 and 100, or -1 if invalid.
	 */
        protected function get_subject_percentage($subject, $mark)
        {
                if (is_array($mark)) {
                        if (isset($mark['total'])) {
                                $obtained_mark = floatval($mark['total']);
                        } else {
                                $obtained_mark = 0;
                                $has_component = false;

                                foreach ($mark as $part_key => $part_value) {
                                        if (in_array($part_key, ['not_taken', 'fourth_subject'], true)) {
                                                continue;
                                        }

                                        if (is_numeric($part_value)) {
                                                $obtained_mark += floatval($part_value);
                                                $has_component   = true;
                                        }
                                }

                                if (! $has_component) {
                                        $obtained_mark = -1;
                                }
                        }
                } else {
                        $obtained_mark = floatval($mark);
                }
                $total_mark    = isset($subject['total']) ? floatval($subject['total']) : 0;

                if ($total_mark <= 0 && isset($subject['full_mark'])) {
                        $total_mark = floatval($subject['full_mark']);
                }

		if ($total_mark <= 0 || $obtained_mark < 0) {
			return -1;
		}

		$percentage = ($obtained_mark / $total_mark) * 100;
$percentage = floor($percentage);
		return max(0, min(100, $percentage));
	}

	/**
	 * Retrieves the grade row for a given percentage.
	 *
	 * @param float $percentage Calculated percentage for the subject.
	 * @return array|null Grade row that matches the percentage, if any.
	 */
        protected function get_grade_row_by_percentage($percentage)
        {
                if ($percentage < 0) {
                        return null;
                }

                global $wpdb;
                $grade_table = esc_sql($this->prefix . 'astgdrmsys_grade_system');

                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery
                $matching_grades = $wpdb->get_results(
                        $wpdb->prepare(
                                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                                "SELECT * FROM $grade_table WHERE min_mark <= %f AND max_mark >= %f ORDER BY (max_mark - min_mark) ASC, min_mark DESC",
                                $percentage,
                                $percentage
                        ),
                        ARRAY_A
                );

                if (empty($matching_grades)) {
                        return null;
                }

                if (count($matching_grades) > 1) {
                        $conflicting_ranges = array_map(
                                static function ($grade) {
                                        return sprintf('%s-%s', $grade['min_mark'], $grade['max_mark']);
                                },
                                $matching_grades
                        );

                        error_log(
                                sprintf(
                                        'ASTGDRMSYS: Overlapping grade ranges detected for percentage %s: %s',
                                        $percentage,
                                        implode(', ', $conflicting_ranges)
                                )
                        );

                        return null;
                }

                return $matching_grades[0];
        }

	/**
	 * Calculates the remark (grade) for a given subject based on the obtained mark.
	 *
	 * This method determines the grade by comparing the obtained mark against the grading system
	 * stored in the database. If the total mark is zero or the obtained mark is invalid, it returns 'F'.
	 *
	 * @param array $subject The subject information, including the total possible mark.
	 * @param mixed $mark The obtained mark, either as a float/int or an array with a 'total' key.
	 * @return string The grade remark (e.g., 'A', 'B', 'C', 'F').
	 */
	protected function calculate_subject_remark($subject, $mark)
	{
		$percentage = $this->get_subject_percentage($subject, $mark);

		if ($percentage < 0) {
			return 'F';
		}

		$grade = $this->get_grade_row_by_percentage($percentage);

		return $grade['grade'] ?? 'F';
	}

	/**
	 * Calculates the grade point for a given subject and obtained mark.
	 *
	 * This method determines the grade point by comparing the obtained mark against
	 * the grade boundaries defined in the database table. If the total mark is zero
	 * or the obtained mark is invalid, it returns 0.
	 *
	 * @param array $subject The subject details, including the total possible mark.
	 * @param mixed $mark The obtained mark, either as a float/int or an array with a 'total' key.
	 * @return float The calculated grade point, or 0 if not applicable.
	 */
	protected function calculate_grade_point($subject, $mark)
	{
		$percentage = $this->get_subject_percentage($subject, $mark);

		if ($percentage < 0) {
			return 0;
		}

		$grade = $this->get_grade_row_by_percentage($percentage);

		return isset($grade['grade_point']) ? floatval($grade['grade_point']) : 0;
	}

	/**
	 * Calculates the GPA (Grade Point Average) for a subject without using a 4-point scale.
	 *
	 * This method determines the grade point based on the obtained marks for a subject,
	 * referencing the grade system table in the database. It handles both array and scalar
	 * mark inputs, validates the marks, and retrieves the corresponding grade point.
	 *
	 * @param array $subject The subject information, including the total possible marks.
	 * @param mixed $mark The obtained mark(s) for the subject. Can be an array with a 'total' key or a numeric value.
	 * @return float The calculated grade point for the obtained mark, or 0 if invalid.
	 */
	protected function calculate_gpa_without_4($subject, $mark)
	{
		$percentage = $this->get_subject_percentage($subject, $mark);

		if ($percentage < 0) {
			return 0;
		}

		$grade = $this->get_grade_row_by_percentage($percentage);

		return isset($grade['grade_point']) ? floatval($grade['grade_point']) : 0;
	}

        /**
         * Retrieves subjects associated with a specific class ID and optional departments.
         *
         * Queries the database for subjects linked to the provided class ID,
         * filtered by department when provided, and returns them as an associative
         * array indexed by subject code.
         *
         * @param int    $class_id          The ID of the class to retrieve subjects for.
         * @param string $department_ids    Optional comma-separated department IDs.
         * @param array  $subject_overrides Optional map of subject codes/IDs to override totals.
         * @return array Associative array of subjects, keyed by subject code.
         */
        public function get_subjects_by_class($class_id, $department_ids = null, $subject_overrides = array())
        {
                global $wpdb;
                $table = $wpdb->prefix . 'astgdrmsys_subject';
                $query  = "SELECT * FROM $table WHERE class = %d";
                $params = array($class_id);
                if (!empty($department_ids)) {
                        $dept_ids = array_map('intval', explode(',', $department_ids));
                        if ($dept_ids) {
                                $placeholders = implode(', ', array_fill(0, count($dept_ids), '%d'));
                                $query      .= " AND department IN ($placeholders)";
                                $params      = array_merge($params, $dept_ids);
                        }
                }
                //phpcs:ignore WordPress.DB.DirectDatabaseQuery
                $results = $wpdb->get_results(
                        $wpdb->prepare($query, $params),
                        ARRAY_A
                );
                $subjects = [];
                $subject_overrides = is_array($subject_overrides) ? $subject_overrides : array();
                foreach ($results as $row) {
                        $normalized_code = trim((string) ($row['subcode'] ?? ''));
                        $subject_id      = isset($row['id']) ? (int) $row['id'] : null;

                        $override_total = null;

                        if ($normalized_code !== '' && isset($subject_overrides[$normalized_code])) {
                                $override_total = floatval($subject_overrides[$normalized_code]);
                        } elseif (isset($row['subcode']) && isset($subject_overrides[$row['subcode']])) {
                                $override_total = floatval($subject_overrides[$row['subcode']]);
                        } elseif ($subject_id !== null && isset($subject_overrides[$subject_id])) {
                                $override_total = floatval($subject_overrides[$subject_id]);
                        }

                        if ($override_total !== null) {
                                $row['total'] = $override_total;
                        } elseif (! isset($row['total']) && isset($row['full_mark'])) {
                                $row['total'] = floatval($row['full_mark']);
                        }

                        $subjects[$row['subcode']] = $row;
                }
                return $subjects;
        }

        /**
         * Normalizes the stored subject full marks for an exam into lookup arrays.
         *
         * @param mixed $raw_value Raw subject data from the exam record.
         * @return array<string|int, float> Map of subject identifiers to their custom full marks.
         */
        protected function parse_exam_subject_full_marks($raw_value)
        {
                if (empty($raw_value)) {
                        return array();
                }

                if (is_string($raw_value)) {
                        $decoded = json_decode($raw_value, true);

                        if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
                                $raw_value = $decoded;
                        } else {
                                $maybe_unserialized = maybe_unserialize($raw_value);

                                if (is_array($maybe_unserialized)) {
                                        $raw_value = $maybe_unserialized;
                                }
                        }
                } elseif (is_object($raw_value)) {
                        $raw_value = (array) $raw_value;
                }

                if (! is_array($raw_value)) {
                        return array();
                }

                $overrides = array();

                foreach ($raw_value as $entry) {
                        if (is_object($entry)) {
                                $entry = (array) $entry;
                        }

                        if (! is_array($entry)) {
                                continue;
                        }

                        $full_mark = null;

                        if (isset($entry['full_mark']) && $entry['full_mark'] !== '') {
                                $full_mark = floatval($entry['full_mark']);
                        } elseif (isset($entry['total']) && $entry['total'] !== '') {
                                $full_mark = floatval($entry['total']);
                        }

                        if ($full_mark === null) {
                                continue;
                        }

                        if (! empty($entry['code'])) {
                                $overrides[trim((string) $entry['code'])] = $full_mark;
                        } elseif (! empty($entry['subcode'])) {
                                $overrides[trim((string) $entry['subcode'])] = $full_mark;
                        }

                        if (! empty($entry['id'])) {
                                $overrides[(int) $entry['id']] = $full_mark;
                        }
                }

                return $overrides;
        }

	/**
	 * Retrieves the marks for a student by their student ID (sid).
	 *
	 * This function queries the database for the serialized marks data associated with the given student ID,
	 * unserializes the data, and returns the first element of the marks array if available.
	 *
	 * @param int $sid The student ID whose marks are to be retrieved.
	 * @return array The marks data for the student, or an empty array if not found or invalid.
	 */
	public function get_student_marks($sid)
	{
		global $wpdb;
		$table = $wpdb->prefix . 'astgdrmsys_mark';

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery
		$result = $wpdb->get_row(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				"SELECT marks FROM $table WHERE sid = %d",
				 $sid
				),
			ARRAY_A
		);

		if (! $result || empty($result['marks'])) {
			return [];
		}

		$data = maybe_unserialize($result['marks']);
		return is_array($data) && isset($data[0]) ? $data[0] : [];
	}

	/**
	 * Calculate the total remark based on the given percentage.
	 *
	 * @param float $percentage The percentage to evaluate for the remark.
	 * @return string The total remark corresponding to the given percentage.
	 */
	private function get_total_remark($percentage)
	{
		global $wpdb;
		$grade_table = $wpdb->prefix . 'astgdrmsys_grade_system';
		
		// Find the row where percentage is between min_mark and max_mark
		$row = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT remarks FROM $grade_table 
				 WHERE %d BETWEEN min_mark AND max_mark 
            	 LIMIT 1",
				$percentage
			)
		);
	
		// Return the remarks if found, otherwise return a default message
		if ($row && !empty($row->remarks)) {
			return $row->remarks;
		}
		
		return 'No Remark';
	}
	
	/**
	 * Retrieve the letter grade that corresponds to a given numeric GPA.
	 *
	 * This method queries the plugin's grade system table ("{prefix}astgdrmsys_grade_system")
	 * to find the row whose numeric grade_point is the largest value less than or equal
	 * to the supplied $gpa. If no row matches and the supplied GPA is at or above the
	 * top scale (e.g., 5.00), the method will fall back to the table's highest grade_point row.
	 *
	 * Behavior:
	 * - Uses the global $wpdb and $this->prefix to construct and query the grade table.
	 * - Primary lookup: prepared query selecting the row with grade_point <= $gpa,
	 *   ordered by grade_point DESC, LIMIT 1.
	 * - Fallback: if no row is returned and $gpa >= 5, selects the row with the highest grade_point.
	 * - Returns the 'grade' column value when found, otherwise returns the literal 'N/A'.
	 *
	 * Parameters:
	 * @param float $gpa Numeric GPA value to map to a letter grade.
	 *
	 * Returns:
	 * @return string Letter grade from the grade table (e.g., "A", "B+") or 'N/A' if no mapping exists.
	 *
	 * Notes:
	 * - Expects the grade table to contain numeric 'grade_point' and textual 'grade' columns.
	 * - The table name is escaped using esc_sql($this->prefix . 'astgdrmsys_grade_system').
	 * - Queries are read-only and have no side effects on the database.
	 */
	private function get_letter_grade_by_gpa($gpa)
	{
		global $wpdb;
		$grade_table = esc_sql($this->prefix . 'astgdrmsys_grade_system');
	
		// Find the grade whose grade_point is the closest match (≤ GPA)
		$row = $wpdb->get_row(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				"SELECT grade, grade_point 
				 FROM $grade_table 
				 WHERE grade_point <= %f 
				 ORDER BY grade_point DESC 
				 LIMIT 1",
				$gpa
			),
			ARRAY_A
		);
	
		// Handle perfect GPA (e.g., 5.00 exactly) or if GPA exceeds table's top range
		if (!$row && $gpa >= 5) {
			$row = $wpdb->get_row("SELECT grade, grade_point FROM $grade_table ORDER BY grade_point DESC LIMIT 1", ARRAY_A);
		}
	
		return $row && !empty($row['grade']) ? $row['grade'] : 'N/A';
	}

}
