# exports/export_crbx.py
from __future__ import annotations
from pathlib import Path
from typing import Dict, Any, List, Optional
import zipfile
import io
import re
import datetime

__all__ = ["export_crbx_all"]

# Fixed positions based on provided SIA-451 Positions.cs (1-based indices)
POS = {
    "Kapitelnummer": (1, 3),
    "Ausgabejahr": (5, 2),
    "Leistungsposition": (7, 6),
    "Variablennummer": (13, 2),
    "Zeilennummer": (15, 2),
    "Objektgliederung": (17, 6),
    "Positionslage": (23, 6),
    "Variantengruppe": (29, 3),
    "VarianteInnerhalbDerGruppe": (32, 3),
    "Verbandskalkulation": (35, 6),
    "Recordtyp": (41, 1),
    # 42 (leer)
    "Mengenart": (43, 1),
    "VorzeichenMenge": (44, 1),
    "Menge": (45, 13),  # scaled x1000 integer
    "Mengeneinheit": (58, 2),
    "Preisart": (60, 1),
    "VorzeichenPreis": (61, 1),
    "Preis": (62, 12),  # scaled x100 integer (EP)
    "Laufnummer": (74, 7),
    "KagCode": (81, 5),
    "Elementcode": (86, 6),
    "Positionstext": (92, 30),
    "SpezielleCodierungen": (122, 134),
}

def _blank_line() -> bytearray:
    return bytearray(b" " * 256)

def _put(line: bytearray, name: str, value: str):
    start, length = POS[name]
    # convert 1-based start to 0-based index
    i = start - 1
    s = (value or "")
    if len(s) > length:
        s = s[:length]
    # For numeric fields (Menge, Preis, Laufnummer) we right-align; otherwise left-align
    if name in ("Menge","Preis","Laufnummer"):
        s = s.rjust(length)
    else:
        s = s.ljust(length)
    line[i:i+length] = s.encode("utf-8", errors="ignore")

def _norm_pos_code(pos: str) -> tuple[str, str]:
    """
    Returns (kapitelnummer, leistungsposition6)
    Examples:
        '211.191' -> ('211','211191')
        '211191'  -> ('211','211191')
    """
    p = re.sub(r"[^0-9]", "", str(pos))
    if len(p) >= 6:
        lp = p[:6]
        return (lp[:3], lp)
    # Fallback: pad to 6
    lp = p.zfill(6)
    return (lp[:3], lp)

def _scale_qty(x) -> int:
    try:
        fx = float(x)
        return int(round(fx * 1000))
    except Exception:
        return 0

def _scale_ep(x) -> int:
    try:
        fx = float(x)
        return int(round(fx * 100))
    except Exception:
        return 0

def _mk_g_record(recordtyp: str, pos: str, qty, unit: str, ep, text: str, laufnr: int,
                 mengenart: str="A", preisart: str="E", vorz_menge: str="+", vorz_preis: str="+",
                 varnr: str="00", zeilennr: str="00") -> str:
    line = _blank_line()
    kap, lp = _norm_pos_code(pos)
    _put(line, "Kapitelnummer", kap)
    _put(line, "Ausgabejahr", "00")
    _put(line, "Leistungsposition", lp)
    _put(line, "Variablennummer", varnr)
    _put(line, "Zeilennummer", zeilennr)
    _put(line, "Objektgliederung", "")
    _put(line, "Positionslage", "")
    _put(line, "Variantengruppe", "")
    _put(line, "VarianteInnerhalbDerGruppe", "")
    _put(line, "Verbandskalkulation", "")
    _put(line, "Recordtyp", recordtyp)
    _put(line, "Mengenart", mengenart or "A")
    _put(line, "VorzeichenMenge", vorz_menge or "+")
    _put(line, "Menge", str(_scale_qty(qty) if qty is not None else 0))
    _put(line, "Mengeneinheit", (unit or "")[:2])
    _put(line, "Preisart", preisart or "E")
    _put(line, "VorzeichenPreis", vorz_preis or "+")
    _put(line, "Preis", str(_scale_ep(ep) if ep is not None else 0))
    _put(line, "Laufnummer", str(laufnr))
    _put(line, "KagCode", "")
    _put(line, "Elementcode", "")
    _put(line, "Positionstext", (text or "")[:30])
    _put(line, "SpezielleCodierungen", "")
    return line.decode("utf-8")

def _vendor_01s_content(vendor: str,
                        lv_struct: Dict[str, Dict[str, Any]],
                        price_map: Dict[str, Dict[str, Any]],
                        meta: Optional[Dict[str, Any]] = None) -> str:
    """
    Build minimal .01s content:
      - one G/5 record per position to define unit
      - one G/6 record per position with qty+EP
    - If EP missing but GP+qty present, EP := GP/qty
    - Positions not present in lv_struct are ignored
    """
    meta = meta or {}
    lines: List[str] = []

    # Minimal header/footer as comments in first/last lines (einige Importe akzeptieren nur G-Lines; A/Z optional)
    today = datetime.date.today().strftime("%Y%m%d")
    lines.append(f"A ERNE Offertvergleich {vendor} {today}".ljust(256))

    laufnr = 1
    for pos, q in sorted(lv_struct.items(), key=lambda kv: kv[0]):
        qty = q.get("qty")
        unit = (q.get("unit") or "")[:2]
        text = (q.get("text") or "")
        entry = (price_map.get(pos) or {})
        ep = entry.get("EP")
        gp = entry.get("GP")
        etext = (entry.get("text") or "")

        # Compute EP if missing
        if (ep is None or ep == "") and qty not in (None, 0) and gp not in (None, ""):
            try:
                ep = float(gp) / float(qty) if float(qty) != 0 else None
            except Exception:
                ep = None

        # Unit record (type 5)
        lines.append(_mk_g_record("5", pos, qty, unit, None, text or etext, laufnr))
        laufnr += 1

        # Price record (type 6) – even if ep is None -> 0
        lines.append(_mk_g_record("6", pos, qty, unit, ep, text or etext, laufnr))
        laufnr += 1

    lines.append("Z".ljust(256))
    return "\n".join(lines) + "\n"

def export_crbx_all(out_dir: Path,
                    lv_struct: Dict[str, Dict[str, Any]],
                    vendor_positions: Dict[str, Dict[str, Dict[str, Any]]],
                    meta: Optional[Dict[str, Any]] = None) -> List[Path]:
    """
    Write one .crbx (zip) per vendor.
      out_dir / <Vendor>.crbx
        └─ <Vendor>.01s   (text, 256-char fixed width lines)\n
    Returns list of .crbx paths.
    """
    out_dir = Path(out_dir); out_dir.mkdir(parents=True, exist_ok=True)
    meta = meta or {}

    created: List[Path] = []
    for vendor, posmap in vendor_positions.items():
        # Only positions present in lv_struct are considered (per user decision 8a)
        content = _vendor_01s_content(vendor, lv_struct, posmap, meta)
        # Write to zip
        crbx_path = out_dir / f"{vendor}.crbx"
        inner_name = f"{vendor}.01s"
        with zipfile.ZipFile(crbx_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
            zf.writestr(inner_name, content, compress_type=zipfile.ZIP_DEFLATED)
        created.append(crbx_path)
    return created
