#!/usr/bin/env python3
"""ESP32 example build matrix + flash/RAM footprint aggregation for CI.

Three subcommands, used by .github/workflows/esp32-build.yml:

  example_footprints.py matrix
      Print a one-line JSON array of {name, ino, flags, feature} for every
      example .ino, to drive the build matrix. `flags` is the first
      `build_flags=...` documented in the example's README (empty if none);
      `feature` is the example's DETWS_ENABLE_* signature (e.g. "TLS+MTLS"), or
      "core/<name>" when it enables nothing.

  example_footprints.py fragment <build.log> <out.json>
      Parse the `Flash:`/`RAM:` "used N bytes" lines from a `pio ci` build log and
      write one footprint fragment {feature, example, flags, board, flash_bytes,
      ram_bytes}, taking feature/example/flags from the EX_* env vars.

  example_footprints.py merge <frag_dir> <out.json>
      Merge every fragment under <frag_dir> (recursively) into <out.json>, keyed
      by `feature`, overwriting an existing entry and keeping entries this run did
      not touch. Output is sorted and stable for a clean diff.

  example_footprints.py table <in.json> <out.md>
      Render the per-feature flash/RAM table (docs/FOOTPRINTS.md) from <in.json>.
"""

import glob
import json
import os
import re
import sys

EX_ROOT = "examples"

# esp32dev capacities (bytes) for the percentage columns; from the pio size report.
FLASH_CAP = 1310720
RAM_CAP = 327680


def find_flags(readme):
    if not os.path.exists(readme):
        return ""
    txt = open(readme, encoding="utf-8", errors="replace").read()
    m = re.search(r'build_flags=([^"]*)"', txt)  # first documented pio ci command
    return m.group(1).strip() if m else ""


def feature_key(flags, name):
    enables = re.findall(r"-D(DETWS_[A-Z0-9_]+)=", flags)
    short = []
    for e in enables:
        s = e.replace("DETWS_ENABLE_", "").replace("DETWS_", "")
        if s and s not in short:
            short.append(s)
    return "+".join(short) if short else f"core/{name}"


def cmd_matrix():
    items = []
    for ino in sorted(glob.glob(f"{EX_ROOT}/**/*.ino", recursive=True)):
        d = os.path.dirname(ino)
        name = os.path.relpath(d, EX_ROOT).replace(os.sep, "/")
        flags = find_flags(os.path.join(d, "README.md"))
        items.append(
            {
                "name": name,
                "ino": ino.replace(os.sep, "/"),
                "flags": flags,
                "feature": feature_key(flags, os.path.basename(d)),
            }
        )
    print(json.dumps(items))


def cmd_fragment(logpath, outpath):
    log = open(logpath, encoding="utf-8", errors="replace").read()

    def used(label):
        m = re.search(label + r":.*?used\s+(\d+)\s+bytes", log)
        return int(m.group(1)) if m else 0

    frag = {
        "feature": os.environ.get("EX_FEATURE", ""),
        "example": os.environ.get("EX_NAME", ""),
        "flags": os.environ.get("EX_FLAGS", ""),
        "board": "esp32dev",
        "flash_bytes": used("Flash"),
        "ram_bytes": used("RAM"),
    }
    json.dump(frag, open(outpath, "w", encoding="utf-8"), indent=2)
    print(frag)


def cmd_merge(frag_dir, out_path):
    data = {}
    if os.path.exists(out_path):
        try:
            data = json.load(open(out_path, encoding="utf-8"))
        except json.JSONDecodeError:
            data = {}
    n = 0
    for frag in sorted(glob.glob(os.path.join(frag_dir, "**", "*.json"), recursive=True)):
        try:
            e = json.load(open(frag, encoding="utf-8"))
        except json.JSONDecodeError:
            continue
        feat = e.get("feature")
        if not feat:
            continue
        data[feat] = {k: e[k] for k in ("example", "flags", "board", "flash_bytes", "ram_bytes") if k in e}
        n += 1
    with open(out_path, "w", encoding="utf-8") as f:
        json.dump(dict(sorted(data.items())), f, indent=2)
        f.write("\n")
    print(f"merged {n} fragments -> {out_path}: {len(data)} feature entries")


def cmd_table(in_path, out_path):
    data = json.load(open(in_path, encoding="utf-8")) if os.path.exists(in_path) else {}

    def fmt(b, cap):
        return f"{b / 1024:.1f} KB ({b / cap * 100:.1f}%)" if b else "-"

    lines = [
        "# ESP32 build footprints",
        "",
        "Per-feature flash and RAM on `esp32dev`, captured by the **ESP32 Build** CI",
        "from each example's `pio ci` size report and aggregated into",
        "[`footprints.json`](footprints.json).",
        "",
        "> Autogenerated by `tools/ci/example_footprints.py table` - do not edit by hand.",
        "",
        "| Feature | Example | Flash | RAM |",
        "| --- | --- | --- | --- |",
    ]
    for feat in sorted(data):
        e = data[feat]
        lines.append(
            f"| `{feat}` | {e.get('example', '')} | "
            f"{fmt(e.get('flash_bytes', 0), FLASH_CAP)} | {fmt(e.get('ram_bytes', 0), RAM_CAP)} |"
        )
    open(out_path, "w", encoding="utf-8").write("\n".join(lines) + "\n")
    print(f"wrote {out_path}: {len(data)} rows")


def main():
    if len(sys.argv) >= 2 and sys.argv[1] == "matrix":
        cmd_matrix()
    elif len(sys.argv) >= 4 and sys.argv[1] == "fragment":
        cmd_fragment(sys.argv[2], sys.argv[3])
    elif len(sys.argv) >= 4 and sys.argv[1] == "merge":
        cmd_merge(sys.argv[2], sys.argv[3])
    elif len(sys.argv) >= 4 and sys.argv[1] == "table":
        cmd_table(sys.argv[2], sys.argv[3])
    else:
        sys.exit(
            "usage: example_footprints.py matrix | fragment <log> <out> | merge <frag_dir> <out> | table <in> <out>"
        )


if __name__ == "__main__":
    main()
