import { AnySchema } from 'yup';
import { ReactNode } from 'react';

/**
 * Base defaults shared by every node descriptor. A new node only has to specify
 * what makes it different — these fill in the rest.
 *
 * - `formSchema: []`            → no config form unless the node defines one
 * - `bpmnEntry` / `bpmnExit`    → incoming/outgoing flow connects to the node id itself
 * - `emitBpmnElements`          → a single generic bpmn:Task; override for real output
 */
export declare const baseNodeDefaults: Pick<NodeDescriptor, 'formSchema' | 'bpmnEntry' | 'bpmnExit' | 'emitBpmnElements'>;

declare interface BpmnElement {
    id: string;
    $type: string;
    [key: string]: unknown;
}

declare type BranchChain = CanvasSlot[];

declare interface BranchMap {
    [filterId: string]: {
        [branchKey: string]: BranchChain;
    };
}

declare interface CanvasSlot {
    id: string;
    kind: SlotKind;
    reactFlowType: string;
    descriptorType?: string;
    formData?: Record<string, unknown>;
    nodeData?: Record<string, unknown>;
    deletable?: boolean;
    label?: string;
    /** Maps branch key → display label. e.g. { pass: 'Pass', fail: 'Fail' } */
    branchLabels?: Record<string, string>;
}

export declare const conditionBranchDescriptor: NodeDescriptor;

/** One column in a per-field condition row */
declare interface ConditionFieldDef {
    /** Key used to store this column's value in ConditionEntry */
    id: string;
    /**
     * 'select'   — native <select> with user-supplied options
     * 'operator' — native <select> pre-filled with the standard operator list (no options needed)
     * 'text'     — plain <input>
     * any other string — falls back to <input>
     */
    type: 'select' | 'operator' | 'text' | string;
    colSpan?: number;
    label?: string;
    placeholder?: string;
    /** Options for select columns — user defines these, renderer just renders them */
    options?: Array<{
        value: string;
        label: string;
    }>;
    optionsSource?: OptionsSource;
    /** When true, renders a FormulaInput chip editor instead of a plain <input> */
    formula?: boolean;
}

/**
 * Build a node descriptor on top of {@link baseNodeDefaults}.
 *
 * Supply the four required identity fields plus only the behaviour you want to
 * override. Everything else inherits the base.
 *
 *   export const startEventDescriptor = defineNode({
 *     type: 'startEvent',
 *     label: 'Start',
 *     matches: (id) => id === 'start',
 *     reactFlowType: 'startNode',
 *     emitBpmnElements: (id) => [{ id, $type: 'bpmn:StartEvent' }], // override
 *   });
 */
export declare function defineNode(overrides: RequiredNodeFields & Partial<NodeDescriptor>): NodeDescriptor;

declare interface DmnDefinition {
    id: string;
    [key: string]: unknown;
}

/**
 * End event — a fixed skeleton node with no user-editable form.
 * The canvas seeds the main-spine end as the literal slot id `'end'`, and one
 * end per branch as `'end-${filterId}-${branch}'` (see WorkflowCanvas.tsx).
 * `matches` must cover both shapes so every terminal node resolves via `forId`
 * during descriptor-driven BPMN load — otherwise branch ends get skipped.
 *
 * Inherits formSchema (none) and bpmnEntry/bpmnExit from the base node.
 */
export declare const endEventDescriptor: NodeDescriptor;

/** Condition definition attached to a FieldDescriptor */
declare interface FieldConditionDef {
    fields: ConditionFieldDef[];
    /** Enables nested condition groups. Defaults to false. */
    allowGroups?: boolean;
    /** Optional depth limit for nested groups when allowGroups is true. */
    maxGroupDepth?: number;
}

declare interface FieldContext {
    /** Full top-level form values (always). */
    values: Record<string, unknown>;
    /**
     * Item-scoped values for a field rendered INSIDE an array item.
     * Lets `shouldHide` / `shouldDisable` / `shouldRequire` / `compute` see
     * sibling-in-same-row fields without having to splice paths manually:
     *
     *   { id: 'template', shouldHide: ({ item }) => item?.valueSource !== 'template' }
     *
     * `undefined` when the field is NOT inside an array item (i.e. top-level
     * fields and fields inside plain `type: 'object'` children at top level).
     * Nested arrays update `item` to the immediate parent row's values.
     */
    item?: Record<string, unknown>;
}

declare interface FieldDescriptor {
    /**
     * The formik key for this field.
     * Use `id` (preferred) or `name` — both are accepted.
     * `id` takes precedence if both are provided.
     */
    id?: string;
    name?: string;
    type: FieldType | string;
    /** Child field descriptors — used by type: 'object' to render a nested group. */
    children?: FieldDescriptor[];
    /** Optional per-field condition guard — renders a condition row below this field. */
    condition?: FieldConditionDef;
    label?: string;
    placeholder?: string;
    required?: boolean;
    showOptional?: boolean;
    disabled?: boolean | ((ctx: FieldContext) => boolean);
    hidden?: boolean;
    colSpan?: number;
    dividerAfter?: boolean;
    info?: string;
    helperText?: string;
    dataTestId?: string;
    validation?: AnySchema;
    shouldHide?: (ctx: FieldContext) => boolean;
    shouldDisable?: (ctx: FieldContext) => boolean;
    shouldRequire?: (ctx: FieldContext) => boolean;
    /**
     * When true, the field's value is auto-cleared (reset to `defaultValue`,
     * or removed if no default) any time `shouldHide` evaluates to true —
     * including on mount when the saved data has orphan values from a prior
     * configuration. Default: false (values persist while hidden, preserving
     * legacy behavior).
     *
     * Use this on conditional fields where stale data would corrupt the
     * payload (e.g. `template` should be empty when `valueSource !== 'template'`).
     */
    clearOnHide?: boolean;
    defaultValue?: unknown;
    compute?: (ctx: FieldContext) => unknown;
    transform?: FieldTransform;
    onChange?: (value: unknown, ctx: FieldContext & {
        setFieldValue: (name: string, value: unknown) => void;
    }) => void;
    /**
     * Declarative dynamic options. The renderer translates this into a paged,
     * cached, abortable async loader via the host's resolver registry.
     * Mutually compatible with static `options`: static wins if both set.
     */
    optionsSource?: OptionsSource;
    [key: string]: unknown;
}

declare interface FieldTransform {
    in?: (value: unknown) => unknown;
    out?: (value: unknown) => unknown;
}

declare type FieldType = 'text' | 'textarea' | 'number' | 'toggle' | 'radio-group' | 'select' | 'descriptive-select' | 'key-value' | 'array' | 'object' | 'reference' | 'cron' | 'schema';

declare interface FixedEdge {
    id: string;
    source: string;
    target: string;
}

declare interface FixedNode {
    id: string;
    type: string;
    data?: Record<string, unknown>;
    deletable?: false;
}

/**
 * Context passed to `NodeDescriptor.canInsert(ctx)` describing where on the
 * canvas the user clicked "+ Add step". Used by position-aware insertion rules.
 */
declare interface InsertContext {
    /** The slot the user clicked under (the one whose "+" edge was clicked). */
    insertAfterSlot: CanvasSlot | null;
    /** Every slot before the insertion point, including the click slot.
     *  When inside a branch: spine up to + including the branching parent, then
     *  the branch's own slots up to the insertion point. */
    upstreamSlots: CanvasSlot[];
    /** Every slot after the insertion point in the same chain. */
    downstreamSlots: CanvasSlot[];
    /** Branch context, if the insertion is happening inside a branch. */
    branch: {
        filterId: string;
        branch: string;
    } | null;
    /** The currently active template (the one whose trigger is in use). */
    template: WorkflowTemplate | null;
}

export declare function makeSubWorkflowDescriptor(opts?: SubWorkflowDescriptorOptions): NodeDescriptor;

export declare interface NodeDescriptor {
    type: string;
    label: string;
    icon?: ReactNode;
    /**
     * Optional "app" this action belongs to (Slack, Salesforce, …). When present,
     * the node picker drills down: stage 1 lists unique apps, stage 2 lists this
     * app's actions, stage 3 is the action's config form. Descriptors without an
     * `app` field appear ungrouped at stage 1, alongside the app cards.
     */
    app?: {
        key: string;
        label: string;
        icon?: ReactNode;
        description?: string;
    };
    matches: (id: string) => boolean;
    bpmnEntry: (nodeId: string) => string;
    bpmnExit: (nodeId: string) => string;
    emitBpmnElements: (nodeId: string, nodeData: unknown) => BpmnElement[];
    emitDmnDefinition?: (nodeId: string, nodeData: unknown) => DmnDefinition;
    outgoingCondition?: (nodeId: string, targetId: string, nodeData: unknown) => string;
    reactFlowType: string;
    previewNodeType?: string;
    /**
     * Optional custom node-card body. When provided, the node renderer shows this
     * instead of the default value badges — lets a node author fully design the
     * card body while keeping the shared frame (title, edit/delete, handles).
     * Receives the node's saved formData.
     */
    renderNode?: (nodeData: Record<string, unknown>) => ReactNode;
    /**
     * Position constraint — this descriptor can be inserted only if at least ONE
     * of the listed tokens appears in the upstream chain of the click site.
     * Tokens can be a descriptorType (e.g. 'createUserTask'), a slot id
     * (e.g. 'event-userCreated'), or a slot kind (e.g. 'triggerFixed').
     * Ignored when `canInsert` is set.
     */
    insertAfter?: string[];
    /**
     * Symmetric — must appear DOWNSTREAM of the click site. Same token rules
     * as `insertAfter`. Ignored when `canInsert` is set.
     */
    insertBefore?: string[];
    /**
     * Full custom rule. Overrides `insertAfter` / `insertBefore`. Receives the
     * click context (upstream/downstream slots, branch info, active template);
     * returns `true` to allow the descriptor at this insertion point.
     */
    canInsert?: (ctx: InsertContext) => boolean;
    /** Maps branch key → display label. e.g. { pass: 'Pass', fail: 'Fail' }. Only applies to branching nodes (filterNode, approvalNode). */
    branchLabels?: Record<string, string>;
    formSchema: FieldDescriptor[] | ((nodeId: string) => Promise<FieldDescriptor[]>);
    maxPerWorkflow?: number;
    relatedNodePrefixes?: string[];
}

export declare class NodeTypeRegistry {
    private descriptors;
    register(descriptor: NodeDescriptor): this;
    forType(type: string): NodeDescriptor | undefined;
    forId(id: string): NodeDescriptor | undefined;
    list(): NodeDescriptor[];
}

export declare const nodeTypeRegistry: NodeTypeRegistry;

export declare const notificationDescriptor: NodeDescriptor;

declare interface OptionsSource {
    /** Resolver id — must match a key in the host's `optionsResolvers` map. */
    id: string;
    dependsOn?: string[] | Record<string, string>;
    params?: Record<string, unknown>;
    resolveValueId?: string;
}

/** The fields every node MUST provide — there is no sensible default for these. */
declare type RequiredNodeFields = Pick<NodeDescriptor, 'type' | 'label' | 'matches' | 'reactFlowType'>;

export declare const restApiDescriptor: NodeDescriptor;

declare type SlotKind = 'start' | 'end' | 'addTrigger' | 'triggerFixed' | 'userTask' | 'addStep' | 'filter' | 'approval';

/**
 * Start event — a fixed skeleton node with no user-editable form.
 * The canvas seeds it as the literal slot id `'start'` (see buildInitialSlots
 * in WorkflowCanvas.tsx). This descriptor exists so the registry can resolve
 * that id via `forId('start')` and emit it during descriptor-driven BPMN save.
 *
 * Inherits formSchema (none) and bpmnEntry/bpmnExit from the base node.
 */
export declare const startEventDescriptor: NodeDescriptor;

/** Pre-built static descriptor (no dynamic workflow list). Swap for makeSubWorkflowDescriptor when you have an API. */
export declare const subWorkflowDescriptor: NodeDescriptor;

/**
 * Sub-Workflow node — calls another saved workflow by ID.
 *
 * The host app must provide `fetchWorkflowOptions` at registration time so the
 * `workflowId` select field can populate. Inject it via the descriptor factory:
 *
 *   import { makeSubWorkflowDescriptor } from '@miniorange/flowforge';
 *   nodeTypeRegistry.register(makeSubWorkflowDescriptor({
 *     fetchWorkflowOptions: async () => myApi.listWorkflows(),
 *   }));
 */
export declare interface SubWorkflowDescriptorOptions {
    /** Return a list of available workflows for the workflowId select field */
    fetchWorkflowOptions?: () => Promise<Array<{
        label: string;
        value: string;
    }>>;
}

export declare const webhookTriggerDescriptor: NodeDescriptor;

export declare const webhookTriggerTemplate: WorkflowTemplate;

declare interface WorkflowState {
    slots: CanvasSlot[];
    branches: BranchMap;
}

declare interface WorkflowTemplate {
    triggerKey: string;
    label: string;
    description?: string;
    icon?: ReactNode;
    triggerCategory?: 'event' | 'scheduler' | 'webhook' | string;
    /**
     * Task IDs (descriptor types) that can be inserted into this workflow.
     * `allowedTasks` is accepted as an alias for backend payloads that use
     * that name — TemplateRegistry.register() normalises them.
     */
    availableTasks?: string[];
    allowedTasks?: string[];
    maxTasks?: number;
    taskLabels?: Record<string, string>;
    fixedNodes?: FixedNode[];
    fixedEdges?: FixedEdge[];
    insertionPoints?: string[];
    source?: string;
    product?: string;
    /** Pre-built workflow state to clone when user selects this template */
    skeletonState?: WorkflowState;
    /** Tags for filtering in the template library */
    tags?: string[];
    /** Category grouping in the library */
    category?: string;
    /** Team or author that owns this template */
    author?: string;
    /** Relative popularity score (higher = shown first in library) */
    popularity?: number;
}

export { }
