/** Shared licensing contract — mirrors the emaapi domain model. */

// Reused rather than redeclared: the department vocabulary belongs to the
// seafarer domain, and two copies would drift.
import type { SeafarerDepartment } from "../seafarer/seafarer.types";

/**
 * A backend `LocaleValidationDto`. Named for the two locales the UI offers, but
 * carries every locale the column stores — location names are seeded with `om`
 * (Finfinnee) and `so` too, and a type that declared only en/am made those
 * unreachable through the typed client.
 */
export type Bilingual = {
  en?: string;
  am?: string;
  om?: string;
  so?: string;
};

/**
 * The application status vocabulary. Single source of truth for both apps —
 * previously this lived in a backoffice mock page.
 */
export type LicenseStatus =
  | "DRAFT"
  | "SUBMITTED"
  | "UNDER_REVIEW"
  // Seafarer registration only: same slot UNDER_REVIEW occupies elsewhere,
  // but approval is blocked until the applicant's profile has a BSID.
  | "AWAITING_BIOMETRICS"
  | "UNDER_EVALUATION"
  // Employee filed their review; parked with the team leader for a decision.
  | "REVIEW_REPORTED"
  | "RESUBMIT_REQUIRED"
  | "INSPECTION_PENDING"
  | "INSPECTION_COMPLETED"
  // Inspector filed the result; parked with the team leader for a decision.
  | "INSPECTION_REPORTED"
  // The inspection was conducted and failed. Approval and issuance are
  // unreachable until a re-inspection passes; the officer chooses between a
  // repeat visit, an adjustment round, and rejection.
  | "INSPECTION_FAILED"
  | "APPROVED"
  | "REJECTED"
  | "ON_HOLD"
  | "PAYMENT_PENDING"
  | "PAID"
  | "PAYMENT_CONFIRMED"
  // Payment confirmed and pickup date set — for a document printed once and
  // handed over in person (seaman book, BTC). Most license types skip this
  // and go straight from PAYMENT_CONFIRMED to CERTIFICATE_ISSUED.
  | "SCHEDULED"
  | "CERTIFICATE_ISSUED"
  | "COMPLETED"
  // Examined certificates (CoC, some CoP): the eligibility assessment fee is
  // due before review starts, then the candidate pays to sit, and the
  // certificate fee falls due on a pass.
  | "ELIGIBILITY_PAYMENT_PENDING"
  | "ELIGIBILITY_PAID"
  | "EXAM_PAYMENT_PENDING"
  | "EXAM_PAID"
  | "EXAM_SCHEDULED"
  | "EXAM_PASSED"
  | "EXAM_FAILED";

export type ApplicationKind = "NEW" | "RENEWAL" | "REISSUE";

/** Half-day window a team leader books an applicant's document pickup into. */
export type IssuancePeriod = "MORNING" | "AFTERNOON";

export type FormFieldType =
  | "TEXT"
  | "TEXTAREA"
  | "NUMBER"
  | "MONEY"
  | "DATE"
  | "SELECT"
  | "BOOLEAN"
  | "EMAIL"
  | "PHONE"
  | "TIN";

export interface FieldCondition {
  /** Omitted when `anyOf` is used instead — see below. */
  field?: string;
  equals?: string | number | boolean;
  notEquals?: string | number | boolean;
  in?: (string | number)[];
  isSet?: boolean;
  /**
   * Alternative to a single-field check: holds when ANY listed condition
   * holds. `field`/`equals`/etc are ignored when this is present. Mirrors
   * the server's `FieldCondition` (form-schema.type.ts).
   */
  anyOf?: FieldCondition[];
}

export interface FormFieldConfig {
  key: string;
  label: Bilingual;
  type: FormFieldType;
  required?: boolean;
  placeholder?: Bilingual;
  helpText?: Bilingual;
  options?: { value: string; label: Bilingual }[];
  min?: number;
  max?: number;
  maxLength?: number;
  showWhen?: FieldCondition;
  readOnly?: boolean;
  source?: string;
  /**
   * Marks `min` as bound to live configuration rather than stored with the
   * field — `"licenseType.capitalThreshold"` for a capital amount. The server
   * resolves it on every read, so `min` already carries the real floor by the
   * time the wizard sees it; this is only here so the builder round-trips the
   * binding instead of dropping it on save.
   */
  minSource?: string;
  sortOrder?: number;
}

export interface FormSectionConfig {
  key: string;
  title: Bilingual;
  description?: Bilingual;
  fields: FormFieldConfig[];
  showWhen?: FieldCondition;
  sortOrder?: number;
  /** Sections sharing a group render together on one wizard step. */
  group?: string;
  /** Position of the group in the stepper; lowest value in a group wins. */
  groupOrder?: number;
  /**
   * Restricts this section to specific application kinds — e.g. the
   * Damaged/Reissue "Damage Information" step. Undefined or empty means
   * every kind.
   */
  applicationKinds?: ApplicationKind[];
}

/** Grouping the portal organises the licence catalogue by. */
export type LicenseCategory =
  | "CARGO_FREIGHT"
  | "SHIPPING_AGENCY"
  | "INVESTMENT"
  | "MARITIME_PERSONNEL"
  | "VESSEL_SERVICES"
  | "WAIVER_SERVICES";

/**
 * Which business concept a licence type actually is — the real data-model
 * classification (emaapi's `license_types.family_kind` column), not a label
 * computed from `key`. A permission granted to a logistics operator, a
 * seafarer's proof of competence, and a seafarer/vessel's identity or
 * statutory record are three different things to the people using this
 * system, even though they run through the identical application pipeline —
 * see BR-MTO-020. Drives terminology, navigation, and which columns a grid
 * shows (Company/TIN only make sense for LOGISTICS_LICENSE rows) — never a
 * workflow or eligibility branch.
 */
export type FamilyKind = "LOGISTICS_LICENSE" | "CERTIFICATE" | "DOCUMENT";

export interface LicenseCategoryDefinition {
  key: LicenseCategory;
  name: Bilingual;
  description: Bilingual;
  sortOrder: number;
}

/**
 * A mode of operation the applicant has declared — the licence types they may
 * file new applications for. `key` and `name` are denormalised from the
 * licence type so the profile screen can render the set without also loading
 * the whole catalogue.
 */
export interface OperatorType {
  licenseTypeId: string;
  key: string | null;
  name: Bilingual | null;
  declaredAt: string | null;
}

export interface LicenseType {
  id: string;
  key: string;
  name: Bilingual;
  description?: Bilingual;
  category: LicenseCategory;
  /** The real data-model classification — see `FamilyKind`. */
  familyKind: FamilyKind;
  certificatePrefix: string;
  feeNewApplication: string | number | null;
  feeRenewal: string | number | null;
  feeCurrency: string;
  capitalThreshold: string | number | null;
  validityMonths: number;
  /**
   * A term in days instead of months, for licences shorter than a month can
   * express. Wins over `validityMonths` when set; null keeps calendar months.
   */
  validityDays?: number | null;
  /**
   * Target turnaround in hours. Null means this type is not tracked against
   * an SLA, which the grid renders as "—" rather than as instantly overdue.
   */
  slaHours: number | null;
  inspectionRequired: boolean;
  issuesCertificate: boolean;
  renewalEnabled: boolean;
  /** Payment confirmation waits for a scheduled pickup date before issuance. */
  requiresIssuanceScheduling: boolean;
  /**
   * False for person-centric registrations (seafarer): they are open to any
   * authenticated applicant, live outside the operator catalogue, and have
   * their own entry points.
   */
  requiresOperatorMode: boolean;
  formSchema: { sections: FormSectionConfig[] };
  /**
   * Which course an application of this type runs. REGISTRATION skips the
   * evaluation and inspection stages, so those statuses are unreachable for it.
   */
  workflowProfile?: WorkflowProfile;
  isActive: boolean;
  /** Display order set by EMA; lower comes first. */
  sortOrder: number;

  // --------------------------------------------------- examined certificates
  /** Approval establishes eligibility; the certificate is earned by exam. */
  requiresExamination?: boolean;
  /** Assessment fee, due on submission before any officer review. */
  feeEligibility?: string | number | null;
  /** Fee per sitting. Falls back to `feeNewApplication` when null. */
  feeExamination?: string | number | null;
  /** Fee to issue after a pass. Falls back to `feeNewApplication` when null. */
  feeCertificate?: string | number | null;

  // ------------------------------------------------------- behaviour config
  // Editable from the backoffice Behavior tab. Optional because the API has
  // only recently begun returning them and older mocks/fixtures omit them.

  /** Licence or registration. Catalogue metadata; no behaviour hangs off it. */
  serviceKind?: ServiceKind;
  /** Platform side effect fired when an application of this type completes. */
  completionEffect?: CompletionEffect | null;
  /** Only ACTIVE registered seafarers may apply. */
  requiresSeafarerRegistration?: boolean;
  /** Submission requires a current, unexpired medical certificate. */
  requiresValidMedical?: boolean;
  /** Minimum VERIFIED sea time in days at submission. Null means no floor. */
  minSeaTimeDays?: number | null;
  /** Whether several open drafts may exist at once, for per-asset registrations. */
  allowMultipleOpenDrafts?: boolean;
  /** Days before expiry that renewal opens. */
  renewalWindowDays?: number;
  /** Days before expiry to remind the holder, most distant first. */
  expiryReminderDays?: number[];
  /**
   * Dotted `formData` path whose answer may appear on only one live
   * application of this type. Null means no such rule.
   */
  uniqueFormKeyPath?: string | null;

  // ---------------------------------------------------------- STCW mapping
  certificateCategory?: CertificateCategory | null;
  stcwControlled?: boolean;
  /** Convention regulation, e.g. `III/2`. */
  stcwRegulation?: string | null;
  /** Code section, e.g. `A-III/2`. */
  stcwCodeSection?: string | null;
  stcwDepartment?: SeafarerDepartment | null;
  competencyLevel?: CompetencyLevel | null;
  stcwFunctions?: StcwFunctionRow[] | null;
  stcwCapacities?: StcwCapacityRow[] | null;
  /** Licence keys that must be held before this may be applied for. */
  prerequisiteLicenseKeys?: string[] | null;
}

/** What kind of document a certificate type produces. */
export type CertificateCategory =
  "COC" | "COP" | "ENDORSEMENT" | "GOC" | "NATIONAL";

/**
 * STCW responsibility level. Cadet is absent by design — under STCW a cadet is
 * a seafarer in training before holding any certificate, not a level of
 * competence.
 */
export type CompetencyLevel = "SUPPORT" | "OPERATIONAL" | "MANAGEMENT";

/** One row of a CoC's function table (STCW Code A-I/2). */
export interface StcwFunctionRow {
  function: Bilingual;
  level: CompetencyLevel;
  limitation?: Bilingual;
  sortOrder?: number;
}

/** One row of a CoC's capacity table — what the holder may serve as. */
export interface StcwCapacityRow {
  capacity: Bilingual;
  limitation?: Bilingual;
  sortOrder?: number;
}

export interface DocumentRequirement {
  id: string;
  /**
   * Null for a personal document — one every applicant keeps in their own
   * vault regardless of what they apply for, rather than a slot on one
   * licence's application form.
   */
  licenseTypeId: string | null;
  key: string;
  name: Bilingual;
  description?: Bilingual;
  applicationKind: ApplicationKind;
  mode: "ALWAYS" | "CONDITIONAL" | "OPTIONAL";
  conditionExpression?: FieldCondition & { previousDocExpired?: string };
  allowedMimeTypes: string[];
  maxSizeMb: number;
  requiresValidityDates: boolean;
  allowMultiple: boolean;
  /**
   * True for a personal document — one the applicant keeps in their own vault
   * — rather than an upload slot on an application form. Orthogonal to
   * `licenseTypeId`, which still says which licences it applies to.
   */
  isPersonal: boolean;
  /** Files the slot accepts: 1, 2 for a front and back, null for unlimited. */
  maxFiles: number | null;
  sortOrder: number;
  isActive: boolean;
}

/**
 * What the form-schema builder may put on a form — the field types the engine
 * understands, which constraints each one honours, the condition operators it
 * supports, and the prefill sources a read-only field may draw from. Drives
 * the builder's pickers so they never hardcode a list the server already owns.
 */
export interface FormSchemaPalette {
  fieldTypes: {
    type: FormFieldType;
    supportsOptions: boolean;
    supportsRange: boolean;
    supportsMaxLength: boolean;
  }[];
  conditionOperators: ("equals" | "notEquals" | "in" | "isSet")[];
  prefillSources: string[];
}

/** One problem the server's form-schema lint found. */
export interface SchemaIssue {
  path: string;
  message: string;
}

export interface StaffEvidenceRequirement {
  docKey: string;
  label: Bilingual;
  mandatory: boolean;
}

export interface StaffRoleRequirement {
  id: string;
  licenseTypeId: string;
  roleKey: string;
  name: Bilingual;
  minCount: number;
  maxCount: number | null;
  requiresExperience: boolean;
  minYearsExperience: number | null;
  requiredEvidence: StaffEvidenceRequirement[];
  sortOrder: number;
  isActive: boolean;
}

export interface LicenseTypeRequirements {
  licenseType: LicenseType;
  applicationKind: ApplicationKind;
  fee: number | null;
  feeCurrency: string;
  documentRequirements: DocumentRequirement[];
  staffRoleRequirements: StaffRoleRequirement[];
}

export interface LicenseApplication {
  id: string;
  applicationNumber: string;
  licenseTypeId: string;
  licenseType?: LicenseType;
  /** Denormalized from `licenseType.familyKind` at submission time. */
  familyKind: FamilyKind;
  applicantUserId: string;
  parentApplicationId?: string | null;
  kind: ApplicationKind;
  status: LicenseStatus;
  assignedOfficerId: string | null;
  claimedAt: string | null;
  formData: Record<string, Record<string, unknown>>;
  companyName: string | null;
  tradeName: string | null;
  tinNumber: string | null;
  businessAddress: string | null;
  capitalAmountDeclared: string | null;
  capitalAmountVerified: string | null;
  adjustmentRound: number;
  submittedAt: string | null;
  decidedAt: string | null;
  rejectionReason: string | null;
  feeAmount: string | null;
  feeCurrency: string;
  issuedLicenseId: string | null;
  /** Set once an officer schedules pickup for a document requiring in-person handover. */
  scheduledIssuanceDate: string | null;
  scheduledIssuancePeriod: IssuancePeriod | null;
  scheduledBy: string | null;
  createdAt: string;
}

export interface ApplicationStaff {
  id: string;
  roleKey: string;
  fullName: string;
  position: string | null;
  roleCategory: string | null;
  yearsOfExperience: number | null;
  documents?: { id: string; documentKey: string; files: AttachmentFile[] }[];
}

export interface AttachmentFile {
  id: string;
  originalName: string;
  mimeType: string;
  sizeBytes: number;
  storageKey: string;
  url?: string;
}

export interface Attachment {
  id: string;
  ownerType: string;
  ownerId: string;
  documentKey: string;
  /**
   * Set when this was filled from the applicant's personal document vault
   * rather than uploaded here — what both apps badge as "From My Documents".
   * Null for an ordinary upload, and cleared the moment the file is replaced.
   */
  copiedFromAttachmentId?: string | null;
  title: string | null;
  validFrom: string | null;
  validTo: string | null;
  files: AttachmentFile[];
  /** From BaseEntity. Used to place the upload on the activity trail. */
  createdAt?: string;
}

export interface StatusHistoryEntry {
  id: string;
  fromStatus: LicenseStatus | null;
  toStatus: LicenseStatus;
  event: string;
  actorUserId: string | null;
  actorName: string | null;
  actorRole: string | null;
  remark: string | null;
  metadata: Record<string, unknown> | null;
  createdAt: string;
}

export type RemarkTargetType = "FORM_SECTION" | "DOCUMENT" | "STAFF";

export interface ApplicationRemark {
  id: string;
  roundNumber: number;
  targetType: RemarkTargetType;
  targetKey: string;
  remark: string;
  isResolved: boolean;
  resolvedAt: string | null;
  createdAt: string;
}

/**
 * Who filed the application, from their profile.
 *
 * Served alongside the form because a person-centric service (seafarer
 * registration, a certificate) has no `companyName` to identify itself by — a
 * reviewer opening one otherwise sees only an application number and has to
 * infer the human from the answers.
 */
export interface ApplicationApplicant {
  profileId: string;
  firstName: string | null;
  middleName: string | null;
  lastName: string | null;
  gender: string | null;
  dob: string | null;
  pob: string | null;
  maritalStatus: string | null;
  seafarerNumber: string | null;
  seafarerStatus: string | null;
  seafarerDepartment: string | null;
  nationality: string | null;
  idType: string | null;
  idNumber: string | null;
  primaryPhoneNumber: string | null;
  email: string | null;
}

export interface ApplicationDetail {
  application: LicenseApplication;
  /**
   * Current status of the license this application issued, independent of
   * the application's own (permanently historical) status — a later
   * reissue/renewal can supersede the license without changing what this
   * application itself accomplished. Null when nothing has been issued yet.
   */
  issuedLicenseStatus: "ACTIVE" | "EXPIRED" | "SUSPENDED" | "CANCELLED" | "SUPERSEDED" | null;
  relatedApplications?: LicenseApplication[];
  /** Null when the applicant has no profile row (never expected in practice). */
  applicant: ApplicationApplicant | null;
  staff: ApplicationStaff[];
  attachments: Attachment[];
  history: StatusHistoryEntry[];
  remarks: ApplicationRemark[];
  openRemarks: ApplicationRemark[];
  availableEvents: string[];
}

export interface Inspection {
  id: string;
  applicationId: string;
  inspectorId: string | null;
  inspectorName: string | null;
  scheduledDate: string | null;
  /** Half-day slot the site visit is booked into. */
  timeSlot: "MORNING" | "AFTERNOON" | null;
  conductedDate: string | null;
  location: string | null;
  status: "SCHEDULED" | "COMPLETED" | "CANCELLED";
  result: "PASSED" | "FAILED" | null;
  findings: string | null;
}

export type PickupAppointmentStatus =
  | "SCHEDULED"
  | "CHECKED_IN"
  | "ISSUED"
  | "NO_SHOW"
  | "RESCHEDULED"
  | "CANCELLED";

export interface PickupOffice {
  id: string;
  name: string;
  address: string | null;
  /** 0=Sunday .. 6=Saturday. */
  workingDays: number[];
  startTime: string;
  endTime: string;
  slotDurationMinutes: number;
  maxApplicantsPerSlot: number;
  rescheduleMinNoticeHours: number;
  isActive: boolean;
}

export interface PickupSlot {
  date: string;
  slotStartTime: string;
  capacity: number;
  booked: number;
  available: number;
}

export interface PickupAppointment {
  id: string;
  applicationId: string;
  appointmentNumber: string;
  officeId: string;
  date: string;
  slotStartTime: string;
  status: PickupAppointmentStatus;
  rescheduledFromId: string | null;
  rescheduleCount: number;
  checkedInAt: string | null;
  checkedInById: string | null;
}

export interface AppNotification {
  id: string;
  subject: Bilingual;
  content: Bilingual;
  isSeen: boolean;
  itemId: string | null;
  itemType: string | null;
  metadata: Record<string, unknown> | null;
  createdAt: string;
}

/** Server-side queue filters. Mirrors ApplicationQueueFilterDto in the API. */
export interface QueueFilter {
  licenseTypeId?: string;
  search?: string;
  status?: LicenseStatus[];
  kind?: ApplicationKind;
  /** Officer uuid, or the literal 'unassigned'. */
  assignee?: string;
  submittedFrom?: string;
  submittedTo?: string;
  overdue?: boolean;
  sortBy?: QueueSortField;
  sortDir?: "ASC" | "DESC";
  take?: number;
  skip?: number;
}

export type QueueSortField =
  | "submittedAt"
  | "applicationNumber"
  | "companyName"
  | "status"
  | "dueAt"
  | "claimedAt";

/** Review → evaluation → (inspection) → approval, or the short registration course. */
export type WorkflowProfile = "STANDARD" | "REGISTRATION";

/** A permission to operate, or a registration granting a status and a number. */
export type ServiceKind = "LICENSE" | "REGISTRATION";

/** Platform side effect fired when an application reaches COMPLETED. */
export type CompletionEffect =
  | "REGISTER_SEAFARER"
  | "REGISTER_VESSEL"
  | "OPEN_SEAFARER_DOCUMENTS";

/** Row counts behind the queue's saved-view tabs. */
export interface QueueCounts {
  unassigned: number;
  mine: number;
  awaitingApplicant: number;
  overdue: number;
  readyToIssue: number;
  all: number;
}

export type DocumentDecision = "ACCEPTED" | "REJECTED";

/** An officer's verdict on one uploaded document. */
export interface DocumentReview {
  id: string;
  applicationId: string;
  documentKey: string;
  attachmentId: string | null;
  decision: DocumentDecision;
  reason: string | null;
  reviewedById: string | null;
  reviewedByName: string | null;
  updatedAt: string;
}

/** An officer-defined queue filter, persisted server-side. */
export interface SavedQueueView {
  id: string;
  ownerUserId: string;
  name: string;
  queryString: string;
  isShared: boolean;
  isDefault: boolean;
  sortOrder: number;
}

export interface AssignableOfficer {
  id: string;
  name: string | null;
}

/** Full filtered result set for export. `truncated` when the cap was hit. */
export interface ExportResult {
  items: LicenseApplication[];
  total: number;
  truncated: boolean;
}

export type TemplateStatus = "DRAFT" | "PUBLISHED" | "ARCHIVED";

export interface TemplatePageOptions {
  format?: "A4" | "A5" | "Letter" | "Legal";
  /**
   * Explicit page dimensions (e.g. "4.92in"), for a size `format` has no
   * named preset for — an ID-3 passport-booklet page, for one. Takes
   * precedence over `format` when both are present.
   */
  width?: string;
  height?: string;
  landscape?: boolean;
  printBackground?: boolean;
}

/** Corner the institute logo is anchored to. */
export type TemplateLogoCorner =
  "TOP_LEFT" | "TOP_CENTER" | "TOP_RIGHT" | "BOTTOM_LEFT" | "BOTTOM_RIGHT";

/** Where the institute logo sits on the certificate. */
export interface TemplateLogoPlacement {
  corner?: TemplateLogoCorner;
  /** Width as a percentage of page width. */
  widthPct?: number;
  /** Inset from the anchored corner, as a percentage of page width. */
  offsetPct?: number;
}

/**
 * One positioned block on the designer canvas.
 *
 * Percentages rather than pixels, so a layout survives an orientation change:
 * the canvas and the rendered PDF agree without either knowing the other's
 * dimensions.
 */
export interface TemplateFieldPlacement {
  id: string;
  /** Variable rendered here, or null when the block carries literal `text`. */
  variable: string | null;
  text?: string;
  /** Renders as `<img>` when "image" — see TemplateVariable.kind. */
  type?: "text" | "image";
  xPct: number;
  yPct: number;
  widthPct: number;
  fontSize?: number;
  fontWeight?: "normal" | "bold";
  fontStyle?: "normal" | "italic";
  align?: "left" | "center" | "right" | "justify";
  color?: string;
}

/** A certificate design authored in the backoffice. */
/** An STCW seafarer department (Deck, Engine, Catering), backoffice-managed. */
export interface Department {
  id: string;
  /** Matches the ESeafarerDepartment value stored elsewhere, e.g. "DECK". */
  code: string;
  name: Bilingual;
  sortOrder: number;
  isActive: boolean;
}

export type RankCertificateCategory = "COC" | "COP";

/** One rung of a CoC/CoP ladder for a department. */
export interface Rank {
  id: string;
  departmentId: string;
  certificateCategory: RankCertificateCategory;
  /** Value stored on License.rank / Certification.rankKey, e.g. "CHIEF_MATE". */
  key: string;
  name: Bilingual;
  /** Rung position within its department+category ladder. 0 is the entry rank. */
  ladderOrder: number;
  sortOrder: number;
  isActive: boolean;
}

export interface LicenseTemplate {
  id: string;
  licenseTypeId: string;
  /** Scopes this design to one rank's certificate. Null = the type's default. */
  rankId?: string | null;
  name: string;
  version: number;
  hbsSource: string;
  pageOptions: TemplatePageOptions | null;
  /**
   * Artwork printed under the rendered text. A background, not the whole
   * certificate — per-certificate data stays in the template layer above it.
   */
  backgroundUrl?: string | null;
  logoUrl?: string | null;
  logoPlacement?: TemplateLogoPlacement | null;
  /**
   * Blocks positioned on the visual canvas. Null for a design authored as raw
   * Handlebars — which is how the two editors stay distinguishable.
   */
  fieldPlacements?: TemplateFieldPlacement[] | null;
  status: TemplateStatus;
  publishedAt: string | null;
  createdAt: string;
  updatedAt: string;
}

/** A placeholder a certificate design may use. */
export interface TemplateVariable {
  key: string;
  label: string;
  /** "image" means the value is a data URI to place as `<img>`, not text. */
  kind?: "text" | "image";
}

export interface Paginated<T> {
  total: number;
  items: T[];
}

/** Per-field problems returned by the server when a submission is incomplete. */
export interface ValidationIssue {
  kind: "field" | "document" | "staff";
  target: string;
  field?: string;
  message: string;
}

/** What the browser must do to complete a payment. */
export interface ClientAction {
  type: "REDIRECT" | "LAUNCH_APP" | "NONE";
  url?: string;
  appId?: string;
  receiveCode?: string;
  shortCode?: string;
}

export type PaymentStatus =
  "PENDING" | "PROCESSING" | "PAID" | "FAILED" | "EXPIRED" | "CANCELLED";

export interface InitiatePaymentResult {
  paymentId: string;
  intentId: string;
  status: PaymentStatus;
  amount: number;
  currency: string;
  provider: string;
  clientAction?: ClientAction;
}

export interface ApplicationPayment {
  id: string;
  applicationId: string | null;
  documentId?: string | null;
  paymentIntentId: string;
  amount: string;
  currency: string;
  provider: string;
  providerRef: string | null;
  merchantOrderId: string | null;
  status: PaymentStatus | null;
  checkoutUrl: string | null;
  paidAt: string | null;
  failureReason: string | null;
}

/** A licence certificate issued to the applicant. */
export interface IssuedLicense {
  /** Why the licence was suspended, revoked or cancelled. */
  statusReason?: string | null;
  statusChangedAt?: string | null;
  statusChangedByName?: string | null;
  id: string;
  certificateNumber: string;
  licenseTypeId: string;
  licenseType?: LicenseType;
  /** Denormalized from `licenseType.familyKind` at issuance time. */
  familyKind: FamilyKind;
  applicationId: string;
  companyName: string | null;
  tinNumber: string | null;
  issueDate: string;
  expiryDate: string;
  status: "ACTIVE" | "EXPIRED" | "SUSPENDED" | "CANCELLED" | "SUPERSEDED";
  /**
   * Days until the expiry date; negative once it has passed. Computed by the
   * API in the authority's timezone — the client must not re-derive it, since
   * a browser in another zone would land on a different day.
   */
  daysUntilExpiry?: number;
  /**
   * Whether a renewal can be filed now: inside the licence type's renewal
   * window, or after expiry. The API decides, because the window is per-type
   * configuration.
   */
  renewable?: boolean;
  /** Whether a Damaged/Reissue replacement may be requested for this licence. */
  reissuable?: boolean;
  verificationCode: string;
  certificateFileKey: string | null;
}

/**
 * One personal document as the backoffice manages it: every configured row
 * sharing a key, which is one slot in the applicant's vault. Several rows mean
 * the document is scoped to several licence types.
 */
export interface PersonalDocumentGroup {
  key: string;
  rows: DocumentRequirement[];
}

export interface PersonalDocumentFilter {
  /** Matches the key and the name in either locale. */
  search?: string;
  /** A licence type also matches the documents every licence asks for. */
  licenseTypeId?: string;
  /** Narrows to the documents configured against no licence type at all. */
  globalOnly?: boolean;
  sortBy?: 'sortOrder' | 'key' | 'name';
  sortDir?: 'ASC' | 'DESC';
  take?: number;
  skip?: number;
  /** Which locale `sortBy: "name"` sorts on. */
  locale?: 'en' | 'am';
}

// No `EligibleExam`: it typed the schedule-exam picker's options, and
// picking a sitting for a named applicant is not something the back office
// does any more — candidates register for a published sitting themselves.

/**
 * Where a certificate application stands in the examination leg, derived
 * server-side from the registration, the attempt and the published mark
 * (ExamStateService). The application status alone cannot answer this — which
 * is why the back office could show "awaiting exam" over a published pass.
 */
export type ExamState =
  | 'NOT_REGISTERED'
  | 'REGISTERED'
  | 'ATTENDANCE_CONFIRMED'
  | 'NOT_SITTING'
  | 'IN_PROGRESS'
  | 'UNDER_EVALUATION'
  | 'PASSED'
  | 'FAILED';

export interface ExamStateView {
  state: ExamState;
  registrationId: string | null;
  admissionNumber: string | null;
  examId: string | null;
  examTitle: { en?: string; am?: string } | null;
  examDate: string | null;
  attendanceStatus: string | null;
  attemptStatus: 'IN_PROGRESS' | 'SUBMITTED' | 'EXPIRED' | null;
  /** Only once the mark is published — before that it is not the candidate's. */
  score: number | null;
  outcome: 'PASSED' | 'FAILED' | null;
  publishedAt: string | null;
  /** Whether the exam engine produced the mark, or a person did. */
  autoGraded: boolean | null;
}
