/**
 * Grading Scales Helper
 * --------------------------------------------------------------------
 * Implements the five supported grading scales for ZIMSEC and Cambridge
 * (primary + secondary) and the resolver that picks which scale to
 * use for any given mark.
 *
 * Design (from .harness/plans/grading-scales.md):
 *  - School-wide default lives in `school_settings.default_grading_scale`.
 *  - Per-exam override on `exam_groups.grading_scale`.
 *  - Per-row override on `grades.grading_scale` / `course_grades.grading_scale`.
 *  - Storage stays as raw marks + percentage. Letter is derived at read time.
 *
 *  Resolver precedence (highest priority first):
 *    rowScale  →  examGroupScale  →  defaultScale  →  DEFAULT_SCALE_KEY
 */

const DEFAULT_SCALE_KEY = 'zimsec_olevel';

/**
 * The five supported scales, normalised to the shape:
 *   { letter, descriptor, min, max, points }
 *
 * `descriptor` is a human-friendly label that some scales carry (e.g. ZIMSEC
 * O-Level uses "A*" for the top band).  When a scale has no descriptor, the
 * letter is shown on its own.
 *
 * `points` is the ZIMSEC/Cambridge numeric "points" weight (1-9 for ZIMSEC,
 * undefined otherwise).  Frontend may use it for ranking.
 *
 * Bands are inclusive on both ends except the first one in each scale; the
 * `letterFor` function picks the FIRST band whose range contains the
 * percentage, with `min <= p <= max` semantics and a top band that is
 * also open-ended.
 */
const SCALES = {
  zimsec_primary: {
    key: 'zimsec_primary',
    label: 'ZIMSEC Primary',
    description: 'Grade 1-7 (lower is better). Primary school banding.',
    bands: [
      { letter: '1', min: 80, max: 100, points: 1 },
      { letter: '2', min: 70, max: 79, points: 2 },
      { letter: '3', min: 60, max: 69, points: 3 },
      { letter: '4', min: 55, max: 59, points: 4 },
      { letter: '5', min: 50, max: 54, points: 5 },
      { letter: '6', min: 40, max: 49, points: 6 },
      { letter: '7', min: 0,  max: 39, points: 7 }
    ]
  },

  zimsec_olevel: {
    key: 'zimsec_olevel',
    label: 'ZIMSEC O-Level',
    description: 'Form 1-4 numeric grade with letter descriptor (1/A* to 9/U).',
    bands: [
      { letter: '1', descriptor: 'A*', min: 80, max: 100, points: 1 },
      { letter: '2', descriptor: 'A',  min: 70, max: 79,  points: 2 },
      { letter: '3', descriptor: 'B',  min: 65, max: 69,  points: 3 },
      { letter: '4', descriptor: 'C',  min: 60, max: 64,  points: 4 },
      { letter: '5', descriptor: 'D',  min: 55, max: 59,  points: 5 },
      { letter: '6', descriptor: 'E',  min: 50, max: 54,  points: 6 },
      { letter: '7', descriptor: 'F',  min: 40, max: 49,  points: 7 },
      { letter: '8', descriptor: 'G',  min: 30, max: 39,  points: 8 },
      { letter: '9', descriptor: 'U',  min: 0,  max: 29,  points: 9 }
    ]
  },

  zimsec_alevel: {
    key: 'zimsec_alevel',
    label: 'ZIMSEC A-Level',
    description: 'Form 5-6 letter grade (A to U).',
    bands: [
      { letter: 'A', min: 80, max: 100 },
      { letter: 'B', min: 70, max: 79 },
      { letter: 'C', min: 60, max: 69 },
      { letter: 'D', min: 50, max: 59 },
      { letter: 'E', min: 40, max: 49 },
      { letter: 'U', min: 0,  max: 39 }
    ]
  },

  cambridge_igcse: {
    key: 'cambridge_igcse',
    label: 'Cambridge IGCSE',
    description: 'IGCSE O-Level equivalent (A* down to U).',
    bands: [
      { letter: 'A*', min: 90, max: 100 },
      { letter: 'A',  min: 80, max: 89 },
      { letter: 'B',  min: 70, max: 79 },
      { letter: 'C',  min: 60, max: 69 },
      { letter: 'D',  min: 50, max: 59 },
      { letter: 'E',  min: 40, max: 49 },
      { letter: 'F',  min: 30, max: 39 },
      { letter: 'G',  min: 20, max: 29 },
      { letter: 'U',  min: 0,  max: 19 }
    ]
  },

  cambridge_alevel: {
    key: 'cambridge_alevel',
    label: 'Cambridge AS/A-Level',
    description: 'Cambridge AS/A-Level (lowercase a-e, u).',
    bands: [
      { letter: 'a', min: 80, max: 100 },
      { letter: 'b', min: 70, max: 79 },
      { letter: 'c', min: 60, max: 69 },
      { letter: 'd', min: 50, max: 59 },
      { letter: 'e', min: 40, max: 49 },
      { letter: 'u', min: 0,  max: 39 }
    ]
  }
};

const SCALE_KEYS = Object.keys(SCALES);

/**
 * Returns true if the given key is one of the supported scale keys.
 * Exported for input validation in controllers.
 */
function isValidScaleKey(key) {
  return typeof key === 'string' && Object.prototype.hasOwnProperty.call(SCALES, key);
}

/**
 * Returns the list of scales as `{ key, label, description }` records,
 * suitable for rendering in a UI dropdown.
 */
function scaleOptions() {
  return SCALE_KEYS.map((k) => ({
    key: k,
    label: SCALES[k].label,
    description: SCALES[k].description
  }));
}

/**
 * Resolves the active scale key given the resolution order:
 *   rowScale  →  examGroupScale  →  defaultScale  →  DEFAULT_SCALE_KEY
 *
 * Each of the three input keys may be null/undefined.  Invalid keys fall
 * through to the next-priority level.  Returns the resolved key plus the
 * human-friendly label of the scale that was actually used.
 */
function resolveScale({ rowScale, parentScale, defaultScale } = {}) {
  const candidates = [rowScale, parentScale, defaultScale];
  for (const candidate of candidates) {
    if (isValidScaleKey(candidate)) {
      return { key: candidate, label: SCALES[candidate].label, source: 'resolved' };
    }
  }
  return { key: DEFAULT_SCALE_KEY, label: SCALES[DEFAULT_SCALE_KEY].label, source: 'fallback' };
}

/**
 * Maps a percentage to a single band in the given scale.
 *
 * `percentage` is the integer percentage 0..100.  We round to the nearest
 * integer before banding so the boundary behaviour matches what the UI shows
 * (e.g. 79.6 -> 80, which then falls into the 80+ band for ZIMSEC O-Level).
 *
 * Returns:
 *   { letter, descriptor, points, scale, label }
 *
 * If the scale key is unknown, falls back to the default scale rather than
 * throwing — the rest of the system still wants a valid letter.
 */
function letterFor(percentage, scaleKey) {
  const resolved = resolveScale({ rowScale: scaleKey });
  const scale = SCALES[resolved.key];
  // Math.floor (not Math.round) so a 79.5% mark goes to band 2, not band 1.
  // Bands are half-open [min, max]; rounding would let .5-boundary marks
  // float up into the next band and break the plan's acceptance test.
  const p = Math.floor(Number(percentage) || 0);
  const clamped = Math.max(0, Math.min(100, p));

  // Bands are stored min..max inclusive; find the first that contains p.
  const band = scale.bands.find((b) => clamped >= b.min && clamped <= b.max)
    || scale.bands[scale.bands.length - 1];

  return {
    letter: band.letter,
    descriptor: band.descriptor || null,
    points: typeof band.points === 'number' ? band.points : null,
    scale: resolved.key,
    label: scale.label
  };
}

/**
 * Reads the school's default grading scale from the `school_settings` table
 * using the supplied better-sqlite3 `db` instance.  Returns the key on
 * success, or DEFAULT_SCALE_KEY when nothing is configured / the row is
 * missing.  This is the bridge between the k/v table and the resolver so
 * controllers don't have to know about the storage detail.
 */
function loadDefaultScaleKey(db) {
  try {
    const row = db.prepare(
      "SELECT value FROM school_settings WHERE key = 'default_grading_scale' AND is_deleted = 0"
    ).get();
    return isValidScaleKey(row?.value) ? row.value : DEFAULT_SCALE_KEY;
  } catch (e) {
    return DEFAULT_SCALE_KEY;
  }
}

/**
 * Resolves the effective scale key for a grade/attempt row given its raw
 * overrides and the school default.  This is the canonical helper used by
 * every controller that reads percentage → letter, so the precedence is
 * identical everywhere.
 *
 *   rowScale  →  examGroupScale  →  defaultScale  →  DEFAULT_SCALE_KEY
 */
function effectiveScaleKey(db, { rowScale, examGroupScale } = {}) {
  return resolveScale({
    rowScale,
    parentScale: examGroupScale,
    defaultScale: loadDefaultScaleKey(db)
  }).key;
}

/**
 * Decoration helper: given a raw row that has `marks` and `total_marks`,
 * plus optional `grading_scale` and `exam_group_scale`, compute percentage
 * (2 dp) and the resolved letter band.  Adds `percentage`, `grade_letter`,
 * `grade_descriptor`, and `grade_scale` to a fresh copy of the row so
 * downstream code (controllers, sync push, dashboards) doesn't have to
 * repeat the math.
 *
 * Safe to call with any row — totals of 0 produce percentage 0 and the
 * lowest band of the resolved scale.
 */
function decorateWithGrade(row, db, extra = {}) {
  if (!row) return row;
  const marks = Number(row.marks ?? row.obtained_marks ?? 0);
  const total = Number(row.total_marks ?? 0);
  const safeTotal = total > 0 ? total : 0;
  const percentage = safeTotal > 0 ? Math.round((marks / safeTotal) * 10000) / 100 : 0;
  const scaleKey = effectiveScaleKey(db, {
    rowScale: row.grading_scale,
    examGroupScale: extra.examGroupScale
  });
  const letter = letterFor(percentage, scaleKey);
  return {
    ...row,
    percentage,
    grade_letter: letter.letter,
    grade_descriptor: letter.descriptor,
    grade_points: letter.points,
    grade_scale: letter.scale,
    grade_scale_label: letter.label
  };
}

module.exports = {
  DEFAULT_SCALE_KEY,
  SCALES,
  SCALE_KEYS,
  isValidScaleKey,
  scaleOptions,
  resolveScale,
  letterFor,
  loadDefaultScaleKey,
  effectiveScaleKey,
  decorateWithGrade
};
