/**
 * Bulk-importer engine types.
 *
 * One generic pipeline parameterised by a per-entity {@link ImportAdapter},
 * mirroring the custom-report registry pattern. The engine (parse → map →
 * classify → commit) never knows about a specific entity; everything
 * entity-specific lives in an adapter.
 *
 * See docs/superpowers/specs/2026-06-01-bulk-import-design.md.
 */
import type { z } from 'zod';

import type { AuditMeta } from '@/lib/audit';

export const IMPORT_ENTITY_KEYS = [
  'companies',
  'clients',
  'yachts',
  'berths',
  'interests',
  'tenancies',
  'expenses',
] as const;

export type ImportEntityKey = (typeof IMPORT_ENTITY_KEYS)[number];

export const CONFLICT_POLICIES = ['skip-matches', 'update-matches', 'error-on-match'] as const;
export type ConflictPolicy = (typeof CONFLICT_POLICIES)[number];

/** Per-target-field definition. Drives the column-mapping UI + row validation. */
export interface ImportField {
  key: string;
  label: string;
  required: boolean;
  /** Extra header spellings for fuzzy auto-mapping (besides key + label). */
  aliases?: string[];
  /** Per-cell validation. Empty optional cells are dropped before this runs. */
  zod: z.ZodTypeAny;
}

/** A raw parsed row: source header → cell value (strings). */
export type RawRow = Record<string, string>;
/** A mapped row: target field key → cell value (strings), empties removed. */
export type MappedRow = Record<string, string>;

export interface ImportCtx {
  /**
   * Tenant scope for every read/write this import performs. Adapters stamp it
   * onto inserts and use it to port-scope their dedup lookups.
   *
   * SECURITY / TRUST BOUNDARY (audit L35) — `portId` is currently sourced from
   * `import_batches.portId` and trusted wholesale. That is safe ONLY because no
   * API route enqueues this engine yet; the batch row is created server-side.
   * Any future commit/dry-run HTTP route MUST, before enqueuing or running the
   * engine:
   *   1. re-derive the acting port from the authenticated session (NEVER take
   *      a portId from the request body), and
   *   2. assert `batch.portId === session.portId` (reject cross-tenant batch
   *      ids — batch ids are guessable enough to warrant the check), and
   *   3. gate on an `import` permission (no permission is checked anywhere in
   *      the engine path today).
   * Do not relax this to read a client-supplied portId.
   */
  portId: string;
  meta: AuditMeta;
}

export interface RowError {
  field: string;
  message: string;
}

export type FkResult =
  | { ok: true; resolved: Record<string, string> }
  | { ok: false; errors: RowError[] };

/** The four dry-run outcomes for a single source row. */
export type RowOutcome = 'insert' | 'update' | 'skip' | 'error';

export interface ClassifiedRow {
  rowNumber: number;
  outcome: RowOutcome;
  /** Set when outcome=update/skip (the matched existing entity). */
  existingId?: string;
  errors?: RowError[];
  /** Validated + FK-resolved payload, carried to the commit step. */
  mapped?: MappedRow;
  resolved?: Record<string, string>;
}

export interface ImportAdapter {
  key: ImportEntityKey;
  label: string;
  /** Dependency order — companies(1) … expenses(7). */
  order: number;
  dependsOn: ImportEntityKey[];
  targetFields: ImportField[];
  /** Optional cross-field validation (e.g. "email or phone required") run
   *  after per-field zod. Returns errors, never throws. */
  extraValidate?: (row: MappedRow) => RowError[];
  /** Natural dedup key derived from a validated mapped row (or null). */
  matchKey: (row: MappedRow) => string | null;
  /** Find an existing entity by its natural key, port-scoped. */
  findExisting: (portId: string, matchKey: string) => Promise<{ id: string } | null>;
  /** Resolve FK ids by natural key. Omit for entities with no FKs. */
  resolveForeignKeys?: (row: MappedRow, ctx: ImportCtx) => Promise<FkResult>;
  /** Delegates to the entity's own create service (audit + validation free). */
  insert: (
    row: MappedRow,
    resolved: Record<string, string>,
    ctx: ImportCtx,
  ) => Promise<{ id: string }>;
  /** Omit when the entity is insert-only under the importer. */
  update?: (
    existingId: string,
    row: MappedRow,
    resolved: Record<string, string>,
    ctx: ImportCtx,
  ) => Promise<void>;
}
