#!/usr/bin/env python3
"""Aplicar el contrato comun del DAT a una tabla, usando Tabular2 para numeros.

Entrada: --dato JSON --tabla ARCHIVO (o - para stdin). Salida: SIMUL en stdout.
No interpreta .ds, ni resuelve sistemas, ni genera graficos. GPL-3.0-or-later.
"""
import argparse
import csv
import hashlib
import json
import os
from pathlib import Path
import re
import subprocess
import sys
import tempfile


def exportar(programa, archivo, args=()):
    p = subprocess.run([programa, '--exportar-simul', str(archivo), *args],
                       capture_output=True, text=True, encoding='utf-8')
    if p.returncode:
        raise ValueError(p.stderr.strip() or 'Tabular2 no pudo leer la tabla')
    return p.stdout


def bloques(texto):
    """Solo se descompone la exportacion canonica de Tabular, no sus formatos fuente."""
    for parte in re.split(r'(?=^# Producido por Tabular;)', texto, flags=re.M):
        if not parte.strip():
            continue
        lineas = parte.splitlines()
        pos = next(i for i, l in enumerate(lineas) if l.startswith('# para Simusol:'))
        claves = lineas[pos].split(':', 1)[1].split()
        meta = {k: re.split(r'\s{2,}', lineas[pos + 1 + i].lstrip('#').strip())
                for i, k in enumerate(claves)}
        filas = [l.split() for l in lineas[pos + 1 + len(claves):]
                 if l.strip() and not l.lstrip().startswith('#')]
        yield meta, filas


def conocido(s):
    return s not in (None, '', '??', '-', '_')


def separar_marcados(texto):
    cabeceras = list(re.finditer(r'^#\s*para Si(?:musol|ru):', texto, re.M | re.I))
    if not cabeceras:
        return []
    inicios = [0]
    for anterior, actual in zip(cabeceras, cabeceras[1:]):
        previos = list(re.finditer(r'^# Producido por Tabular;',
                                  texto[anterior.end():actual.start()], re.M))
        inicios.append(anterior.end() + previos[-1].start() if previos else actual.start())
    inicios.append(len(texto))
    return [texto[a:b] for a, b in zip(inicios, inicios[1:])]


def celda(s):
    s = str(s)
    if not s or re.search(r'[\r\n\t,]|\s{2,}', s):
        raise ValueError(f'metadato no representable en SIMUL: {s!r}')
    return s


def escribir(meta, filas, firma, numero=1, columnas=None):
    n = len(meta['sceptrevars'])
    if not n or any(len(r) != n for r in filas):
        raise ValueError('tabla vacia o filas de ancho inconsistente')
    claves = ['sceptrevars', 'variables', 'magnitudes', 'unidades']
    lineas = ['# Producido por Tabular; 17 cifras significativas.',
              '# RESULTADOS_APLICADOS ' + firma, f'# CORRIDA {numero}',
              '# RESULTADOS_COLUMNAS ' + json.dumps(columnas or list(range(1, n + 1))),
              '# para Simusol: ' + ' '.join(claves)]
    lineas += ['#  ' + '  '.join(celda(x) for x in meta[k]) for k in claves]
    lineas += [' '.join(r) for r in filas]
    return '\n'.join(lineas) + '\n\n'


def preparar(g, archivo, programa):
    pedido = g.get('resultados') or {}
    entradas = dict(g.get('unidades_entrada') or {})
    # Compatibilidad del contrato antiguo: alias exactos, no reglas fisicas
    # duplicadas por consumidores ni coincidencias arbitrarias por prefijo.
    for d in pedido.get('defectos', []):
        nombre = d.get('alias') or d['prefijo']
        regla = {k: d[k] for k in ('unidad', 'magnitud')}
        if nombre in entradas and entradas[nombre]['unidad'] != regla['unidad']:
            raise ValueError(f'UNIDADES ambiguas para {nombre}')
        entradas.setdefault(nombre, regla)
    calculo = g.get('unidades_calculo') or {}
    contrato = {'resultados': pedido, 'unidades_entrada': entradas,
                'unidades_calculo': calculo}
    firma = hashlib.sha256(json.dumps(contrato, sort_keys=True, ensure_ascii=True).encode()).hexdigest()
    # La marca es especifica del contrato; no basta que diga "SI" ni que haya unidades.
    contenido = Path(archivo).read_bytes() if '#' not in str(archivo) else b''
    if contenido and b'# RESULTADOS_APLICADOS ' in contenido:
        # Una marca en el primer bloque no cubre el resto del documento.
        # Procesar individualmente tambien conserva los indices originales
        # en bloques ya seleccionados, sin seleccionar por segunda vez.
        texto_marcado = contenido.decode('utf-8')
        partes = separar_marcados(texto_marcado)
        marca = '# RESULTADOS_APLICADOS ' + firma
        if partes:
            if all(re.search(r'^' + re.escape(marca) + r'$', p, re.M) for p in partes):
                return texto_marcado
            with tempfile.TemporaryDirectory(prefix='tabular-bloques-') as tmp:
                salidas = []
                for i, p in enumerate(partes):
                    if re.search(r'^' + re.escape(marca) + r'$', p, re.M):
                        salidas.append(p)
                    else:
                        # Quitar marcas ajenas evita recursividad sin avance.
                        p = re.sub(r'^# RESULTADOS_APLICADOS[^\n]*\n', '', p, flags=re.M)
                        ruta = Path(tmp) / f'bloque-{i}.simul'
                        ruta.write_text(p, encoding='utf-8')
                        salidas.append(preparar(g, ruta, programa))
                return ''.join(salidas)
    texto = exportar(programa, archivo)
    salida = []
    with tempfile.TemporaryDirectory(prefix='tabular-resultados-') as tmp:
        for nb, (meta, filas) in enumerate(bloques(texto)):
            nombres = meta['sceptrevars']
            aliases = meta['variables']
            def indice(selector):
                if 'indice' in selector:
                    k = selector['indice'] - 1
                    if isinstance(k, int) and 0 <= k < len(nombres):
                        return k
                else:
                    nom = selector.get('nombre', '').casefold()
                    ks = [i for i in range(len(nombres))
                          if nom in (nombres[i].casefold(), aliases[i].casefold())]
                    if len(ks) == 1:
                        return ks[0]
                raise ValueError(f'columna inexistente o ambigua: {selector}')
            series = pedido.get('series') or []
            pedidos = {}
            orden = []
            # Tiempo es una coordenada, no una variable agregada a un punto estatico.
            if series and nombres[0].casefold() in ('t', 'time', 'tiempo'):
                orden.append(0)
            for p in series:
                k = indice(p['columna'])
                if k in pedidos:
                    raise ValueError(f'RESULTADOS repetido: {nombres[k]}')
                pedidos[k] = p
                if k not in orden:
                    orden.append(k)
            if not series:
                orden = list(range(len(nombres)))
            args = []
            entradas_ci = {k.casefold(): v for k, v in entradas.items()}
            calculo_ci = {k.casefold(): v for k, v in calculo.items()}
            for k in orden:
                d = entradas_ci.get(nombres[k].casefold(), {})
                c = calculo_ci.get(nombres[k].casefold(), {})
                p = pedidos.get(k, {})
                origen = meta['unidades'][k]
                # Metadatos de una tabla recibida prevalecen: no volver a tomar
                # tempC del modelo para valores que el productor envio en K.
                if not conocido(origen) and d.get('unidad'):
                    origen = d['unidad']
                    args += ['--unidad-origen', str(k + 1), origen]
                destino = p.get('unidad')
                if destino:
                    args += ['--unidad', str(k + 1), destino]
                mag = p.get('magnitud') or (meta['magnitudes'][k] if conocido(meta['magnitudes'][k])
                                           else d.get('magnitud') or c.get('magnitud'))
                if mag:
                    meta['magnitudes'][k] = mag
            if args:
                # Reutiliza el bloque normalizado, no depende del formato fuente.
                ruta = Path(tmp) / f'bloque-{nb}.simul'
                ruta.write_text(escribir(meta, filas, 'interno'), encoding='utf-8')
                meta, filas = next(bloques(exportar(programa, ruta, args)))
            for k, p in pedidos.items():
                if 'leyenda' in p:
                    meta['variables'][k] = p['leyenda']
                if 'magnitud' in p:
                    meta['magnitudes'][k] = p['magnitud']
            meta = {k: [v[i] for i in orden] for k, v in meta.items()}
            filas = [[r[i] for i in orden] for r in filas]
            salida.append(escribir(meta, filas, firma, nb + 1, [i + 1 for i in orden]))
    if not salida:
        raise ValueError('tabla sin bloques')
    return ''.join(salida)


def main():
    p = argparse.ArgumentParser(description=__doc__)
    p.add_argument('--dato', required=True)
    p.add_argument('--tabla', required=True)
    p.add_argument('--formato', choices=('simul', 'csv'), default='simul')
    local = Path(__file__).resolve().with_name('interpola')
    p.add_argument('--interpola', default=os.environ.get('TABULAR_INTERPOLA') or
                   (str(local) if local.is_file() else 'interpola'))
    op = p.parse_args()
    with open(op.dato, encoding='utf-8') as f:
        dato = json.load(f)
    g = dato.get('GLOBAL', dato)
    with tempfile.TemporaryDirectory(prefix='tabular-stdin-') as tmp:
        archivo = op.tabla
        if archivo == '-':
            archivo = str(Path(tmp) / 'entrada.simul')
            Path(archivo).write_bytes(sys.stdin.buffer.read())
        texto = preparar(g, archivo, op.interpola)
    if op.formato == 'csv':
        w = csv.writer(sys.stdout)
        for meta, filas in bloques(texto):
            w.writerow(meta['variables'])
            w.writerows(filas)
    else:
        sys.stdout.write(texto)


if __name__ == '__main__':
    try:
        main()
    except (ValueError, OSError, KeyError, StopIteration) as e:
        print(f'preparar-resultados: {e}', file=sys.stderr)
        sys.exit(1)
