#define _DEFAULT_SOURCE /* for timegm, parsing "date" back into utc_moment */

#include "reading_io.h"
#include "json.h"

#include <stdio.h>
#include <string.h>

/* Mirrors astro.c's own k_body_slug/k_aspect_slug (its i18n lookup-key
 * tables), duplicated here rather than linking astro.c into this
 * otherwise header-only, independently testable module - same policy
 * and same reasoning as significance.c's max_orb_for_pair(). Keep in
 * sync if those slugs ever change. */
static const char *const k_body_slug[NUM_BODIES] = {
  "sun", "moon", "mercury", "venus", "mars",
  "jupiter", "saturn", "uranus", "neptune", "pluto",
};

static const char *const k_aspect_slug[5] = {
  "conjunction", "sextile", "square", "trine", "opposition",
};

static const char *const k_sign_slug[12] = {
  "aries", "taurus", "gemini", "cancer", "leo", "virgo",
  "libra", "scorpio", "sagittarius", "capricorn", "aquarius", "pisces",
};

/* Mirrors tarot_data.c's own k_card_slug/k_position_slug, same
 * duplication policy as k_body_slug above. */
static const char *const k_card_slug[TAROT_DECK_SIZE] = {
  "fool", "magician", "high_priestess", "empress", "emperor", "hierophant",
  "lovers", "chariot", "strength", "hermit", "wheel_of_fortune", "justice",
  "hanged_man", "death", "temperance", "devil", "tower", "star", "moon",
  "sun", "judgement", "world",
};

static const char *const k_position_slug[TAROT_SPREAD_SIZE] = {
  "present", "challenge", "crown", "foundation", "recent_past",
  "near_future", "attitude", "environment", "hopes_and_fears", "outcome",
};

/* Mirrors tarot_data.c's own k_gender_slug, same duplication policy as
 * k_card_slug/k_position_slug above (no reverse lookup exposed there). */
static const char *const k_gender_slug[3] = {
  "unspecified", "male", "female",
};

static bool body_from_slug(const char *slug, Body *out) {
  for (int i = 0; i < NUM_BODIES; i++) {
    if (strcmp(slug, k_body_slug[i]) == 0) {
      *out = (Body)i;
      return true;
    }
  }
  return false;
}

static bool sign_from_slug(const char *slug, ZodiacSign *out) {
  for (int i = 0; i < 12; i++) {
    if (strcmp(slug, k_sign_slug[i]) == 0) {
      *out = (ZodiacSign)i;
      return true;
    }
  }
  return false;
}

/* Fills out[NUM_BODIES] from a "bodies" JSON array, the shape shared by
 * both "natal" and "transits" (see reading_print_json in engine/src/
 * reading.c). Used only for sign/house context in significant-event
 * reporting, not for scoring, so this is deliberately lenient: an entry
 * with an unrecognized/missing "body" slug is skipped, and a missing
 * "house" leaves that body's house at 0 - not a valid whole-sign house
 * number (always 1-12), so callers use house == 0 as "no position data
 * available" rather than this failing the whole load. */
static void parse_bodies(const JsonValue *bodies, PlanetPosition out[NUM_BODIES]) {
  int count = json_array_count(bodies);
  for (int i = 0; i < count; i++) {
    const JsonValue *item = json_array_get(bodies, i);
    Body body;
    if (!body_from_slug(json_as_string(json_object_get(item, "body")), &body)) continue;

    ZodiacSign sign;
    if (sign_from_slug(json_as_string(json_object_get(item, "sign")), &sign)) {
      out[body].sign = sign;
    }
    out[body].degree_in_sign = json_as_number(json_object_get(item, "degree_in_sign"));
    out[body].ecliptic_longitude = json_as_number(json_object_get(item, "ecliptic_longitude"));
    out[body].house = (int)json_as_number(json_object_get(item, "house"));
  }
}

static bool aspect_type_from_slug(const char *slug, AspectType *out) {
  for (int i = 0; i < 5; i++) {
    if (strcmp(slug, k_aspect_slug[i]) == 0) {
      *out = (AspectType)i;
      return true;
    }
  }
  return false;
}

static bool card_from_slug(const char *slug, TarotCard *out) {
  for (int i = 0; i < TAROT_DECK_SIZE; i++) {
    if (strcmp(slug, k_card_slug[i]) == 0) {
      *out = (TarotCard)i;
      return true;
    }
  }
  return false;
}

static bool gender_from_slug(const char *slug, Gender *out) {
  for (int i = 0; i < 3; i++) {
    if (strcmp(slug, k_gender_slug[i]) == 0) {
      *out = (Gender)i;
      return true;
    }
  }
  return false;
}

static bool position_from_slug(const char *slug, CelticCrossPosition *out) {
  for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
    if (strcmp(slug, k_position_slug[i]) == 0) {
      *out = (CelticCrossPosition)i;
      return true;
    }
  }
  return false;
}

/* Fills out->positions[] from a "spread"."positions" JSON array. Used only
 * for guidance_print()'s Attitude/Outcome framing, never for scoring, so
 * this is deliberately lenient like parse_bodies(): an entry with an
 * unrecognized/missing "position" or "card" slug is skipped, and a
 * missing "spread" key entirely just leaves every position zeroed
 * (card = the first enum value, reversed = false) rather than failing
 * the whole load. */
static void parse_spread(const JsonValue *positions, CelticCrossSpread *out) {
  int count = json_array_count(positions);
  for (int i = 0; i < count; i++) {
    const JsonValue *item = json_array_get(positions, i);
    CelticCrossPosition position;
    TarotCard card;
    if (!position_from_slug(json_as_string(json_object_get(item, "position")), &position)) continue;
    if (!card_from_slug(json_as_string(json_object_get(item, "card")), &card)) continue;

    out->positions[position].card = card;
    const JsonValue *reversed = json_object_get(item, "reversed");
    out->positions[position].reversed = reversed && reversed->type == JSON_BOOL && reversed->as.boolean;
  }
}

/* Parses the top-level "date" field ("YYYY-MM-DD", see reading_print_json
 * in engine/src/reading.c) back into out->utc_moment, as midnight UTC of
 * that date - reading_print_json only ever wrote the calendar date, not a
 * time of day, so that's the only value that round-trips through
 * gmtime() to the same date. Lenient like parse_bodies()/parse_spread():
 * a missing or malformed "date" just leaves out->utc_moment at 0 (from
 * reading_load_json's own memset), not a load failure - nothing here
 * depends on it for scoring, only for display. */
static void parse_date(const JsonValue *date, DailyReading *out) {
  const char *date_str = date ? json_as_string(date) : NULL;
  if (!date_str) return;

  struct tm tm_date = {0};
  if (sscanf(date_str, "%d-%d-%d", &tm_date.tm_year, &tm_date.tm_mon, &tm_date.tm_mday) != 3) return;
  tm_date.tm_year -= 1900;
  tm_date.tm_mon -= 1;
  out->utc_moment = timegm(&tm_date);
}

/* Parses the top-level "gender" field (see reading_print_json in
 * engine/src/reading.c) into out->gender. Lenient like parse_date(): a
 * missing or unrecognized value just leaves out->gender at
 * GENDER_UNSPECIFIED (0, from reading_load_json's own memset), not a
 * load failure - it only affects Celtic Cross position text pronouns,
 * nothing scoring depends on it. */
static void parse_gender(const JsonValue *gender, DailyReading *out) {
  const char *gender_str = gender ? json_as_string(gender) : NULL;
  if (!gender_str) return;
  gender_from_slug(gender_str, &out->gender);
}

bool reading_load_json(const char *text, size_t length, DailyReading *out) {
  memset(out, 0, sizeof(*out));

  JsonValue *root = json_parse(text, length);
  if (!root) return false;

  const JsonValue *transits = json_object_get(root, "transits");
  const JsonValue *aspects = transits ? json_object_get(transits, "aspects") : NULL;
  if (!aspects) {
    json_free(root);
    return false;
  }

  bool ok = true;
  int filled = 0;
  int count = json_array_count(aspects);
  for (int i = 0; i < count && filled < MAX_ASPECTS; i++) {
    const JsonValue *item = json_array_get(aspects, i);
    const char *transiting_slug = json_as_string(json_object_get(item, "transiting_planet"));
    const char *natal_slug = json_as_string(json_object_get(item, "natal_planet"));
    const char *type_slug = json_as_string(json_object_get(item, "type"));
    const JsonValue *orb_value = json_object_get(item, "orb");

    Body transiting, natal;
    AspectType type;
    if (!body_from_slug(transiting_slug, &transiting) ||
        !body_from_slug(natal_slug, &natal) ||
        !aspect_type_from_slug(type_slug, &type) ||
        !orb_value) {
      ok = false;
      break;
    }

    out->transits.aspects[filled].transiting_planet = transiting;
    out->transits.aspects[filled].natal_planet = natal;
    out->transits.aspects[filled].type = type;
    out->transits.aspects[filled].orb = json_as_number(orb_value);
    filled++;
  }
  out->transits.aspect_count = filled;

  parse_date(json_object_get(root, "date"), out);
  parse_gender(json_object_get(root, "gender"), out);

  const JsonValue *natal = json_object_get(root, "natal");
  parse_bodies(natal ? json_object_get(natal, "bodies") : NULL, out->natal.bodies);
  parse_bodies(json_object_get(transits, "bodies"), out->transits.bodies);

  const JsonValue *spread = json_object_get(root, "spread");
  parse_spread(spread ? json_object_get(spread, "positions") : NULL, &out->spread);

  json_free(root);
  return ok;
}
