#!/usr/bin/env python3
"""
HC900 Modbus TCP Tag Reader

Usage:
  # read specific tags
  /tmp/hc900_venv/bin/python3 test/read_tags.py FICQ3101.PV FICQ3101.SP FICQ3101.MODE LT3101

  # read all tags (will be slow)
  /tmp/hc900_venv/bin/python3 test/read_tags.py --all

  # connect to simulator instead
  /tmp/hc900_venv/bin/python3 test/read_tags.py --port 5020 FICQ3101.PV
"""
import json
import struct
import argparse
import sys
from pathlib import Path

from pymodbus.client import ModbusTcpClient

REG_MAP_PATH = Path(__file__).parent.parent / "docs" / "register-map.json"
DEFAULT_HOST = "192.168.0.240"
DEFAULT_PORT = 502


def load_register_map(path) -> dict:
    with open(path) as f:
        return json.load(f)


def find_regs(map_data, tag_names: list[str]):
    """Find register entries matching the given tag names (prefix match allowed)."""
    regs = map_data["registers"]
    hits = []
    for name in tag_names:
        name = name.upper()
        matched = [r for r in regs if r["tag"].upper() == name]
        if not matched:
            # try prefix match
            matched = [r for r in regs if r["tag"].upper().startswith(name)]
        if matched:
            hits.extend(matched)
        else:
            print(f"  ! tag '{name}' not found in register map", file=sys.stderr)
    return hits


def read_registers(client: ModbusTcpClient, regs: list[dict]) -> list[dict]:
    """Read values from Modbus TCP and decode them in-place."""
    for r in regs:
        addr = r["addr"]
        count = r["count"]
        resp = client.read_holding_registers(addr, count=count)
        if resp.isError():
            r["value"] = f"<error: {resp}>"
            r["raw"] = None
            continue
        raw = list(resp.registers)
        r["raw"] = raw

        if r["type"] == "float32":
            packed = struct.pack(">HH", raw[0], raw[1])
            r["value"] = struct.unpack(">f", packed)[0]
        elif r["type"] == "uint16":
            r["value"] = raw[0]
        else:
            r["value"] = raw
    return regs


def print_results(regs: list[dict]):
    header = f"{'Tag':<30s} {'Addr':>6s} {'Type':<10s} {'Value':>12s} {'Raw':>16s}  {'Description'}"
    print(header)
    print("-" * len(header))
    for r in regs:
        val = r.get("value", "")
        raw = r.get("raw", "")
        if isinstance(val, float):
            val_str = f"{val:.3f}"
        else:
            val_str = str(val)
        # raw hex
        if raw and isinstance(raw, list):
            raw_str = " ".join(f"0x{v:04X}" for v in raw)
        else:
            raw_str = ""
        desc = r.get("description", "")
        print(f"{r['tag']:<30s} 0x{r['addr']:04X}  {r['type']:<10s} {val_str:>12s} {raw_str:>16s}  {desc}")


def main():
    parser = argparse.ArgumentParser(description="Read HC900 Modbus TCP tags")
    parser.add_argument("tags", nargs="*", help="Tag names to read (e.g. FICQ3101.PV)")
    parser.add_argument("--all", action="store_true", help="Read ALL registers (slow!)")
    parser.add_argument("--host", default=DEFAULT_HOST)
    parser.add_argument("--port", type=int, default=DEFAULT_PORT)
    parser.add_argument("--limit", type=int, default=20, help="Max tags to display")
    args = parser.parse_args()

    if not args.tags and not args.all:
        parser.print_help()
        sys.exit(1)

    map_data = load_register_map(REG_MAP_PATH)

    if args.all:
        regs = map_data["registers"]
    else:
        regs = find_regs(map_data, args.tags)

    if not regs:
        print("No registers matched.")
        sys.exit(1)

    print(f"Connecting to {args.host}:{args.port} ...")
    client = ModbusTcpClient(args.host, port=args.port, timeout=5)
    client.connect()

    if not client.connected:
        print("ERROR: connection failed")
        sys.exit(1)

    try:
        read_registers(client, regs)
    finally:
        client.close()

    # deduplicate by tag name
    seen = set()
    unique = []
    for r in regs:
        if r["tag"] not in seen:
            seen.add(r["tag"])
            unique.append(r)

    print(f"\nRead {len(unique)} tags (showing first {min(args.limit, len(unique))}):\n")
    print_results(unique[:args.limit])


if __name__ == "__main__":
    main()
