package puzzle;

import module java.base;
import anno.DictGen;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.val;
import puzzle.SwedishGenerator.Rng;

import static puzzle.Export.*;
import static puzzle.SwedishGenerator.*;
@DictGen(
        packageName = "puzzle.dict800",
        className = "DictData800",
        scv = "/home/mike/dev/puzzle-generator/nl_score_hints_v4.csv",
        simpleMax = 800,
        minLen = 2,
        maxLen = 8
)
public class Main {
   
   final static String         OUT_DIR          = envOrDefault("OUT_DIR", "/data/puzzle");
   final static Path           PUZZLE_DIR       = Paths.get(OUT_DIR, "puzzles");
   static final Path           INDEX_FILE       = PUZZLE_DIR.resolve("index.json");
   static final OffsetDateTime now              = OffsetDateTime.now(ZoneOffset.UTC);
   static final String         CREATED_AT       = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'"));
   static final String         FILE_ID          = CREATED_AT.replace(":", "-") + "_" + System.currentTimeMillis() / 1000;
   static final String         FILE_NAME        = FILE_ID + ".json";
   static final Path           OUTPUT_PATH      = PUZZLE_DIR.resolve(FILE_NAME);
   static final String         DATE_STRING      = now.toLocalDate().toString();
   static final boolean        VERBOSE          = false;
   static final AtomicLong     TOTAL_NODES      = new AtomicLong(0);
   static final AtomicLong     TOTAL_BACKTRACKS = new AtomicLong(0);
   static final AtomicLong     TOTAL_ATTEMPTS   = new AtomicLong(0);
   static final AtomicLong     TOTAL_SUCCESS    = new AtomicLong(0);
   
   @Data
   @AllArgsConstructor
   @NoArgsConstructor
   public static class Opts {
      
      static int     SSIZE         = 23;
      public int     seed          = (int) (System.nanoTime() ^ System.currentTimeMillis());
      public int     clueSize      = SSIZE;
      public int     pop           = SSIZE * 2;
      public int     offspring     = SSIZE * 3;
      public int     gens          = 600;
      public double  minSimplicity = 0; // 0 means no limit
      public int     threads       = Math.max(1, Runtime.getRuntime().availableProcessors());
      public int     tries         = threads;
      public boolean reindex       = false;
      public boolean verbose       = true;
      
   }
   
   void main(String[] args) {
      _main(args);
   }
   public void _main(String[] args) {
      var opts = parseArgs(args);
      
      if (opts.reindex) {
         
         section("Reindex");
         info("OutputDir  : " + OUT_DIR);
         rebuildIndex();
         return;
      }
      
      section("Puzzle Generator");
      info("OutputDir  : " + OUT_DIR);
      
      section("Settings");
      printSettings(opts);
      
      var res = generatePuzzle(opts);
      if (res == null) {
         err("Search status : UNSOLVED");
         err("Reason        : No solution found within tries.");
         System.exit(1);
         return;
      }
      
      section("Mask");
      System.out.print(indentLines(res.clues().gridToString(), "  "));
      
      section("Grid (raw)");
      System.out.print(indentLines(res.grid().gridToString(), "  "));
      
      section("Grid (human)");
      System.out.print(indentLines(res.grid().renderHuman(), "  "));
      
      var exported = res.exportFormatFromFilled(new Rewards(50, 2, 1));
      
      section("Clues");
      info("status       : generating...");
      info("generatedFor : " + exported.words().length);
      info("status       : done");
      info("simpel       : " + exported.difficulty());
      
      section("Words");
      printWordsTable(exported.words());
      
      section("Grid");
      for (var row : exported.grid()) System.out.println("  " + row);
      
      var theme = "algemeen";
      section("Export");
      info("file         : " + OUTPUT_PATH);
      
      try {
         Files.createDirectories(PUZZLE_DIR);
         var json = toJson(exported, DATE_STRING, theme);
         Files.writeString(OUTPUT_PATH, json, StandardCharsets.UTF_8);
         
         // Update index.json
         var pathInIndex = "/puzzles/" + FILE_NAME;
         var indexRecord = toIndexRecordJson(FILE_ID, pathInIndex, DATE_STRING, theme, exported.difficulty(), CREATED_AT);
         if (1 != 1) updateIndex(PUZZLE_DIR.toString(), indexRecord);
         else rebuildIndex();
         info("indexUpdated : " + INDEX_FILE);
      } catch (IOException e) {
         err("Failed to write: " + FILE_NAME);
         err("Reason       : " + e.getMessage());
         System.exit(2);
      }
   }
   
   // ---------------- Output helpers ----------------
   
   private static void info(String msg) { System.out.println("[INFO ] " + msg); }
   private static void warn(String msg) { System.out.println("[WARN ] " + msg); }
   private static void err(String msg)  { System.err.println("[ERROR] " + msg); }
   
   private static void section(String title) {
      System.out.println();
      System.out.println(title);
   }
   
   private static String envOrDefault(String key, String def) {
      var v = System.getenv(key);
      return v == null || v.isBlank() ? def : v;
   }
   
   private static void printSettings(Opts o) {
      System.out.printf(Locale.ROOT, "  %-14s: %d%n", "seed", o.seed);
      System.out.printf(Locale.ROOT, "  %-14s: %d%n", "clues", o.clueSize);
      System.out.printf(Locale.ROOT, "  %-14s: %d%n", "population", o.pop);
      System.out.printf(Locale.ROOT, "  %-14s: %d%n", "offspring", o.offspring);
      System.out.printf(Locale.ROOT, "  %-14s: %d%n", "generations", o.gens);
      System.out.printf(Locale.ROOT, "  %-14s: %.2f%n", "minSimplicity", o.minSimplicity);
      System.out.printf(Locale.ROOT, "  %-14s: %d%n", "threads", o.threads);
   }
   
   private static String fmtPoint(int r, int c) { return String.format(Locale.ROOT, "(%d,%d)", r, c); }
   
   private static void printWordsTable(WordOut[] words) {
      System.out.println("  #  WORD         CX DIR  START     ARROW     CLUE");
      for (int j = 0; j < words.length; j++) {
         var w = words[j];
         System.out.printf(
                 Locale.ROOT,
                 "  %-2d %-12s %-4s %-3s %-9s %-9s %s%n",
                 j,
                 safe(w.word(), 12),
                 safe("" + w.complex(), 4),
                 safe(String.valueOf(w.direction()), 3),
                 fmtPoint(w.startRow(), w.startCol()),
                 fmtPoint(w.arrowRow(), w.arrowCol()),
                 Arrays.toString(w.clue()));
      }
   }
   
   private static String safe(String s, int max) {
      if (s == null) return "";
      if (s.length() <= max) return s;
      return s.substring(0, Math.max(0, max - 1)) + "…";
   }
   
   static String indentLines(String s, String indent) {
      if (s == null || s.isEmpty()) return "";
      var lines = s.split("\\R", -1);
      var sb    = new StringBuilder();
      for (var line : lines) sb.append(indent).append(line).append('\n');
      return sb.toString();
   }
   
   static void usage() {
      System.out.printf("""
                                Usage:
                                  java puzzle.Main [--seed N] [--clues N] [--pop N] [--offspring N] [--gens N] [--tries N] [--words FILE] [--min-simplicity N.N] [--threads N] [--reindex]
                                
                                Defaults:
                                  --clues 18
                                  --pop 40
                                  --offspring 60
                                  --gens 500
                                  --words nl_score_hints.csv
                                  --min-simplicity 0 (no limit)
                                  --threads %d
                                %n""", Math.max(1, Runtime.getRuntime().availableProcessors()));
   }
   
   static Opts parseArgs(String[] argv) {
      var out = new Opts();
      for (var i = 0; i < argv.length; i++) {
         var a = argv[i];
         var v = i + 1 < argv.length ? argv[i + 1] : null;
         
         if (a.equals("--help") || a.equals("-h")) {
            usage();
            System.exit(0);
         }
         
         if (a.equals("--seed")) {
            out.seed = Integer.parseInt(v);
            i++;
         } else if (a.equals("--clues")) {
            out.clueSize = Integer.parseInt(v);
            i++;
         } else if (a.equals("--pop")) {
            out.pop = Integer.parseInt(v);
            i++;
         } else if (a.equals("--offspring")) {
            out.offspring = Integer.parseInt(v);
            i++;
         } else if (a.equals("--gens")) {
            out.gens = Integer.parseInt(v);
            i++;
         } else if (a.equals("--tries")) {
            out.tries = Integer.parseInt(v);
            i++;
         } else if (a.equals("--min-simplicity")) {
            out.minSimplicity = Double.parseDouble(v);
            i++;
         } else if (a.equals("--threads")) {
            out.threads = Integer.parseInt(v);
            i++;
         } else if (a.equals("--reindex")) {
            out.reindex = true;
         } else {
            throw new IllegalArgumentException("Unknown arg: " + a);
         }
      }
      return out;
   }
   
   // ---------------- Generation ----------------
   
   // Package-private method for testing
   PuzzleResult generatePuzzle(Opts opts) {
      
      var  tLoad0 = System.nanoTime();
      Dict dict   = puzzle.dict800.DictData800.DICT800;//loadDict(opts.wordsPath);
      var  tLoad1 = System.nanoTime();
      
      section("Load");
      info(String.format(Locale.ROOT, "words        : %,d", dict.length()));
      info(String.format(Locale.ROOT, "loadTime     : %.3f s", (tLoad1 - tLoad0) / 1e9));
      
      section("Search");
      var tSearch0 = System.currentTimeMillis();
      var deadline = System.currentTimeMillis() + 40_000;
      
      PuzzleResult resFinal = null;
      if (opts.threads > 1) {
         info("mode         : multi-threaded (" + opts.threads + ")");
         var executor          = Executors.newFixedThreadPool(opts.threads);
         var completionService = new ExecutorCompletionService<PuzzleResult>(executor);
         int submitted         = 0;
         
         try {
            // Keep at least some tasks in flight
            for (int i = 0; i < opts.threads; i++) {
               final int attemptIdx = ++submitted;
               completionService.submit(() -> attempt(new Rng(opts.seed + attemptIdx), dict, opts));
            }
            
            while (System.currentTimeMillis() < deadline) {
               var future = completionService.poll(deadline - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
               if (future == null) break;
               
               var result = future.get();
               if (result != null) {
                  info("status       : SOLVED");
                  resFinal = result;
                  break;
               }
               
               // Submit another task if we still have time
               if (System.currentTimeMillis() < deadline) {
                  final int attemptIdx = ++submitted;
                  completionService.submit(() -> attempt(new Rng(opts.seed + attemptIdx), dict, opts));
               }
            }
            if (resFinal == null) warn("status       : UNSOLVED (timeout)");
         } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            warn("status       : INTERRUPTED");
         } catch (ExecutionException e) {
            warn("status       : ERROR (" + e.getMessage() + ")");
         } finally {
            executor.shutdownNow();
            try {
               executor.awaitTermination(5, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
               Thread.currentThread().interrupt();
            }
         }
         
      } else {
         info("mode         : single-threaded");
         var rng     = new Rng(opts.seed);
         int attempt = 0;
         
         while (System.currentTimeMillis() < deadline) {
            attempt++;
            info("try          : " + attempt + " (remaining: " + (deadline - System.currentTimeMillis()) / 1000 + "s)");
            
            var result = attempt(rng, dict, opts);
            if (result != null) {
               info("status       : SOLVED");
               info("foundAtTry   : " + attempt);
               resFinal = result;
               break;
            }
         }
         
         if (resFinal == null) info("status       : UNSOLVED (timeout)");
      }
      
      var tSearch1   = System.currentTimeMillis();
      var searchTime = (tSearch1 - tSearch0) / 1000.0;
      
      section("Performance");
      info(String.format(Locale.ROOT, "totalNodes   : %,d", TOTAL_NODES.get()));
      info(String.format(Locale.ROOT, "totalBacks   : %,d", TOTAL_BACKTRACKS.get()));
      info(String.format(Locale.ROOT, "searchTime   : %.2f s", searchTime));
      info(String.format(Locale.ROOT, "nodes/sec    : %,d", (int) (TOTAL_NODES.get() / Math.max(0.001, searchTime))));
      
      section("Material");
      info(String.format(Locale.ROOT, "attempts     : %,d", TOTAL_ATTEMPTS.get()));
      info(String.format(Locale.ROOT, "successRate  : %.1f%%", TOTAL_ATTEMPTS.get() == 0 ? 0 : TOTAL_SUCCESS.get() * 100.0 / TOTAL_ATTEMPTS.get()));
      info(String.format(Locale.ROOT, "dictWords    : %,d", dict.length()));
      
      return resFinal;
   }
   
   static PuzzleResult attempt(Rng rng, Dict dict, Opts opts) {
      try {
         return _attempt(rng, dict, opts);
      } catch (Exception e) {
         System.err.println("Failed to operate" + e.getMessage());
         return null;
      }
   }
   static Clues generateNewClues(Rng rng, Opts opts) {
      var masker = new Masker(rng, new int[Masker.STACK_SIZE], Clues.createEmpty());
      return masker.generateMask(opts.clueSize, opts.pop, opts.gens, opts.offspring);
   }
   static PuzzleResult _attempt(Rng rng, Dict dict, Opts opts) {
      val  multiThreaded = Thread.currentThread().getName().contains("pool");
      long t0            = System.currentTimeMillis();
      TOTAL_ATTEMPTS.incrementAndGet();
      val mask = generateNewClues(rng, opts);
      //val mask = generateClues();
      if (mask == null) return null;
      
      val slotInfo = Masker.slots(mask, dict.index(), dict.reversed());
      var grid     = Slotinfo.grid(slotInfo);// mask.toGrid();
      var filled   = fillMask(rng, slotInfo, grid.lo, grid.hi, grid.g);
      
      if (!multiThreaded) {
         System.out.print("\r" + " ".repeat(120 - "".length()) + "\r");
         System.out.flush();
      }
      // print a final progress line
      if (Main.VERBOSE && !multiThreaded) {
         System.out.printf(Locale.ROOT,
                           "[######################] %d/%d slots | nodes=%d | backtracks=%d | mrv=%d | %.1fs%n",
                           Slotinfo.wordCount(0, slotInfo), slotInfo.length, filled.nodes(), filled.backtracks(), filled.lastMRV(), filled.elapsed() * 0.001
                          );
      }
      
      TOTAL_NODES.addAndGet(filled.nodes());
      TOTAL_BACKTRACKS.addAndGet(filled.backtracks());
      if (filled.ok()) {
         TOTAL_SUCCESS.incrementAndGet();
      }
      
      var name      = Thread.currentThread().getName();
      var status    = filled.ok() ? "SUCCESS" : "FAILED";
      var nps       = (int) (filled.nodes() / Math.max(0.001, filled.elapsed() * 0.001));
      var totalTime = (System.currentTimeMillis() - t0) / 1000.0;
      
      System.out.printf(Locale.ROOT,
                        "[ATTEMPT] thread=%s | status=%s | nodes=%d | backtracks=%d | nps=%d | simplicity=%s | time=%.1fs%n",
                        name, status, filled.nodes(), filled.backtracks(), nps, 1, totalTime
                       );
      if (!filled.ok()) {
         //System.out.println(Arrays.stream(new Clued(mask).gridToString().split("\n")).map(s -> "\"" + s + "\\n\" +").collect(Collectors.joining("\n")));
      }
      if (filled.ok()) {
         grid.lo = ~mask.lo;
         grid.hi = 0xFFL & ~mask.hi;
         return new PuzzleResult(new Signa(mask), new Puzzle(grid, mask), slotInfo, filled);
      }
      
      return null;
   }
   
   // ---------------- Export (unchanged logic) ----------------
   
   record JsonExportedPuzzle(String date, String theme, int difficulty, Rewards rewards, String[] grid, WordOut[] words) { }
   private static String toJson(ExportedPuzzle puzzle, String date, String theme) {
      return Meta.GSON.toJson(new JsonExportedPuzzle(date, theme, puzzle.difficulty(), puzzle.rewards(), puzzle.grid(), puzzle.words()));
   }
   
   private static String escapeJson(String s) {
      return s.replace("\\", "\\\\")
              .replace("\"", "\\\"")
              .replace("\n", "\\n")
              .replace("\r", "\\r")
              .replace("\t", "\\t");
   }
   
   private static String toIndexRecordJson(String id, String path, String date, String theme, int difficulty, String createdAt) {
      return String.format(
              Locale.ROOT,
              "{\"id\":\"%s\",\"path\":\"%s\",\"date\":\"%s\",\"theme\":\"%s\",\"difficulty\":%d,\"createdAt\":\"%s\"}",
              escapeJson(id), escapeJson(path), escapeJson(date), escapeJson(theme), difficulty, escapeJson(createdAt)
                          );
   }
   
   private static void updateIndex(String outDir, String newRecordJson) {
      var indexPath = Paths.get(outDir, "index.json");
      try {
         var content = Files.exists(indexPath) ? Files.readString(indexPath, StandardCharsets.UTF_8).trim() : "";
         
         if (content.isEmpty() || content.equals("[]")) {
            content = "[\n  " + newRecordJson + "\n]";
         } else {
            var firstBracket = content.indexOf('[');
            if (firstBracket != -1) {
               content = content.substring(0, firstBracket + 1) + "\n  " + newRecordJson + "," + content.substring(firstBracket + 1);
            } else {
               content = "[\n  " + newRecordJson + "\n]";
            }
         }
         
         Files.writeString(indexPath, content, StandardCharsets.UTF_8);
         info("indexUpdated : " + indexPath);
      } catch (IOException e) {
         err("Failed to update index.json: " + e.getMessage());
      }
   }
   
   private void rebuildIndex() {
      
      if (!Files.exists(PUZZLE_DIR)) {
         err("Puzzles directory does not exist: " + PUZZLE_DIR);
         return;
      }
      
      info("Rebuilding index from: " + PUZZLE_DIR);
      
      var records = new ArrayList<String>();
      try (var stream = Files.list(PUZZLE_DIR)) {
         stream.filter(p -> p.toString().endsWith(".json") && !p.getFileName().toString().equals("index.json"))
               .sorted(Comparator.comparing(Path::getFileName).reversed())
               .forEach(path -> {
                  try {
                     var filename = path.getFileName().toString();
                     var id       = filename.substring(0, filename.length() - 5);
                     var content  = Files.readString(path, StandardCharsets.UTF_8);
                     
                     var date       = extractValue(content, "date");
                     var theme      = extractValue(content, "theme");
                     var difficulty = 1;
                     try { difficulty = Integer.parseInt(extractValue(content, "difficulty")); } catch (Exception ignored) { }
                     
                     var createdAt = id;
                     if (id.length() >= 20 && id.charAt(10) == 'T') {
                        var parts  = id.split("_");
                        var dtPart = parts[0]; // 2025-12-24T04-25-06Z
                        if (dtPart.length() >= 19) {
                           createdAt = dtPart.substring(0, 13) + ":" + dtPart.substring(14, 16) + ":" + dtPart.substring(17);
                        }
                     }
                     
                     var pathInIndex = "/puzzles/" + filename;
                     records.add(toIndexRecordJson(id, pathInIndex, date, theme, difficulty, createdAt));
                  } catch (IOException e) {
                     err("Failed to read " + path + ": " + e.getMessage());
                  }
               });
      } catch (IOException e) {
         err("Failed to list puzzles: " + e.getMessage());
         return;
      }
      
      var indexPath = PUZZLE_DIR.resolve("index.json");
      var content   = "[\n  " + String.join(",\n  ", records) + "\n]";
      try {
         Files.writeString(indexPath, content, StandardCharsets.UTF_8);
         info("Successfully rebuilt index.json with " + records.size() + " records.");
      } catch (IOException e) {
         err("Failed to write index.json: " + e.getMessage());
      }
   }
   
   private static String extractValue(String json, String key) {
      var pattern = java.util.regex.Pattern.compile("\"" + key + "\":\\s*\"?([^\",\\n\\r}]*)\"?");
      var matcher = pattern.matcher(json);
      if (matcher.find()) return matcher.group(1).trim();
      return "";
   }
}
