import { z } from "zod";
import {
  ASSESSMENT_STATUSES,
  CURRICULUM_STATUSES,
  IMPORT_MODES,
  NON_UNIT_PROGRESS_STATUSES,
  ROLES,
  RULE_TYPES,
  YEAR_LONG_POLICIES,
} from "@/types";
import { STUDENT_NUMBER_NORMALIZATIONS } from "@/lib/settings/constants";

/** Validation schemas (SPEC 27). Every server action parses its input here. */

const CURRENT_YEAR = new Date().getFullYear();

const trimmed = (max: number) => z.string().trim().max(max);
const optionalText = (max: number) =>
  z
    .string()
    .trim()
    .max(max)
    .optional()
    .nullable()
    .transform((v) => (v === undefined || v === "" ? null : v));

const optionalYear = z
  .union([z.string(), z.number(), z.null(), z.undefined()])
  .transform((v) => {
    if (v === null || v === undefined || v === "") return null;
    const n = Number(v);
    return Number.isFinite(n) ? Math.trunc(n) : Number.NaN;
  })
  .refine((v) => v === null || (Number.isFinite(v) && v >= 1900 && v <= CURRENT_YEAR + 10), {
    message: `Enter a valid 4-digit year between 1900 and ${CURRENT_YEAR + 10}.`,
  });

const optionalNumber = (min: number, max: number, label: string) =>
  z
    .union([z.string(), z.number(), z.null(), z.undefined()])
    .transform((v) => {
      if (v === null || v === undefined || v === "") return null;
      const n = Number(v);
      return Number.isFinite(n) ? n : Number.NaN;
    })
    .refine((v) => v === null || (Number.isFinite(v) && v >= min && v <= max), {
      message: `${label} must be a number between ${min} and ${max}.`,
    });

// -----------------------------------------------------------------------------
// Students
// -----------------------------------------------------------------------------

export const studentProfileSchema = z.object({
  student_number: trimmed(50).min(1, "Student number is required."),
  last_name: trimmed(100).min(1, "Last name is required."),
  first_name: trimmed(100).min(1, "First name is required."),
  middle_name: optionalText(100),
  suffix: optionalText(20),
  premed_course: optionalText(150),
  premed_school: optionalText(200),
  graduation_year: optionalYear,
  gwa: optionalText(20),
  nmat_score: optionalNumber(0, 100, "NMAT score"),
  nmat_year: optionalYear,
  notes: optionalText(2000),
  active: z.boolean().default(true),
});

export type StudentProfileInput = z.infer<typeof studentProfileSchema>;

export const curriculumOverrideSchema = z.object({
  student_id: z.string().min(1),
  curriculum_id: z.string().min(1, "Select a curriculum."),
  // A manual override is never accepted without a reason (SPEC 12 #8, 49 #5).
  reason: z
    .string()
    .trim()
    .min(10, "Provide a reason of at least 10 characters for this curriculum override."),
});

// -----------------------------------------------------------------------------
// Curriculum
// -----------------------------------------------------------------------------

export const curriculumSchema = z.object({
  code: trimmed(50).min(1, "Curriculum code is required."),
  name: trimmed(200).min(1, "Curriculum name is required."),
  effective_year: optionalYear,
  version: trimmed(20).default("1"),
  year_long_credit_policy: z.enum(YEAR_LONG_POLICIES).default("ON_FINAL_PASS"),
  notes: optionalText(2000),
});

export const curriculumStatusSchema = z.enum(CURRICULUM_STATUSES);

export const institutionalTypeSchema = z.object({
  curriculum_id: z.string().min(1),
  institutional_type: z.enum(["Curriculum 2023", "Curriculum 2024", "Curriculum 2025", ""]),
  reason: z.string().trim().min(5, "Provide a short reason for this mapping."),
});

export const courseSchema = z.object({
  course_code: trimmed(50).min(1, "Course code is required."),
  course_title: trimmed(300).min(1, "Course title is required."),
  units: z.coerce.number().min(0, "Units cannot be negative.").max(100),
  lecture_units: optionalNumber(0, 100, "Lecture units"),
  laboratory_units: optionalNumber(0, 100, "Laboratory units"),
  hours_per_week: optionalNumber(0, 100, "Hours per week"),
  intended_year: z.coerce.number().int().min(1).max(10),
  term: trimmed(50).min(1, "Term is required."),
  category: optionalText(100),
  prerequisite_text: optionalText(500),
  required: z.boolean().default(true),
  counts_toward_program_units: z.boolean().default(true),
  notes: optionalText(1000),
});

// -----------------------------------------------------------------------------
// Assignment rules
// -----------------------------------------------------------------------------

export const assignmentRuleSchema = z
  .object({
    name: trimmed(150).min(1, "Rule name is required."),
    curriculum_id: z.string().min(1, "Select the curriculum this rule assigns."),
    rule_type: z.enum(RULE_TYPES),
    prefix: optionalText(50),
    regex_pattern: optionalText(300),
    exact_student_number: optionalText(50),
    numeric_start: optionalNumber(0, Number.MAX_SAFE_INTEGER, "Range start"),
    numeric_end: optionalNumber(0, Number.MAX_SAFE_INTEGER, "Range end"),
    priority: z.coerce.number().int().min(0).max(10000).default(100),
    active: z.boolean().default(true),
    description: optionalText(500),
  })
  .superRefine((value, ctx) => {
    if (value.rule_type === "PREFIX" && !value.prefix) {
      ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["prefix"], message: "A prefix is required." });
    }
    if (value.rule_type === "REGEX") {
      if (!value.regex_pattern) {
        ctx.addIssue({
          code: z.ZodIssueCode.custom,
          path: ["regex_pattern"],
          message: "A regular expression is required.",
        });
      } else {
        try {
          new RegExp(value.regex_pattern);
        } catch {
          ctx.addIssue({
            code: z.ZodIssueCode.custom,
            path: ["regex_pattern"],
            message: "This is not a valid regular expression.",
          });
        }
      }
    }
    if (value.rule_type === "EXACT" && !value.exact_student_number) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        path: ["exact_student_number"],
        message: "An exact student number is required.",
      });
    }
    if (value.rule_type === "NUMERIC_RANGE") {
      if (value.numeric_start === null || value.numeric_end === null) {
        ctx.addIssue({
          code: z.ZodIssueCode.custom,
          path: ["numeric_start"],
          message: "Both a range start and end are required.",
        });
      } else if (value.numeric_start > value.numeric_end) {
        ctx.addIssue({
          code: z.ZodIssueCode.custom,
          path: ["numeric_end"],
          message: "Range end must be greater than or equal to range start.",
        });
      }
    }
  });

// -----------------------------------------------------------------------------
// Assessment
// -----------------------------------------------------------------------------

export const assessmentUpdateSchema = z.object({
  student_id: z.string().min(1),
  curriculum_course_id: z.string().min(1),
  status: z.enum(ASSESSMENT_STATUSES),
  grade_raw: optionalText(50),
  remarks: optionalText(1000),
  equivalent_source_school: optionalText(200),
  equivalent_course_code: optionalText(50),
  equivalent_course_title: optionalText(300),
  equivalent_source_units: optionalNumber(0, 100, "Source units"),
  approved_credited_units_override: optionalNumber(0, 100, "Approved credited units"),
});

export const bulkAssessmentSchema = z.object({
  student_id: z.string().min(1),
  curriculum_course_ids: z.array(z.string().min(1)).min(1, "Select at least one course."),
  status: z.enum(ASSESSMENT_STATUSES),
});

export const nonUnitProgressSchema = z.object({
  student_id: z.string().min(1),
  non_unit_requirement_id: z.string().min(1),
  selected: z.boolean(),
  status: z.enum(NON_UNIT_PROGRESS_STATUSES),
  remarks: optionalText(500),
});

// -----------------------------------------------------------------------------
// Programs
// -----------------------------------------------------------------------------

export const programSchema = z.object({
  code: trimmed(20)
    .min(1, "Program code is required.")
    .regex(/^[A-Za-z0-9-]+$/, "Use letters, numbers and hyphens only."),
  name: trimmed(150).min(1, "Program name is required."),
  active: z.boolean().default(true),
  notes: optionalText(500),
});

// -----------------------------------------------------------------------------
// Users and settings
// -----------------------------------------------------------------------------

export const userSchema = z.object({
  email: z.string().trim().toLowerCase().email("Enter a valid email address.").max(200),
  full_name: trimmed(150).min(1, "Full name is required."),
  role: z.enum(ROLES),
  active: z.boolean().default(true),
  /** Empty string means "all programs" — only valid for a Super Administrator. */
  program_id: z
    .string()
    .trim()
    .optional()
    .nullable()
    .transform((v) => (v === undefined || v === "" ? null : v)),
});

export const settingsSchema = z.object({
  institution_name: trimmed(150).min(1),
  college_name: trimmed(150).min(1),
  display_precision: z.coerce.number().int().min(0).max(6),
  student_number_normalization: z.enum(STUDENT_NUMBER_NORMALIZATIONS),
  manual_override_roles: z.array(z.enum(ROLES)).min(1, "At least one role must be able to override."),
  max_import_file_mb: z.coerce.number().int().min(1).max(50),
  encoder_can_import_students: z.boolean(),
  archive_inactive_students: z.boolean(),
  logo_data_url: z.string().max(2_000_000).optional().default(""),
});

// -----------------------------------------------------------------------------
// Imports
// -----------------------------------------------------------------------------

export const importModeSchema = z.enum(IMPORT_MODES);

/** Flattens a ZodError into `field -> message` for inline form display. */
export function fieldErrors(error: z.ZodError): Record<string, string> {
  const out: Record<string, string> = {};
  for (const issue of error.issues) {
    const key = issue.path.join(".") || "_form";
    if (!out[key]) out[key] = issue.message;
  }
  return out;
}
