"""
gcode_meta.py - GCode file metadata extraction helpers (estimated print time,
layer heights, embedded thumbnail, per-slot filament info).

Extracted from kobrax_moonraker_bridge.py; re-exported from there so existing
call sites keep working. Used by the file-upload and print-start paths.

────────────────────────────────────────────────────────────────────────────
Copyright (C) 2026 viewit (KX-Bridge contributors)

Licensed under GPLv3 — see LICENSE in the project root. See NOTICE.md.
"""

import re
import base64
import logging

log = logging.getLogger("bridge")


def _parse_gcode_estimated_time(data: bytes) -> int:
    """Reads the estimated print time from GCode (OrcaSlicer + PrusaSlicer).
    Returns seconds, 0 when not found.
    PrusaSlicer writes the time into the header (first 16KB),
    OrcaSlicer writes it at the end of the file (last 16KB)."""
    import re
    # Search the beginning + end of the file (OrcaSlicer writes the time at the end)
    search_text = (data[:16384] + data[-65536:]).decode("utf-8", errors="ignore")
    # OrcaSlicer:  ; total estimated time: 9m 20s
    # PrusaSlicer: ; estimated printing time (normal mode) = 1h 9m 20s
    m = (re.search(r";\s*total estimated time:\s*(.*)", search_text) or
         re.search(r";\s*estimated printing time \(normal mode\)\s*=\s*(.*)", search_text))
    if not m:
        return 0
    parts = re.findall(r"(\d+)\s*([hms])", m.group(1))
    secs = 0
    for val, unit in parts:
        if unit == "h":   secs += int(val) * 3600
        elif unit == "m": secs += int(val) * 60
        elif unit == "s": secs += int(val)
    if secs:
        log.info(f"Slicer estimate: {secs}s ({m.group(1).strip()})")
    return secs


def _parse_gcode_layer_heights(data: bytes) -> tuple[float, float]:
    """Reads (layer_height, initial_layer_height) from the OrcaSlicer/PrusaSlicer
    GCode header. Both are stored as a config block at the end of the GCode.

    Beispiel-Zeilen:
      ; layer_height = 0.2
      ; initial_layer_print_height = 0.2

    Returns (0.0, 0.0) when not found - the caller decides what to do
    (typisch: keinen Z-Wert anzeigen)."""
    import re
    head = data[:16384].decode("utf-8", errors="ignore")
    tail = data[-65536:].decode("utf-8", errors="ignore")
    search = head + "\n" + tail
    def _grab(pat):
        m = re.search(pat, search)
        if not m:
            return 0.0
        try:
            return float(m.group(1))
        except Exception:
            return 0.0
    layer_h   = _grab(r";\s*layer_height\s*=\s*([0-9.]+)")
    first_h   = (_grab(r";\s*initial_layer_print_height\s*=\s*([0-9.]+)") or
                 _grab(r";\s*first_layer_height\s*=\s*([0-9.]+)") or
                 layer_h)
    return layer_h, first_h


def _extract_thumbnail(data: bytes) -> str:
    """Extracts the base64 PNG thumbnail from GCode (OrcaSlicer format)."""
    try:
        marker = b"; thumbnail begin"
        end_marker = b"; thumbnail end"
        start = data.find(marker)
        if start == -1:
            return ""
        start = data.find(b"\n", start) + 1
        end = data.find(end_marker, start)
        if end == -1:
            return ""
        lines = data[start:end].split(b"\n")
        b64 = b"".join(
            line[2:].strip() if line.startswith(b"; ") else line.strip()
            for line in lines
        )
        return b64.decode("ascii")
    except Exception:
        return ""


def _extract_filament_info(data: bytes) -> list[dict]:
  """Reads filament colors/materials incl. tool order from Orca/Prusa GCode.

  Gibt Liste von {slot_index, color_hex, material} in Tool-/Paint-Reihenfolge
  (T0, T1, ...).
  Searches both the start and the end of the file since Orca can insert
  large thumbnail blocks, pushing the metadata into the tail.
  """
  try:
    head = data[:131072]
    tail = data[-131072:] if len(data) > 131072 else b""
    header = (head + b"\n" + tail).decode("utf-8", errors="ignore")
    colors, materials = [], []
    paint_count_hint = 0
    tool_filament_order = []
    for line in header.splitlines():
      if re.match(r"^\s*;\s*filament_colour\s*=", line):
        val = line.split("=", 1)[-1].strip()
        colors = [c.strip().lstrip("#") for c in val.split(";") if c.strip()]
      elif re.match(r"^\s*;\s*filament_multi_colour\s*=", line) and not colors:
        val = line.split("=", 1)[-1].strip()
        colors = [c.strip().lstrip("#") for c in val.split(";") if c.strip()]
      elif re.match(r"^\s*;\s*filament_type\s*=", line):
        val = line.split("=", 1)[-1].strip()
        parts = [m.strip() for m in re.split(r"[;,]", val) if m.strip()]
        materials = parts
        paint_count_hint = max(paint_count_hint, len(parts))
      elif re.match(r"^\s*;\s*filament_density\s*:", line):
        val = line.split(":", 1)[-1].strip()
        parts = [x.strip() for x in re.split(r"[;,]", val) if x.strip()]
        paint_count_hint = max(paint_count_hint, len(parts))
      elif re.match(r"^\s*;\s*filament_diameter\s*:", line):
        val = line.split(":", 1)[-1].strip()
        parts = [x.strip() for x in re.split(r"[;,]", val) if x.strip()]
        paint_count_hint = max(paint_count_hint, len(parts))
      elif re.match(r"^\s*;\s*filament\s*:", line):
        raw = line.split(":", 1)[-1]
        parsed = []
        for p in [x.strip() for x in raw.split(",") if x.strip()]:
          try:
            parsed.append(int(p))
          except Exception:
            pass
        if parsed:
          tool_filament_order = parsed
    total_paints = max(len(colors), len(materials), paint_count_hint)
    if tool_filament_order:
      total_paints = max(total_paints, max(tool_filament_order))
    if total_paints <= 0:
      return []

    # Keep full paint list visible; mark paints referenced by Orca tool order as used.
    if len(colors) < total_paints:
      colors.extend(["FFFFFF"] * (total_paints - len(colors)))
    if len(materials) < total_paints:
      materials.extend(["PLA"] * (total_paints - len(materials)))
    # Prefer actual tool-change commands from the GCode body.
    # This avoids forwarding paints that are present in metadata but never used.
    used_paints_zero_based = set()
    try:
      for m in re.finditer(br"(?m)^[ \t]*T([0-9]+)\b", data):
        used_paints_zero_based.add(int(m.group(1)))
    except Exception:
      used_paints_zero_based = set()

    # Fallback for slicers that only provide paint usage in header metadata.
    used_paints_from_header = set()
    for n in tool_filament_order:
      try:
        # Orca/Prusa filament: list is typically 1-based.
        used_paints_from_header.add(max(0, int(n) - 1))
      except Exception:
        pass

    result = []
    for i in range(total_paints):
      hex_color = colors[i] if i < len(colors) else "FFFFFF"
      result.append({
        "slot_index": i,
        "color_hex":  "#" + hex_color.upper() if hex_color else "#FFFFFF",
        "material":   materials[i] if i < len(materials) else "PLA",
        "is_used":    (i in used_paints_zero_based) if used_paints_zero_based else ((i in used_paints_from_header) if used_paints_from_header else True),
      })
    return result
  except Exception:
    return []
