"""DNC Info card.

Surfaces the handful of NTLARS settings a tech actually asks about on the
machine's page, so the common question ("what is this machine's controller
talking to?") is answered without downloading and reading a .reg.

Sections, drawn from the machine's LATEST ntlars revision:

  eFocas   Fanuc ethernet link - IP, socket, dual-path. Present on 143 of the
           147 known-good backups, so it is shown whenever it has content.
  Serial   RS-232 link parameters. Always populated (Baud, Data Bits and
           friends carry defaults even where the link is unused), so it is
           always shown.
  NTSHR    Network share the controller pulls programs from. Populated on only
           18 of 147, hence the has-content gate: showing an empty NTSHR block
           on 129 machines would be noise.
  MARK     Part-marker settings. Gated on the ASSET being a Part Marker in
           ShopDB, NOT on the key having content.

WHY MARK IS GATED ON THE ASSET, NOT THE KEY:
    The obvious rule - show MARK when it has content - does not work. MARK is
    populated on 145 of 147 machines because Baud/Data Bits carry serial
    defaults everywhere, and the fields that would identify a marker
    (CageCode, DataHost, DataPath, MarkMasterPath) are empty across the entire
    corpus. The one field that is set, DncPatterns, reads YES on 103 of 147
    including ordinary lathes, so it is a DNC pattern-matching option and not a
    marker flag. No value in the DNC tree distinguishes a part marker, so the
    machine's type in ShopDB is the only reliable signal.
"""

# Values equal to these (case-insensitively) count as "no content".
EMPTYISH = ('', '0', 'no', 'none')

DEFAULTPARTMARKERTYPES = ('Part Marker',)


def _keyvalues(projection, path):
    """Values under one subkey of the stored projection, or {}."""
    for key in (projection or {}).get('keys', []):
        if (key.get('path') or '').lower() == path.lower():
            return key.get('values') or {}
    return {}


def _hascontent(values):
    """True when at least one value carries something meaningful."""
    for entry in values.values():
        data = entry.get('data')
        if isinstance(data, (list, tuple)):
            if any(str(x).strip() for x in data):
                return True
            continue
        if str(data).strip().lower() not in EMPTYISH:
            return True
    return False


def _fields(values, mono=()):
    """Render a subkey's values as keyvalue panel fields, empties dropped."""
    out = []
    for name in sorted(values):
        data = values[name].get('data')
        if isinstance(data, (list, tuple)):
            data = ', '.join(str(x) for x in data)
        text = '' if data is None else str(data)
        if not text.strip():
            continue
        out.append({
            'label': name,
            'value': text,
            'mono': name in mono,
        })
    return out


def ispartmarker(assetid, typenames=None):
    """True when this asset is a Part Marker according to ShopDB.

    Reads the machines plugin defensively: a lean per-site build (ADR-014) may
    not install it, and the DNC Info card must degrade to "no MARK section"
    rather than erroring the whole panel.
    """
    typenames = tuple(t.lower() for t in (typenames or DEFAULTPARTMARKERTYPES))
    try:
        from shopdb.api import db
        from plugins.machines.models import Machine, MachineType
    except ImportError:
        return False
    try:
        row = (db.session.query(MachineType.machinetype)
               .join(Machine, Machine.machinetypeid == MachineType.machinetypeid)
               .filter(Machine.assetid == assetid)
               .first())
    except Exception:
        return False
    return bool(row) and (row[0] or '').strip().lower() in typenames


def _cncismarker(projection):
    """True when NTLARS itself says the controller is a marker.

    General\\Cnc reads 'MARKER' on the part markers (0600 and 0614 in the
    known-good corpus) and a controller family - Fanuc 30, Fanuc 16, OKUMA,
    Fidia - everywhere else. This is the one place the DNC tree does
    distinguish a marker; nothing inside the MARK key does, since MARK carries
    serial defaults on nearly every machine.
    """
    general = _keyvalues(projection, 'General')
    cnc = str((general.get('Cnc') or {}).get('data') or '').strip().upper()
    return cnc == 'MARKER'


def build(projection, assetid, partmarkertypes=None):
    """Build the DNC Info card payload from a stored ntlars projection.

    Returns {'fields': [...]} in the shape the generic keyvalue renderer wants,
    with section headings inlined as labelless separators.
    """
    sections = []

    # General first: what the controller IS, before what it talks to. Cnc gives
    # the controller family, NcIF the interface actually in use (EFOCAS on 127
    # of 147, NTSHR on 16, SERIAL on 3, HSSB on 1), HostType the DNC host.
    general = _keyvalues(projection, 'General')
    wanted = ('Cnc', 'NcIF', 'HostType')
    generalfields = _fields(
        {n: v for n, v in general.items() if n in wanted})
    if generalfields:
        sections.append(('General', generalfields))

    efocas = _keyvalues(projection, 'eFocas')
    if _hascontent(efocas):
        sections.append(('eFocas (ethernet link)', _fields(efocas, mono=('IpAddr',))))

    serial = _keyvalues(projection, 'Serial')
    if serial:
        sections.append(('Serial (RS-232)', _fields(serial)))

    ntshr = _keyvalues(projection, 'NTSHR')
    if _hascontent(ntshr):
        sections.append(('NTSHR (program share)',
                         _fields(ntshr, mono=('ShrFolder', 'ShrFolder2',
                                              'ShrFolder3', 'ShrHost'))))

    # Either signal is enough: NTLARS's own Cnc=MARKER works before anyone has
    # set the machine's type in ShopDB and on lean builds with no machines
    # plugin, while the ShopDB type still covers a marker whose Cnc says
    # something else.
    if _cncismarker(projection) or ispartmarker(assetid, partmarkertypes):
        mark = _keyvalues(projection, 'MARK')
        if mark:
            sections.append(('MARK (part marker)',
                             _fields(mark, mono=('DataPath', 'MarkMasterPath'))))

    # Emitted as SECTIONS for the tabs renderer: one visible at a time, so the
    # card stays the height of its largest section. Flattened into one list it
    # ran to 30 rows, which the two-column multicol layout cannot split, so it
    # dragged one column far past the other.
    #
    # Everything DNC lives on this one card - General, the interface sections
    # and MARK - rather than MARK getting a card of its own.
    out = [{'label': title, 'fields': entries}
           for title, entries in sections if entries]
    return {'sections': out, 'sectioncount': len(out)}
