import type { EnrollmentCompletionPolicy } from '@i-learned/core';
import { DEFAULT_ENROLLMENT_POLICY } from '@i-learned/core';

import type { Db } from './client';
import { prisma } from './client';

/**
 * Institutional policy values (SPEC.md §0.7, §33.17).
 *
 * Every value an institution might change — tuition rules, the NMAT threshold,
 * the minimum down payment, the maximum unit load, whether students see their
 * GWA — is a row in `system_setting`, not a constant in code. This module is
 * the typed accessor for them.
 *
 * The `SETTING_KEYS` catalogue is the single place a key is named, so a typo
 * in a service is a compile error rather than a silently missing setting.
 */

export const SETTING_KEYS = {
  // Admissions
  medicineNmatMinimumPercentile: 'admissions.medicine.nmat_min_percentile',
  catPassingScore: 'admissions.cat.passing_score',
  applicationDraftRetentionMonths: 'admissions.application_draft_retention_months',
  duplicateDetectionEnabled: 'admissions.duplicate_detection_enabled',

  // Enrollment
  enrollmentCompletionRequirements: 'enrollment.completion_requirements',
  maxUnitsDefault: 'enrollment.max_units_default',
  enforcePrerequisites: 'enrollment.enforce_prerequisites',
  allowSectionOverload: 'scheduling.allow_overload',

  // Finance
  minimumDownPaymentPercent: 'finance.minimum_downpayment_percent',
  currencyCode: 'finance.currency_code',

  // Grading
  showGwaToStudents: 'grading.show_gwa_to_students',

  // Analytics and privacy
  smallCellThreshold: 'analytics.small_cell_threshold',

  // Documents
  corWatermarkText: 'documents.cor_watermark_text',
  documentsAreOfficial: 'documents.are_official',
} as const;

export type SettingKey = (typeof SETTING_KEYS)[keyof typeof SETTING_KEYS];

/**
 * Fallbacks used only when a setting row is missing — before the seed has run,
 * or when a new setting is introduced by a deployment that precedes its
 * migration. They are deliberately the safest option in each case: enforce the
 * rule, hide the data, suppress the small cell.
 */
const DEFAULTS: Record<SettingKey, unknown> = {
  [SETTING_KEYS.medicineNmatMinimumPercentile]: 40,
  [SETTING_KEYS.catPassingScore]: null,
  [SETTING_KEYS.applicationDraftRetentionMonths]: 12,
  [SETTING_KEYS.duplicateDetectionEnabled]: true,

  [SETTING_KEYS.enrollmentCompletionRequirements]: DEFAULT_ENROLLMENT_POLICY,
  [SETTING_KEYS.maxUnitsDefault]: 27,
  [SETTING_KEYS.enforcePrerequisites]: true,
  [SETTING_KEYS.allowSectionOverload]: false,

  [SETTING_KEYS.minimumDownPaymentPercent]: 20,
  [SETTING_KEYS.currencyCode]: 'PHP',

  [SETTING_KEYS.showGwaToStudents]: false,

  [SETTING_KEYS.smallCellThreshold]: 5,

  [SETTING_KEYS.corWatermarkText]: 'UNOFFICIAL — PROTOTYPE',
  [SETTING_KEYS.documentsAreOfficial]: false,
};

export class SettingsService {
  /**
   * Per-instance cache. Settings change rarely and are read on nearly every
   * request; without this, a single enrollment validation would issue a dozen
   * identical queries.
   */
  private cache = new Map<string, unknown>();
  private loadedAt = 0;

  constructor(
    private readonly institutionId: string,
    private readonly db: Db = prisma,
    private readonly ttlMs = 30_000,
  ) {}

  /** Drops the cache, so a settings edit takes effect immediately. */
  invalidate(): void {
    this.cache.clear();
    this.loadedAt = 0;
  }

  private async load(): Promise<void> {
    if (this.loadedAt > 0 && Date.now() - this.loadedAt < this.ttlMs) return;

    const rows = await this.db.systemSetting.findMany({
      where: { institutionId: this.institutionId },
      select: { key: true, value: true },
    });

    this.cache = new Map(rows.map((row) => [row.key, row.value]));
    this.loadedAt = Date.now();
  }

  async getRaw(key: SettingKey): Promise<unknown> {
    await this.load();
    return this.cache.has(key) ? this.cache.get(key) : DEFAULTS[key];
  }

  async getNumber(key: SettingKey): Promise<number> {
    const value = await this.getRaw(key);
    const parsed = typeof value === 'string' ? Number(value) : value;
    return typeof parsed === 'number' && Number.isFinite(parsed) ? parsed : 0;
  }

  /** Returns null for settings that are meaningfully "not configured". */
  async getNullableNumber(key: SettingKey): Promise<number | null> {
    const value = await this.getRaw(key);
    if (value === null || value === undefined) return null;
    const parsed = typeof value === 'string' ? Number(value) : value;
    return typeof parsed === 'number' && Number.isFinite(parsed) ? parsed : null;
  }

  async getBoolean(key: SettingKey): Promise<boolean> {
    const value = await this.getRaw(key);
    if (typeof value === 'boolean') return value;
    if (typeof value === 'string') return value === 'true';
    return Boolean(value);
  }

  async getString(key: SettingKey): Promise<string> {
    const value = await this.getRaw(key);
    return typeof value === 'string' ? value : String(value ?? '');
  }

  /**
   * The enrollment gate (SPEC.md §33.7), assembled from the stored policy
   * object with the individually stored values layered on top — so an
   * administrator can change just the down-payment percentage without
   * rewriting the whole JSON blob.
   */
  async getEnrollmentPolicy(): Promise<EnrollmentCompletionPolicy> {
    const stored = await this.getRaw(SETTING_KEYS.enrollmentCompletionRequirements);
    const base: EnrollmentCompletionPolicy = {
      ...DEFAULT_ENROLLMENT_POLICY,
      ...(stored && typeof stored === 'object' ? (stored as Partial<EnrollmentCompletionPolicy>) : {}),
    };

    return {
      ...base,
      minimumDownPaymentPercent: await this.getNumber(SETTING_KEYS.minimumDownPaymentPercent),
      maxUnitsDefault: await this.getNumber(SETTING_KEYS.maxUnitsDefault),
      enforcePrerequisites: await this.getBoolean(SETTING_KEYS.enforcePrerequisites),
      allowSectionOverload: await this.getBoolean(SETTING_KEYS.allowSectionOverload),
    };
  }

  /**
   * Writes a setting. The caller is responsible for authorizing
   * `setting.manage` and for emitting the SETTING_CHANGE audit entry — this
   * method deliberately does neither, so the permission check cannot be
   * accidentally skipped by calling a "convenient" wrapper.
   */
  async set(key: SettingKey, value: unknown, updatedByUserId: string, db: Db = this.db): Promise<void> {
    await db.systemSetting.update({
      where: { institutionId_key: { institutionId: this.institutionId, key } },
      data: { value: value as never, updatedByUserId },
    });
    this.invalidate();
  }
}

/** Convenience factory used by the request container. */
export function createSettingsService(institutionId: string, db: Db = prisma): SettingsService {
  return new SettingsService(institutionId, db);
}
