#define _POSIX_C_SOURCE 200809L /* for open_memstream */

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

#include "../../engine/src/i18n.h"
#include "guidance.h"
#include "narrative.h"
#include "reading_io.h"
#include "significance.h"

typedef enum { FORMAT_TEXT, FORMAT_HTML, FORMAT_JSON } OutputFormat;

static void print_usage(const char *prog) {
  fprintf(stderr,
    "Usage: %s [--format text|html|json] [--lang <code>] [--i18n-dir <path>]\n"
    "          [reading.json]\n\n"
    "Reads a DailyReading in deck-engine's --format json shape - from the\n"
    "given file, or from stdin if no file is given (or it is \"-\") - and\n"
    "prints a full report: the day's overall significance level, the\n"
    "top aspects driving it (each with a short narrative note), a full\n"
    "readout of the Celtic Cross spread with every card's meaning, and\n"
    "finally guidance tying the day's top transit to the spread's\n"
    "Attitude/Outcome cards.\n\n"
    "  --format    Output format, defaults to text. html links card art via\n"
    "              img/<file>, same convention as deck-engine's own html\n"
    "              output - save/run this next to the img/ directory\n"
    "              `make images` copies into dist/. json embeds the same\n"
    "              narrative/guidance prose as text (in whatever --lang was\n"
    "              loaded) alongside stable, language-independent slugs.\n"
    "  --lang      Reading language code, defaults to en. Matches a file\n"
    "              named <code>.lang in --i18n-dir (see engine/i18n/) -\n"
    "              note this is this binary's OWN output language, not\n"
    "              related to whatever --lang deck-engine was run with to\n"
    "              produce its --format json input (which is always\n"
    "              language-independent - see docs/input-output-format.md).\n"
    "  --i18n-dir  Directory to look up <lang>.lang in. Defaults to the\n"
    "              i18n/ directory next to this binary (`make i18n`\n"
    "              copies engine/i18n/ there as dist/i18n/).\n\n"
    "Typical use:\n"
    "  dist/deck-engine ... --format json | %s\n"
    "  dist/deck-engine ... --format json > reading.json && %s reading.json\n",
    prog, prog, prog);
}

/* Same convention as engine/src/main.c's own default_i18n_path() -
 * duplicated rather than shared, since the two binaries are never
 * linked together (see CLAUDE.md's "Interpretation" section). */
static void default_i18n_path(const char *argv0, const char *lang, char *out, size_t out_size) {
  const char *slash = strrchr(argv0, '/');
  if (slash) {
    int dir_len = (int)(slash - argv0);
    snprintf(out, out_size, "%.*s/i18n/%s.lang", dir_len, argv0, lang);
  } else {
    snprintf(out, out_size, "i18n/%s.lang", lang);
  }
}

static char *read_all(FILE *f, size_t *out_length) {
  size_t cap = 4096;
  size_t len = 0;
  char *buf = malloc(cap);

  size_t n;
  while ((n = fread(buf + len, 1, cap - len, f)) > 0) {
    len += n;
    if (len == cap) {
      cap *= 2;
      buf = realloc(buf, cap);
    }
  }
  buf = realloc(buf, len + 1);
  buf[len] = '\0';
  *out_length = len;
  return buf;
}

static const char *ui(const char *key, const char *fallback) { return i18n_get(key, fallback); }

/* Slugs for i18n key building (body.<slug>/sign.<slug>/aspect.<slug>,
 * mirroring astro.c's own tables) and reused as-is for --format json's
 * language-independent fields - deliberately a small local table rather
 * than linking astro.c (would pull in the vendored Astronomy Engine just
 * for cosmetic text/slugs). Same policy as reading_io.c's own tables. */
static const char *const k_body_slug[NUM_BODIES] = {
  "sun", "moon", "mercury", "venus", "mars",
  "jupiter", "saturn", "uranus", "neptune", "pluto",
};
static const char *const k_sign_slug[12] = {
  "aries", "taurus", "gemini", "cancer", "leo", "virgo",
  "libra", "scorpio", "sagittarius", "capricorn", "aquarius", "pisces",
};
static const char *const k_aspect_slug[5] = {
  "conjunction", "sextile", "square", "trine", "opposition",
};
static const char *const k_day_level_slug[DAY_SIGNIFICANCE_COUNT] = {
  "quiet", "notable", "significant", "major",
};

static const char *body_display_name(Body body) {
  static const char *const fallback[NUM_BODIES] = {
    "Sun", "Moon", "Mercury", "Venus", "Mars",
    "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto",
  };
  char key[32];
  snprintf(key, sizeof key, "body.%s", k_body_slug[body]);
  return ui(key, fallback[body]);
}

static const char *aspect_display_name(AspectType type) {
  static const char *const fallback[5] = {
    "Conjunction", "Sextile", "Square", "Trine", "Opposition",
  };
  char key[32];
  snprintf(key, sizeof key, "aspect.%s", k_aspect_slug[type]);
  return ui(key, fallback[type]);
}

static const char *sign_display_name(ZodiacSign sign) {
  static const char *const fallback[12] = {
    "Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
    "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
  };
  char key[32];
  snprintf(key, sizeof key, "sign.%s", k_sign_slug[sign]);
  return ui(key, fallback[sign]);
}

static const char *day_level_name(DaySignificance level) {
  switch (level) {
    case DAY_QUIET: return ui("interp.day_level.quiet", "Quiet");
    case DAY_NOTABLE: return ui("interp.day_level.notable", "Notable");
    case DAY_SIGNIFICANT: return ui("interp.day_level.significant", "Significant");
    case DAY_MAJOR: return ui("interp.day_level.major", "Major");
    default: return "Unknown";
  }
}

static const char *reversed_marker(bool reversed) {
  static char buf[32];
  if (!reversed) return "";
  snprintf(buf, sizeof buf, " %s", ui("ui.reversed", "(Reversed)"));
  return buf;
}

/* pos->house is 0 when reading_load_json had no sign/house data for this
 * body (see parse_bodies() in reading_io.c) - a valid whole-sign house
 * is always 1-12, so this is a safe "unknown, say nothing" sentinel. */
static void print_body_in_sign_text(FILE *out, const PlanetPosition *pos) {
  if (pos->house < 1 || pos->house > 12) return;
  char buf[64];
  snprintf(buf, sizeof buf, ui("interp.in_sign_house", "in %s (house %d)"),
           sign_display_name(pos->sign), pos->house);
  fprintf(out, " %s", buf);
}

/* Captures narrative_print()/guidance_print()'s output (they only know
 * how to write to a FILE*) into a heap string, for embedding into JSON -
 * avoids changing either module's public API just for this one caller.
 * Strips a single trailing newline; caller must free() the result. */
static char *capture_narrative(const Aspect *aspect, int house) {
  char *buf = NULL;
  size_t size = 0;
  FILE *mem = open_memstream(&buf, &size);
  narrative_print(mem, "", aspect, house);
  fclose(mem);
  if (size > 0 && buf[size - 1] == '\n') buf[size - 1] = '\0';
  return buf;
}

static char *capture_guidance(const DailyInterpretation *interp, const CelticCrossSpread *spread) {
  char *buf = NULL;
  size_t size = 0;
  FILE *mem = open_memstream(&buf, &size);
  guidance_print(mem, "", interp, spread);
  fclose(mem);
  if (size > 0 && buf[size - 1] == '\n') buf[size - 1] = '\0';
  return buf;
}

static void print_top_transits_header(FILE *out, int count) {
  if (count == 1) {
    fprintf(out, "%s\n", ui("interp.top_transits.one", "Top significant transit:"));
  } else {
    char buf[64];
    snprintf(buf, sizeof buf, ui("interp.top_transits.many", "Top %d significant transits:"), count);
    fprintf(out, "%s\n", buf);
  }
}

/* ===== --format text ===== */

static void print_celtic_cross_text(FILE *out, const CelticCrossSpread *spread) {
  fprintf(out, "%s:\n", ui("ui.celtic_cross", "Celtic Cross"));
  for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
    const TarotDraw *draw = &spread->positions[i];
    fprintf(out, "  %d. %s: %s%s\n", i + 1, tarot_position_name((CelticCrossPosition)i),
            tarot_card_name(draw->card), reversed_marker(draw->reversed));
    fprintf(out, "     %s\n", tarot_position_description((CelticCrossPosition)i));
    fprintf(out, "     %s\n", tarot_card_meaning(draw->card, draw->reversed));
  }
}

static void interp_print_text(FILE *out, const DailyReading *reading, const DailyInterpretation *interp) {
  fprintf(out, "%s %s (%d/%d)\n", ui("interp.day_significance", "Day significance:"),
          day_level_name(interp->day_level), interp->day_level + 1, DAY_SIGNIFICANCE_COUNT);
  if (interpretation_deserves_framing(interp)) {
    fprintf(out, "%s\n", ui("interp.major_framing",
                             "(a major transit today - worth a deeper Celtic Cross look)"));
  }
  fprintf(out, "\n");

  if (interp->top_item_count == 0) {
    fprintf(out, "%s\n", ui("interp.no_notable_transits", "No notable transits today."));
  } else {
    print_top_transits_header(out, interp->top_item_count);
    for (int i = 0; i < interp->top_item_count; i++) {
      const SignificantItem *item = &interp->top_items[i];
      const Aspect *a = &item->aspect;

      fprintf(out, "  %d. %s %s", i + 1, ui("ui.transiting", "Transiting"),
              body_display_name(a->transiting_planet));
      print_body_in_sign_text(out, &reading->transits.bodies[a->transiting_planet]);
      fprintf(out, " %s %s %s", aspect_display_name(a->type), ui("ui.natal", "natal"),
              body_display_name(a->natal_planet));
      print_body_in_sign_text(out, &reading->natal.bodies[a->natal_planet]);
      fprintf(out, " (%s %.1f\xc2\xb0, %s %.2f)\n", ui("ui.orb", "orb"), a->orb,
              ui("interp.score", "score"), item->score);
      narrative_print(out, "     ", a, reading->transits.bodies[a->transiting_planet].house);
    }
  }
  fprintf(out, "\n");

  print_celtic_cross_text(out, &reading->spread);
  fprintf(out, "\n");

  guidance_print(out, "", interp, &reading->spread);
}

/* ===== --format html ===== */

static void interp_print_html(FILE *out, const DailyReading *reading, const DailyInterpretation *interp) {
  fprintf(out,
    "<!doctype html>\n<html><head><meta charset=\"utf-8\">\n"
    "<title>%s</title>\n"
    "<style>\n"
    "body { font-family: sans-serif; max-width: 900px; margin: 2em auto; }\n"
    "table { border-collapse: collapse; margin-bottom: 1.5em; }\n"
    "td { padding: 2px 10px 2px 0; }\n"
    "h1, h2 { border-bottom: 1px solid #ccc; }\n"
    ".spread { display: flex; flex-wrap: wrap; gap: 1.5em; }\n"
    ".card { width: 160px; }\n"
    ".card img { width: 140px; display: block; }\n"
    ".card img.reversed { transform: rotate(180deg); }\n"
    ".card .position { font-weight: bold; }\n"
    ".card .name { font-style: italic; }\n"
    ".guidance { background: #f6f6f6; padding: 1em; border-radius: 6px; }\n"
    "</style></head><body>\n",
    ui("interp.page_title", "Deck in a Dash - Interpretation"));

  fprintf(out, "<h1>%s</h1>\n", ui("interp.heading", "Daily Interpretation"));

  fprintf(out, "<h2>%s</h2>\n<p>%s %s (%d/%d)",
          ui("interp.heading_day_significance", "Day Significance"),
          ui("interp.day_significance", "Day significance:"), day_level_name(interp->day_level),
          interp->day_level + 1, DAY_SIGNIFICANCE_COUNT);
  if (interpretation_deserves_framing(interp)) {
    fprintf(out, "<br>%s", ui("interp.major_framing",
                               "(a major transit today - worth a deeper Celtic Cross look)"));
  }
  fprintf(out, "</p>\n");

  fprintf(out, "<h2>%s</h2>\n", ui("interp.heading_transits", "Significant Transits"));
  if (interp->top_item_count == 0) {
    fprintf(out, "<p>%s</p>\n", ui("interp.no_notable_transits", "No notable transits today."));
  } else {
    fprintf(out, "<table>\n");
    for (int i = 0; i < interp->top_item_count; i++) {
      const SignificantItem *item = &interp->top_items[i];
      const Aspect *a = &item->aspect;
      char *text = capture_narrative(a, reading->transits.bodies[a->transiting_planet].house);

      fprintf(out, "<tr><td>%d.</td><td>%s %s %s %s %s</td>"
                   "<td>%s %.1f&deg;, %s %.2f</td></tr>\n",
              i + 1, ui("ui.transiting", "Transiting"), body_display_name(a->transiting_planet),
              aspect_display_name(a->type), ui("ui.natal", "natal"),
              body_display_name(a->natal_planet), ui("ui.orb", "orb"), a->orb,
              ui("interp.score", "score"), item->score);
      if (text[0] != '\0') fprintf(out, "<tr><td></td><td colspan=\"2\">%s</td></tr>\n", text);
      free(text);
    }
    fprintf(out, "</table>\n");
  }

  fprintf(out, "<h2>%s</h2>\n<div class=\"spread\">\n", ui("ui.celtic_cross", "Celtic Cross"));
  for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
    const TarotDraw *draw = &reading->spread.positions[i];
    fprintf(out,
      "<div class=\"card\">\n"
      "  <div class=\"position\">%s</div>\n"
      "  <img class=\"%s\" src=\"img/%s\" alt=\"%s\">\n"
      "  <div class=\"name\">%s%s</div>\n"
      "  <div class=\"desc\">%s</div>\n"
      "  <div class=\"meaning\">%s</div>\n"
      "</div>\n",
      tarot_position_name((CelticCrossPosition)i), draw->reversed ? "reversed" : "",
      tarot_card_image_file(draw->card), tarot_card_name(draw->card), tarot_card_name(draw->card),
      reversed_marker(draw->reversed), tarot_position_description((CelticCrossPosition)i),
      tarot_card_meaning(draw->card, draw->reversed));
  }
  fprintf(out, "</div>\n");

  char *guidance = capture_guidance(interp, &reading->spread);
  fprintf(out, "<h2>%s</h2>\n<div class=\"guidance\">\n", ui("interp.heading_guidance", "Guidance"));
  const char *start = guidance;
  for (const char *p = guidance; ; p++) {
    if (*p == '\n' || *p == '\0') {
      if (p > start) fprintf(out, "<p>%.*s</p>\n", (int)(p - start), start);
      if (*p == '\0') break;
      start = p + 1;
    }
  }
  free(guidance);
  fprintf(out, "</div>\n");

  fprintf(out, "</body></html>\n");
}

/* ===== --format json ===== */

static void json_string(FILE *out, const char *s) {
  fputc('"', out);
  for (; *s; s++) {
    unsigned char c = (unsigned char)*s;
    switch (c) {
      case '"': fputs("\\\"", out); break;
      case '\\': fputs("\\\\", out); break;
      case '\n': fputs("\\n", out); break;
      case '\r': fputs("\\r", out); break;
      case '\t': fputs("\\t", out); break;
      default:
        if (c < 0x20) fprintf(out, "\\u%04x", c);
        else fputc((char)c, out);
    }
  }
  fputc('"', out);
}

static void interp_print_json(FILE *out, const DailyReading *reading, const DailyInterpretation *interp) {
  fprintf(out, "{\n");

  fprintf(out, "  \"day_significance\": {\n");
  fprintf(out, "    \"level\": \"%s\",\n", k_day_level_slug[interp->day_level]);
  fprintf(out, "    \"rank\": %d,\n", interp->day_level + 1);
  fprintf(out, "    \"count\": %d,\n", DAY_SIGNIFICANCE_COUNT);
  fprintf(out, "    \"deserves_framing\": %s\n", interpretation_deserves_framing(interp) ? "true" : "false");
  fprintf(out, "  },\n");

  fprintf(out, "  \"significant_transits\": [\n");
  for (int i = 0; i < interp->top_item_count; i++) {
    const SignificantItem *item = &interp->top_items[i];
    const Aspect *a = &item->aspect;
    const PlanetPosition *tp = &reading->transits.bodies[a->transiting_planet];
    const PlanetPosition *np = &reading->natal.bodies[a->natal_planet];
    char *text = capture_narrative(a, tp->house);

    fprintf(out, "    {\n");
    fprintf(out, "      \"transiting_planet\": \"%s\",\n", k_body_slug[a->transiting_planet]);
    fprintf(out, "      \"transiting_sign\": \"%s\",\n", k_sign_slug[tp->sign]);
    fprintf(out, "      \"transiting_house\": %d,\n", tp->house);
    fprintf(out, "      \"natal_planet\": \"%s\",\n", k_body_slug[a->natal_planet]);
    fprintf(out, "      \"natal_sign\": \"%s\",\n", k_sign_slug[np->sign]);
    fprintf(out, "      \"natal_house\": %d,\n", np->house);
    fprintf(out, "      \"aspect\": \"%s\",\n", k_aspect_slug[a->type]);
    fprintf(out, "      \"orb\": %.4f,\n", a->orb);
    fprintf(out, "      \"score\": %.4f,\n", item->score);
    fprintf(out, "      \"narrative\": ");
    json_string(out, text);
    fprintf(out, "\n    }%s\n", i + 1 < interp->top_item_count ? "," : "");
    free(text);
  }
  fprintf(out, "  ],\n");

  fprintf(out, "  \"celtic_cross\": [\n");
  for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
    const TarotDraw *draw = &reading->spread.positions[i];
    fprintf(out, "    {\"position\": \"%s\", \"card\": \"%s\", \"reversed\": %s, \"card_name\": ",
            tarot_position_slug((CelticCrossPosition)i), tarot_card_slug(draw->card),
            draw->reversed ? "true" : "false");
    json_string(out, tarot_card_name(draw->card));
    fprintf(out, ", \"meaning\": ");
    json_string(out, tarot_card_meaning(draw->card, draw->reversed));
    fprintf(out, "}%s\n", i + 1 < TAROT_SPREAD_SIZE ? "," : "");
  }
  fprintf(out, "  ],\n");

  char *guidance = capture_guidance(interp, &reading->spread);
  fprintf(out, "  \"guidance\": ");
  json_string(out, guidance);
  fprintf(out, "\n");
  free(guidance);

  fprintf(out, "}\n");
}

int main(int argc, char **argv) {
  const char *format_str = "text";
  const char *lang = "en", *i18n_dir_arg = NULL;
  const char *input_path = NULL;

  for (int i = 1; i < argc; i++) {
    const char *arg = argv[i];
    if (strcmp(arg, "--help") == 0 || strcmp(arg, "-h") == 0) {
      print_usage(argv[0]);
      return 0;
    } else if (strcmp(arg, "--format") == 0) {
      if (++i >= argc) { print_usage(argv[0]); return 1; }
      format_str = argv[i];
    } else if (strcmp(arg, "--lang") == 0) {
      if (++i >= argc) { print_usage(argv[0]); return 1; }
      lang = argv[i];
    } else if (strcmp(arg, "--i18n-dir") == 0) {
      if (++i >= argc) { print_usage(argv[0]); return 1; }
      i18n_dir_arg = argv[i];
    } else if (arg[0] == '-' && strcmp(arg, "-") != 0) {
      fprintf(stderr, "Unknown argument: %s\n", arg);
      print_usage(argv[0]);
      return 1;
    } else if (input_path == NULL) {
      input_path = arg;
    } else {
      print_usage(argv[0]);
      return 1;
    }
  }

  OutputFormat format;
  if (strcmp(format_str, "text") == 0) format = FORMAT_TEXT;
  else if (strcmp(format_str, "html") == 0) format = FORMAT_HTML;
  else if (strcmp(format_str, "json") == 0) format = FORMAT_JSON;
  else {
    fprintf(stderr, "Invalid --format, expected text, html, or json\n");
    return 1;
  }

  char i18n_path[512];
  if (i18n_dir_arg) {
    snprintf(i18n_path, sizeof i18n_path, "%s/%s.lang", i18n_dir_arg, lang);
  } else {
    default_i18n_path(argv[0], lang, i18n_path, sizeof i18n_path);
  }
  if (!i18n_load(i18n_path) && strcmp(lang, "en") != 0) {
    fprintf(stderr, "warning: could not load translations for '%s' from %s - "
                     "falling back to built-in English\n", lang, i18n_path);
  }

  FILE *in = stdin;
  bool opened_file = false;
  if (input_path && strcmp(input_path, "-") != 0) {
    in = fopen(input_path, "r");
    if (!in) {
      fprintf(stderr, "error: could not open %s\n", input_path);
      return 1;
    }
    opened_file = true;
  }

  size_t length;
  char *text = read_all(in, &length);
  if (opened_file) fclose(in);

  DailyReading reading;
  bool loaded = reading_load_json(text, length, &reading);
  free(text);
  if (!loaded) {
    fprintf(stderr, "error: could not parse input as deck-engine --format json output\n");
    return 1;
  }

  DailyInterpretation interp;
  interpret_daily_reading(&reading, &interp);

  if (format == FORMAT_HTML) {
    interp_print_html(stdout, &reading, &interp);
  } else if (format == FORMAT_JSON) {
    interp_print_json(stdout, &reading, &interp);
  } else {
    interp_print_text(stdout, &reading, &interp);
  }
  return 0;
}
