#include "significance.h"

/* Higher = this transiting planet's aspects matter more when they occur.
 * Slow/outer planets transit rarely, so an aspect from one is a bigger
 * deal than the same aspect from a fast-moving personal planet - this is
 * about how rare the *transiting* body's activity is, independent of
 * which natal point it's touching (see natal_point_bonus for that). */
static const double k_transiting_weight[NUM_BODIES] = {
  [PLANET_SUN] = 2.0,
  [PLANET_MOON] = 1.0,
  [PLANET_MERCURY] = 1.5,
  [PLANET_VENUS] = 1.5,
  [PLANET_MARS] = 2.0,
  [PLANET_JUPITER] = 4.0,
  [PLANET_SATURN] = 6.0,
  [PLANET_URANUS] = 8.0,
  [PLANET_NEPTUNE] = 9.0,
  [PLANET_PLUTO] = 10.0,
};

static double natal_point_bonus(Body natal_planet) {
  /* A transit to a natal luminary (Sun/Moon) reads as more personally
   * significant than one to any other planet. */
  return (natal_planet == PLANET_SUN || natal_planet == PLANET_MOON) ? 1.5 : 1.0;
}

/* Mirrors astro.c's own orb_for_pair() (its detection-time orb rule),
 * duplicated here as a single ternary rather than linking the full
 * vendored Astronomy Engine into this otherwise header-only,
 * independently testable module just to reuse one line. Keep this in
 * sync if that rule in astro.c ever changes. */
static double max_orb_for_pair(Body transiting, Body natal) {
  bool luminary = transiting == PLANET_SUN || transiting == PLANET_MOON ||
                  natal == PLANET_SUN || natal == PLANET_MOON;
  return luminary ? 8.0 : 6.0;
}

static double aspect_score(const Aspect *a) {
  double max_orb = max_orb_for_pair(a->transiting_planet, a->natal_planet);
  double tightness = 1.0 - (a->orb / max_orb); /* 1.0 = exact, ~0 = at the edge */
  if (tightness < 0.0) tightness = 0.0;
  return k_transiting_weight[a->transiting_planet] * tightness *
         natal_point_bonus(a->natal_planet);
}

/* Inserts (kind, aspect, score) into out->top_items in descending-score
 * order, keeping only the top MAX_SIGNIFICANT_ITEMS. O(n * 5), fine for
 * n <= MAX_ASPECTS (100). */
static void insert_ranked(DailyInterpretation *out, SignificantItemKind kind,
                           const Aspect *aspect, double score) {
  int insert_at = out->top_item_count;
  while (insert_at > 0 && out->top_items[insert_at - 1].score < score) insert_at--;
  if (insert_at >= MAX_SIGNIFICANT_ITEMS) return;

  int shift_from = out->top_item_count < MAX_SIGNIFICANT_ITEMS
                       ? out->top_item_count
                       : MAX_SIGNIFICANT_ITEMS - 1;
  for (int j = shift_from; j > insert_at; j--) {
    out->top_items[j] = out->top_items[j - 1];
  }

  out->top_items[insert_at].kind = kind;
  out->top_items[insert_at].aspect = *aspect;
  out->top_items[insert_at].score = score;
  if (out->top_item_count < MAX_SIGNIFICANT_ITEMS) out->top_item_count++;
}

/* Thresholds are tunable, not derived from anything physical - chosen so
 * an exact outer-planet-to-luminary hit (the classic "big transit") lands
 * solidly in DAY_MAJOR while a single loose, minor-planet aspect stays
 * DAY_QUIET. Adjust here if real usage says the cutoffs feel off. */
static DaySignificance classify_day(int top_item_count, double top_score) {
  if (top_item_count == 0) return DAY_QUIET;
  if (top_score >= 9.0) return DAY_MAJOR;
  if (top_score >= 5.0) return DAY_SIGNIFICANT;
  if (top_score >= 2.5) return DAY_NOTABLE;
  return DAY_QUIET;
}

void interpret_daily_reading(const DailyReading *reading, DailyInterpretation *out) {
  out->top_item_count = 0;

  for (int i = 0; i < reading->transits.aspect_count; i++) {
    const Aspect *a = &reading->transits.aspects[i];
    insert_ranked(out, ITEM_ASPECT, a, aspect_score(a));
  }

  double top_score = out->top_item_count > 0 ? out->top_items[0].score : 0.0;
  out->day_level = classify_day(out->top_item_count, top_score);
}

bool interpretation_deserves_framing(const DailyInterpretation *interp) {
  return interp->day_level == DAY_MAJOR;
}
