import mongoose, { Schema, models, model } from "mongoose";
import { EMPLOYMENT_TYPES, TERMINATION_TYPES, type EmploymentType, type TerminationType } from "@/lib/employment";

// Gleiche Form wie OrderChecklistItem (models/Order.ts) — bewusst hier separat
// definiert statt geteilt importiert, um Client.ts nicht von Order.ts
// abhängig zu machen.
export interface OnboardingChecklistItem {
  _id: string;
  text: string;
  done: boolean;
  doneAt: Date | null;
}

const onboardingChecklistItemSchema = new Schema<OnboardingChecklistItem>({
  text: { type: String, required: true, trim: true, maxlength: 200 },
  done: { type: Boolean, default: false },
  doneAt: { type: Date, default: null },
});

export interface EmploymentRecordDoc {
  _id: string;
  type: EmploymentType;
  startDate: Date;
  // Nur bei befristeten Verhältnissen gesetzt (Praktikum/Ausbildung/Teamler-
  // Projektzeitraum) — bei "employee" typischerweise leer (unbefristet).
  endDate: Date | null;
  // Nur bei "employee" gepflegt — Probezeit in Monaten ab startDate.
  probationMonths: number | null;
  probationReminderSent: boolean;
  // Nur bei type === "termination" gepflegt.
  terminationType: TerminationType | null;
  note: string;
  createdAt: Date;
}

const employmentRecordSchema = new Schema<EmploymentRecordDoc>(
  {
    type: { type: String, enum: EMPLOYMENT_TYPES, required: true },
    startDate: { type: Date, required: true },
    endDate: { type: Date, default: null },
    probationMonths: { type: Number, default: null, min: 1, max: 24 },
    probationReminderSent: { type: Boolean, default: false },
    terminationType: { type: String, enum: TERMINATION_TYPES, default: null },
    note: { type: String, default: "", trim: true, maxlength: 500 },
  },
  { timestamps: { createdAt: true, updatedAt: false } }
);

// Gehaltserhöhungs-Historie (Personalsystem, 25.09.2026) - wird automatisch
// von setSalaryFieldsAction angehängt, sobald sich hourlyWage tatsächlich
// ändert (kein eigenes Formular nötig). Nur über die Admin-Personalakte
// erreichbar (setSalaryFieldsAction dort team_management_edit-gated), daher
// repräsentiert jede Änderung immer eine echte HR-Entscheidung, nie eine
// Selbstbearbeitung durch den Mitarbeiter.
export interface SalaryHistoryEntry {
  _id: string;
  oldHourlyWage: number | null;
  newHourlyWage: number | null;
  reason: string;
  changedByClientId: mongoose.Types.ObjectId;
  createdAt: Date;
}

const salaryHistoryEntrySchema = new Schema<SalaryHistoryEntry>(
  {
    oldHourlyWage: { type: Number, default: null },
    newHourlyWage: { type: Number, default: null },
    reason: { type: String, default: "", trim: true, maxlength: 300 },
    changedByClientId: { type: Schema.Types.ObjectId, ref: "Client", required: true },
  },
  { timestamps: { createdAt: true, updatedAt: false } }
);

export interface ClientDoc {
  _id: string;
  email: string;
  passwordHash: string;
  name: string;
  firstName: string;
  lastName: string;
  nickname: string;
  // "" bei Bestandskonten von vor Einführung dieses Felds (22.09.2026) —
  // wird dort nicht erzwungen, siehe salutationPromptDismissedAt weiter
  // unten. Bei der Registrierung seitdem Pflichtfeld.
  salutation: "herr" | "frau" | "divers" | "";
  // Nur bei salutation === "divers" gepflegt (optional, Kunde kann es frei
  // lassen) — eigene Pronomen, vom Kunden selbst formuliert statt einer
  // festen Auswahl.
  pronouns: string;
  // Gesetzt, sobald ein Bestandskunde den Hinweis "Anrede nachtragen?" im
  // Konto-Dashboard weggeklickt hat (siehe SalutationPrompt) — verhindert,
  // dass der Hinweis bei jedem Login erneut erscheint, ohne salutation zu
  // erzwingen (ausdrücklicher Wunsch, nicht verpflichtend für Bestandskonten).
  salutationPromptDismissedAt: Date | null;
  // Gesetzt, sobald jemand das "Quick Tips"-Ersteinrichtungs-Modal auf dem
  // Konto-Dashboard weggeklickt hat (Onboarding-System, 25.09.2026) - zeigt
  // rollenabhängig unterschiedliche Tipps (Mitarbeiter vs. Kunde), einmal pro
  // Konto, gleiches Dismiss-Muster wie salutationPromptDismissedAt.
  onboardingTipsSeenAt: Date | null;
  company: string;
  // Freie Schlagworte fürs Team, z. B. "Stammkunde", "Convention-Kontakt" —
  // rein intern, nirgends dem Kunden selbst angezeigt.
  tags: string[];
  avatarPath: string;
  // Seit der Registrierungspflicht (wie Geburtsdatum/Anschrift) für
  // Kundenkonten verpflichtend — Mitarbeiter-Profile bleiben davon unberührt.
  phone: string;
  address: string;
  billingStreet: string;
  billingPostCode: string;
  billingCity: string;
  billingCountryCode: string;
  departmentId: mongoose.Types.ObjectId | null;
  // Berechtigungen hängen an der Position (Stelle), nicht am einzelnen
  // Mitarbeiter — siehe models/Position.ts.
  positionId: mongoose.Types.ObjectId | null;
  teamIds: mongoose.Types.ObjectId[];
  // Personalakte: mehrere Beschäftigungs-Einträge über die Zeit (z. B. erst
  // Praktikum, später fest angestellt) — neuester zuerst in der Anzeige,
  // aber hier chronologisch angehängt gespeichert. Der zuletzt hinzugefügte
  // Eintrag gilt als der aktuell gültige.
  employmentRecords: EmploymentRecordDoc[];
  // Einmalige Einarbeitungs-Checkliste fürs Team — unabhängig von
  // employmentRecords (die ist eine Historie mehrerer Einträge, die
  // Checkliste gehört zur Person, nicht zu einem bestimmten Eintrag).
  onboardingChecklist: OnboardingChecklistItem[];
  // Nur für Mitarbeiter gepflegt (Personalakte) — u. a. für die {{geburtsdatum}}-
  // Platzhalter in Arbeitsvertrags-/Zeugnis-Vorlagen im Dokumenten-Creator.
  birthDate: Date | null;
  // Vertraglich vereinbarte Zielwerte (Personalsystem, 24.09.2026) - NICHT
  // dasselbe wie die tatsächlich über TimeEntry erfasste Arbeitszeit. Leer =
  // noch nicht gepflegt, Vertrags-Creator schlägt dann Position.defaultHourlyWage
  // vor (siehe getEffectiveHourlyWage in lib/salary.ts).
  hourlyWage: number | null;
  monthlyHours: number | null;
  salaryHistory: SalaryHistoryEntry[];
  // Konfigurierbares Urlaubstage-Kontingent pro Jahr (Personalsystem-
  // Ergänzung, 25.09.2026) - von HR gepflegt, siehe setVacationAllowanceAction.
  // Bewusst ein einzelner Wert ohne Jahres-Historie/Übertrag - Restanspruch
  // wird pro Kalenderjahr aus genehmigten VacationRequest-Einträgen berechnet
  // (lib/actions/vacation.ts), kein Vortrag ins Folgejahr modelliert.
  vacationDaysPerYear: number | null;
  // Verschlüsselt gespeichert (AES-256-GCM, siehe lib/encryption.ts) -
  // niemals direkt lesen/loggen, nur über lib/actions/sensitiveData.ts, das
  // den Zugriff auf die eigene Person + die eigene, enge Berechtigung
  // "payroll_sensitive_data" beschränkt (Personalsystem, 24.09.2026).
  encryptedIban: string;
  encryptedSocialSecurityNumber: string;
  encryptedTaxId: string;
  // Eigener Empfehlungscode, mit dem der Kunde neue Kunden werben kann —
  // siehe lib/actions/referrals.ts. Bei Bestandskonten (vor Einführung des
  // Empfehlungsprogramms) noch leer, wird lazy beim ersten Aufruf vergeben.
  referralCode: string;
  // Echtes Kontoguthaben (Guthaben-System, 28.09.2026) — Quelle der Wahrheit
  // ist der Saldo hier, jede Änderung läuft ausschließlich über
  // lib/creditLedger.ts bookCredit() (atomarer $inc + CreditTransaction-
  // Protokolleintrag). Bei Bestandskonten ohne dieses Feld verhält sich ein
  // $inc wie eine Initialisierung auf 0 — keine Backfill-Migration nötig,
  // beim Lesen trotzdem immer mit `|| 0` absichern (lean() wendet Mongoose-
  // Defaults nicht auf bereits existierende Dokumente an).
  creditBalance: number;
  // Letztes shootDate, für das schon eine "ein Jahr seit deinem letzten
  // Shooting"-Mail verschickt wurde — verhindert Doppelversand beim
  // täglichen Cron-Lauf (siehe lib/reengagement.ts). Bewusst das shootDate
  // selbst statt nur eines Zeitstempels, damit ein neuer Auftrag mit
  // späterem shootDate nach Ablauf der Frist wieder eine frische Erinnerung
  // auslösen kann.
  lastReengagementShootDate: Date | null;
  // Letztes Jahr, für das schon eine Jahresrückblick-Mail verschickt wurde
  // (siehe lib/yearlyRecap.ts) — verhindert Doppelversand beim täglichen
  // Cron-Lauf, analog zu lastReengagementShootDate.
  lastYearlyRecapSentYear: number | null;
  // Globaler Schalter für optionale Benachrichtigungs-E-Mails (Wiedervorlage,
  // Vor-Shooting-Erinnerung, Wetter-Hinweis, Stammkunden-/Bewertungs-Prämie)
  // — betrifft NICHT kritische Transaktions-Mails (Bestätigung, Rechnung,
  // Zahlungserinnerung), die laufen unabhängig davon immer weiter. In-App-
  // Benachrichtigungen (Glocke) sind ebenfalls unabhängig, laufen immer.
  emailNotificationsEnabled: boolean;
  // Login-Schutz: zählt aufeinanderfolgende Fehlversuche, sperrt das Konto
  // nach LOGIN_MAX_ATTEMPTS für LOGIN_LOCKOUT_MINUTES (siehe actions/auth.ts)
  // — wird bei jedem erfolgreichen Login zurückgesetzt.
  failedLoginCount: number;
  lockedUntil: Date | null;
  // Erzwingt einen Passwort-Wechsel vor der ersten echten Session — gesetzt
  // bei spontan vom Team vor Ort angelegten Kundenkonten (Übergangspasswort,
  // siehe actions/clientAccounts.ts adminCreateClientAction) sowie bei einem
  // nachträglich vom Team gesetzten Passwort. Greift bereits im Login-Flow,
  // bevor createSession() aufgerufen wird (siehe actions/auth.ts).
  mustChangePassword: boolean;
  // Zwei-Faktor-Login (TOTP) — nur für Mitarbeiter-Accounts nutzbar.
  // totpSecret ist schon während des Einrichtungsvorgangs gesetzt, bevor
  // totpEnabled auf true springt (erst nach erfolgreicher Bestätigung mit
  // einem echten Code aus der Authenticator-App). Recovery-Codes werden wie
  // Passwörter gehasht gespeichert, nie im Klartext.
  totpEnabled: boolean;
  totpSecret: string;
  totpRecoveryCodeHashes: string[];
  locale: "de" | "en";
  role: "client" | "staff";
  verified: boolean;
  verificationToken: string | null;
  verificationTokenExpires: Date | null;
  // Nachweis der Zustimmung zu AGB + Datenschutzerklärung bei der
  // Registrierung (rechtlich sinnvoll, im Streitfall belegbar) — bei älteren,
  // vor Einführung dieser Pflicht angelegten Konten bleibt das Feld leer.
  termsAcceptedAt: Date | null;
  passwordResetToken: string | null;
  passwordResetExpires: Date | null;
  // Selbst beantragte Löschung (Kundenkonto hat noch Rechnungen, die aus
  // steuerrechtlichen Gründen 10 Jahre aufbewahrt werden müssen — kann also
  // nicht sofort hart gelöscht werden, siehe lib/accountDeletion.ts).
  deletionRequestedAt: Date | null;
  // Konto wurde vom Team anonymisiert statt hart gelöscht (gleicher Grund:
  // vorhandene Rechnungen). Personendaten überschrieben, Konto bleibt als
  // Platzhalter für die Rechnungszuordnung bestehen.
  anonymizedAt: Date | null;
  createdAt: Date;
}

const clientSchema = new Schema<ClientDoc>(
  {
    email: { type: String, required: true, unique: true, lowercase: true, trim: true },
    passwordHash: { type: String, required: true },
    name: { type: String, required: true, trim: true },
    firstName: { type: String, default: "", trim: true },
    lastName: { type: String, default: "", trim: true },
    nickname: { type: String, default: "", trim: true },
    salutation: { type: String, enum: ["herr", "frau", "divers", ""], default: "" },
    pronouns: { type: String, default: "", trim: true, maxlength: 60 },
    salutationPromptDismissedAt: { type: Date, default: null },
    onboardingTipsSeenAt: { type: Date, default: null },
    company: { type: String, default: "", trim: true, maxlength: 200 },
    tags: { type: [String], default: [] },
    avatarPath: { type: String, default: "", trim: true },
    phone: { type: String, default: "", trim: true, maxlength: 30 },
    address: { type: String, default: "", trim: true, maxlength: 500 },
    billingStreet: { type: String, default: "", trim: true, maxlength: 200 },
    billingPostCode: { type: String, default: "", trim: true, maxlength: 20 },
    billingCity: { type: String, default: "", trim: true, maxlength: 100 },
    billingCountryCode: { type: String, default: "DE", trim: true, maxlength: 2 },
    // Bewusst nicht vom Mitarbeiter selbst im Profil editierbar — wird
    // zentral im Team-/Rechtesystem zugewiesen (/admin/team).
    departmentId: { type: Schema.Types.ObjectId, ref: "Department", default: null },
    positionId: { type: Schema.Types.ObjectId, ref: "Position", default: null },
    teamIds: { type: [Schema.Types.ObjectId], ref: "Team", default: [] },
    employmentRecords: { type: [employmentRecordSchema], default: [] },
    onboardingChecklist: { type: [onboardingChecklistItemSchema], default: [] },
    birthDate: { type: Date, default: null },
    hourlyWage: { type: Number, default: null, min: 0 },
    monthlyHours: { type: Number, default: null, min: 0 },
    salaryHistory: { type: [salaryHistoryEntrySchema], default: [] },
    vacationDaysPerYear: { type: Number, default: null, min: 0 },
    encryptedIban: { type: String, default: "" },
    encryptedSocialSecurityNumber: { type: String, default: "" },
    encryptedTaxId: { type: String, default: "" },
    referralCode: { type: String, default: "", trim: true, uppercase: true },
    creditBalance: { type: Number, default: 0 },
    lastReengagementShootDate: { type: Date, default: null },
    lastYearlyRecapSentYear: { type: Number, default: null },
    emailNotificationsEnabled: { type: Boolean, default: true },
    failedLoginCount: { type: Number, default: 0 },
    lockedUntil: { type: Date, default: null },
    mustChangePassword: { type: Boolean, default: false },
    totpEnabled: { type: Boolean, default: false },
    totpSecret: { type: String, default: "" },
    totpRecoveryCodeHashes: { type: [String], default: [] },
    locale: { type: String, enum: ["de", "en"], default: "de" },
    role: { type: String, enum: ["client", "staff"], default: "client" },
    verified: { type: Boolean, default: false },
    verificationToken: { type: String, default: null },
    verificationTokenExpires: { type: Date, default: null },
    termsAcceptedAt: { type: Date, default: null },
    passwordResetToken: { type: String, default: null },
    passwordResetExpires: { type: Date, default: null },
    deletionRequestedAt: { type: Date, default: null },
    anonymizedAt: { type: Date, default: null },
  },
  { timestamps: true }
);

export const Client =
  (models.Client as mongoose.Model<ClientDoc>) || model<ClientDoc>("Client", clientSchema);
