from flask import Blueprint, render_template, request, jsonify, flash, redirect, url_for, json
from flask_login import login_required, current_user
from app.extensions import db
from app.models.project import Project
from app.models.company import Company
from app.models.aufmass import Aufmass
from app.models.aufmass_typ import AufmassTyp
from app.models.position import Position
from app.models.lv import LVPosition
from app.models.contract import Contract
from app.models.project_access import ProjectAccess
from app.models.user import User
from app.models.aufmass_history import AufmassHistory
from app.services.license_service import get_aktive_module
from app.models.custom_module import CustomModule, CustomModuleAssignment
from datetime import datetime

aufmass_bp = Blueprint('aufmass', __name__)

def _check_zugriff(project, required='lesen'):
    if not current_user.hat_zugriff(project, required):
        return False
    return True

def _get_aufmass_or_404(aufmass_id, project_id):
    a = Aufmass.query.get_or_404(aufmass_id)
    if a.project_id != project_id:
        return None
    return a

def _build_history_description(action_type, pos_data_list, user_name=''):
    """Baut detaillierte History-Beschreibung.
    
    Args:
        action_type: 'add', 'delete', 'edit', 'move', 'copy'
        pos_data_list: Liste von Dict mit position_id, pos_nr, kurztext, old, new
        user_name: Name des Benutzers
    
    Returns:
        Tuple (kurze_beschreibung, lange_beschreibung)
    """
    from datetime import datetime
    now = datetime.now().strftime('%d.%m.%Y %H:%M')
    
    def _format_field_diff(old_val, new_val, field):
        """Formatiert Unterschied für ein Feld."""
        old_str = str(old_val) if old_val is not None else 'leer'
        new_str = str(new_val) if new_val is not None else 'leer'
        if old_str == new_str:
            return None
        return f"{field}: {old_str} → {new_str}"
    
    if not pos_data_list:
        return ('', '')
    
    short_parts = []
    long_parts = []
    
    for pd in pos_data_list:
        pos_label = f"Zeile ?, Pos {pd.get('pos_nr', pd.get('position_id', '?'))}"
        row_idx = pd.get('row_idx', 0) + 1 if pd.get('row_idx') is not None else '?'
        kurztext = pd.get('kurztext', '')[:30]
        if kurztext:
            kurztext = f'"{kurztext}"'
        
        if action_type == 'add':
            short_parts.append(f"+ Pos {pd.get('pos_nr', '?')} ({kurztext})")
            long_parts.append(f"+ Zeile {row_idx}: Pos {pd.get('pos_nr', '?')} {kurztext}")
        
        elif action_type == 'delete':
            short_parts.append(f"- Pos {pd.get('pos_nr', '?')} ({kurztext})")
            long_parts.append(f"- Zeile {row_idx}: Pos {pd.get('pos_nr', '?')} {kurztext}")
        
        elif action_type == 'edit':
            old_vals = pd.get('old', {})
            new_vals = pd.get('new', {})
            field_labels = {
                'faktor': 'Faktor', 'laenge': 'Länge', 'breite': 'Breite', 'tiefe': 'Tiefe',
                'menge': 'Menge', 'menge_hinten': 'Menge hinten', 'einheit': 'EH',
                'abschnitt': 'Abschnitt', 'bemerkung': 'Bemerkung', 'formel': 'Formel',
                'formel_typ': 'Z-Art', 'kurztext': 'Kurztext'
            }
            changes = []
            for field in set(list(old_vals.keys()) + list(new_vals.keys())):
                if field.startswith('_') or field == 'id':
                    continue
                label = field_labels.get(field, field)
                old_v = old_vals.get(field)
                new_v = new_vals.get(field)
                diff = _format_field_diff(old_v, new_v, label)
                if diff:
                    changes.append(diff)
            if changes:
                short_parts.append(f"✎ Pos {pd.get('pos_nr', '?')}: {', '.join(changes[:2])}")
                long_parts.append(f"✎ Zeile {row_idx}: Pos {pd.get('pos_nr', '?')} {kurztext} - {'; '.join(changes)}")
        
        elif action_type == 'move':
            old_idx = pd.get('old_idx', pd.get('old', {}).get('sort_order', '?'))
            new_idx = pd.get('new_idx', pd.get('new', {}).get('sort_order', '?'))
            if old_idx != new_idx:
                direction = '↓' if new_idx > old_idx else '↑'
                short_parts.append(f"↔ Pos {pd.get('pos_nr', '?')}: {old_idx} {direction} {new_idx}")
                long_parts.append(f"↔ Zeile {old_idx}→{new_idx}: Pos {pd.get('pos_nr', '?')} {kurztext} {direction}")
        
        elif action_type == 'copy':
            short_parts.append(f"⎘ Pos {pd.get('pos_nr', '?')} kopiert")
            long_parts.append(f"⎘ Zeile {row_idx}: Pos {pd.get('pos_nr', '?')} {kurztext} kopiert")
    
    short = ', '.join(short_parts[:3])
    if len(short_parts) > 3:
        short += f' (+{len(short_parts)-3})'
    
    long = f"[{now}] {user_name}\n" + '\n'.join(long_parts)
    
    return (short, long)

# === Projekt-Liste (alle Aufmaß-Projekte) ===

@aufmass_bp.route('/')
@login_required
def index():
    if current_user.is_firmadmin():
        projekte = Project.query.filter_by(
            company_id=current_user.company_id
        ).order_by(db.func.coalesce(Project.bezeichnung, '').asc()).all()
    elif current_user.is_superadmin():
        projekte = Project.query.order_by(db.func.coalesce(Project.bezeichnung, '').asc()).all()
    else:
        zugriff_ids = db.session.query(ProjectAccess.project_id).filter_by(
            user_id=current_user.id
        ).subquery()
        projekte = Project.query.filter(Project.id.in_(zugriff_ids)).order_by(db.func.coalesce(Project.bezeichnung, '').asc()).all()
    preise_sichtbar = current_user.is_superadmin() or current_user.is_firmadmin() or current_user.darf_preise_sehen
    data = []
    from app.models.position import Position
    gesamt_summe = 0.0
    gesamt_positionen = 0
    for p in projekte:
        aufmass_liste = Aufmass.query.filter_by(project_id=p.id).order_by(Aufmass.sortierung).all()
        projekt_summe = 0.0
        projekt_positionen = 0
        aufmass_data = []
        for a in aufmass_liste:
            summe = db.session.query(db.func.coalesce(db.func.sum(Position.gesamtpreis), 0)).filter(
                Position.aufmass_id == a.id
            ).scalar() or 0.0
            projekt_summe += summe
            anzahl = Position.query.filter_by(aufmass_id=a.id).count()
            projekt_positionen += anzahl
            aufmass_data.append({'aufmass': a, 'summe': summe, 'positionen': anzahl})
        gesamt_summe += projekt_summe
        gesamt_positionen += projekt_positionen
        data.append({'project': p, 'aufmass_liste': aufmass_data, 'summe': projekt_summe, 'positionen': projekt_positionen})
    typen = AufmassTyp.query.order_by(AufmassTyp.sortierung).all()
    if current_user.company_id:
        contracts = Contract.query.filter_by(
            company_id=current_user.company_id
        ).order_by(Contract.name).all()
        company = Company.query.get(current_user.company_id)
    else:
        contracts = Contract.query.order_by(Contract.name).all()
        company = None
    return render_template('aufmass/index.html', projekte=data, titel='Aufmaß-Projekte',
                         gesamt_summe=gesamt_summe, gesamt_positionen=gesamt_positionen,
                         preise_sichtbar=preise_sichtbar, typen=typen, contracts=contracts, company=company)

@aufmass_bp.route('/neu', methods=['GET', 'POST'])
@login_required
def neu():
    if not current_user.is_firmadmin() and not current_user.darf_projekte_anlegen:
        flash('Keine Berechtigung.', 'danger')
        return redirect(url_for('aufmass.index'))
    contracts = Contract.query.filter_by(
        company_id=current_user.company_id
    ).order_by(Contract.name).all()
    if request.method == 'POST':
        contract_id = request.form.get('contract_id', type=int)
        contract = Contract.query.get(contract_id) if contract_id else None
        bezeichnung = request.form.get('bezeichnung', '').strip()
        project = Project(
            company_id=current_user.company_id,
            contract_id=contract_id,
            sm_nr='',
            bezeichnung=bezeichnung,
            vertrag=contract.name if contract else request.form.get('vertrag', '').strip(),
            lv_name=request.form.get('lv_name', '').strip(),
            status='aktiv',
            erstellt_von=current_user.id,
        )
        db.session.add(project)
        db.session.commit()
        flash(f'Projekt "{bezeichnung or project.id}" angelegt.', 'success')
        return redirect(url_for('aufmass.aufmass_list', project_id=project.id))
    return render_template('aufmass/neu.html', contracts=contracts, titel='Neues Aufmaß')

# === Aufmaß-Liste innerhalb eines Projekts ===

@aufmass_bp.route('/<int:project_id>')
@login_required
def aufmass_list(project_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'lesen'):
        return 'Zugriff verweigert', 403
    aufmass_liste = Aufmass.query.filter_by(project_id=project_id).order_by(Aufmass.sortierung).all()
    typen = AufmassTyp.query.order_by(AufmassTyp.sortierung).all()
    contracts = Contract.query.filter_by(company_id=project.company_id).order_by(Contract.name).all()
    preise_sichtbar = current_user.is_superadmin() or current_user.is_firmadmin() or current_user.darf_preise_sehen
    # Preise pro Aufmaß berechnen
    from app.models.position import Position
    aufmass_preise = {}
    for a in aufmass_liste:
        summe = db.session.query(db.func.coalesce(db.func.sum(Position.gesamtpreis), 0)).filter(
            Position.aufmass_id == a.id
        ).scalar() or 0.0
        aufmass_preise[a.id] = summe
    lv_names = [r[0] for r in db.session.query(LVPosition.lv_name).filter_by(
        company_id=project.company_id
    ).distinct().order_by(LVPosition.lv_name).all()]
    return render_template('aufmass/list.html', project=project,
                         aufmass_liste=aufmass_liste, typen=typen,
                         preise_sichtbar=preise_sichtbar, aufmass_preise=aufmass_preise,
                         contracts=contracts, lv_names=lv_names, company=current_user.company,
                         titel=f'Aufmaße – {project.bezeichnung or project.sm_nr}')

# === Aufmaß CRUD ===

@aufmass_bp.route('/<int:project_id>/aufmass/neu', methods=['POST'])
@login_required
def aufmass_neu(project_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'schreiben'):
        return jsonify({'error': 'Zugriff verweigert'}), 403
    name = request.form.get('name', '').strip() or 'Neues Aufmaß'
    typ = request.form.get('typ', '').strip()
    max_sort = db.session.query(db.func.max(Aufmass.sortierung)).filter_by(project_id=project_id).scalar() or 0
    a = Aufmass(project_id=project_id, name=name, typ=typ, sortierung=max_sort + 1, erstellt_von=current_user.id)
    db.session.add(a)
    db.session.commit()
    flash(f'Aufmaß "{name}" angelegt.', 'success')
    return redirect(url_for('aufmass.aufmass_list', project_id=project_id))

@aufmass_bp.route('/<int:project_id>/aufmass/neu-voll', methods=['POST'])
@login_required
def aufmass_neu_voll(project_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'schreiben'):
        return jsonify({'error': 'Zugriff verweigert'}), 403
    contract_id = request.form.get('contract_id', type=int)
    contract = Contract.query.get(contract_id) if contract_id else None
    bauabschnitt = request.form.get('bauabschnitt', '').strip()
    import re
    invalid_chars = r'[<>:"/\\|?*&#%{}~\[\]]'
    if re.search(invalid_chars, bauabschnitt):
        flash('Bauabschnitt enthält ungültige Zeichen.', 'danger')
        return redirect(url_for('aufmass.index'))
    project.baustelle = request.form.get('baustelle', '').strip()
    project.sm_nr = request.form.get('sm_nr', '').strip()
    project.vertrag = contract.name if contract else request.form.get('vertrag', '').strip()
    project.abruf_nr = request.form.get('abruf_nr', '').strip()
    project.lv_name = request.form.get('lv_name', '').strip()
    project.datum_start = _parse_date(request.form.get('datum_start'))
    project.datum_ende = _parse_date(request.form.get('datum_ende'))
    project.datum = _parse_date(request.form.get('datum'))
    project.ev_details_id = request.form.get('ev_details_id', '').strip()
    project.ansprechpartner_vorname = request.form.get('ansprechpartner_vorname', '').strip()
    project.ansprechpartner_nachname = request.form.get('ansprechpartner_nachname', '').strip()
    project.ansprechpartner_tel = request.form.get('ansprechpartner_tel', '').strip()
    project.ansprechpartner_email = request.form.get('ansprechpartner_email', '').strip()
    project.bauabschnitt = bauabschnitt
    typ = request.form.get('typ', '').strip()
    name_parts = [s for s in [project.bezeichnung, project.bauabschnitt, project.sm_nr, project.abruf_nr] if s]
    name = ' - '.join(name_parts) if name_parts else 'Neues Aufmass'
    name = re.sub(r'\s+', ' ', name).strip()
    max_sort = db.session.query(db.func.max(Aufmass.sortierung)).filter_by(project_id=project_id).scalar() or 0
    a = Aufmass(project_id=project_id, name=name, typ=typ, sortierung=max_sort + 1, erstellt_von=current_user.id)
    db.session.add(a)
    db.session.commit()
    flash(f'Aufmaß "{name}" für "{project.bezeichnung or project.sm_nr}" angelegt.', 'success')
    return redirect(url_for('aufmass.index'))


@aufmass_bp.route('/<int:project_id>/aufmass/import', methods=['POST'])
@login_required
def aufmass_import(project_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'schreiben'):
        return jsonify({'error': 'Zugriff verweigert'}), 403
    if 'file' not in request.files:
        flash('Keine Datei ausgewählt.', 'danger')
        return redirect(url_for('aufmass.aufmass_list', project_id=project_id))
    f = request.files['file']
    if f.filename == '':
        flash('Keine Datei ausgewählt.', 'danger')
        return redirect(url_for('aufmass.aufmass_list', project_id=project_id))
    try:
        content = f.read().decode('utf-8-sig').splitlines()
    except Exception:
        content = f.read().decode('latin-1').splitlines()
    lines = [l.strip() for l in content if l.strip()]
    if len(lines) < 16 or lines[0] != '[Kopfdaten]':
        flash('Ungültiges Dateiformat.', 'danger')
        return redirect(url_for('aufmass.aufmass_list', project_id=project_id))

    def kv(val):
        return val.split('=', 1)[1] if '=' in val else ''

    teilaufma = kv(lines[1])
    schlussaufma = kv(lines[2])
    datum_str = kv(lines[3])
    baustelle = kv(lines[4])
    abruf_nr = kv(lines[5])
    sm_nr = kv(lines[6])
    startz_str = kv(lines[8])
    endz_str = kv(lines[9])
    aspa_name = kv(lines[10])
    aspa_tel = kv(lines[11])
    bauabschnitt = kv(lines[12])

    # Typ ermitteln
    typ = ''
    if schlussaufma == 'X':
        typ = 'Schlussaufmaß'
    elif teilaufma == 'X':
        typ = 'Teilaufmaß/AZ'

    aspa_vorname = aspa_name
    aspa_nachname = ''
    if ' ' in aspa_name.strip():
        p = aspa_name.strip().rsplit(' ', 1)
        aspa_vorname = p[0]
        aspa_nachname = p[1]

    project.baustelle = baustelle or project.baustelle
    project.bauabschnitt = bauabschnitt or project.bauabschnitt
    project.abruf_nr = abruf_nr or project.abruf_nr
    project.sm_nr = sm_nr or project.sm_nr
    project.datum = _parse_date(datum_str) or project.datum
    project.datum_start = _parse_date(startz_str) or project.datum_start
    project.datum_ende = _parse_date(endz_str) or project.datum_ende
    project.ansprechpartner_vorname = aspa_vorname or project.ansprechpartner_vorname
    project.ansprechpartner_nachname = aspa_nachname or project.ansprechpartner_nachname
    project.ansprechpartner_tel = aspa_tel or project.ansprechpartner_tel

    import re
    _tofloat = lambda s: float(s.replace(',', '.')) if s else 0.0

    positions_data = []
    errors = []
    in_data = False
    for line in lines:
        if line == '[Aufmaßdaten]':
            in_data = True
            continue
        if in_data and '|' in line:
            cols = line.split('|')
            if len(cols) < 13:
                continue

            pos_nr = cols[1].strip()
            faktor = _tofloat(cols[2])
            laenge = _tofloat(cols[3])
            breite = _tofloat(cols[4])
            tiefe = _tofloat(cols[5])
            menge_aus_cols = _tofloat(cols[6])
            einheit = cols[7].strip() or 'ST'
            kurztext = cols[8].strip()
            bemerkung = cols[9].strip()
            menge2 = _tofloat(cols[10])
            einzelpreis = _tofloat(cols[11])
            gesamtpreis = _tofloat(cols[12])

            if not pos_nr:
                # Separatorzeilen (nur Pipes) leise überspringen
                if not any(cols[i].strip() for i in range(1, len(cols))):
                    continue
                errors.append('Position ohne Nummer übersprungen.')
                continue
            if not kurztext:
                errors.append(f'Position {pos_nr}: Kein Kurztext.')

            menge = menge_aus_cols or menge2
            if menge == 0.0 and einheit in ('M', 'M2', 'M3'):
                if einheit == 'M':
                    menge = laenge
                elif einheit == 'M2':
                    menge = laenge * breite
                elif einheit == 'M3':
                    menge = laenge * breite * tiefe
                menge *= faktor
            else:
                menge *= faktor

            berechneter_gp = round(menge * einzelpreis, 2)
            if gesamtpreis > 0 and abs(berechneter_gp - gesamtpreis) > 0.05:
                errors.append(f'Position {pos_nr}: GP-Differenz {berechneter_gp} vs {gesamtpreis}')

            positions_data.append({
                'pos_nr': pos_nr,
                'kurztext': kurztext,
                'einheit': einheit,
                'einzelpreis': einzelpreis,
                'menge': menge,
                'gesamtpreis': gesamtpreis or berechneter_gp,
                'laenge': laenge,
                'breite': breite,
                'tiefe': tiefe,
                'faktor': faktor,
                'bemerkung': bemerkung,
            })

    if not positions_data:
        flash('Keine gültigen Positionen in der Datei gefunden.', 'warning')
        return redirect(url_for('aufmass.aufmass_list', project_id=project_id))

    # Aufmaß-Name
    name_parts = [s for s in [baustelle or project.bezeichnung, bauabschnitt, sm_nr, abruf_nr] if s]
    name = ' - '.join(name_parts) if name_parts else 'Importiertes Aufmaß'
    name = re.sub(r'\s+', ' ', name).strip()
    name = re.sub(r'[<>:"/\\|?*&#%{}~\[\]]', '', name)
    # Doppelten Namen vermeiden
    existing = {n[0] for n in db.session.query(Aufmass.name).filter_by(project_id=project_id).all()}
    if name in existing:
        i = 1
        while f'{name} ({i})' in existing:
            i += 1
        name = f'{name} ({i})'

    max_sort = db.session.query(db.func.max(Aufmass.sortierung)).filter_by(project_id=project_id).scalar() or 0
    a = Aufmass(project_id=project_id, name=name, typ=typ, sortierung=max_sort + 1, erstellt_von=current_user.id)
    db.session.add(a)
    db.session.flush()

    for idx, pd in enumerate(positions_data):
        pos = Position(
            project_id=project_id, aufmass_id=a.id,
            pos_nr=pd['pos_nr'], sortierung=idx + 1,
            kurztext=pd['kurztext'], einheit=pd['einheit'] or 'ST',
            einzelpreis=pd['einzelpreis'], menge=pd['menge'],
            faktor=pd['faktor'], laenge=pd['laenge'],
            breite=pd['breite'], tiefe=pd['tiefe'],
            gesamtpreis=pd['gesamtpreis'], formel_typ='standard',
            bemerkung=pd['bemerkung'],
        )
        pos.menge_hinten = pos.faktor * pos.menge
        db.session.add(pos)

    db.session.commit()

    msg = f'Aufmaß "{name}" mit {len(positions_data)} Positionen importiert.'
    flash(msg, 'success')
    ref = request.referrer or ''
    if ref.rstrip('/').endswith('/projekt'):
        return redirect(url_for('aufmass.index'))
    return redirect(url_for('aufmass.aufmass_list', project_id=project_id))


@aufmass_bp.route('/<int:project_id>/<int:aufmass_id>/positionen/import-txt', methods=['POST'])
@login_required
def positionen_import_txt(project_id, aufmass_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'schreiben'):
        return jsonify({'error': 'Zugriff verweigert'}), 403
    a = _get_aufmass_or_404(aufmass_id, project_id)
    if not a:
        flash('Aufmaß nicht gefunden.', 'danger')
        return redirect(url_for('aufmass.bearbeiten', project_id=project_id, aufmass_id=aufmass_id))
    if 'file' not in request.files:
        flash('Keine Datei ausgewählt.', 'danger')
        return redirect(url_for('aufmass.bearbeiten', project_id=project_id, aufmass_id=aufmass_id))
    f = request.files['file']
    if f.filename == '':
        flash('Keine Datei ausgewählt.', 'danger')
        return redirect(url_for('aufmass.bearbeiten', project_id=project_id, aufmass_id=aufmass_id))
    try:
        content = f.read().decode('utf-8-sig').splitlines()
    except Exception:
        content = f.read().decode('latin-1').splitlines()
    lines = [l.strip() for l in content if l.strip()]
    import re
    _tofloat = lambda s: float(s.replace(',', '.')) if s else 0.0
    max_sort = db.session.query(db.func.max(Position.sortierung)).filter_by(aufmass_id=aufmass_id).scalar() or 0
    positions_added = 0
    errors = []
    in_data = False
    for line in lines:
        if line == '[Aufmaßdaten]':
            in_data = True
            continue
        if in_data and '|' in line:
            cols = line.split('|')
            if len(cols) < 13:
                continue
            pos_nr = cols[1].strip()
            faktor = _tofloat(cols[2])
            laenge = _tofloat(cols[3])
            breite = _tofloat(cols[4])
            tiefe = _tofloat(cols[5])
            menge_aus_cols = _tofloat(cols[6])
            einheit = cols[7].strip() or 'ST'
            kurztext = cols[8].strip()
            bemerkung = cols[9].strip()
            menge2 = _tofloat(cols[10])
            einzelpreis = _tofloat(cols[11])
            gesamtpreis = _tofloat(cols[12])
            if not pos_nr:
                continue
            menge = menge_aus_cols or menge2
            if menge == 0.0 and einheit in ('M', 'M2', 'M3'):
                if einheit == 'M':
                    menge = laenge
                elif einheit == 'M2':
                    menge = laenge * breite
                elif einheit == 'M3':
                    menge = laenge * breite * tiefe
                menge *= faktor
            else:
                menge *= faktor
            max_sort += 1
            pos = Position(
                project_id=project_id, aufmass_id=aufmass_id,
                pos_nr=pos_nr, sortierung=max_sort,
                kurztext=kurztext, einheit=einheit or 'ST',
                einzelpreis=einzelpreis, menge=menge,
                faktor=faktor, laenge=laenge,
                breite=breite, tiefe=tiefe,
                gesamtpreis=gesamtpreis or round(menge * einzelpreis, 2),
                formel_typ='standard', bemerkung=bemerkung,
            )
            pos.menge_hinten = pos.faktor * pos.menge
            db.session.add(pos)
            positions_added += 1
    db.session.commit()
    msg = f'{positions_added} Positionen importiert.'
    if errors:
        msg += ' Hinweise: ' + '; '.join(errors[:3])
    flash(msg, 'success' if not errors else 'warning')
    return redirect(url_for('aufmass.bearbeiten', project_id=project_id, aufmass_id=aufmass_id))


@aufmass_bp.route('/<int:project_id>/kopfdaten-ev-holen', methods=['POST'])
@login_required
def kopfdaten_ev_holen(project_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'schreiben'):
        return jsonify({'error': 'Zugriff verweigert'}), 403
    if not current_user.darf_evergabe_nutzen or not current_user.darf_kopfdaten_holen:
        return jsonify({'error': 'Keine Berechtigung'}), 403
    company = Company.query.get(current_user.company_id)
    if not company or not company.evergabe_aktiviert:
        return jsonify({'error': 'E-Vergabe nicht freigeschaltet'}), 403
    if not company.evergabe_benutzer or not company.evergabe_passwort:
        return jsonify({'error': 'Keine Logindaten hinterlegt'}), 400
    data = request.get_json(silent=True) or {}
    sm_nr = data.get('sm_nr', '').strip()
    if not sm_nr:
        sm_nr = project.sm_nr.strip()
    if not sm_nr:
        return jsonify({'error': 'Keine SM-Nr angegeben'}), 400
    try:
        from app.services.evergabe_service import EVergabeClient
        client = EVergabeClient(
            username=company.evergabe_benutzer,
            password=company.evergabe_passwort,
            name=company.evergabe_name or ''
        )
        ev_data = client.hole_kopfdaten(sm_nr)
        import logging
        logging.getLogger(__name__).info(f'EV-Daten: {ev_data}')
        def _convert_date(d):
            if not d:
                return ''
            parts = d.strip().split('.')
            if len(parts) == 3:
                return f'{parts[2]}-{parts[1]}-{parts[0]}'
            return d
        ap_name = ev_data.get('ansprechpartner_name', '')
        ap_parts = ap_name.split()
        ap_vorname = ap_parts[0] if len(ap_parts) > 1 else ''
        ap_nachname = ' '.join(ap_parts[1:]) if len(ap_parts) > 1 else ap_name
        return jsonify({
            'bezeichnung': ev_data.get('bezeichnung', ''),
            'bauabschnitt': '',
            'sm_nr': ev_data.get('sm_nr', ''),
            'abruf_nr': ev_data.get('abruf_nr', ''),
            'datum_start': _convert_date(ev_data.get('beleg_eingang', '')),
            'datum_ende': _convert_date(ev_data.get('ausfuehrungsfrist', '')),
            'datum': _convert_date(ev_data.get('beleg_eingang', '')),
            'ev_details_id': ev_data.get('detail_id', ''),
            'ansprechpartner_vorname': ap_vorname,
            'ansprechpartner_nachname': ap_nachname,
            'ansprechpartner_tel': ev_data.get('ansprechpartner_tel', ''),
            'ansprechpartner_email': ev_data.get('ansprechpartner_email', ''),
        })
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@aufmass_bp.route('/<int:project_id>/aufmass/<int:aufmass_id>/evergabe-uebertragen', methods=['POST'])
@login_required
def aufmass_evergabe_uebertragen(project_id, aufmass_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'schreiben'):
        return jsonify({'error': 'Zugriff verweigert'}), 403
    if not current_user.darf_evergabe_nutzen or not current_user.darf_aufmass_uebertragen:
        return jsonify({'error': 'Keine Berechtigung'}), 403
    company = Company.query.get(current_user.company_id)
    if not company or not company.evergabe_aktiviert:
        return jsonify({'error': 'E-Vergabe nicht freigeschaltet'}), 403
    if not company.evergabe_benutzer or not company.evergabe_passwort:
        return jsonify({'error': 'Keine Logindaten hinterlegt'}), 400
    aufmass = Aufmass.query.get_or_404(aufmass_id)
    if aufmass.project_id != project_id:
        return jsonify({'error': 'Aufmaß nicht gefunden'}), 404
    positionen = Position.query.filter_by(aufmass_id=aufmass_id).order_by(Position.sortierung).all()
    if not positionen:
        return jsonify({'error': 'Keine Positionen vorhanden'}), 400
    pos_liste = []
    for p in positionen:
        pos_liste.append({
            'pos_nr': str(p.pos_nr or ''),
            'bezeichnung': str(p.bezeichnung or ''),
            'einheit': str(p.einheit or 'ST'),
            'menge': str(p.menge or ''),
            'faktor': str(p.faktor or '1'),
            'laenge': str(getattr(p, 'laenge', '') or ''),
            'breite': str(getattr(p, 'breite', '') or ''),
            'tiefe': str(getattr(p, 'tiefe', '') or ''),
            'langtext': str(p.langtext or ''),
            'bauabschnitt': str(project.bauabschnitt or ''),
        })
    try:
        from app.services.evergabe_service import EVergabeClient
        client = EVergabeClient(
            username=company.evergabe_benutzer,
            password=company.evergabe_passwort,
            name=company.evergabe_name or ''
        )
        ergebnisse = client.aufmass_uebertragen(
            sm_nr=project.sm_nr,
            positionen=pos_liste,
            bauabschnitt=project.bauabschnitt or '',
            leist_zeitv=str(project.datum_start or ''),
            leist_zeitb=str(project.datum_ende or ''),
            leistungsort=project.bezeichnung or '',
            schlussaufmass=(aufmass.status == 'abgeschlossen'),
        )
        ok_count = sum(1 for e in ergebnisse if e['status'] == 'ok')
        return jsonify({
            'success': True,
            'gesamt': len(ergebnisse),
            'ok': ok_count,
            'fehler': len(ergebnisse) - ok_count,
            'details': ergebnisse
        })
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@aufmass_bp.route('/<int:project_id>/aufmass/<int:aufmass_id>/umbenennen', methods=['POST'])
@login_required
def aufmass_umbenennen(project_id, aufmass_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'schreiben'):
        return jsonify({'error': 'Zugriff verweigert'}), 403
    a = _get_aufmass_or_404(aufmass_id, project_id)
    if not a:
        return jsonify({'error': 'Aufmaß nicht gefunden'}), 404
    err = _lock_error(aufmass_id)
    if err:
        return err
    data = request.get_json(silent=True) or {}
    if 'name' in data:
        a.name = data['name']
    if 'typ' in data:
        a.typ = data['typ']
    if 'status' in data and data['status'] in ('aktiv', 'abgeschlossen', 'storniert'):
        a.status = data['status']
    db.session.commit()
    return jsonify({'status': 'ok', 'name': a.name, 'typ': a.typ, 'status': a.status})

@aufmass_bp.route('/<int:project_id>/aufmass/<int:aufmass_id>/loeschen', methods=['POST'])
@login_required
def aufmass_loeschen(project_id, aufmass_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'schreiben'):
        return jsonify({'error': 'Zugriff verweigert'}), 403
    a = _get_aufmass_or_404(aufmass_id, project_id)
    if not a:
        return jsonify({'error': 'Aufmaß nicht gefunden'}), 404
    db.session.delete(a)
    db.session.commit()
    flash('Aufmaß gelöscht.', 'success')
    ref = request.referrer or ''
    if ref.rstrip('/').endswith('/projekt'):
        return redirect(url_for('aufmass.index'))
    return redirect(url_for('aufmass.aufmass_list', project_id=project_id))

@aufmass_bp.route('/<int:project_id>/<int:aufmass_id>/duplizieren', methods=['POST'])
@login_required
def aufmass_duplizieren(project_id, aufmass_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'schreiben'):
        return jsonify({'error': 'Zugriff verweigert'}), 403
    orig = _get_aufmass_or_404(aufmass_id, project_id)
    if not orig:
        return jsonify({'error': 'Aufmaß nicht gefunden'}), 404
    import re
    name = orig.name or 'Neues Aufmass'
    name = re.sub(r'\s*\(Kopie\)\s*$', '', name)
    name = name.strip() + ' (Kopie)'
    max_sort = db.session.query(db.func.max(Aufmass.sortierung)).filter_by(project_id=project_id).scalar() or 0
    new_a = Aufmass(
        project_id=project_id, name=name, typ=orig.typ, status='aktiv',
        sortierung=max_sort + 1, bemerkung=orig.bemerkung,
        erstellt_von=current_user.id
    )
    db.session.add(new_a)
    db.session.flush()
    for pos in orig.positionen.order_by(Position.sortierung).all():
        new_pos = Position(
            project_id=project_id, aufmass_id=new_a.id,
            lv_position_id=pos.lv_position_id, pos_nr=pos.pos_nr,
            sortierung=pos.sortierung, rsa=pos.rsa, abschnitt=pos.abschnitt,
            kurztext=pos.kurztext, langtext=pos.langtext,
            einheit=pos.einheit, einzelpreis=pos.einzelpreis,
            menge=pos.menge, gesamtpreis=pos.gesamtpreis,
            faktor=pos.faktor, laenge=pos.laenge, breite=pos.breite, tiefe=pos.tiefe,
            formel_typ=pos.formel_typ, formel=pos.formel,
            bemerkung=pos.bemerkung, menge_hinten=pos.menge_hinten
        )
        db.session.add(new_pos)
    db.session.commit()
    flash(f'Aufmaß "{name}" dupliziert.', 'success')
    ref = request.referrer or ''
    if ref.rstrip('/').endswith('/projekt'):
        return redirect(url_for('aufmass.index'))
    return redirect(url_for('aufmass.aufmass_list', project_id=project_id))

# === Aufmaß-Typen verwalten ===

@aufmass_bp.route('/typen')
@login_required
def typen_liste():
    if not current_user.is_firmadmin() and not current_user.is_superadmin():
        flash('Keine Berechtigung.', 'danger')
        return redirect(url_for('aufmass.index'))
    typen = AufmassTyp.query.order_by(AufmassTyp.sortierung).all()
    return render_template('aufmass/typen.html', typen=typen, titel='Aufmaß-Typen')

@aufmass_bp.route('/typen/neu', methods=['POST'])
@login_required
def typ_neu():
    if not current_user.is_firmadmin() and not current_user.is_superadmin():
        return jsonify({'error': 'Keine Berechtigung'}), 403
    name = request.form.get('name', '').strip()
    if not name:
        flash('Name erforderlich.', 'danger')
        return redirect(url_for('aufmass.typen_liste'))
    if AufmassTyp.query.filter_by(name=name).first():
        flash('Typ existiert bereits.', 'danger')
        return redirect(url_for('aufmass.typen_liste'))
    t = AufmassTyp(name=name, company_id=current_user.company_id,
                   sortierung=(db.session.query(db.func.max(AufmassTyp.sortierung)).scalar() or 0) + 1)
    db.session.add(t)
    db.session.commit()
    flash(f'Typ "{name}" angelegt.', 'success')
    return redirect(url_for('aufmass.typen_liste'))

@aufmass_bp.route('/typen/<int:typ_id>/edit', methods=['POST'])
@login_required
def typ_edit(typ_id):
    if not current_user.is_firmadmin() and not current_user.is_superadmin():
        flash('Keine Berechtigung.', 'danger')
        return redirect(url_for('aufmass.typen_liste'))
    t = AufmassTyp.query.get_or_404(typ_id)
    name = request.form.get('name', '').strip()
    if name:
        t.name = name
        db.session.commit()
        flash('Typ aktualisiert.', 'success')
    return redirect(url_for('aufmass.typen_liste'))

@aufmass_bp.route('/typen/<int:typ_id>/loeschen', methods=['POST'])
@login_required
def typ_loeschen(typ_id):
    if not current_user.is_firmadmin() and not current_user.is_superadmin():
        flash('Keine Berechtigung.', 'danger')
        return redirect(url_for('aufmass.typen_liste'))
    t = AufmassTyp.query.get_or_404(typ_id)
    db.session.delete(t)
    db.session.commit()
    flash('Typ gelöscht.', 'success')
    return redirect(url_for('aufmass.typen_liste'))

# === Editor (Positionen verwalten) ===

@aufmass_bp.route('/<int:project_id>/<int:aufmass_id>')
@login_required
def bearbeiten(project_id, aufmass_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'lesen'):
        return 'Zugriff verweigert', 403
    a = _get_aufmass_or_404(aufmass_id, project_id)
    if not a:
        flash('Aufmaß nicht gefunden.', 'danger')
        return redirect(url_for('aufmass.aufmass_list', project_id=project_id))
    positionen = Position.query.filter_by(aufmass_id=aufmass_id).order_by(Position.sortierung).all()
    lv_names = db.session.query(LVPosition.lv_name).filter_by(
        company_id=current_user.company_id
    ).distinct().order_by(LVPosition.lv_name).all()
    lv_names = [r[0] for r in lv_names]
    lv_positionen = []
    if project.lv_name:
        lv_positionen = LVPosition.query.filter_by(
            company_id=current_user.company_id,
            lv_name=project.lv_name
        ).order_by(LVPosition.order_index).all()
    modules = get_aktive_module(current_user.company_id, user=current_user)

    custom_modules = []
    all_cm = CustomModule.query.filter(
        CustomModule.is_active == True,
        CustomModule.is_template == False,
        CustomModule.company_id == current_user.company_id
    ).order_by(CustomModule.sort_index).all()
    for cm in all_cm:
        if current_user.is_superadmin() or current_user.is_firmadmin():
            custom_modules.append(cm)
        elif CustomModuleAssignment.query.filter_by(
            module_id=cm.id, user_id=current_user.id
        ).first():
            custom_modules.append(cm)

    hidden_mods = current_user.get_hidden_modules()
    hidden_set = {(h['type'], str(h['id'])) for h in hidden_mods}

    # Lock-Prüfung
    locked, holder_id = a.is_locked()
    locked_by_name = None
    if locked and holder_id == current_user.id:
        a.refresh_lock(current_user.id)
        db.session.commit()
    elif locked:
        lh = User.query.get(holder_id)
        locked_by_name = lh.full_name if lh else 'Unbekannt'
    else:
        a.try_lock(current_user.id)
        db.session.commit()

    return render_template('aufmass/bearbeiten.html',
                         project=project, aufmass=a,
                         positionen=positionen,
                         lv_names=lv_names,
                         lv_positionen=lv_positionen,
                         modules=modules,
                         custom_modules=custom_modules,
                         hidden_set=hidden_set,
                         locked_by_name=locked_by_name,
                         titel=f'Aufmaß {project.sm_nr} – {a.name}')

# === Lock-Management ===

def _lock_error(aufmass_id):
    a = Aufmass.query.get(aufmass_id)
    if not a:
        return None
    locked, holder_id = a.is_locked()
    if locked and holder_id != current_user.id:
        holder = User.query.get(holder_id)
        name = holder.full_name if holder else 'Unbekannt'
        return jsonify({'locked': True, 'holder': name}), 423
    return None

@aufmass_bp.route('/<int:project_id>/<int:aufmass_id>/lock', methods=['POST'])
@login_required
def lock_aufmass(project_id, aufmass_id):
    a = _get_aufmass_or_404(aufmass_id, project_id)
    if not a:
        return jsonify({'error': 'Aufmaß nicht gefunden'}), 404
    locked, holder_id = a.is_locked()
    if locked and holder_id != current_user.id:
        holder = User.query.get(holder_id)
        return jsonify({'locked': True, 'holder': holder.full_name if holder else 'Unbekannt'}), 423
    a.try_lock(current_user.id)
    db.session.commit()
    return jsonify({'locked': False, 'ok': True})

@aufmass_bp.route('/<int:project_id>/<int:aufmass_id>/unlock', methods=['POST'])
@login_required
def unlock_aufmass(project_id, aufmass_id):
    a = _get_aufmass_or_404(aufmass_id, project_id)
    if not a:
        return jsonify({'error': 'Aufmaß nicht gefunden'}), 404
    if a.locked_by == current_user.id:
        a.unlock()
        db.session.commit()
    return jsonify({'ok': True})

@aufmass_bp.route('/<int:project_id>/<int:aufmass_id>/heartbeat', methods=['POST'])
@login_required
def heartbeat_aufmass(project_id, aufmass_id):
    a = _get_aufmass_or_404(aufmass_id, project_id)
    if not a:
        return jsonify({'error': 'Aufmaß nicht gefunden'}), 404
    a.refresh_lock(current_user.id)
    db.session.commit()
    return jsonify({'ok': True})

# === Position CRUD (innerhalb eines Aufmaßes) ===

@aufmass_bp.route('/<int:project_id>/<int:aufmass_id>/position/hinzufuegen', methods=['POST'])
@login_required
def position_hinzufuegen(project_id, aufmass_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'schreiben'):
        return 'Zugriff verweigert', 403
    a = _get_aufmass_or_404(aufmass_id, project_id)
    if not a:
        return 'Aufmaß nicht gefunden', 404
    err = _lock_error(aufmass_id)
    if err:
        return err
    lv_pos_id = request.form.get('lv_position_id')
    if lv_pos_id:
        lv_pos = LVPosition.query.get(int(lv_pos_id))
        if not lv_pos or lv_pos.company_id != current_user.company_id:
            return 'Ungültige LV-Position', 400
        max_sort = db.session.query(db.func.max(Position.sortierung)).filter_by(
            aufmass_id=aufmass_id
        ).scalar() or 0
        pos = Position(
            project_id=project_id, aufmass_id=aufmass_id,
            lv_position_id=lv_pos.id,
            pos_nr=lv_pos.pos_nr,
            sortierung=max_sort + 1,
            rsa=lv_pos.rsa,
            abschnitt=lv_pos.abschnitt,
            kurztext=lv_pos.kurztext,
            langtext=lv_pos.langtext,
            einheit=lv_pos.einheit,
            einzelpreis=lv_pos.einzelpreis,
            faktor=1.0, laenge=0, breite=0, tiefe=0,
            menge=0, gesamtpreis=0, formel_typ='standard',
        )
        pos.berechne_menge()
    else:
        max_sort = db.session.query(db.func.max(Position.sortierung)).filter_by(
            aufmass_id=aufmass_id
        ).scalar() or 0
        pos = Position(
            project_id=project_id, aufmass_id=aufmass_id,
            pos_nr=request.form.get('pos_nr', ''),
            sortierung=max_sort + 1,
            kurztext=request.form.get('kurztext', ''),
            einheit=request.form.get('einheit', 'ST'),
            einzelpreis=float(request.form.get('einzelpreis', 0)),
            faktor=1.0, laenge=0, breite=0, tiefe=0,
            menge=0, gesamtpreis=0, formel_typ='standard',
        )
    db.session.add(pos); db.session.commit()
    return redirect(url_for('aufmass.bearbeiten', project_id=project_id, aufmass_id=aufmass_id))

@aufmass_bp.route('/<int:project_id>/<int:aufmass_id>/position/<int:pos_id>/aktualisieren', methods=['POST'])
@login_required
def position_aktualisieren(project_id, aufmass_id, pos_id):
    pos = Position.query.get_or_404(pos_id)
    if pos.project.company_id != current_user.company_id:
        return 'Zugriff verweigert', 403
    if not _check_zugriff(pos.project, 'schreiben'):
        return 'Zugriff verweigert', 403
    err = _lock_error(aufmass_id)
    if err:
        return err
    from app.utils import capture_diff
    new_state = {
        'pos_nr': request.form.get('pos_nr', pos.pos_nr),
        'kurztext': request.form.get('kurztext', pos.kurztext),
        'einheit': request.form.get('einheit', pos.einheit),
        'einzelpreis': float(request.form.get('einzelpreis', pos.einzelpreis)),
        'faktor': float(request.form.get('faktor', pos.faktor)),
        'laenge': float(request.form.get('laenge', pos.laenge)),
        'breite': float(request.form.get('breite', pos.breite)),
        'tiefe': float(request.form.get('tiefe', pos.tiefe)),
        'bemerkung': request.form.get('bemerkung', pos.bemerkung),
        'abschnitt': request.form.get('abschnitt', pos.abschnitt),
        'formel_typ': request.form.get('formel_typ', pos.formel_typ or 'standard'),
        'formel': request.form.get('formel', pos.formel)
    }
    diff = capture_diff(pos, new_state)
    for k, v in new_state.items():
        setattr(pos, k, v)
    pos.berechne_menge()
    from app.models.aufmass_history import AufmassHistory
    hist = AufmassHistory(aufmass_id=aufmass_id, changed_by=current_user.id, action='change', diff=json.dumps(diff))
    db.session.add(hist)
    db.session.commit()
    return redirect(url_for('aufmass.bearbeiten', project_id=project_id, aufmass_id=aufmass_id))

FIELDS = {'pos_nr','kurztext','einheit','einzelpreis','faktor','laenge','breite','tiefe','menge','abschnitt','bemerkung','formel_typ','formel','menge_hinten'}

@aufmass_bp.route('/<int:project_id>/<int:aufmass_id>/position/<int:pos_id>/update-cell', methods=['POST'])
@login_required
def position_update_cell(project_id, aufmass_id, pos_id):
    pos = Position.query.get_or_404(pos_id)
    if pos.project.company_id != current_user.company_id:
        return jsonify({'error': 'Zugriff verweigert'}), 403
    if not _check_zugriff(pos.project, 'schreiben'):
        return jsonify({'error': 'Zugriff verweigert'}), 403
    err = _lock_error(aufmass_id)
    if err:
        return err
    data = request.get_json() or {}
    field = data.get('field')
    value = data.get('value')
    if field not in FIELDS:
        return jsonify({'error': 'Ungültiges Feld'}), 400
    
    old_val = getattr(pos, field)
    setattr(pos, field, value)
    if field in ('faktor','laenge','breite','tiefe','formel_typ','formel','einheit','menge','menge_hinten'):
        pos.berechne_menge(recalc_hinten=(field != 'menge_hinten'), skip_menge_recalc=(field == 'menge'))
    
    # Create history entry for inline cell edit
    field_labels = {
        'kurztext': 'Kurztext', 'formel_typ': 'Z-Art', 'einheit': 'EH',
        'einzelpreis': 'EP', 'faktor': 'Faktor', 'laenge': 'Länge', 'breite': 'Breite',
        'tiefe': 'Tiefe', 'abschnitt': 'Abschnitt', 'bemerkung': 'Bemerkung',
        'menge_hinten': 'Menge hinten', 'menge': 'Menge', 'formel': 'Formel'
    }
    old_str = str(old_val) if old_val is not None else 'leer'
    new_str = str(value) if value is not None else 'leer'
    if old_str != new_str:
        desc = f"✎ Pos {pos.pos_nr or pos.id}: {field_labels.get(field, field)}: {old_str} → {new_str}"
        hist = AufmassHistory(
            aufmass_id=aufmass_id,
            position_id=pos.id,
            changed_by=current_user.id,
            action='change',
            description=desc,
            diff=json.dumps([{
                'position_id': pos.id,
                'pos_nr': pos.pos_nr or '',
                'kurztext': pos.kurztext or '',
                'row_idx': pos.sortierung - 1,
                'old': {field: old_val},
                'new': {field: value}
            }])
        )
        db.session.add(hist)
    
    db.session.commit()
    resp = {'status':'ok','menge':pos.menge,'gesamtpreis':pos.gesamtpreis,'menge_hinten':pos.menge_hinten}
    if field == 'formel_typ':
        resp['formel_typ_changed'] = True
    return jsonify(resp)

@aufmass_bp.route('/<int:project_id>/<int:aufmass_id>/position/<int:pos_id>/loeschen', methods=['POST'])
@login_required
def position_loeschen(project_id, aufmass_id, pos_id):
    pos = Position.query.get_or_404(pos_id)
    if pos.project.company_id != current_user.company_id:
        return 'Zugriff verweigert', 403
    if not _check_zugriff(pos.project, 'schreiben'):
        return 'Zugriff verweigert', 403
    err = _lock_error(aufmass_id)
    if err:
        return err
    db.session.delete(pos)
    db.session.commit()
    return redirect(url_for('aufmass.bearbeiten', project_id=project_id, aufmass_id=aufmass_id))

@aufmass_bp.route('/<int:project_id>/<int:aufmass_id>/positionen/leeren', methods=['POST'])
@login_required
def positionen_leeren(project_id, aufmass_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'schreiben'):
        return jsonify({'error': 'Zugriff verweigert'}), 403
    a = _get_aufmass_or_404(aufmass_id, project_id)
    if not a:
        return jsonify({'error': 'Aufmaß nicht gefunden'}), 404
    err = _lock_error(aufmass_id)
    if err:
        return err
    positions = Position.query.filter_by(aufmass_id=aufmass_id).all()
    changes = []
    for p in positions:
        changes.append({
            'position_id': p.id,
            'pos_nr': p.pos_nr or '',
            'old': {
                'id': p.id, 'lv_pos_id': p.lv_position_id, 'pos_nr': p.pos_nr or '',
                'kurztext': p.kurztext or '', 'faktor': p.faktor, 'laenge': p.laenge,
                'breite': p.breite, 'tiefe': p.tiefe, 'menge': p.menge,
                'einheit': p.einheit or '', 'formel_typ': p.formel_typ or 'standard',
                'formel': p.formel or '', 'abschnitt': p.abschnitt or '',
                'bemerkung': p.bemerkung or '', 'sort_order': p.sortierung
            },
            'new': {'_action': 'delete', 'id': p.id}
        })
    Position.query.filter_by(aufmass_id=aufmass_id).delete()
    if changes:
        hist = AufmassHistory(
            aufmass_id=aufmass_id,
            changed_by=current_user.id,
            action='change',
            description=f'Alle {len(changes)} Position(en) gelöscht',
            diff=json.dumps(changes)
        )
        db.session.add(hist)
    db.session.commit()
    return jsonify({'status': 'ok'})

@aufmass_bp.route('/<int:project_id>/<int:aufmass_id>/positionen/batch-loeschen', methods=['POST'])
@login_required
def positionen_batch_loeschen(project_id, aufmass_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'schreiben'):
        return jsonify({'error': 'Zugriff verweigert'}), 403
    a = _get_aufmass_or_404(aufmass_id, project_id)
    if not a:
        return jsonify({'error': 'Aufmaß nicht gefunden'}), 404
    err = _lock_error(aufmass_id)
    if err:
        return err
    data = request.get_json() or {}
    ids = data.get('ids', [])
    if not ids:
        return jsonify({'error': 'Keine IDs'}), 400
    positions = Position.query.filter(Position.id.in_(ids), Position.aufmass_id == aufmass_id).all()
    changes = []
    for p in positions:
        changes.append({
            'position_id': p.id,
            'pos_nr': p.pos_nr or '',
            'old': {
                'id': p.id, 'lv_pos_id': p.lv_position_id, 'pos_nr': p.pos_nr or '',
                'kurztext': p.kurztext or '', 'faktor': p.faktor, 'laenge': p.laenge,
                'breite': p.breite, 'tiefe': p.tiefe, 'menge': p.menge,
                'einheit': p.einheit or '', 'formel_typ': p.formel_typ or 'standard',
                'formel': p.formel or '', 'abschnitt': p.abschnitt or '',
                'bemerkung': p.bemerkung or '', 'sort_order': p.sortierung
            },
            'new': {'_action': 'delete', 'id': p.id}
        })
    Position.query.filter(Position.id.in_(ids), Position.aufmass_id == aufmass_id).delete(synchronize_session=False)
    if changes:
        hist = AufmassHistory(
            aufmass_id=aufmass_id,
            changed_by=current_user.id,
            action='change',
            description=f'{len(changes)} Position(en) gelöscht',
            diff=json.dumps(changes)
        )
        db.session.add(hist)
    db.session.commit()
    return jsonify({'status': 'ok'})

@aufmass_bp.route('/<int:project_id>/<int:aufmass_id>/positionen/kopieren', methods=['POST'])
@login_required
def positionen_kopieren(project_id, aufmass_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'schreiben'):
        return jsonify({'error': 'Zugriff verweigert'}), 403
    a = _get_aufmass_or_404(aufmass_id, project_id)
    if not a:
        return jsonify({'error': 'Aufmaß nicht gefunden'}), 404
    data = request.get_json() or {}
    ids = data.get('ids', [])
    insert_idx = data.get('insert_idx')
    if not ids:
        return jsonify({'error': 'Keine IDs'}), 400
    originals = Position.query.filter(Position.id.in_(ids), Position.aufmass_id == aufmass_id).order_by(Position.sortierung).all()
    if not originals:
        return jsonify({'error': 'Positionen nicht gefunden'}), 404
    existing = Position.query.filter_by(aufmass_id=aufmass_id).order_by(Position.sortierung).all()
    if insert_idx is not None and insert_idx >= 0:
        if insert_idx > len(existing): insert_idx = len(existing)
        sort_base = insert_idx + 1
        for i, p in enumerate(existing):
            new_sort = i + 1
            if new_sort >= sort_base:
                p.sortierung = new_sort + len(originals)
        db.session.flush()
    else:
        max_sort = db.session.query(db.func.max(Position.sortierung)).filter_by(aufmass_id=aufmass_id).scalar() or 0
        sort_base = max_sort + 1
    added = []
    added_details = []
    for orig in originals:
        copy = Position(
            project_id=project_id, aufmass_id=aufmass_id,
            lv_position_id=orig.lv_position_id,
            pos_nr=orig.pos_nr, sortierung=sort_base,
            rsa=orig.rsa or '', abschnitt=orig.abschnitt or '',
            kurztext=orig.kurztext or '', langtext=orig.langtext or '',
            einheit=orig.einheit or '', einzelpreis=orig.einzelpreis or 0,
            faktor=orig.faktor, laenge=orig.laenge, breite=orig.breite, tiefe=orig.tiefe,
            menge=orig.menge, gesamtpreis=orig.gesamtpreis,
            formel_typ=orig.formel_typ or 'standard', formel=orig.formel or '',
            bemerkung=orig.bemerkung or '',
            menge_hinten=orig.menge_hinten or 0,
        )
        db.session.add(copy)
        db.session.flush()
        added.append(copy.id)
        added_details.append({'old_id': orig.id, 'new_id': copy.id, 'pos_nr': copy.pos_nr})
        sort_base += 1
    hist = AufmassHistory(
        aufmass_id=aufmass_id,
        changed_by=current_user.id,
        action='change',
        description=f'{len(added)} Position(en) kopiert',
        diff=json.dumps([{
            'position_id': d['new_id'],
            'pos_nr': d['pos_nr'],
            'old': {'id': d['old_id']},
            'new': {'_action': 'copy', 'old_id': d['old_id'], 'new_id': d['new_id']}
        } for d in added_details])
    )
    db.session.add(hist)
    db.session.commit()
    return jsonify({'status': 'ok', 'added': added})

@aufmass_bp.route('/<int:project_id>/<int:aufmass_id>/positionen/reihenfolge', methods=['POST'])
@login_required
def positionen_reihenfolge(project_id, aufmass_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'schreiben'):
        return jsonify({'error': 'Zugriff verweigert'}), 403
    a = _get_aufmass_or_404(aufmass_id, project_id)
    if not a:
        return jsonify({'error': 'Aufmaß nicht gefunden'}), 404
    data = request.get_json()
    if not data or 'reihenfolge' not in data:
        return jsonify({'error': 'Keine Daten'}), 400
    for item in data['reihenfolge']:
        pos = Position.query.get(item['id'])
        if pos and pos.aufmass_id == aufmass_id:
            pos.sortierung = item['sortierung']
    db.session.commit()
    return jsonify({'status': 'ok'})

# === Projekt-Level Operationen (kein spezifisches Aufmaß) ===

@aufmass_bp.route('/<int:project_id>/status', methods=['POST'])
@login_required
def status_aendern(project_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'schreiben'):
        return 'Zugriff verweigert', 403
    project.status = request.form.get('status', project.status)
    db.session.commit()
    flash(f'Status geändert auf {project.status}.', 'success')
    return redirect(url_for('aufmass.aufmass_list', project_id=project_id))

@aufmass_bp.route('/<int:project_id>/loeschen', methods=['POST'])
@login_required
def project_loeschen(project_id):
    project = Project.query.get_or_404(project_id)
    if project.company_id != current_user.company_id:
        return 'Zugriff verweigert', 403
    if not _check_zugriff(project, 'schreiben'):
        return 'Zugriff verweigert', 403
    db.session.delete(project)
    db.session.commit()
    flash('Projekt gelöscht.', 'success')
    return redirect(url_for('aufmass.index'))

@aufmass_bp.route('/<int:project_id>/lv/search')
@login_required
def lv_search(project_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'lesen'):
        return jsonify([]), 403
    q = request.args.get('q', '').strip()
    base = LVPosition.query.filter_by(company_id=current_user.company_id, lv_name=project.lv_name)
    if q:
        like = f'%{q}%'
        base = base.filter(db.or_(LVPosition.pos_nr.like(like), LVPosition.kurztext.like(like), LVPosition.langtext.like(like)))
    pos = base.order_by(LVPosition.order_index).limit(200).all()
    return jsonify([{
        'id': p.id, 'pos_nr': p.pos_nr, 'kurztext': p.kurztext,
        'langtext': p.langtext, 'einheit': p.einheit, 'einzelpreis': p.einzelpreis,
        'rsa': p.rsa or '', 'abschnitt': p.abschnitt or '',
    } for p in pos])

@aufmass_bp.route('/<int:project_id>/<int:aufmass_id>/positionen/batch-add', methods=['POST'])
@login_required
def positionen_batch_add(project_id, aufmass_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'schreiben'):
        return jsonify({'error': 'Zugriff verweigert'}), 403
    a = _get_aufmass_or_404(aufmass_id, project_id)
    if not a:
        return jsonify({'error': 'Aufmaß nicht gefunden'}), 404
    err = _lock_error(aufmass_id)
    if err:
        return err
    data = request.get_json() or {}
    ids = data.get('ids', [])
    if not ids:
        return jsonify({'error': 'Keine IDs'}), 400
    insert_idx = data.get('insert_idx')
    if insert_idx is not None:
        insert_idx = int(insert_idx)
        existing = Position.query.filter_by(aufmass_id=aufmass_id).order_by(Position.sortierung).all()
        if insert_idx < 0: insert_idx = 0
        if insert_idx > len(existing): insert_idx = len(existing)
        sort_base = insert_idx + 1
        for i, p in enumerate(existing):
            new_sort = i + 1
            if new_sort >= sort_base:
                new_sort += len(ids)
            p.sortierung = new_sort
        db.session.flush()
    else:
        max_sort = db.session.query(db.func.max(Position.sortierung)).filter_by(aufmass_id=aufmass_id).scalar() or 0
        sort_base = max_sort + 1
    overrides = data.get('overrides', {})
    added_objs = []
    added = []
    for lv_id in ids:
        lv_pos = LVPosition.query.get(int(lv_id))
        if not lv_pos or lv_pos.company_id != current_user.company_id:
            continue
        pos = Position(
            project_id=project_id, aufmass_id=aufmass_id,
            lv_position_id=lv_pos.id,
            pos_nr=overrides.get('pos_nr', lv_pos.pos_nr),
            sortierung=sort_base,
            rsa=lv_pos.rsa or '', abschnitt=overrides.get('abschnitt', lv_pos.abschnitt or ''),
            kurztext=overrides.get('kurztext', lv_pos.kurztext or ''),
            langtext=lv_pos.langtext or '',
            einheit=lv_pos.einheit or '',
            einzelpreis=lv_pos.einzelpreis or 0,
            faktor=overrides.get('faktor', 1.0),
            laenge=overrides.get('laenge', 0),
            breite=overrides.get('breite', 0),
            tiefe=overrides.get('tiefe', 0),
            menge=overrides.get('menge', 0),
            gesamtpreis=overrides.get('gesamtpreis', 0),
            formel_typ=overrides.get('formel_typ', 'standard'),
            formel=overrides.get('formel', ''),
            bemerkung=overrides.get('bemerkung', ''),
            menge_hinten=overrides.get('menge_hinten', 0),
        )
        if overrides:
            pos.berechne_menge()
        else:
            pos.menge_hinten = pos.faktor * pos.menge
            pos.gesamtpreis = pos.menge_hinten * pos.einzelpreis
        db.session.add(pos)
        added_objs.append(pos)
        added.append({'id': pos.id, 'pos_nr': pos.pos_nr, 'kurztext': pos.kurztext})
        sort_base += 1
    hist = AufmassHistory(
        aufmass_id=aufmass_id,
        changed_by=current_user.id,
        action='change',
        description=f'{len(added_objs)} Position(en) aus LV hinzugefügt',
        diff=json.dumps([{
            'position_id': p.id,
            'pos_nr': p.pos_nr,
            'kurztext': p.kurztext or '',
            'row_idx': added_objs.index(p),
            'old': {'id': None},
            'new': {'_action': 'add', 'id': p.id}
        } for p in added_objs])
    )
    db.session.add(hist)
    db.session.commit()
    return jsonify({'status': 'ok', 'added': added})

@aufmass_bp.route('/<int:project_id>/<int:aufmass_id>/positionen/batch-edit', methods=['POST'])
@login_required
def positionen_batch_edit(project_id, aufmass_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'schreiben'):
        return jsonify({'error': 'Zugriff verweigert'}), 403
    a = _get_aufmass_or_404(aufmass_id, project_id)
    if not a:
        return jsonify({'error': 'Aufmaß nicht gefunden'}), 404
    err = _lock_error(aufmass_id)
    if err:
        return err
    data = request.get_json(force=True, silent=True)
    if not data:
        return jsonify({'error': 'Keine Daten (data is None)', 'raw': request.data[:200].decode('utf-8', errors='replace')}), 400
    if 'positions' not in data:
        return jsonify({'error': 'Keine positions in data', 'keys': list(data.keys())}), 400
    FIELDS = {'kurztext','formel_typ','einheit','einzelpreis','faktor','laenge','breite','tiefe','abschnitt','bemerkung','menge_hinten','menge','formel'}
    changes = []
    for pos_id, updates in data['positions'].items():
        pos = Position.query.get(int(pos_id))
        if not pos or pos.aufmass_id != aufmass_id or pos.project.company_id != current_user.company_id:
            continue
        old_values = {}
        for field, value in updates.items():
            if field not in FIELDS:
                continue
            old_values[field] = getattr(pos, field)
            setattr(pos, field, value)
        if old_values:
            row_idx = pos.sortierung - 1
            changes.append({
                'position_id': pos.id, 
                'pos_nr': pos.pos_nr, 
                'kurztext': pos.kurztext or '',
                'row_idx': row_idx,
                'old': old_values, 
                'new': updates
            })
        ft = updates.get('formel_typ') or pos.formel_typ or 'standard'
        skip_me = ('menge' in updates) and ft != 'frei'
        pos.berechne_menge(recalc_hinten=('menge_hinten' not in updates), skip_menge_recalc=skip_me)
    if changes:
        field_labels = {
            'kurztext': 'Kurztext', 'formel_typ': 'Z-Art', 'einheit': 'EH',
            'einzelpreis': 'EP', 'faktor': 'Faktor', 'laenge': 'Länge', 'breite': 'Breite',
            'tiefe': 'Tiefe', 'abschnitt': 'Abschnitt', 'bemerkung': 'Bemerkung',
            'menge_hinten': 'Menge hinten', 'menge': 'Menge', 'formel': 'Formel'
        }
        desc_parts = []
        long_parts = []
        for c in changes:
            row_label = f"Zeile {c['row_idx']+1}" if c.get('row_idx') is not None else "?"
            pos_label = f"{row_label}: Pos {c['pos_nr'] or '#'+str(c['position_id'])}"
            kurztext = c.get('kurztext', '')[:20]
            if kurztext:
                kurztext = f' "{kurztext}"'
            else:
                kurztext = ''
            changes_detail = []
            for field in c['new']:
                old_val = c['old'].get(field)
                new_val = c['new'].get(field)
                if old_val is None and new_val is None:
                    continue
                old_str = str(old_val) if old_val is not None else 'leer'
                new_str = str(new_val) if new_val is not None else 'leer'
                if old_str != new_str:
                    label = field_labels.get(field, field)
                    changes_detail.append(f'{label}: {old_str} → {new_str}')
            if changes_detail:
                short = f"✎ {pos_label}: {', '.join(changes_detail[:2])}"
                if len(changes_detail) > 2:
                    short += f' (+{len(changes_detail)-2})'
                desc_parts.append(short)
                long_parts.append(f"✎ {pos_label}{kurztext}\n   " + '\n   '.join(changes_detail))
        description = ', '.join(desc_parts[:3])
        if len(desc_parts) > 3:
            description += f' (+{len(desc_parts)-3} weitere)'
        hist = AufmassHistory(
            aufmass_id=aufmass_id,
            changed_by=current_user.id,
            action='change',
            description=description,
            diff=json.dumps(changes)
        )
        db.session.add(hist)
    db.session.commit()
    return jsonify({'ok': True})


@aufmass_bp.route('/<int:project_id>/<int:aufmass_id>/position/leer', methods=['POST'])
@login_required
def position_leer(project_id, aufmass_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'schreiben'):
        return jsonify({'error': 'Zugriff verweigert'}), 403
    a = _get_aufmass_or_404(aufmass_id, project_id)
    if not a:
        return jsonify({'error': 'Aufmaß nicht gefunden'}), 404
    err = _lock_error(aufmass_id)
    if err:
        return err
    data = request.get_json(silent=True) or {}
    insert_idx = data.get('insert_idx')
    if insert_idx is not None:
        insert_idx = int(insert_idx)
        existing = Position.query.filter_by(aufmass_id=aufmass_id).order_by(Position.sortierung).all()
        if insert_idx < 0: insert_idx = 0
        if insert_idx > len(existing): insert_idx = len(existing)
        sort_base = insert_idx + 1
        for i, p in enumerate(existing):
            new_sort = i + 1
            if new_sort >= sort_base:
                p.sortierung = new_sort + 1
        db.session.flush()
    else:
        max_sort = db.session.query(db.func.max(Position.sortierung)).filter_by(aufmass_id=aufmass_id).scalar() or 0
        sort_base = max_sort + 1
    pos = Position(
        project_id=project_id, aufmass_id=aufmass_id,
        pos_nr='', sortierung=sort_base,
        einheit='', faktor=0, laenge=0, breite=0, tiefe=0,
        menge=0, einzelpreis=0, gesamtpreis=0,
        formel_typ='standard', menge_hinten=0,
    )
    db.session.add(pos)
    db.session.flush()
    hist = AufmassHistory(
        aufmass_id=aufmass_id,
        position_id=pos.id,
        changed_by=current_user.id,
        action='change',
        description=f'Leere Zeile hinzugefügt (Pos #{pos.id})',
        diff=json.dumps([{
            'position_id': pos.id,
            'pos_nr': '',
            'old': {'id': None},
            'new': {'_action': 'add_empty', 'id': pos.id}
        }])
    )
    db.session.add(hist)
    db.session.commit()
    return jsonify({'status': 'ok', 'id': pos.id, 'pos_nr': pos.pos_nr})

@aufmass_bp.route('/<int:project_id>/<int:aufmass_id>/positionen')
@login_required
def positionen_liste(project_id, aufmass_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'lesen'):
        return jsonify([]), 403
    a = _get_aufmass_or_404(aufmass_id, project_id)
    if not a:
        return jsonify([]), 404
    pos = Position.query.filter_by(aufmass_id=aufmass_id).order_by(Position.sortierung).all()
    return jsonify([{
        'id': p.id, 'pos_nr': p.pos_nr, 'sortierung': p.sortierung,
        'rsa': p.rsa or '', 'abschnitt': p.abschnitt or '',
        'kurztext': p.kurztext, 'langtext': p.langtext,
        'einheit': p.einheit, 'einzelpreis': p.einzelpreis,
        'menge': p.menge, 'gesamtpreis': p.gesamtpreis,
        'faktor': p.faktor, 'laenge': p.laenge, 'breite': p.breite, 'tiefe': p.tiefe,
        'bemerkung': p.bemerkung or '',
        'formel_typ': p.formel_typ or 'standard', 'formel': p.formel or '',
        'menge_hinten': p.menge_hinten or 0,
    } for p in pos])

@aufmass_bp.route('/<int:project_id>/<int:aufmass_id>/positionen/farben')
@login_required
def positionen_farben(project_id, aufmass_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'lesen'):
        return jsonify({}), 403
    a = _get_aufmass_or_404(aufmass_id, project_id)
    if not a:
        return jsonify({}), 404
    pos = Position.query.filter_by(aufmass_id=aufmass_id).order_by(Position.sortierung).all()
    lv_pos_nrs = set()
    if project.lv_name:
        lvps = LVPosition.query.filter_by(company_id=current_user.company_id, lv_name=project.lv_name).all()
        lv_pos_nrs = {lp.pos_nr for lp in lvps}
    result = {}
    for p in pos:
        color_map = {}
        is_empty = not p.pos_nr and p.einheit in ('', None)
        if is_empty:
            color_map['all'] = '#F7CA14'
        else:
            e = p.einheit or ''
            m = p.menge or 0
            f = p.faktor or 0
            l = p.laenge or 0
            b = p.breite or 0
            t = p.tiefe or 0
            if e in ('ST', 'LE', 'STD', 'h', 'Psch') and m == 0:
                color_map['menge'] = '#ED686B'
            if e == 'M' and l == 0:
                color_map['laenge'] = '#ED686B'
            if e == 'M2':
                if l == 0: color_map['laenge'] = '#ED686B'
                if b == 0: color_map['breite'] = '#ED686B'
            if e in ('M3', 't'):
                if l == 0: color_map['laenge'] = '#ED686B'
                if b == 0: color_map['breite'] = '#ED686B'
                if t == 0: color_map['tiefe'] = '#ED686B'
            if p.pos_nr and p.pos_nr not in lv_pos_nrs:
                color_map['pos_nr'] = '#3370AD'
        result[str(p.id)] = color_map
    return jsonify(result)

@aufmass_bp.route('/<int:project_id>/<int:aufmass_id>/positionen/batch-reorder', methods=['POST'])
@login_required
def positionen_batch_reorder(project_id, aufmass_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'schreiben'):
        return jsonify({'error': 'Zugriff verweigert'}), 403
    a = _get_aufmass_or_404(aufmass_id, project_id)
    if not a:
        return jsonify({'error': 'Aufmaß nicht gefunden'}), 404
    err = _lock_error(aufmass_id)
    if err:
        return err
    try:
        data = request.get_json() or {}
    except Exception:
        return jsonify({'error': 'Invalid JSON'}), 400
    ids = data.get('ids', [])
    if not ids:
        return jsonify({'error': 'Keine IDs'}), 400
    try:
        ids = [int(i) for i in ids]
    except (ValueError, TypeError):
        return jsonify({'error': 'Invalid IDs'}), 400
    insert_idx = data.get('insert_idx')
    existing = Position.query.filter_by(aufmass_id=aufmass_id).order_by(Position.sortierung).all()
    if not existing:
        return jsonify({'status': 'ok', 'changes': []})
    moved_set = set(ids)
    remaining = [p for p in existing if p.id not in moved_set]
    if insert_idx is None:
        new_order = remaining + [p for p in existing if p.id in moved_set]
    elif insert_idx < 0 or insert_idx >= len(remaining) + len(moved_set):
        new_order = remaining + [p for p in existing if p.id in moved_set]
    else:
        new_order = remaining[:insert_idx] + [p for p in existing if p.id in moved_set] + remaining[insert_idx:]
    changes = []
    pos_list_for_desc = []
    for i, p in enumerate(new_order):
        old_idx = p.sortierung
        new_idx = i + 1
        if old_idx != new_idx:
            p.sortierung = new_idx
            changes.append({
                'position_id': p.id,
                'pos_nr': p.pos_nr or '',
                'row_idx': i,
                'old_idx': old_idx,
                'new_idx': new_idx,
                'kurztext': p.kurztext or '',
                'old': {'sort_order': old_idx},
                'new': {'_action': 'reorder', 'sort_order': new_idx, 'old_idx': old_idx}
            })
            pos_list_for_desc.append({
                'position_id': p.id,
                'pos_nr': p.pos_nr or '',
                'kurztext': p.kurztext or '',
                'row_idx': i,
                'old_idx': old_idx,
                'new_idx': new_idx
            })
        else:
            p.sortierung = new_idx
    if changes:
        try:
            short_desc, long_desc = _build_history_description('move', pos_list_for_desc, current_user.name or current_user.email)
            if not short_desc:
                short_desc = f'{len(changes)} Position(en) verschoben'
        except Exception:
            short_desc = f'{len(changes)} Position(en) verschoben'
        hist = AufmassHistory(
            aufmass_id=aufmass_id,
            changed_by=current_user.id,
            action='change',
            description=short_desc,
            diff=json.dumps(changes)
        )
        db.session.add(hist)
    db.session.commit()
    return jsonify({'status': 'ok', 'changes': changes})

@aufmass_bp.route('/<int:project_id>/lv-set', methods=['POST'])
@login_required
def project_lv_set(project_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'schreiben'):
        return jsonify({'error': 'Zugriff verweigert'}), 403
    project.lv_name = request.form.get('lv_name', '').strip()
    contract_id = request.form.get('contract_id', type=int)
    if contract_id:
        project.contract_id = contract_id
        contract = Contract.query.get(contract_id)
        if contract:
            project.vertrag = contract.name
    db.session.commit()
    first_aufmass = Aufmass.query.filter_by(project_id=project_id).order_by(Aufmass.sortierung).first()
    if first_aufmass:
        return redirect(url_for('aufmass.bearbeiten', project_id=project_id, aufmass_id=first_aufmass.id))
    return redirect(url_for('aufmass.aufmass_list', project_id=project_id))

@aufmass_bp.route('/<int:project_id>/kopfdaten', methods=['POST'])
@login_required
def project_kopfdaten_save(project_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'schreiben'):
        return jsonify({'error': 'Zugriff verweigert'}), 403
    project.vertrag = request.form.get('vertrag', '').strip()
    project.lv_name = request.form.get('lv_name', '').strip()
    project.bezeichnung = request.form.get('bezeichnung', '').strip()
    project.baustelle = request.form.get('baustelle', '').strip()
    project.bauabschnitt = request.form.get('bauabschnitt', '').strip()
    project.sm_nr = request.form.get('sm_nr', '').strip()
    project.abruf_nr = request.form.get('abruf_nr', '').strip()
    project.datum_start = _parse_date(request.form.get('datum_start'))
    project.datum_ende = _parse_date(request.form.get('datum_ende'))
    project.ansprechpartner_vorname = request.form.get('ansprechpartner_vorname', '').strip()
    project.ansprechpartner_nachname = request.form.get('ansprechpartner_nachname', '').strip()
    project.ansprechpartner_tel = request.form.get('ansprechpartner_tel', '').strip()
    project.ansprechpartner_email = request.form.get('ansprechpartner_email', '').strip()
    aufmass_id = request.form.get('aufmass_id', type=int)
    if aufmass_id:
        aufmass = Aufmass.query.get(aufmass_id)
        if aufmass and aufmass.project_id == project_id:
            aufmass.typ = request.form.get('typ', '').strip()
            aufmass.datum = _parse_date(request.form.get('datum'))
    if request.form.get('datum'):
        project.datum = _parse_date(request.form.get('datum'))
    db.session.commit()
    flash('Kopfdaten gespeichert.', 'success')
    return redirect(request.referrer or url_for('aufmass.bearbeiten', project_id=project_id, aufmass_id=aufmass_id or 0))

@aufmass_bp.route('/<int:project_id>/update-name', methods=['POST'])
@login_required
def project_update_name(project_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'schreiben'):
        return jsonify({'error': 'Zugriff verweigert'}), 403
    name = request.form.get('name', '').strip()
    if not name:
        return jsonify({'error': 'Name darf nicht leer sein'}), 400
    import re
    invalid_chars = r'[<>:"/\\|?*&#%{}~\[\]]'
    if re.search(invalid_chars, name):
        return jsonify({'error': 'Name enthält ungültige Zeichen'}), 400
    project.bezeichnung = name
    db.session.commit()
    return jsonify({'name': name})


@aufmass_bp.route('/module-reorder', methods=['POST'])
@login_required
def module_reorder():
    data = request.get_json(silent=True)
    if not data or 'order' not in data:
        return jsonify({'error': 'Keine Daten'}), 400

    from app.models.module import Module
    from app.models.custom_module import CustomModule

    for i, item in enumerate(data['order']):
        if item.get('type') == 'module':
            mod = Module.query.get(int(item['id']))
            if mod:
                mod.sortierung = i
        elif item.get('type') == 'custom':
            cm = CustomModule.query.get(int(item['id']))
            if cm and cm.company_id == current_user.company_id:
                cm.sort_index = i
    db.session.commit()
    return jsonify({'ok': True})


@aufmass_bp.route('/module-toggle-hidden', methods=['POST'])
@login_required
def module_toggle_hidden():
    data = request.get_json(silent=True)
    if not data or 'type' not in data or 'id' not in data:
        return jsonify({'error': 'Ungültige Daten'}), 400

    hidden = current_user.get_hidden_modules()
    entry = {'type': data['type'], 'id': data['id']}

    found = False
    for h in hidden:
        if h['type'] == entry['type'] and h['id'] == entry['id']:
            hidden.remove(h)
            found = True
            break
    if not found:
        hidden.append(entry)

    current_user.set_hidden_modules(hidden)
    db.session.commit()
    return jsonify({'hidden': found, 'type': data['type'], 'id': data['id']})


@aufmass_bp.route('/module-toggle-hidden-batch', methods=['POST'])
@login_required
def module_toggle_hidden_batch():
    data = request.get_json(silent=True)
    if not data or 'entries' not in data or 'action' not in data:
        return jsonify({'error': 'Ungültige Daten'}), 400

    hidden = current_user.get_hidden_modules()
    action = data['action']  # 'hide' or 'show'

    for entry in data['entries']:
        key = {'type': entry['type'], 'id': str(entry['id'])}
        found = False
        for h in hidden:
            if h['type'] == key['type'] and h['id'] == key['id']:
                if action == 'show':
                    hidden.remove(h)
                found = True
                break
        if not found and action == 'hide':
            hidden.append(key)

    current_user.set_hidden_modules(hidden)
    db.session.commit()
    return jsonify({'ok': True, 'action': action, 'count': len(data['entries'])})


def _parse_date(s):
    if not s:
        return None
    for fmt in ('%Y-%m-%d', '%d.%m.%Y', '%Y.%m.%d'):
        try:
            return datetime.strptime(s.strip(), fmt).date()
        except ValueError:
            continue
    return None


@aufmass_bp.route('/<int:project_id>/<int:aufmass_id>/undo', methods=['POST'])
@login_required
def undo(project_id, aufmass_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'schreiben'):
        return jsonify({'error': 'Zugriff verweigert'}), 403
    a = _get_aufmass_or_404(aufmass_id, project_id)
    if not a:
        return jsonify({'error': 'Aufmaß nicht gefunden'}), 404
    last_change = (AufmassHistory.query
                   .filter_by(aufmass_id=aufmass_id, action='change')
                   .order_by(AufmassHistory.id.desc())
                   .first())
    if not last_change:
        return jsonify({'error': 'Nichts zum Rückgängig machen'}), 400
    changes = json.loads(last_change.diff)
    for change in changes:
        pos = Position.query.get(change['position_id'])
        if not pos:
            continue
        new_data = change.get('new', {})
        old_data = change.get('old', {})
        if new_data.get('_action') in ('add', 'add_empty', 'add_form'):
            db.session.delete(pos)
        elif new_data.get('_action') == 'delete':
            pos_id = change.get('position_id')
            pos_deleted = Position.query.get(pos_id)
            if not pos_deleted:
                lv_id = old_data.get('lv_pos_id') if old_data.get('lv_pos_id') else None
                pos_deleted = Position(
                    aufmass_id=aufmass_id,
                    lv_pos_id=lv_id,
                    pos_nr=old_data.get('pos_nr', ''),
                    kurztext=old_data.get('kurztext', ''),
                    faktor=old_data.get('faktor', 1.0),
                    laenge=old_data.get('laenge', 0),
                    breite=old_data.get('breite', 0),
                    tiefe=old_data.get('tiefe', 0),
                    menge=old_data.get('menge', 0),
                    einheit=old_data.get('einheit', ''),
                    formel_typ=old_data.get('formel_typ', 'standard'),
                    formel=old_data.get('formel', ''),
                    abschnitt=old_data.get('abschnitt', ''),
                    bemerkung=old_data.get('bemerkung', ''),
                    sort_order=old_data.get('sort_order', 0)
                )
                db.session.add(pos_deleted)
            else:
                for field, val in old_data.items():
                    if field not in ('_action', 'id'):
                        setattr(pos_deleted, field, val)
                db.session.add(pos_deleted)
        elif new_data.get('_action') == 'copy':
            new_pos = Position.query.get(new_data.get('new_id'))
            if new_pos:
                db.session.delete(new_pos)
        elif new_data.get('_action') == 'reorder':
            # For reorder undo, we need to restore the old order of ALL positions
            # The old order is stored in row_idx and order in new_data
            old_order = new_data.get('order', [])
            if old_order:
                # Get all positions for this aufmass and reorder them
                all_pos = Position.query.filter_by(aufmass_id=aufmass_id).order_by(Position.sortierung).all()
                for i, pos_id in enumerate(old_order):
                    pos = Position.query.get(pos_id)
                    if pos:
                        pos.sortierung = i + 1
            else:
                # Fallback: just restore sort_order from old data
                old_sort = old_data.get('sort_order')
                if old_sort:
                    pos.sortierung = old_sort
        else:
            for field, old_val in old_data.items():
                if field != '_action':
                    setattr(pos, field, old_val)
            ft = pos.formel_typ or 'standard'
            skip_me = ('menge' in old_data) and ft != 'frei'
            pos.berechne_menge(recalc_hinten=('menge_hinten' not in old_data), skip_menge_recalc=skip_me)
    undo_hist = AufmassHistory(
        aufmass_id=aufmass_id,
        changed_by=current_user.id,
        action='undo',
        description=f"Rückgängig: {last_change.description}",
        diff=json.dumps({'undo_of': last_change.id})
    )
    db.session.add(undo_hist)
    db.session.commit()
    return jsonify({'ok': True, 'message': f'Rückgängig: {last_change.description[:50]}'})


@aufmass_bp.route('/<int:project_id>/<int:aufmass_id>/redo', methods=['POST'])
@login_required
def redo(project_id, aufmass_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'schreiben'):
        return jsonify({'error': 'Zugriff verweigert'}), 403
    a = _get_aufmass_or_404(aufmass_id, project_id)
    if not a:
        return jsonify({'error': 'Aufmaß nicht gefunden'}), 404
    last_undo = (AufmassHistory.query
                 .filter_by(aufmass_id=aufmass_id, action='undo')
                 .order_by(AufmassHistory.id.desc())
                 .first())
    if not last_undo:
        return jsonify({'error': 'Nichts zum Wiederholen'}), 400
    changes = json.loads(last_undo.diff)
    if 'undo_of' not in changes:
        return jsonify({'error': 'Ungültiger Undo-Eintrag'}), 400
    original = AufmassHistory.query.get(changes['undo_of'])
    if not original:
        return jsonify({'error': 'Original nicht gefunden'}), 400
    original_changes = json.loads(original.diff)
    for change in original_changes:
        pos = Position.query.get(change['position_id'])
        new_data = change.get('new', {})
        old_data = change.get('old', {})
        if new_data.get('_action') in ('add', 'add_empty', 'add_form'):
            # Redo add: Position wiederherstellen
            if not pos:
                pos = Position(
                    aufmass_id=aufmass_id,
                    lv_position_id=old_data.get('lv_pos_id'),
                    pos_nr=old_data.get('pos_nr', ''),
                    kurztext=old_data.get('kurztext', ''),
                    faktor=old_data.get('faktor', 1.0),
                    laenge=old_data.get('laenge', 0),
                    breite=old_data.get('breite', 0),
                    tiefe=old_data.get('tiefe', 0),
                    menge=old_data.get('menge', 0),
                    einheit=old_data.get('einheit', ''),
                    formel_typ=old_data.get('formel_typ', 'standard'),
                    formel=old_data.get('formel', ''),
                    abschnitt=old_data.get('abschnitt', ''),
                    bemerkung=old_data.get('bemerkung', ''),
                    sort_order=old_data.get('sort_order', 0)
                )
                db.session.add(pos)
                db.session.flush()
        elif new_data.get('_action') == 'delete':
            # Redo delete: Position löschen
            if pos:
                db.session.delete(pos)
        elif new_data.get('_action') == 'copy':
            # Redo copy: Kopie wiederherstellen
            old_id = new_data.get('old_id')
            orig = Position.query.get(old_id)
            if orig:
                copy = Position(
                    project_id=project_id, aufmass_id=aufmass_id,
                    lv_position_id=orig.lv_position_id,
                    pos_nr=orig.pos_nr,
                    sortierung=pos.sortierung if pos else old_data.get('sort_order', 0) + 1,
                    rsa=orig.rsa or '', abschnitt=orig.abschnitt or '',
                    kurztext=orig.kurztext or '', langtext=orig.langtext or '',
                    einheit=orig.einheit or '', einzelpreis=orig.einzelpreis or 0,
                    faktor=orig.faktor, laenge=orig.laenge, breite=orig.breite, tiefe=orig.tiefe,
                    menge=orig.menge, gesamtpreis=orig.gesamtpreis,
                    formel_typ=orig.formel_typ or 'standard', formel=orig.formel or '',
                    bemerkung=orig.bemerkung or '',
                    menge_hinten=orig.menge_hinten or 0,
                )
                db.session.add(copy)
                db.session.flush()
                # Update the original position reference
                if pos:
                    pos.sortierung = new_data.get('new_id')
        elif new_data.get('_action') == 'reorder':
            # Redo reorder: Neue Reihenfolge wiederherstellen
            new_order = new_data.get('order', [])
            if new_order:
                for i, pos_id in enumerate(new_order):
                    p = Position.query.get(pos_id)
                    if p:
                        p.sortierung = i + 1
        else:
            # Normal edit: new values wiederherstellen
            if pos:
                for field, new_val in new_data.items():
                    if field != '_action':
                        setattr(pos, field, new_val)
                ft = pos.formel_typ or 'standard'
                skip_me = ('menge' in new_data) and ft != 'frei'
                pos.berechne_menge(recalc_hinten=('menge_hinten' not in new_data), skip_menge_recalc=skip_me)
    redo_hist = AufmassHistory(
        aufmass_id=aufmass_id,
        changed_by=current_user.id,
        action='redo',
        description=f"Wiederholt: {original.description}",
        diff=json.dumps({'redo_of': original.id})
    )
    db.session.add(redo_hist)
    db.session.commit()
    return jsonify({'ok': True, 'message': f'Wiederholt: {original.description[:50]}'})


@aufmass_bp.route('/<int:project_id>/<int:aufmass_id>/history', methods=['GET'])
@login_required
def history(project_id, aufmass_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'lesen'):
        return jsonify({'error': 'Zugriff verweigert'}), 403
    a = _get_aufmass_or_404(aufmass_id, project_id)
    if not a:
        return jsonify({'error': 'Aufmaß nicht gefunden'}), 404
    limit = request.args.get('limit', 10, type=int)
    hist_list = (AufmassHistory.query
                 .filter_by(aufmass_id=aufmass_id)
                 .order_by(AufmassHistory.changed_at.desc())
                 .limit(limit)
                 .all())
    
    data = []
    for h in hist_list:
        user = User.query.get(h.changed_by)
        user_name = f"{user.vorname} {user.nachname}".strip() if user else 'Unbekannt'
        try:
            changes = json.loads(h.diff) if h.diff else []
        except Exception as e:
            
            changes = []
        
        # Build long description
        long_desc = h.description or ''
        if h.action == 'undo' and changes.get('undo_of'):
            # For undo, get original action description and build long version
            orig = AufmassHistory.query.get(changes['undo_of'])
            if orig:
                try:
                    orig_changes = json.loads(orig.diff) if orig.diff else []
                    long_desc = _build_long_description(orig_changes, user_name, h.changed_at, prefix='↩ Rückgängig: ')
                except:
                    long_desc = f"↩ Rückgängig: {orig.description}"
        elif h.action == 'redo' and changes.get('undo_of'):
            # For redo, the diff contains undo_of pointing to the undo entry
            # We need to get the original change from that undo entry
            undo_entry = AufmassHistory.query.get(changes['undo_of'])
            if undo_entry and undo_entry.action == 'undo':
                undo_diff = json.loads(undo_entry.diff) if undo_entry.diff else {}
                if undo_diff.get('undo_of'):
                    orig = AufmassHistory.query.get(undo_diff['undo_of'])
                    if orig:
                        try:
                            orig_changes = json.loads(orig.diff) if orig.diff else []
                            long_desc = _build_long_description(orig_changes, user_name, h.changed_at, prefix='↪ Wiederholt: ')
                        except:
                            long_desc = f"↪ Wiederholt: {orig.description}"
        elif changes:
# Normal case: build from changes
            try:
                long_desc = _build_long_description(changes, user_name, h.changed_at)
            except Exception as e:
                long_desc = h.description or ''
        
        data.append({
            'id': h.id,
            'user': user.email if user else 'unbekannt',
            'user_name': user_name,
            'time': h.changed_at.strftime('%d.%m.%Y %H:%M:%S'),
            'time_iso': h.changed_at.isoformat(),
            'action': h.action,
            'description': h.description or '',
            'long_description': long_desc,
            'diff': changes
        })
    return jsonify(data)


def _build_long_description(changes, user_name, dt, prefix=''):
    """Baut die lange History-Beschreibung."""
    if not changes:
        return ''
    from datetime import datetime
    now = dt.strftime('%d.%m.%Y %H:%M') if dt else datetime.now().strftime('%d.%m.%Y %H:%M')
    lines = [f"[{now}] {user_name}"]
    
    field_labels = {
        'faktor': 'Faktor', 'laenge': 'Länge', 'breite': 'Breite', 'tiefe': 'Tiefe',
        'menge': 'Menge', 'menge_hinten': 'Menge hinten', 'einheit': 'EH',
        'abschnitt': 'Abschnitt', 'bemerkung': 'Bemerkung', 'formel': 'Formel',
        'formel_typ': 'Z-Art', 'kurztext': 'Kurztext', 'pos_nr': 'Pos-Nr'
    }
    
    for c in changes:
        pos_nr = c.get('pos_nr', c.get('position_id', '?'))
        kurztext = c.get('kurztext', '')
        row_idx = c.get('row_idx')
        line_num = f"Zeile {row_idx + 1}" if row_idx is not None else "?"
        kt = f' "{kurztext}"' if kurztext else ''
        
        old_data = c.get('old', {})
        new_data = c.get('new', {})
        action = new_data.get('_action', 'edit') if new_data else 'edit'
        
        if action in ('add', 'add_empty', 'add_form'):
            lines.append(f"+ {line_num}: Pos {pos_nr}{kt} - HINZUGEFÜGT")
        elif action == 'delete':
            lines.append(f"- {line_num}: Pos {pos_nr}{kt} - GELÖSCHT")
            if old_data:
                details = []
                for k, v in old_data.items():
                    if k not in ('_action', 'id') and v:
                        lbl = field_labels.get(k, k)
                        details.append(f"  {lbl}: {v}")
                if details:
                    lines.extend(details[:5])
        elif action == 'copy':
            lines.append(f"⎘ {line_num}: Pos {pos_nr}{kt} - KOPIERT")
        elif action == 'reorder':
            old_idx = old_data.get('sort_order', '?')
            new_idx = new_data.get('sort_order', '?')
            if old_idx != new_idx:
                direction = '↓' if new_idx > old_idx else '↑'
                lines.append(f"↔ {line_num}: Pos {pos_nr}{kt} - {old_idx} {direction} {new_idx}")
        else:
            # Edit
            diffs = []
            for field in set(list(old_data.keys()) + list(new_data.keys())):
                if field.startswith('_') or field == 'id':
                    continue
                old_v = old_data.get(field)
                new_v = new_data.get(field)
                if old_v is None and new_v is None:
                    continue
                old_s = str(old_v) if old_v is not None else 'leer'
                new_s = str(new_v) if new_v is not None else 'leer'
                if old_s != new_s:
                    lbl = field_labels.get(field, field)
                    diffs.append(f"  {lbl}: {old_s} → {new_s}")
            if diffs:
                lines.append(f"✎ {line_num}: Pos {pos_nr}{kt}")
                lines.extend(diffs[:4])
                if len(diffs) > 4:
                    lines.append(f"  ... (+{len(diffs)-4} weitere)")
    
    if prefix and lines:
        lines[0] = prefix + lines[0]
    return '\n'.join(lines)


@aufmass_bp.route('/<int:project_id>/<int:aufmass_id>/history/count', methods=['GET'])
@login_required
def history_count(project_id, aufmass_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'lesen'):
        return jsonify({'error': 'Zugriff verweigert'}), 403
    a = _get_aufmass_or_404(aufmass_id, project_id)
    if not a:
        return jsonify({'error': 'Aufmaß nicht gefunden'}), 404
    change_count = AufmassHistory.query.filter_by(aufmass_id=aufmass_id, action='change').count()
    undo_count = AufmassHistory.query.filter_by(aufmass_id=aufmass_id, action='undo').count()
    return jsonify({'changes': change_count, 'undo_count': undo_count, 'can_undo': change_count > undo_count})

@aufmass_bp.route('/<int:project_id>/<int:aufmass_id>/history/<int:history_id>/undo', methods=['POST'])
@login_required
def history_undo_specific(project_id, aufmass_id, history_id):
    project = Project.query.get_or_404(project_id)
    if not _check_zugriff(project, 'schreiben'):
        return jsonify({'error': 'Zugriff verweigert'}), 403
    a = _get_aufmass_or_404(aufmass_id, project_id)
    if not a:
        return jsonify({'error': 'Aufmaß nicht gefunden'}), 404
    
    h = AufmassHistory.query.get_or_404(history_id)
    if h.aufmass_id != aufmass_id:
        return jsonify({'error': 'History-Eintrag gehört nicht zu diesem Aufmaß'}), 400
    
    if h.action != 'change':
        return jsonify({'error': 'Nur Änderungen können rückgängig gemacht werden'}), 400
    
    try:
        changes = json.loads(h.diff) if h.diff else []
    except:
        return jsonify({'error': 'Ungültige History-Daten'}), 400
    
    ft_cache = {}
    for c in changes:
        pos_id = c.get('position_id')
        if not pos_id:
            continue
        pos = Position.query.get(pos_id)
        if not pos:
            continue
        
        old_data = c.get('old', {})
        new_data = c.get('new', {})
        action = new_data.get('_action', 'edit') if new_data else 'edit'
        
        if action in ('add', 'add_empty', 'add_form'):
            # Delete the position
            Position.query.filter_by(id=pos_id).delete()
        elif action == 'delete':
            # Restore the position from old_data
            rest_pos = Position(
                aufmass_id=aufmass_id,
                lv_position_id=old_data.get('lv_pos_id'),
                pos_nr=old_data.get('pos_nr', ''),
                kurztext=old_data.get('kurztext', ''),
                faktor=old_data.get('faktor', 1.0),
                laenge=old_data.get('laenge', 0),
                breite=old_data.get('breite', 0),
                tiefe=old_data.get('tiefe', 0),
                menge=old_data.get('menge', 0),
                einheit=old_data.get('einheit', ''),
                formel_typ=old_data.get('formel_typ', 'standard'),
                formel=old_data.get('formel', ''),
                abschnitt=old_data.get('abschnitt', ''),
                bemerkung=old_data.get('bemerkung', ''),
                sort_order=old_data.get('sort_order', 0)
            )
            db.session.add(rest_pos)
        elif action == 'copy':
            # Delete the copied position
            Position.query.filter_by(id=pos_id).delete()
        elif action == 'reorder':
            # Restore old sort order
            old_order = old_data.get('sort_order')
            if old_order:
                pos.sortierung = old_order
        else:
            # Edit - restore old values
            for field, old_val in old_data.items():
                if field != '_action' and field != 'id':
                    setattr(pos, field, old_val)
            ft = pos.formel_typ or 'standard'
            skip_me = ('menge' in old_data) and ft != 'frei'
            pos.berechne_menge(recalc_hinten=('menge_hinten' not in old_data), skip_menge_recalc=skip_me)
    
    # Create undo history entry
    undo_hist = AufmassHistory(
        aufmass_id=aufmass_id,
        changed_by=current_user.id,
        action='undo',
        description=f"Rückgängig (aus History): {h.description[:50]}",
        diff=json.dumps({'undo_of': h.id})
    )
    db.session.add(undo_hist)
    db.session.commit()
    return jsonify({'ok': True, 'message': f'Rückgängig gemacht: {h.description[:50]}'})
