#!/usr/bin/env python3
"""
NVS Test Matrix Generator (ADR-061)

Generates NVS partition binaries for 14 test configurations using the
provision.py script's CSV builder and NVS binary generator. Each binary
can be injected into a QEMU flash image at offset 0x9000 for automated
firmware testing under different NVS configurations.

Usage:
    python3 generate_nvs_matrix.py --output-dir build/nvs_matrix

    # Generate only specific configs:
    python3 generate_nvs_matrix.py --output-dir build/nvs_matrix --only default,full-adr060

Requirements:
    - esp_idf_nvs_partition_gen (pip install) or ESP-IDF nvs_partition_gen.py
    - Python 3.8+
"""

import argparse
import csv
import io
import os
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional, Tuple


# NVS partition size must match partitions_display.csv: 0x6000 = 24576 bytes
NVS_PARTITION_SIZE = 0x6000


@dataclass
class NvsEntry:
    """A single NVS key-value entry."""
    key: str
    type: str       # "data" or "namespace"
    encoding: str   # "string", "u8", "u16", "u32", "hex2bin", ""
    value: str


@dataclass
class NvsConfig:
    """A named NVS configuration with a list of entries."""
    name: str
    description: str
    entries: List[NvsEntry] = field(default_factory=list)

    def to_csv(self) -> str:
        """Generate NVS CSV content."""
        buf = io.StringIO()
        writer = csv.writer(buf)
        writer.writerow(["key", "type", "encoding", "value"])
        writer.writerow(["csi_cfg", "namespace", "", ""])
        for entry in self.entries:
            writer.writerow([entry.key, entry.type, entry.encoding, entry.value])
        return buf.getvalue()


def define_configs() -> List[NvsConfig]:
    """Define all 14 NVS test configurations."""
    configs = []

    # 1. default - no NVS entries (firmware uses Kconfig defaults)
    configs.append(NvsConfig(
        name="default",
        description="No NVS entries; firmware uses Kconfig defaults",
        entries=[],
    ))

    # 2. wifi-only - just WiFi credentials
    configs.append(NvsConfig(
        name="wifi-only",
        description="WiFi SSID and password only",
        entries=[
            NvsEntry("ssid", "data", "string", "TestNetwork"),
            NvsEntry("password", "data", "string", "testpass123"),
        ],
    ))

    # 3. full-adr060 - channel override + MAC filter
    configs.append(NvsConfig(
        name="full-adr060",
        description="ADR-060: channel override + MAC filter + full config",
        entries=[
            NvsEntry("ssid", "data", "string", "TestNetwork"),
            NvsEntry("password", "data", "string", "testpass123"),
            NvsEntry("target_ip", "data", "string", "10.0.2.2"),
            NvsEntry("target_port", "data", "u16", "5005"),
            NvsEntry("node_id", "data", "u8", "1"),
            NvsEntry("csi_channel", "data", "u8", "6"),
            NvsEntry("filter_mac", "data", "hex2bin", "aabbccddeeff"),
        ],
    ))

    # 4. edge-tier0 - raw passthrough (no DSP)
    configs.append(NvsConfig(
        name="edge-tier0",
        description="Edge tier 0: raw CSI passthrough, no on-device DSP",
        entries=[
            NvsEntry("ssid", "data", "string", "TestNetwork"),
            NvsEntry("password", "data", "string", "testpass123"),
            NvsEntry("target_ip", "data", "string", "10.0.2.2"),
            NvsEntry("edge_tier", "data", "u8", "0"),
        ],
    ))

    # 5. edge-tier1 - basic presence/motion detection
    configs.append(NvsConfig(
        name="edge-tier1",
        description="Edge tier 1: basic presence and motion detection",
        entries=[
            NvsEntry("ssid", "data", "string", "TestNetwork"),
            NvsEntry("password", "data", "string", "testpass123"),
            NvsEntry("target_ip", "data", "string", "10.0.2.2"),
            NvsEntry("edge_tier", "data", "u8", "1"),
            NvsEntry("pres_thresh", "data", "u16", "50"),
        ],
    ))

    # 6. edge-tier2-custom - full pipeline with custom thresholds
    configs.append(NvsConfig(
        name="edge-tier2-custom",
        description="Edge tier 2: full pipeline with custom thresholds",
        entries=[
            NvsEntry("ssid", "data", "string", "TestNetwork"),
            NvsEntry("password", "data", "string", "testpass123"),
            NvsEntry("target_ip", "data", "string", "10.0.2.2"),
            NvsEntry("edge_tier", "data", "u8", "2"),
            NvsEntry("pres_thresh", "data", "u16", "100"),
            NvsEntry("fall_thresh", "data", "u16", "3000"),
            NvsEntry("vital_win", "data", "u16", "256"),
            NvsEntry("vital_int", "data", "u16", "500"),
            NvsEntry("subk_count", "data", "u8", "16"),
        ],
    ))

    # 7. tdm-3node - TDM mesh with 3 nodes (slot 0)
    configs.append(NvsConfig(
        name="tdm-3node",
        description="TDM mesh: 3-node schedule, this node is slot 0",
        entries=[
            NvsEntry("ssid", "data", "string", "TestNetwork"),
            NvsEntry("password", "data", "string", "testpass123"),
            NvsEntry("target_ip", "data", "string", "10.0.2.2"),
            NvsEntry("node_id", "data", "u8", "0"),
            NvsEntry("tdm_slot", "data", "u8", "0"),
            NvsEntry("tdm_nodes", "data", "u8", "3"),
        ],
    ))

    # 8. wasm-signed - WASM runtime with signature verification
    configs.append(NvsConfig(
        name="wasm-signed",
        description="WASM runtime enabled with Ed25519 signature verification",
        entries=[
            NvsEntry("ssid", "data", "string", "TestNetwork"),
            NvsEntry("password", "data", "string", "testpass123"),
            NvsEntry("target_ip", "data", "string", "10.0.2.2"),
            NvsEntry("edge_tier", "data", "u8", "2"),
            # wasm_verify=1 + a 32-byte dummy Ed25519 pubkey
            NvsEntry("wasm_verify", "data", "u8", "1"),
            NvsEntry("wasm_pubkey", "data", "hex2bin",
                     "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"),
        ],
    ))

    # 9. wasm-unsigned - WASM runtime without signature verification
    configs.append(NvsConfig(
        name="wasm-unsigned",
        description="WASM runtime with signature verification disabled",
        entries=[
            NvsEntry("ssid", "data", "string", "TestNetwork"),
            NvsEntry("password", "data", "string", "testpass123"),
            NvsEntry("target_ip", "data", "string", "10.0.2.2"),
            NvsEntry("edge_tier", "data", "u8", "2"),
            NvsEntry("wasm_verify", "data", "u8", "0"),
            NvsEntry("wasm_max", "data", "u8", "2"),
        ],
    ))

    # 10. 5ghz-channel - 5 GHz channel override
    configs.append(NvsConfig(
        name="5ghz-channel",
        description="ADR-060: 5 GHz channel 36 override",
        entries=[
            NvsEntry("ssid", "data", "string", "TestNetwork5G"),
            NvsEntry("password", "data", "string", "testpass123"),
            NvsEntry("target_ip", "data", "string", "10.0.2.2"),
            NvsEntry("csi_channel", "data", "u8", "36"),
        ],
    ))

    # 11. boundary-max - maximum VALID values for all numeric fields
    # Uses firmware-validated max ranges (not raw u8/u16 max):
    #   vital_win: 32-256, top_k: 1-32, power_duty: 10-100
    configs.append(NvsConfig(
        name="boundary-max",
        description="Boundary test: maximum valid values per firmware validation ranges",
        entries=[
            NvsEntry("ssid", "data", "string", "TestNetwork"),
            NvsEntry("password", "data", "string", "testpass123"),
            NvsEntry("target_ip", "data", "string", "10.0.2.2"),
            NvsEntry("target_port", "data", "u16", "65535"),
            NvsEntry("node_id", "data", "u8", "255"),
            NvsEntry("edge_tier", "data", "u8", "2"),
            NvsEntry("pres_thresh", "data", "u16", "65535"),
            NvsEntry("fall_thresh", "data", "u16", "65535"),
            NvsEntry("vital_win", "data", "u16", "256"),     # max validated
            NvsEntry("vital_int", "data", "u16", "10000"),
            NvsEntry("subk_count", "data", "u8", "32"),
            NvsEntry("power_duty", "data", "u8", "100"),
        ],
    ))

    # 12. boundary-min - minimum VALID values for all numeric fields
    configs.append(NvsConfig(
        name="boundary-min",
        description="Boundary test: minimum valid values per firmware validation ranges",
        entries=[
            NvsEntry("ssid", "data", "string", "TestNetwork"),
            NvsEntry("password", "data", "string", "testpass123"),
            NvsEntry("target_ip", "data", "string", "10.0.2.2"),
            NvsEntry("target_port", "data", "u16", "1024"),
            NvsEntry("node_id", "data", "u8", "0"),
            NvsEntry("edge_tier", "data", "u8", "0"),
            NvsEntry("pres_thresh", "data", "u16", "1"),
            NvsEntry("fall_thresh", "data", "u16", "100"),    # min valid (0.1 rad/s²)
            NvsEntry("vital_win", "data", "u16", "32"),       # min validated
            NvsEntry("vital_int", "data", "u16", "100"),
            NvsEntry("subk_count", "data", "u8", "1"),
            NvsEntry("power_duty", "data", "u8", "10"),
        ],
    ))

    # 13. power-save - low power duty cycle configuration
    configs.append(NvsConfig(
        name="power-save",
        description="Power-save mode: 10% duty cycle for battery-powered nodes",
        entries=[
            NvsEntry("ssid", "data", "string", "TestNetwork"),
            NvsEntry("password", "data", "string", "testpass123"),
            NvsEntry("target_ip", "data", "string", "10.0.2.2"),
            NvsEntry("edge_tier", "data", "u8", "1"),
            NvsEntry("power_duty", "data", "u8", "10"),
        ],
    ))

    # 14. empty-strings - empty SSID/password to test fallback to Kconfig
    configs.append(NvsConfig(
        name="empty-strings",
        description="Empty SSID and password to verify Kconfig fallback",
        entries=[
            NvsEntry("ssid", "data", "string", ""),
            NvsEntry("password", "data", "string", ""),
            NvsEntry("target_ip", "data", "string", "10.0.2.2"),
        ],
    ))

    return configs


def generate_nvs_binary(csv_content: str, size: int) -> bytes:
    """Generate an NVS partition binary from CSV content.

    Tries multiple methods to find nvs_partition_gen:
    1. Subprocess invocation (most reliable across package versions)
    2. esp_idf_nvs_partition_gen pip package (direct import)
    3. Legacy nvs_partition_gen pip package
    4. ESP-IDF bundled script (via IDF_PATH)
    """
    import subprocess
    import tempfile

    with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) as f_csv:
        f_csv.write(csv_content)
        csv_path = f_csv.name

    bin_path = csv_path.replace(".csv", ".bin")

    try:
        # Method 1: subprocess invocation (most reliable — avoids API changes)
        for module_name in ["esp_idf_nvs_partition_gen", "nvs_partition_gen"]:
            try:
                subprocess.check_call(
                    [sys.executable, "-m", module_name, "generate",
                     csv_path, bin_path, hex(size)],
                    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
                )
                with open(bin_path, "rb") as f:
                    return f.read()
            except (subprocess.CalledProcessError, FileNotFoundError):
                continue

        # Method 2: direct import (handles older API where generate() takes int)
        for module_name in ["esp_idf_nvs_partition_gen.nvs_partition_gen",
                            "nvs_partition_gen"]:
            try:
                mod = __import__(module_name, fromlist=["generate"])
                # Try int size first, then hex string (API varies by version)
                for size_arg in [size, hex(size)]:
                    try:
                        mod.generate(csv_path, bin_path, size_arg)
                        with open(bin_path, "rb") as f:
                            return f.read()
                    except (TypeError, AttributeError):
                        continue
            except ImportError:
                continue

        # Method 3: ESP-IDF bundled script
        idf_path = os.environ.get("IDF_PATH", "")
        gen_script = os.path.join(
            idf_path, "components", "nvs_flash",
            "nvs_partition_generator", "nvs_partition_gen.py"
        )
        if os.path.isfile(gen_script):
            subprocess.check_call([
                sys.executable, gen_script, "generate",
                csv_path, bin_path, hex(size)
            ])
            with open(bin_path, "rb") as f:
                return f.read()

        print("ERROR: NVS partition generator tool not found.", file=sys.stderr)
        print("Install: pip install esp-idf-nvs-partition-gen", file=sys.stderr)
        print("Or set IDF_PATH to your ESP-IDF installation", file=sys.stderr)
        raise RuntimeError(
            "NVS partition generator not available. "
            "Install: pip install esp-idf-nvs-partition-gen"
        )

    finally:
        for p in set((csv_path, bin_path)):
            if os.path.isfile(p):
                os.unlink(p)


def main():
    parser = argparse.ArgumentParser(
        description="Generate NVS partition binaries for QEMU firmware test matrix (ADR-061)",
    )
    parser.add_argument(
        "--output-dir", required=True,
        help="Directory to write NVS binary files",
    )
    parser.add_argument(
        "--only", type=str, default=None,
        help="Comma-separated list of config names to generate (default: all)",
    )
    parser.add_argument(
        "--csv-only", action="store_true",
        help="Only generate CSV files, skip binary generation",
    )
    parser.add_argument(
        "--list", action="store_true", dest="list_configs",
        help="List all available configurations and exit",
    )

    args = parser.parse_args()

    all_configs = define_configs()

    if args.list_configs:
        print(f"{'Name':<20} {'Description'}")
        print("-" * 70)
        for cfg in all_configs:
            print(f"{cfg.name:<20} {cfg.description}")
        sys.exit(0)

    # Filter configs if --only specified
    if args.only:
        selected = set(args.only.split(","))
        configs = [c for c in all_configs if c.name in selected]
        missing = selected - {c.name for c in configs}
        if missing:
            print(f"WARNING: Unknown config names: {', '.join(sorted(missing))}",
                  file=sys.stderr)
    else:
        configs = all_configs

    output_dir = Path(args.output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)

    print(f"Generating {len(configs)} NVS configurations in {output_dir}/")
    print()

    success = 0
    errors = 0

    for cfg in configs:
        csv_content = cfg.to_csv()

        # Always write the CSV for reference
        csv_path = output_dir / f"nvs_{cfg.name}.csv"
        csv_path.write_text(csv_content)

        if cfg.name == "default" and not cfg.entries:
            # "default" means no NVS — just produce an empty marker
            print(f"  [{cfg.name}] No NVS entries (uses Kconfig defaults)")
            # Write a zero-filled NVS partition (erased state = 0xFF)
            bin_path = output_dir / f"nvs_{cfg.name}.bin"
            bin_path.write_bytes(b"\xff" * NVS_PARTITION_SIZE)
            success += 1
            continue

        if args.csv_only:
            print(f"  [{cfg.name}] CSV only: {csv_path}")
            success += 1
            continue

        try:
            nvs_bin = generate_nvs_binary(csv_content, NVS_PARTITION_SIZE)
            bin_path = output_dir / f"nvs_{cfg.name}.bin"
            bin_path.write_bytes(nvs_bin)
            print(f"  [{cfg.name}] {len(nvs_bin)} bytes -> {bin_path}")
            success += 1
        except Exception as e:
            print(f"  [{cfg.name}] ERROR: {e}", file=sys.stderr)
            errors += 1

    print()
    print(f"Done: {success} succeeded, {errors} failed")

    if errors > 0:
        sys.exit(1)


if __name__ == "__main__":
    main()
