#!/usr/bin/env python3
"""
Build register-map.json from Sinam_Tag_all.xlsx SourceAddress columns.

Reads Sinam_Tag_all.xlsx (Experion tag→HC900 address mapping), filters
for C3 controller entries, converts Experion indexed addresses to
fixed Modbus addresses, and writes register-map.json.

Usage:
    python3 build_register_map_from_sinam.py \
        --sinam docs/Sinam_Tag_all.xlsx \
        --csv docs/C3-All-Modbus-Map.csv \
        -o docs/register-map.json
"""

import re
import json
import argparse
import csv
from pathlib import Path

import openpyxl

# ─── Experion indexed address → Modbus address ───

PARAM_MAP = {
    'PV':       (0x00, 2, 'float32'),
    'RSP':      (0x02, 2, 'float32'),
    'WSP':      (0x04, 2, 'float32'),
    'SP':       (0x04, 2, 'float32'),
    'OP':       (0x06, 2, 'float32'),
    'OPWORK':   (0x06, 2, 'float32'),
    'LSP1':     (0x2A, 2, 'float32'),
    'LSP2':     (0x2C, 2, 'float32'),
    'AMSTAT':   (0xBA, 1, 'uint16'),
    'LOOPSTAT': (0xBE, 1, 'uint16'),
}


def parse_indexed_addr(addr_str: str, csv_lookup: dict) -> dict | None:
    """Convert an Experion indexed address (e.g. 'C3 LOOP 1 OP')
    to a dict with modbus_addr, count, type, and optional description.

    csv_lookup: {(partition, index): {'addr': int, 'dtype': str, 'tag': str}}
    """
    addr_str = addr_str.strip()

    # LOOP / LOOPX
    m = re.match(r'C3 (LOOP|LOOPX) (\d+) (\w+)', addr_str)
    if m:
        loop_type, n_str, param = m.group(1), int(m.group(2)), m.group(3)
        if loop_type == 'LOOP':
            assert 1 <= n_str <= 24, f'LOOP number {n_str} out of range 1-24'
            base = 0x0040 + (n_str - 1) * 0x0100
        else:
            assert 25 <= n_str <= 32, f'LOOPX number {n_str} out of range 25-32'
            base = 0x7840 + (n_str - 25) * 0x0100
        param_info = PARAM_MAP.get(param)
        if param_info is None:
            print(f'  ⚠ Unknown param "{param}" in "{addr_str}", skipping')
            return None
        off, count, dtype = param_info
        addr = base + off
        return {
            'modbus_addr': addr,
            'count': count,
            'type': dtype,
            'access': 'R',
            'description': f'{loop_type} #{n_str} {param}',
        }

    # TAG (Signal Tag)
    m = re.match(r'C3 TAG (\d+) VALUE', addr_str)
    if m:
        n = int(m.group(1))
        addr = 0x2000 + (n - 1) * 2
        info = csv_lookup.get(('TAG', n), {})
        dtype = info.get('dtype', 'float32')
        count = 2 if dtype == 'float32' else 1
        tag_name = info.get('tag', f'SIG_TAG_{n}')
        return {
            'modbus_addr': addr,
            'count': count,
            'type': dtype,
            'access': 'R',
            'description': f'Signal Tag #{n} ({tag_name})',
        }

    # MATH_VAR (Variable)
    m = re.match(r'C3 MATH_VAR (\d+) VALUE', addr_str)
    if m:
        n = int(m.group(1))
        addr = 0x18C0 + (n - 1) * 2
        info = csv_lookup.get(('MATH_VAR', n), {})
        dtype = info.get('dtype', 'float32')
        count = 2 if dtype == 'float32' else 1
        tag_name = info.get('tag', f'VAR_{n}')
        return {
            'modbus_addr': addr,
            'count': count,
            'type': dtype,
            'access': 'RW',
            'description': f'Variable #{n} ({tag_name})',
        }

    # Raw address format: C3 4:0x789e
    m = re.match(r'C3 (\d+):0x([0-9a-fA-F]+)', addr_str)
    if m:
        addr = int(m.group(2), 16)
        dtype_str = addr_str.split()[-1] if 'UINT' in addr_str.upper() else 'uint16'
        return {
            'modbus_addr': addr,
            'count': 1,
            'type': 'uint16',
            'access': 'R',
            'description': f'Raw addr 0x{addr:04X}',
        }

    print(f'  ⚠ Could not parse indexed address: "{addr_str}", skipping')
    return None


# ─── CSV lookup ───

def build_csv_lookup(csv_path: Path) -> dict:
    """Build a lookup dict from C3-All-Modbus-Map.csv.

    Returns: {(partition, index): {'tag': str, 'dtype': str, 'addr': int}}
    """
    lookup = {}
    with open(csv_path, 'r', encoding='utf-8-sig') as f:
        reader = csv.reader(f)
        rows = list(reader)

    for row in rows:
        if len(row) < 6:
            continue
        hex_addr_raw = row[0].strip()
        if not hex_addr_raw.startswith('0x'):
            continue
        addr = int(hex_addr_raw, 16)
        tag = row[2].strip()
        typ = row[4].strip()
        num_str = row[5].strip()
        dtype_raw = row[6].strip() if len(row) > 6 else 'float 32'

        # Normalize dtype
        if 'unsigned 16' in dtype_raw.lower() or 'signed 16' in dtype_raw.lower() or 'integer' in dtype_raw.lower():
            dtype = 'uint16'
        else:
            dtype = 'float32'

        # PID loop → map loop number
        if typ == 'PID' and num_str.startswith('#'):
            n = int(num_str[1:])
            lookup[('LOOP', n)] = {'tag': tag, 'dtype': dtype, 'addr': addr}

        # Signal Tag
        if typ == 'Signal Tag' and num_str.isdigit():
            n = int(num_str)
            lookup[('TAG', n)] = {'tag': tag, 'dtype': dtype, 'addr': addr}

        # Variable / Math Variable
        if typ in ('Variable', 'Math Variable') and num_str.isdigit():
            n = int(num_str)
            lookup[('MATH_VAR', n)] = {'tag': tag, 'dtype': dtype, 'addr': addr}

    return lookup


# ─── Sinam parsing ───

def iter_sinam_c3_entries(sinam_path: Path, csv_lookup: dict) -> list[dict]:
    """Iterate over Sinam_Tag_all.xlsx rows with C3 source addresses.

    Yields register entries dicts ready for JSON output.
    """
    wb = openpyxl.load_workbook(sinam_path, read_only=True, data_only=True)
    ws = wb['Sheet1']

    registers = []
    seen_global = set()  # {(tag_name, addr)} — global dedup

    for row in ws.iter_rows(min_row=2, values_only=True):
        item_name = row[0]
        cls = row[1]
        if not item_name:
            continue
        item_name = str(item_name).strip()
        if not cls:
            continue
        cls = str(cls).strip()

        pv = str(row[28]).strip() if row[28] else ''
        op = str(row[29]).strip() if row[29] else ''
        md = str(row[30]).strip() if row[30] else ''

        # Collect C3 source addresses
        sources = []
        for field, val in [('PV', pv), ('OP', op), ('MD', md)]:
            if val and val.upper().startswith('C3'):
                sources.append((field, val))

        if not sources:
            continue

        # Per-row dedup: track addresses to avoid OP/MD pointing to same reg
        row_addrs = set()

        # Determine primary field: the main source that drops the suffix
        # AnalogPoint: PV = main analog value, OP = loop status (supplement)
        # StatusPoint: OP = digital state (main), MD = supplement (if different)
        if cls == 'AnalogPoint':
            primary_field = 'PV'
        elif cls == 'StatusPoint':
            primary_field = 'OP'
        else:
            primary_field = sources[0][0]

        # For each source address, create register entry
        for field, addr_str in sources:
            result = parse_indexed_addr(addr_str, csv_lookup)
            if result is None:
                continue

            addr = result['modbus_addr']

            # Per-row address dedup (OP/MD often point to same register)
            if addr in row_addrs:
                continue
            row_addrs.add(addr)

            # Tag naming:
            #   single source              → ItemName
            #   multi-source, primary field → ItemName (no suffix)
            #   multi-source, other field   → ItemName.field
            if len(sources) == 1 or field == primary_field:
                tag_name = item_name
            else:
                tag_name = f'{item_name}.{field}'

            # Global dedup by (tag_name, addr)
            key = (tag_name, addr)
            if key in seen_global:
                continue
            seen_global.add(key)

            entry = {
                'tag': tag_name,
                'addr': result['modbus_addr'],
                'count': result['count'],
                'type': result['type'],
                'access': result['access'],
                'description': result['description'],
            }
            registers.append(entry)

    wb.close()
    return registers


# ─── main ───

def build(sinam_path: Path, csv_path: Path | None, output_path: Path):
    csv_lookup = build_csv_lookup(csv_path) if csv_path else {}

    print(f'Building CSV lookup from {csv_path}...')
    print(f'  LOOP entries:    {sum(1 for k in csv_lookup if k[0] == "LOOP")}')
    print(f'  TAG entries:     {sum(1 for k in csv_lookup if k[0] == "TAG")}')
    print(f'  MATH_VAR entries: {sum(1 for k in csv_lookup if k[0] == "MATH_VAR")}')

    print(f'Parsing C3 entries from {sinam_path}...')
    registers = iter_sinam_c3_entries(sinam_path, csv_lookup)

    registers.sort(key=lambda r: r['addr'])

    output = {
        'controller': 'C3',
        'report_generated': '2026-06-03',
        'float_format': 'FP_B',
        'notes': 'Register map built from Sinam_Tag_all.xlsx SourceAddress columns (C3 only). '
                 'Tags are mapped from Experion indexed addresses to fixed Modbus addresses.',
        'register_count': len(registers),
        'registers': registers,
    }

    with open(output_path, 'w', encoding='utf-8') as f:
        json.dump(output, f, indent=2, ensure_ascii=False)

    print(f'\n✓ Register map written to {output_path}')
    print(f'  Total C3 registers from Sinam: {len(registers)}')

    # Summary by type
    by_field = {}
    for r in registers:
        suffix = r['tag'].split('.')[-1] if '.' in r['tag'] else '(main)'
        by_field[suffix] = by_field.get(suffix, 0) + 1
    print('  By suffix:')
    for s, c in sorted(by_field.items(), key=lambda x: -x[1]):
        print(f'    {c:4d}  {s}')


if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        description='Build register-map.json from Sinam_Tag_all.xlsx (C3 controller)')
    parser.add_argument('--sinam', required=True,
                        help='Path to Sinam_Tag_all.xlsx')
    parser.add_argument('--csv',
                        default='docs/C3-All-Modbus-Map.csv',
                        help='Path to C3-All-Modbus-Map.csv for type/tag lookup (optional)')
    parser.add_argument('-o', '--output', default='docs/register-map.json',
                        help='Output JSON path')
    parser.add_argument('--no-csv', action='store_true',
                        help='Skip CSV lookup (use defaults for type)')
    args = parser.parse_args()

    csv_path = None if args.no_csv else Path(args.csv)
    if csv_path and not csv_path.exists():
        print(f'Warning: CSV path {csv_path} not found, continuing without lookup')
        csv_path = None

    build(Path(args.sinam), csv_path, Path(args.output))
