/* ---------------------------------------------------------------------------
 * Resource type definitions for the Backstage admin surface.
 *
 * Every admin-managed entity is declared as a single Resource<T> object.
 * The shared components (AdminLayout, ResourceListView, ResourceEditPanel)
 * consume these objects and never know about specific entities.
 *
 * Adding a new entity = write a Resource config + register it. That is the
 * load-bearing invariant of the rebuild.
 * ------------------------------------------------------------------------- */

// ── Option (used for select / select-async / multi-select-async) ────────────
export interface Option<V = string | number> {
  value: V;
  label: string;
}

// ── Pill variants (status/kind columns + pill display elsewhere) ────────────
export interface PillVariant {
  label: string;
  /** CSS class defined in src/admin/admin.css — e.g. 'pill-published'. */
  class: string;
}
export type PillVariants = Record<string, PillVariant>;

// ── Field context — passed to visibleWhen / defaultValue resolvers ──────────
export interface FieldContext {
  /** Current form values keyed by field.key. */
  formValues: Record<string, unknown>;
  /** The item being edited, or null on create. */
  item: Record<string, unknown> | null;
  /** Acting admin's user id, available for current-user defaults. */
  actingUserId: number;
}

// ── Fields ──────────────────────────────────────────────────────────────────
interface FieldBase {
  key: string;
  label: string;
  required?: boolean;
  helperText?: string;
  /** Default for create. Can be a literal or a resolver receiving FieldContext. */
  defaultValue?: unknown | ((ctx: FieldContext) => unknown);
  /** Hide the field entirely when this returns false. Re-evaluated on every render. */
  visibleWhen?: (ctx: FieldContext) => boolean;
  /** Display the field but disable editing. */
  readOnly?: boolean;
}

export interface TextField extends FieldBase {
  kind: 'text';
  maxLength?: number;
  pattern?: RegExp;
  placeholder?: string;
}
export interface TextareaField extends FieldBase {
  kind: 'textarea';
  rows?: number;
  maxLength?: number;
  placeholder?: string;
}
export interface MarkdownField extends FieldBase {
  kind: 'markdown';
  rows?: number;
  maxLength?: number;
}
export interface SelectField extends FieldBase {
  kind: 'select';
  options: Option[];
}
export interface SelectAsyncField extends FieldBase {
  kind: 'select-async';
  loadOptions: () => Promise<Option[]> | Option[];
}
export interface MultiSelectAsyncField extends FieldBase {
  kind: 'multi-select-async';
  loadOptions: () => Promise<Option[]> | Option[];
}
/** Series of free-text inputs — used for pulse options (2–4 entries). */
export interface MultiTextField extends FieldBase {
  kind: 'multi-text';
  minItems?: number;
  maxItems?: number;
  placeholderEach?: string;
}
export interface DateField extends FieldBase {
  kind: 'date';
}
export interface DatetimeField extends FieldBase {
  kind: 'datetime';
}
export interface NumberField extends FieldBase {
  kind: 'number';
  min?: number;
  max?: number;
  step?: number;
}
/** Display-only — never edited; renders the value verbatim or via render(). */
export interface ReadonlyField extends FieldBase {
  kind: 'readonly';
  render?: (value: unknown, item: Record<string, unknown> | null) => string;
}
/** Image upload — uploads a png/jpg to /api/admin/upload and stores the
 *  returned URL as the field value (also pasteable as a plain URL). */
export interface ImageUploadField extends FieldBase {
  kind: 'image-upload';
  maxLength?: number;
}

export type Field =
  | TextField
  | TextareaField
  | MarkdownField
  | SelectField
  | SelectAsyncField
  | MultiSelectAsyncField
  | MultiTextField
  | DateField
  | DatetimeField
  | NumberField
  | ReadonlyField
  | ImageUploadField;

// ── Columns ─────────────────────────────────────────────────────────────────
interface ColumnBase {
  key: string;
  label: string;
  /** CSS grid-template-columns track (e.g. '1.7fr', '120px'). */
  width?: string;
  /** Primary column gets the larger title styling. At most one per columns array. */
  primary?: boolean;
  /** When set, the list can be sorted by this column. */
  sortable?: boolean;
}

export interface TextColumn<T> extends ColumnBase {
  kind?: 'text';
  /** Override the default <td>{item[key]}</td> rendering. */
  render?: (item: T) => { title: string; subtitle?: string };
}
export interface PillColumn<T> extends ColumnBase {
  kind: 'pill';
  pillVariants: PillVariants;
  /** Override which value to look up in pillVariants (default = item[key]). */
  value?: (item: T) => string;
}
export interface RelativeDateColumn<T> extends ColumnBase {
  kind: 'relative-date';
  /** Shown when the value is null/undefined. */
  emptyFallback?: string;
  value?: (item: T) => string | null | undefined;
}
export interface NumberColumn<T> extends ColumnBase {
  kind: 'number';
  value?: (item: T) => number | null | undefined;
}
/** Compact list of pills — for focus_tags, audience, etc. */
export interface TagListColumn<T> extends ColumnBase {
  kind: 'tag-list';
  value: (item: T) => string[];
}

export type Column<T> =
  | TextColumn<T>
  | PillColumn<T>
  | RelativeDateColumn<T>
  | NumberColumn<T>
  | TagListColumn<T>;

// ── Filters ─────────────────────────────────────────────────────────────────
export interface Filter<T> {
  key: string;
  label: string;
  predicate: (item: T) => boolean;
  isDefault?: boolean;
}

// ── Search ──────────────────────────────────────────────────────────────────
export interface SearchConfig<T> {
  placeholder: string;
  /** Object keys to search; coerced to string and matched case-insensitively. */
  fields: (keyof T & string)[];
}

// ── Sort ────────────────────────────────────────────────────────────────────
export interface SortConfig {
  key: string;
  direction: 'asc' | 'desc';
}

// ── List view config ────────────────────────────────────────────────────────
export interface ListConfig<T> {
  queryFn: () => T[] | Promise<T[]>;
  /** Default column set. */
  columns: Column<T>[];
  /**
   * Override columns when a specific filter is active. The key matches a
   * Filter.key. Used by the Users resource (council vs pilots) to swap
   * member_number/focus_tags for role/last_seen_at.
   */
  columnsByFilter?: Record<string, Column<T>[]>;
  filters?: Filter<T>[];
  search?: SearchConfig<T>;
  defaultSort?: SortConfig;
  pageSize?: number;
}

// ── Embedded sub-forms (the Pulse fieldset inside dispatch edit) ────────────
export interface FormEmbed {
  /** Unique key inside the parent form. */
  key: string;
  title: string;
  /**
   * Discriminator the panel uses to pick a renderer component.
   * Keep this small — new embed kinds are explicit additions, not generic.
   */
  component: 'pulse-sub-form';
  visibleWhen?: (ctx: FieldContext) => boolean;
}

// ── Form config ─────────────────────────────────────────────────────────────
export interface FormConfig {
  fields: Field[];
  /** Optional embedded sub-form sections (e.g. pulse inside dispatch). */
  embeds?: FormEmbed[];
}

// ── Op context — passed to CRUD ops and actions ─────────────────────────────
export interface OpContext {
  user: { id: number; role: string };
  /** Request origin (e.g. "https://bifrost.fenja.ai") — used to build absolute
   *  URLs in ActionResults like invite links. Always set by the route handler. */
  origin: string;
  /**
   * Raw POST FormData — opt-in escape hatch for resources whose form has
   * embedded sub-forms (e.g. the pulse fieldset inside dispatches). Most
   * resources ignore this and work off the typed `data` argument.
   */
  formData?: FormData;
  /**
   * Set by an op or action to surface a one-shot result on the next render
   * (e.g. the magic link after an invite is created). The route handler
   * reads this after the op returns and propagates it via the redirect URL.
   */
  result?: ActionResult;
}

// ── CRUD operations ─────────────────────────────────────────────────────────
export interface ResourceOps<T> {
  /** Returns the new item's id. */
  create?: (data: Record<string, unknown>, ctx: OpContext) => number | Promise<number>;
  update?: (id: number, data: Record<string, unknown>, ctx: OpContext) => void | Promise<void>;
  delete?: (id: number, ctx: OpContext) => void | Promise<void>;
  getById?: (id: number) => T | null | Promise<T | null>;
}

// ── Action results — surfaced inside the edit panel ─────────────────────────
/** No additional UI — just close the panel with a toast. */
export interface ActionResultToast {
  kind: 'toast';
}
/** Render the generated invite link in the panel with a Copy button. */
export interface ActionResultInviteLink {
  kind: 'invite-link';
  url: string;
}
/** Render a freshly-generated temp password in the panel with a Copy button. */
export interface ActionResultTempPassword {
  kind: 'temp-password';
  password: string;
}
export type ActionResult = ActionResultToast | ActionResultInviteLink | ActionResultTempPassword;

// ── Actions (publish, archive, approve, etc.) ───────────────────────────────
export interface ResourceAction<T> {
  key: string;
  label: string;
  /** Hide the action when this returns false for the current item. */
  visibleWhen?: (item: T) => boolean;
  /** Confirm dialog text. If omitted, no confirmation is shown. */
  confirmText?: string;
  /** Destructive actions render in terracotta. */
  destructive?: boolean;
  handler: (id: number, ctx: OpContext) => ActionResult | void | Promise<ActionResult | void>;
}

// ── Notification count (sidebar badge in terracotta if > 0) ─────────────────
export interface NotifyCount<T> {
  /** Return the count of items needing attention (pending requests, stale drafts, etc.). */
  count: (items: T[]) => number;
}

// ── Resource ────────────────────────────────────────────────────────────────
export interface Resource<T = Record<string, unknown>> {
  /** URL slug — /admin/<key>. */
  key: string;
  label: string;
  pluralLabel: string;
  singularLabel: string;
  /** Matches a ResourceGroup.key in the registry. */
  groupKey: string;
  /** Optional one-line subtitle under the page title. */
  description?: string;
  /** Returns the member-facing URL for an item (for the "View on portal" link). */
  publicRoutePattern?: (item: T) => string | null;
  list: ListConfig<T>;
  /** null marks the resource as read-only (no edit panel, no "+ New" button). */
  form: FormConfig | null;
  /**
   * When form is null but the resource still has actions (e.g. join_requests),
   * this defines the read-only fields the review panel renders above the
   * action buttons. Returns label/value pairs in display order.
   */
  summary?: (item: T) => { label: string; value: string }[];
  ops: ResourceOps<T>;
  actions?: ResourceAction<T>[];
  notifyCount?: NotifyCount<T>;
}

// ── Resource groups (sidebar sections) ──────────────────────────────────────
export interface ResourceGroup {
  key: string;
  label: string;
  // Each resource carries its own item type. Erase the generic at the
  // registration boundary so different resources can coexist in one array.
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  resources: Resource<any>[];
}
