/* engram_reason.h — the REASONING layer: compositions over the §5 geometry
 * OPERATORS (engram_geometry.h). Where the operators are a relational ALGEBRA over
 * neighborhood descriptors, these are reasoning MODES built by CHAINING that algebra:
 *
 *   ANALOGY   A:B :: C:?     — learn the A→B transform (Procrustes), apply to C.
 *   INDUCTION {E_i} → rule   — pool example geometries; a generalizing structure
 *                              + a membership test.
 *   ABDUCTION x → best H     — the structure whose geometry best PLACES an
 *                              observation in-distribution (inverse of prediction).
 *   CAUSAL    x ? y | Z, t   — separate mere overlap (correlation) from directed
 *                              influence (temporal precedence + association that
 *                              SURVIVES controlling for confounders via subtract).
 *   PLANNING  start → goal   — a trajectory (sequence of neighborhoods) through the
 *                              manifold: shortest path over geo-distance edges.
 *
 * PURE + READ-ONLY (stdlib + libm only): every function consumes GeoDescriptor(s)
 * (+ a few scalars / timestamps) and NEVER touches the store, index, or activation.
 * All geometry is delegated to the engram_geo_* primitives; this file only composes.
 *
 * FRAME CONTRACT (inherited): descriptors passed together MUST share emb `dim` and
 * `global_mean` frame — exactly the §5 operator contract. A function returns <0 on
 * a dim/frame mismatch or bad argument.
 */
#ifndef ENGRAM_REASON_H
#define ENGRAM_REASON_H

#include <stdint.h>
#include "engram_geometry.h"

/* ═══════════════════════════════════════════════════════════════════════════
 * SHARED PRIMITIVE — point-to-manifold FIT. How well does a single point x sit
 * inside a neighborhood's ellipsoid?  Splits the residual (x − centroid) into:
 *   - the IN-SUBSPACE part, scaled by each axis extent → a Mahalanobis distance
 *     (how many "radii" out along the modeled directions), and
 *   - the ORTHOGONAL part outside the retained axes → energy the model does not
 *     explain at all (charged at the extent floor).
 * This is the common engine under INDUCTION's membership test and ABDUCTION's
 * explanation ranking. ext_floor (>0) guards zero-extent axes / the null model.
 * ═══════════════════════════════════════════════════════════════════════════ */
typedef struct {
    double mahalanobis;    /* sqrt( Σ_k ((a_k·(x−c)) / max(ext_k,floor))² )         */
    double ortho_residual; /* ‖(x−c) projected off the retained axes‖ (raw L2)      */
    double distance;       /* sqrt( maha² + (ortho_residual/floor)² ) — full fit    */
    double score;          /* 1 / (1 + distance²) ∈ (0,1]  (1 = dead-center)        */
} GeoFit;
int engram_reason_point_fit(const GeoDescriptor* g, const float* x,
                            double ext_floor, GeoFit* out);

/* ═══════════════════════════════════════════════════════════════════════════
 * ANALOGY — "A:B :: C:?". Learn the transform that carries A to B (orthogonal
 * Procrustes rotation R between their principal frames + the residual translation),
 * apply it to C, and return the mapped point + the nearest candidate neighborhood.
 * Composes: engram_geo_analogy (R) + engram_geo_analogy_apply + engram_geo_distance.
 * ═══════════════════════════════════════════════════════════════════════════ */
typedef struct {
    int      dim;
    float*   mapped_point;    /* predicted D location = R·c_C + (c_B − R·c_A)  (owned)*/
    double   analogy_residual;/* Procrustes ‖A−B R‖_F — frame-alignment quality      */
    int      best;            /* index of nearest candidate to mapped_point, or −1   */
    double   best_distance;   /* centroid L2 from mapped_point to the winner         */
    int      n_candidates;
    double*  distances;       /* centroid L2 mapped_point→candidate[i]         (owned)*/
} GeoAnalogyResult;
/* candidates may be NULL/0 (then best=−1 and only mapped_point is filled). */
int engram_reason_analogy(const GeoDescriptor* A, const GeoDescriptor* B,
                          const GeoDescriptor* C,
                          const GeoDescriptor* const* candidates, int n_candidates,
                          GeoAnalogyResult* out);
void engram_reason_analogy_free(GeoAnalogyResult* r);

/* ═══════════════════════════════════════════════════════════════════════════
 * INDUCTION — from a SET of example neighborhoods to the generalizing structure.
 * Pools the examples (law-of-total-variance via engram_geo_combine, folded left to
 * right) into a single "rule" descriptor whose top principal axes are the directions
 * CONSISTENTLY present across the examples (the shared subspace surfaces as the
 * dominant pooled axes; idiosyncratic per-example directions fall to the tail).
 * The rule carries a membership test (point-to-manifold fit against the pool).
 * ═══════════════════════════════════════════════════════════════════════════ */
typedef struct {
    GeoDescriptor* rule;      /* induced generalizing geometry (owned; geo_free)     */
    double         ext_floor; /* extent floor used by the membership test            */
    int            n_examples;/* how many examples were pooled                       */
} GeoInduction;
/* top_axes<=0 → 8. ext_floor<=0 → derived from the pooled radius. */
int engram_reason_induce(const GeoDescriptor* const* examples, int n_examples,
                         int top_axes, double ext_floor, GeoInduction* out);
/* Membership of a point in the induced rule ∈ (0,1] (the fit score). <0 on error. */
double engram_reason_membership(const GeoInduction* ind, const float* x);
void engram_reason_induction_free(GeoInduction* out);

/* ═══════════════════════════════════════════════════════════════════════════
 * ABDUCTION — inference to the best explanation. Given an observation POINT, rank a
 * set of candidate structures by how well each PLACES the observation in-distribution
 * (min point-to-manifold distance = the structure that, if assumed, best accounts for
 * the observation). The inverse of prediction.
 * ═══════════════════════════════════════════════════════════════════════════ */
typedef struct {
    int     best;             /* index of best-explaining hypothesis, or −1          */
    double  best_score;
    int     n;
    double* scores;           /* fit score per hypothesis (higher = better)   (owned)*/
    double* distances;        /* explanation distance per hypothesis          (owned)*/
    int*    rank;             /* hypothesis indices sorted best→worst          (owned)*/
} GeoAbduction;
int engram_reason_abduce(const float* obs, int dim,
                         const GeoDescriptor* const* hypotheses, int n,
                         double ext_floor, GeoAbduction* out);
void engram_reason_abduction_free(GeoAbduction* out);

/* ═══════════════════════════════════════════════════════════════════════════
 * CAUSAL — correlation vs causation. Over two variables' geometries (+ candidate
 * confounders + temporal order), distinguish:
 *   - mere co-occurrence / overlap (correlation), from
 *   - directed influence: association that (a) SURVIVES controlling for confounders
 *     (subtract each Z's subspace from both centroids, re-measure) and (b) is oriented
 *     by temporal PRECEDENCE.
 * Composes: centroid cosine (correlation) + engram_geo_subtract (control) + timestamps.
 * ═══════════════════════════════════════════════════════════════════════════ */
typedef enum {
    GEO_CAUSAL_NONE       = 0,  /* no meaningful association                        */
    GEO_CAUSAL_DIRECTED   = 1,  /* survives control + temporally ordered → cause→eff */
    GEO_CAUSAL_CONFOUNDED = 2   /* correlated but association dies under control     */
} GeoCausalVerdict;
typedef struct {
    double           assoc_raw;        /* |cos(c_x,c_y)| — the raw correlation       */
    double           assoc_controlled; /* |cos| of residual centroids after control  */
    int              temporal_dir;     /* +1 x→y, −1 y→x, 0 tie/unknown              */
    GeoCausalVerdict verdict;
    int              confounded;       /* 1 iff verdict==CONFOUNDED (the flag)       */
    double           strength;         /* directed influence estimate ∈[0,1] (0 else)*/
} GeoCausal;
/* confounders may be NULL/0. t_x,t_y are comparable timestamps (any monotone unit);
 * pass equal values for "unknown order". drop_frac∈(0,1): a controlled association
 * below (1−drop_frac)·assoc_raw AND below an absolute floor ⇒ CONFOUNDED. */
int engram_reason_causal(const GeoDescriptor* x, const GeoDescriptor* y,
                         const GeoDescriptor* const* confounders, int n_conf,
                         int64_t t_x, int64_t t_y,
                         double drop_frac, GeoCausal* out);

/* ═══════════════════════════════════════════════════════════════════════════
 * PLANNING — trajectory construction. Given a set of neighborhoods (manifold nodes),
 * a start and a goal, build a PATH (sequence of intermediate neighborhoods) by
 * shortest path over the graph whose edges connect neighborhoods within
 * neighbor_radius, weighted by geo-distance. Long straight jumps are not edges, so
 * the path follows the manifold's curvature through intermediates (a discrete geodesic).
 * Composes: engram_geo_distance (edge weights) + Dijkstra.
 * ═══════════════════════════════════════════════════════════════════════════ */
typedef struct {
    int*    path;             /* node indices start..goal (owned)                    */
    int     path_len;
    double  total_cost;       /* summed centroid-distance edge weights along path    */
    int     reached;          /* 1 if goal reachable within neighbor_radius graph    */
} GeoPlan;
/* neighbor_radius>0: max centroid distance for two neighborhoods to be adjacent.
 * Use "wasserstein"!=0 to weight edges by Wasserstein-2 instead of centroid L2. */
int engram_reason_plan(const GeoDescriptor* const* nodes, int n,
                       int start, int goal, double neighbor_radius,
                       int use_wasserstein, GeoPlan* out);
void engram_reason_plan_free(GeoPlan* out);

#endif /* ENGRAM_REASON_H */
