/**
 * Comprehension rules for the compare screen, kept pure and unit-tested.
 *
 * These encode the expert-review requirements (spec §8 of the compare
 * redesign): report-card summaries count graded areas only (safeguarding is
 * a separate binary judgement), problem areas are always NAMED rather than
 * folded into counts, grade labels pass through from the API (live-sampled
 * Ofsted vocabulary — never invented here), admissions chips use one
 * consistent metric, and progress bands follow DfE's confidence-interval
 * methodology instead of thresholding point estimates.
 */

import type { OfstedInspection, SchoolAdmissions } from './types';

// ---------------------------------------------------------------------------
// Verdicts against an anchor (England average or state-school benchmark)
// ---------------------------------------------------------------------------

export type Verdict = 'above' | 'close' | 'below';

export function verdict(value: number, anchor: number, tolerance = 2): Verdict {
  if (value >= anchor + tolerance) return 'above';
  if (value <= anchor - tolerance) return 'below';
  return 'close';
}

// ---------------------------------------------------------------------------
// Ofsted — three regimes, one display model
// ---------------------------------------------------------------------------

export const OFSTED_LEGACY_GRADES: Record<number, string> = {
  1: 'Outstanding',
  2: 'Good',
  3: 'Requires improvement',
  4: 'Inadequate',
};

/** rc_ key → the area label used across the reviewed mockups. */
const RC_AREA_LABELS: Record<string, string> = {
  rc_inclusion: 'Inclusion',
  rc_curriculum_teaching: 'Curriculum & teaching',
  rc_achievement: 'Achievement',
  rc_attendance_behaviour: 'Attendance & behaviour',
  rc_personal_development: 'Personal development',
  rc_leadership_governance: 'Leadership & governance',
  rc_early_years: 'Early years',
  rc_sixth_form: 'Sixth form',
};

export function rcAreaLabel(key: string): string {
  return RC_AREA_LABELS[key] ?? key;
}

export interface ReportCardSummary {
  /** Graded areas only, grouped by label, best grade first. */
  counts: Array<{ label: string; count: number }>;
  /** Areas rated Needs attention / Urgent improvement — always named. */
  problems: Array<{ areaLabel: string; label: string }>;
  safeguarding: 'met' | 'not_met' | null;
  /** True when every graded area is Expected standard or better and
   * safeguarding is not "not met". */
  allClear: boolean;
}

const PROBLEM_CODES = new Set([4, 5]);

export function summariseReportCard(ofsted: OfstedInspection): ReportCardSummary {
  const entries = Object.entries(ofsted.report_card ?? {});
  const byCode = new Map<number, { label: string; count: number }>();
  const problems: ReportCardSummary['problems'] = [];

  for (const [key, entry] of entries) {
    if (PROBLEM_CODES.has(entry.code)) {
      problems.push({ areaLabel: rcAreaLabel(key), label: entry.label });
    } else {
      const existing = byCode.get(entry.code);
      if (existing) existing.count += 1;
      else byCode.set(entry.code, { label: entry.label, count: 1 });
    }
  }

  const counts = [...byCode.entries()]
    .sort(([a], [b]) => a - b)
    .map(([, v]) => v);

  const safeguarding =
    ofsted.rc_safeguarding_met === true
      ? 'met'
      : ofsted.rc_safeguarding_met === false
        ? 'not_met'
        : null;

  return {
    counts,
    problems,
    safeguarding,
    allClear: entries.length > 0 && problems.length === 0 && safeguarding !== 'not_met',
  };
}

export type OfstedDisplay =
  | { kind: 'none' }
  | { kind: 'graded'; grade: number; gradeLabel: string; carriedForward: false }
  | { kind: 'carried_forward'; grade: number; gradeLabel: string; carriedForward: true }
  | { kind: 'transitional' }
  | { kind: 'report_card'; summary: ReportCardSummary };

export function ofstedDisplay(
  ofsted: OfstedInspection | null | undefined,
): OfstedDisplay {
  if (!ofsted) return { kind: 'none' };

  // A report card is the newest inspection format; when present it wins —
  // never derive or prefer an overall grade alongside it.
  if (ofsted.report_card && Object.keys(ofsted.report_card).length > 0) {
    return { kind: 'report_card', summary: summariseReportCard(ofsted) };
  }

  const grade = ofsted.overall_effectiveness;
  const gradeLabel = grade != null ? OFSTED_LEGACY_GRADES[grade] : undefined;
  if (grade == null || gradeLabel === undefined) {
    if (ofsted.inspection_date) {
      return { kind: 'transitional' };
    }
    return { kind: 'none' };
  }

  if (ofsted.grade_source === 'ungraded_carried_forward') {
    return { kind: 'carried_forward', grade, gradeLabel, carriedForward: true };
  }
  return { kind: 'graded', grade, gradeLabel, carriedForward: false };
}

// ---------------------------------------------------------------------------
// Admissions — one consistent chip metric (first-preference success)
// ---------------------------------------------------------------------------

export interface AdmissionsSummary {
  firstPrefPct: number | null;
  chip: { tone: 'good' | 'warn' | 'neutral'; text: string } | null;
  /** e.g. "Named on 457 forms · 180 places" — total preferences at any rank,
   * deliberately not phrased as head-to-head applications. */
  interest: string | null;
}

/**
 * Pick the admissions round for the ACTIVE phase tab. An all-through school
 * can carry only a Year 7 (Secondary) round — rendering that beside pure
 * primaries' Reception rounds made 433-forms-for-173-places read as
 * Reception odds. Rows matching the target phase win (latest year first);
 * rows tagged with the OTHER phase are never substituted. Untagged rows
 * (legacy data, no school_phase) are used only when no row carries a phase.
 */
export function admissionsForPhase(
  data:
    | { admissions?: SchoolAdmissions | null; admissions_history?: SchoolAdmissions[] }
    | null
    | undefined,
  isSecondary: boolean,
): SchoolAdmissions | null {
  if (!data) return null;
  const rows: SchoolAdmissions[] = [
    ...(data.admissions_history ?? []),
    ...(data.admissions ? [data.admissions] : []),
  ];
  if (rows.length === 0) return null;
  const target = isSecondary ? 'secondary' : 'primary';
  const byYearDesc = (a: SchoolAdmissions, b: SchoolAdmissions) => (b.year ?? 0) - (a.year ?? 0);

  const matching = rows
    .filter((r) => r.school_phase?.toLowerCase() === target)
    .sort(byYearDesc);
  if (matching.length > 0) return matching[0];

  const tagged = rows.some((r) => r.school_phase != null);
  if (!tagged) return [...rows].sort(byYearDesc)[0];

  return null;
}

export function summariseAdmissions(
  a: SchoolAdmissions | null | undefined,
): AdmissionsSummary {
  if (!a) return { firstPrefPct: null, chip: null, interest: null };

  const pct =
    a.first_preference_offer_pct != null
      ? Math.round(a.first_preference_offer_pct)
      : null;

  let chip: AdmissionsSummary['chip'] = null;
  if (pct != null) {
    if (pct >= 100) {
      chip = { tone: 'good', text: 'All first choices offered' };
    } else if (pct < 50) {
      // Banded, not one blanket chip: "Over 1 in 4" on a school where more
      // than half missed out understated the worst cases by half.
      chip = { tone: 'warn', text: 'More than half of first choices missed out' };
    } else if (pct < 67) {
      chip = { tone: 'warn', text: 'About 1 in 3 first choices missed out' };
    } else if (pct < 75) {
      chip = { tone: 'warn', text: 'Over 1 in 4 first choices missed out' };
    } else {
      chip = { tone: pct >= 90 ? 'good' : 'neutral', text: `${pct}% of first choices offered` };
    }
  }

  const interest =
    a.total_applications != null && a.places_offered != null
      ? `Named on ${a.total_applications.toLocaleString('en-GB')} forms · ${a.places_offered.toLocaleString('en-GB')} places`
      : null;

  return { firstPrefPct: pct, chip, interest };
}

// ---------------------------------------------------------------------------
// Progress bands — DfE confidence-interval methodology
// ---------------------------------------------------------------------------

export function progressBand(
  score: number | null,
  lower: number | null,
  upper: number | null,
): 'above' | 'average' | 'below' | null {
  if (score == null || lower == null || upper == null) return null;
  if (lower > 0) return 'above';
  if (upper < 0) return 'below';
  return 'average';
}

// ---------------------------------------------------------------------------
// Dot-strip geometry
// ---------------------------------------------------------------------------

export interface StripPoint {
  /** 0–100 percentage position along the track. */
  pos: number;
  labelAbove: boolean;
  value: number;
  schoolIndex: number;
}

/** Labels within 4% of the domain of a lower neighbour flip above the strip
 * (the reviewed mockups' collision nudge). */
export function stripPositions(
  values: Array<number | null>,
  min = 0,
  max = 100,
): StripPoint[] {
  const span = max - min;
  const points = values
    .map((value, schoolIndex) => ({ value, schoolIndex }))
    .filter((p): p is { value: number; schoolIndex: number } => p.value != null)
    .map((p) => ({
      value: p.value,
      schoolIndex: p.schoolIndex,
      pos: Math.min(100, Math.max(0, ((p.value - min) / span) * 100)),
      labelAbove: false,
    }));

  const nudge = span * 0.04;
  let lastBelow = -Infinity;
  for (const p of [...points].sort((a, b) => a.value - b.value)) {
    if (p.value - lastBelow < nudge) {
      p.labelAbove = true;
    } else {
      lastBelow = p.value;
    }
  }
  return points;
}

// ---------------------------------------------------------------------------
// Metric extraction
// ---------------------------------------------------------------------------

/** Latest non-null yearly value of `metricKey` per school, in `urns` order. */
export function latestValues(
  data: Record<string, { yearly_data: Array<{ year: number }> }>,
  urns: number[],
  metricKey: string,
): Array<number | null> {
  return urns.map((urn) => {
    const rows = data[String(urn)]?.yearly_data ?? [];
    for (let i = rows.length - 1; i >= 0; i--) {
      const v = (rows[i] as Record<string, unknown>)[metricKey];
      if (typeof v === 'number' && !Number.isNaN(v)) return v;
    }
    return null;
  });
}
