import fs from 'fs';
import path from 'path';
import { Op } from 'sequelize';
import { PDFDocument, StandardFonts } from 'pdf-lib';
import sequelize from '../database.js';
import BillingTemplate from '../models/BillingTemplate.js';
import BillingTemplateField from '../models/BillingTemplateField.js';
import BillingRun from '../models/BillingRun.js';
import BillingDocument from '../models/BillingDocument.js';
import BillingDocumentValue from '../models/BillingDocumentValue.js';
import BillingUserSetting from '../models/BillingUserSetting.js';
import DiaryDate from '../models/DiaryDates.js';
import { checkAccess, getUserByToken } from '../utils/userUtils.js';

const ensureNumber = (value, fallback = 0) => {
  const parsed = Number.parseFloat(value);
  if (!Number.isFinite(parsed)) return fallback;
  return Number(parsed.toFixed(2));
};

const toMonthDate = (value, endOfMonth = false) => {
  const monthMatch = String(value || '').match(/^(\d{4})-(\d{2})$/);
  if (!monthMatch) return null;
  const year = Number(monthMatch[1]);
  const month = Number(monthMatch[2]);
  if (!year || month < 1 || month > 12) return null;
  if (!endOfMonth) return `${monthMatch[1]}-${monthMatch[2]}-01`;
  const lastDay = new Date(year, month, 0).getDate();
  return `${monthMatch[1]}-${monthMatch[2]}-${String(lastDay).padStart(2, '0')}`;
};

class BillingService {
  _normalizeLocale(locale) {
    const fallbackLocale = 'de-DE';
    const raw = String(locale || '').trim();
    if (!raw) return fallbackLocale;
    const normalized = raw.replace('_', '-');
    try {
      const [resolved] = Intl.NumberFormat.supportedLocalesOf([normalized]);
      if (resolved) return resolved;
      const baseLanguage = normalized.split('-')[0];
      const [resolvedBase] = Intl.NumberFormat.supportedLocalesOf([baseLanguage]);
      return resolvedBase || fallbackLocale;
    } catch (error) {
      return fallbackLocale;
    }
  }

  _formatNumberForLocale(value, locale, digits = 2) {
    const numberValue = ensureNumber(value, 0);
    return new Intl.NumberFormat(this._normalizeLocale(locale), {
      minimumFractionDigits: digits,
      maximumFractionDigits: digits,
      useGrouping: false
    }).format(numberValue);
  }

  _resolveExistingFilePath(storedPath) {
    const rawPath = String(storedPath || '').trim();
    if (!rawPath) return null;

    const candidates = [];
    if (path.isAbsolute(rawPath)) {
      candidates.push(rawPath);
    } else {
      const normalizedRawPath = rawPath.replace(/^\.?[\\/]/, '');
      candidates.push(path.resolve(rawPath));
      candidates.push(path.resolve(process.cwd(), rawPath));
      candidates.push(path.resolve(process.cwd(), normalizedRawPath));
      candidates.push(path.resolve(process.cwd(), 'backend', normalizedRawPath));
      candidates.push(path.resolve(process.cwd(), '..', normalizedRawPath));
    }

    for (const candidate of candidates) {
      if (candidate && fs.existsSync(candidate)) {
        return candidate;
      }
    }

    // Fallback fuer migrierte/verschobene Workspaces:
    // Datei ueber den Dateinamen in bekannten Upload-Ordnern suchen.
    const baseName = path.basename(rawPath);
    if (!baseName || baseName === '.' || baseName === '/') {
      return null;
    }
    const templateDirs = [
      path.resolve(process.cwd(), 'uploads', 'billing-templates'),
      path.resolve(process.cwd(), 'backend', 'uploads', 'billing-templates'),
      path.resolve(process.cwd(), '..', 'backend', 'uploads', 'billing-templates'),
      path.resolve(process.cwd(), '..', 'uploads', 'billing-templates')
    ];
    for (const dir of templateDirs) {
      const candidate = path.join(dir, baseName);
      if (fs.existsSync(candidate)) {
        return candidate;
      }
    }

    return null;
  }

  _normalizeIban(raw, withoutCountry = false) {
    const cleaned = String(raw || '').replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
    if (!cleaned) return '';
    if (!withoutCountry) return cleaned;
    return cleaned.replace(/^[A-Z]{2}/, '');
  }

  _normalizeFieldName(value) {
    return String(value || '')
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, '');
  }

  _setPdfFieldText(field, value) {
    if (value == null) return false;
    const text = String(value);
    if (typeof field?.setText === 'function') {
      field.setText(text);
      return true;
    }
    if (typeof field?.select === 'function') {
      field.select(text);
      return true;
    }
    return false;
  }

  _setPdfFieldCheckbox(field, checked) {
    if (typeof field?.check === 'function' && typeof field?.uncheck === 'function') {
      if (checked) field.check();
      else field.uncheck();
      return true;
    }
    // Fallback for templates where "checkbox" is implemented as text field.
    if (typeof field?.setText === 'function') {
      field.setText(checked ? 'X' : '');
      return true;
    }
    return false;
  }

  _setByCandidates(fieldEntries, candidates, value, type = 'text') {
    const normalizedCandidates = (candidates || []).map((c) => this._normalizeFieldName(c));
    for (const entry of fieldEntries) {
      if (!normalizedCandidates.some((candidate) => entry.normalizedName.includes(candidate))) continue;
      if (type === 'checkbox') {
        if (this._setPdfFieldCheckbox(entry.field, Boolean(value))) return true;
      } else if (this._setPdfFieldText(entry.field, value)) {
        return true;
      }
    }
    return false;
  }

  _setByExactName(fieldEntries, exactName, value, type = 'text') {
    const wanted = this._normalizeFieldName(exactName);
    const hit = (fieldEntries || []).find((entry) => entry.normalizedName === wanted);
    if (!hit) return false;
    if (type === 'checkbox') return this._setPdfFieldCheckbox(hit.field, Boolean(value));
    return this._setPdfFieldText(hit.field, value);
  }

  _removeByCandidates(form, fieldEntries, candidates = []) {
    const normalizedCandidates = (candidates || []).map((c) => this._normalizeFieldName(c));
    let removed = 0;
    for (const entry of fieldEntries || []) {
      if (!normalizedCandidates.some((candidate) => entry.normalizedName.includes(candidate))) continue;
      try {
        form.removeField(entry.field);
        removed += 1;
      } catch (error) {
        // Ignore removal issues for individual fields.
      }
    }
    return removed;
  }

  _toGermanDate(dateValue) {
    const raw = String(dateValue || '').trim();
    const m = raw.match(/^(\d{4})-(\d{2})-(\d{2})$/);
    if (!m) return raw;
    return `${m[3]}.${m[2]}.${m[1]}`;
  }

  _formatIbanGrouped(raw) {
    const cleaned = String(raw || '').replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
    if (!cleaned) return '';
    return cleaned.replace(/(.{4})/g, '$1 ').trim();
  }

  _buildFormFieldDiagnostics(fieldEntries = []) {
    const checks = [
      { key: 'self_recipient_name', candidates: ['self_recipient_name', 'trainer_name', 'name_des_uebungsleiters', 'uebungsleiter', 'name'] },
      { key: 'iban', candidates: ['iban', 'konto', 'kontonummer'] },
      { key: 'location_text', candidates: ['location_text', 'ort'] },
      { key: 'document_date', candidates: ['document_date', 'datum', 'date', 'abrechnungsdatum', 'datum_abrechnung', 'ortdatum'] },
      { key: 'hours_total', candidates: ['hours_total', 'gesamtstunden', 'summe_stunden', 'uestdn', 'u_stdn', 'dauer_summe', 'dauergesamt'] },
      { key: 'hours_total_2', candidates: ['hours_total_2', 'gesamtstunden2', 'summe_stunden2', 'dauer_abrechnung', 'dauerabrechnung'] },
      { key: 'hourly_rate', candidates: ['hourly_rate', 'stunden_gehalt', 'stundensatz', 'betrag_pro_stunde', 'gehalt_stunden'] },
      { key: 'amount_total', candidates: ['amount_total', 'gesamtbetrag', 'summe_eur', 'betrag_gesamt', 'gehalt_gesamt'] },
      { key: 'same_account_checkbox', candidates: ['same_account_checkbox', 'same_account', 'gleicheskonto', 'gleiches_konto'] }
    ];
    const found = [];
    const missing = [];
    for (const check of checks) {
      const hasMatch = (fieldEntries || []).some((entry) => {
        return check.candidates.some((candidate) => entry.normalizedName.includes(this._normalizeFieldName(candidate)));
      });
      if (hasMatch) found.push(check.key);
      else missing.push(check.key);
    }
    const availableFieldNames = (fieldEntries || []).map((entry) => entry.name);
    return { found, missing, availableFieldNames };
  }

  _fillSessionRows(fieldEntries, sessions, locale = 'de-DE') {
    if (!Array.isArray(sessions) || !sessions.length) return;
    const rows = sessions.slice(0, 40);
    for (let idx = 0; idx < rows.length; idx += 1) {
      const rowNo = idx + 1;
      const row = rows[idx];
      const label = row.label || `${row.startTime || ''}-${row.endTime || ''}`.trim();
      const hours = this._formatNumberForLocale(row.durationHours || 0, locale, 2);

      const dateCandidates = [
        `date_${rowNo}`, `datum_${rowNo}`, `datum${rowNo}`, `lesson_date_${rowNo}`, `training_date_${rowNo}`, `zeile${rowNo}datum`
      ];
      const labelCandidates = [
        `label_${rowNo}`, `bezeichner_${rowNo}`, `bezeichnung${rowNo}`, `bezeichung${rowNo}`, `zeit_${rowNo}`, `lesson_label_${rowNo}`, `training_label_${rowNo}`, `zeile${rowNo}zeit`
      ];
      const hoursCandidates = [
        `hours_${rowNo}`, `stunden_${rowNo}`, `dauer_${rowNo}`, `dauer${rowNo}`, `lesson_hours_${rowNo}`, `training_hours_${rowNo}`, `zeile${rowNo}stunden`
      ];

      const rowDate = this._toGermanDate(row.date || '');
      this._setByCandidates(fieldEntries, dateCandidates, rowDate);
      this._setByCandidates(fieldEntries, labelCandidates, label);
      this._setByCandidates(fieldEntries, hoursCandidates, hours);
    }
  }

  async _renderBillingPdfFromTemplate(templatePath, outputPath, run, computedSessions, hoursTotal, amountTotal, locale = 'de-DE') {
    const templateBytes = fs.readFileSync(templatePath);
    const pdfDoc = await PDFDocument.load(templateBytes);
    const form = pdfDoc.getForm();
    const fieldEntries = form.getFields().map((field) => {
      const name = field.getName();
      return { field, name, normalizedName: this._normalizeFieldName(name) };
    });
    const diagnostics = this._buildFormFieldDiagnostics(fieldEntries);
    if (!fieldEntries.length) {
      throw new Error('Vorlagen-PDF enthält keine Formularfelder (AcroForm). Fehlende Felder: self_recipient_name, iban, location_text, document_date, hours_total, hourly_rate, amount_total, same_account_checkbox.');
    }

    const nameValue = run.omitSelfRecipientName ? '' : (run.selfRecipientName || '');
    const ibanRawValue = run.omitIban ? '' : this._normalizeIban(run.iban, run.ibanWithoutCountry);
    const ibanValue = this._formatIbanGrouped(ibanRawValue);
    const locationValue = run.omitLocationText ? '' : (run.locationText || '');
    const dateValue = run.omitDocumentDate ? '' : (run.documentDate || '');
    const germanDateValue = this._toGermanDate(dateValue);
    const hoursText = this._formatNumberForLocale(hoursTotal || 0, locale, 2);
    const amountNumericText = this._formatNumberForLocale(amountTotal || 0, locale, 2);
    const amountText = `${amountNumericText} EUR`;
    const hourlyRateText = this._formatNumberForLocale(run.hourlyRate || 0, locale, 2);

    this._setByCandidates(fieldEntries, ['self_recipient_name', 'trainer_name', 'name_des_uebungsleiters', 'uebungsleiter', 'name'], nameValue);
    this._setByCandidates(fieldEntries, ['iban', 'konto', 'kontonummer'], ibanValue);
    if (!String(ibanValue || '').trim()) {
      // If IBAN is omitted/empty, remove matching form fields so no border/frame remains visible.
      this._removeByCandidates(form, fieldEntries, ['iban', 'konto', 'kontonummer']);
    }
    this._setByCandidates(fieldEntries, ['location_text', 'ort'], locationValue);
    const dateCandidates = ['document_date', 'datum', 'date', 'abrechnungsdatum', 'datum_abrechnung', 'ortdatum'];
    // Prefer exact field names first, so "datum" does not accidentally hit "datum1..14".
    const dateSetExact = this._setByExactName(fieldEntries, 'datum', germanDateValue)
      || this._setByExactName(fieldEntries, 'document_date', germanDateValue)
      || this._setByExactName(fieldEntries, 'ortdatum', germanDateValue)
      || this._setByExactName(fieldEntries, 'datum_abrechnung', germanDateValue)
      || this._setByExactName(fieldEntries, 'abrechnungsdatum', germanDateValue);
    if (!dateSetExact) {
      this._setByCandidates(fieldEntries, dateCandidates, germanDateValue);
    }
    this._setByCandidates(fieldEntries, ['hours_total', 'gesamtstunden', 'summe_stunden', 'uestdn', 'u_stdn', 'dauer_summe', 'dauergesamt'], hoursText);
    this._setByCandidates(fieldEntries, ['hours_total_2', 'gesamtstunden2', 'summe_stunden2', 'dauer_abrechnung', 'dauerabrechnung'], hoursText);
    // First: explicit exact matches for this TSV Bonames form.
    const hourlyExactSet = this._setByExactName(fieldEntries, 'gehalt_stunden', hourlyRateText)
      || this._setByExactName(fieldEntries, 'gehalt_stunden', hourlyRateText);
    const amountExactSet = this._setByExactName(fieldEntries, 'gehalt_gesamt', amountNumericText)
      || this._setByExactName(fieldEntries, 'gehalt_gesamt', amountNumericText);

    if (!hourlyExactSet) {
      this._setByCandidates(fieldEntries, ['hourly_rate', 'stunden_gehalt', 'stundensatz', 'betrag_pro_stunde', 'gehalt_stunden'], hourlyRateText);
    }
    const amountSet = amountExactSet || this._setByCandidates(fieldEntries, ['gehalt_gesamt'], amountNumericText);
    if (!amountSet) {
      this._setByCandidates(fieldEntries, ['amount_total', 'gesamtbetrag', 'summe_eur', 'betrag_gesamt'], amountText);
    }
    this._setByCandidates(fieldEntries, ['same_account_checkbox', 'same_account', 'gleicheskonto', 'gleiches_konto'], Boolean(run.sameAccountCheckbox), 'checkbox');

    this._fillSessionRows(fieldEntries, computedSessions, locale);

    if (!diagnostics.found.length) {
      const fieldPreview = diagnostics.availableFieldNames.slice(0, 30).join(', ');
      throw new Error(`Keine erwarteten Formularfelder erkannt. Verfuegbare Feldnamen: ${fieldPreview || '(leer)'}. Fehlend: ${diagnostics.missing.join(', ')}.`);
    }

    // Ensure visible appearances for all filled fields in final PDF.
    const appearanceFont = await pdfDoc.embedFont(StandardFonts.Helvetica);
    form.updateFieldAppearances(appearanceFont);
    form.flatten();

    const outBytes = await pdfDoc.save();
    fs.mkdirSync(path.dirname(outputPath), { recursive: true });
    fs.writeFileSync(outputPath, outBytes);
  }

  async _collectTrainingSessions(clubId, periodStart, periodEnd) {
    const diaryDates = await DiaryDate.findAll({
      where: {
        clubId,
        date: { [Op.between]: [periodStart, periodEnd] },
        trainingStart: { [Op.ne]: null },
        trainingEnd: { [Op.ne]: null },
        excludeFromBilling: false,
      },
      attributes: ['date', 'trainingStart', 'trainingEnd'],
      order: [['date', 'ASC'], ['trainingStart', 'ASC']]
    });

    const sessions = [];
    let totalHours = 0;
    for (const entry of diaryDates) {
      const start = String(entry.trainingStart || '');
      const end = String(entry.trainingEnd || '');
      const [sh = 0, sm = 0] = start.split(':').map((v) => Number.parseInt(v, 10) || 0);
      const [eh = 0, em = 0] = end.split(':').map((v) => Number.parseInt(v, 10) || 0);
      const startMinutes = sh * 60 + sm;
      const endMinutes = eh * 60 + em;
      if (endMinutes <= startMinutes) continue;
      const durationHours = Number((((endMinutes - startMinutes) / 60)).toFixed(2));
      totalHours += durationHours;
      sessions.push({
        date: entry.date,
        startTime: start.slice(0, 5),
        endTime: end.slice(0, 5),
        durationHours
      });
    }

    return {
      sessions,
      totalHours: Number(totalHours.toFixed(2))
    };
  }

  async getUserSettings(userToken, clubId) {
    await checkAccess(userToken, clubId);
    const user = await getUserByToken(userToken);
    const settings = await BillingUserSetting.findOne({ where: { clubId, userId: user.id } });
    return {
      status: 200,
      response: {
        success: true,
        settings: {
          lastHourlyRate: settings ? ensureNumber(settings.lastHourlyRate, 0) : null,
          lastSelfRecipientName: settings?.lastSelfRecipientName || null,
          lastLocationText: settings?.lastLocationText || null
        }
      }
    };
  }

  async calculateHoursPreview(userToken, clubId, monthFrom, monthTo) {
    await checkAccess(userToken, clubId);
    const periodStart = toMonthDate(monthFrom, false);
    const periodEnd = toMonthDate(monthTo, true);
    if (!periodStart || !periodEnd) {
      return { status: 400, response: { success: false, error: 'Monat oder Monatsbereich ungültig' } };
    }
    const preview = await this._collectTrainingSessions(clubId, periodStart, periodEnd);
    return {
      status: 200,
      response: {
        success: true,
        computedHoursTotal: preview.totalHours,
        sessions: preview.sessions
      }
    };
  }

  async listTemplates(userToken, clubId) {
    await checkAccess(userToken, clubId);
    const templates = await BillingTemplate.findAll({
      where: { clubId },
      include: [{ model: BillingTemplateField, as: 'fields', required: false }],
      order: [['updatedAt', 'DESC'], [{ model: BillingTemplateField, as: 'fields' }, 'sortOrder', 'ASC']]
    });
    return { status: 200, response: { success: true, templates } };
  }

  async createTemplate(userToken, clubId, payload, file) {
    await checkAccess(userToken, clubId);
    if (!file) {
      return { status: 400, response: { success: false, error: 'PDF-Datei fehlt' } };
    }
    const name = String(payload?.name || '').trim();
    if (!name) {
      return { status: 400, response: { success: false, error: 'Vorlagenname fehlt' } };
    }

    const user = await getUserByToken(userToken);
    const existing = await BillingTemplate.count({ where: { clubId, name } });
    const template = await BillingTemplate.create({
      clubId,
      name,
      description: payload?.description ? String(payload.description).trim() : null,
      pdfStoragePath: path.resolve(file.path),
      pdfFilename: file.originalname,
      pdfMimeType: file.mimetype || 'application/pdf',
      isActive: true,
      version: existing + 1,
      createdByUserId: user.id
    });

    return { status: 201, response: { success: true, template } };
  }

  async deleteTemplate(userToken, templateId) {
    const template = await BillingTemplate.findByPk(templateId);
    if (!template) {
      return { status: 404, response: { success: false, error: 'Vorlage nicht gefunden' } };
    }
    await checkAccess(userToken, template.clubId);

    const runCount = await BillingRun.count({ where: { templateId: template.id } });
    if (runCount > 0) {
      return {
        status: 409,
        response: {
          success: false,
          error: 'Vorlage kann nicht gelöscht werden, da bereits Abrechnungsläufe darauf basieren.'
        }
      };
    }

    const transaction = await sequelize.transaction();
    try {
      await BillingTemplateField.destroy({ where: { templateId: template.id }, transaction });
      await BillingTemplate.destroy({ where: { id: template.id }, transaction });
      await transaction.commit();
    } catch (error) {
      await transaction.rollback();
      throw error;
    }

    const storedTemplatePath = this._resolveExistingFilePath(template.pdfStoragePath);
    if (storedTemplatePath) {
      try {
        fs.unlinkSync(storedTemplatePath);
      } catch (error) {
        // Ignore file cleanup errors, DB deletion already succeeded.
      }
    }

    return { status: 200, response: { success: true } };
  }

  async saveTemplateFields(userToken, templateId, fields = []) {
    const template = await BillingTemplate.findByPk(templateId);
    if (!template) {
      return { status: 404, response: { success: false, error: 'Vorlage nicht gefunden' } };
    }
    await checkAccess(userToken, template.clubId);
    if (!Array.isArray(fields)) {
      return { status: 400, response: { success: false, error: 'Feldliste ungültig' } };
    }

    const transaction = await sequelize.transaction();
    try {
      await BillingTemplateField.destroy({ where: { templateId }, transaction });
      for (let i = 0; i < fields.length; i += 1) {
        const field = fields[i] || {};
        if (!field.fieldKey || !field.label || !field.fieldType) continue;
        await BillingTemplateField.create({
          templateId,
          fieldKey: String(field.fieldKey).trim(),
          label: String(field.label).trim(),
          fieldType: field.fieldType,
          sourceType: field.sourceType || 'manual',
          sourcePath: field.sourcePath || null,
          constantValue: field.constantValue || null,
          formatter: field.formatter || 'none',
          isRequired: Boolean(field.isRequired),
          mappingMode: field.mappingMode || 'overlay',
          acroformFieldName: field.acroformFieldName || null,
          pageNumber: field.pageNumber ?? null,
          x: field.x ?? null,
          y: field.y ?? null,
          width: field.width ?? null,
          height: field.height ?? null,
          fontSize: field.fontSize ?? null,
          align: field.align ?? null,
          formulaExpression: field.formulaExpression || null,
          tableGroup: field.tableGroup || null,
          rowIndex: field.rowIndex ?? null,
          sortOrder: field.sortOrder ?? i
        }, { transaction });
      }
      await transaction.commit();
      const refreshed = await BillingTemplate.findByPk(templateId, {
        include: [{ model: BillingTemplateField, as: 'fields', required: false }]
      });
      return { status: 200, response: { success: true, template: refreshed } };
    } catch (error) {
      await transaction.rollback();
      throw error;
    }
  }

  async createRun(userToken, clubId, payload) {
    await checkAccess(userToken, clubId);
    const user = await getUserByToken(userToken);
    const templateId = Number.parseInt(payload?.templateId, 10);
    const template = Number.isInteger(templateId) ? await BillingTemplate.findByPk(templateId) : null;
    if (!template || String(template.clubId) !== String(clubId)) {
      return { status: 400, response: { success: false, error: 'Vorlage ungültig' } };
    }
    const periodStart = toMonthDate(payload?.monthFrom, false);
    const periodEnd = toMonthDate(payload?.monthTo, true);
    if (!periodStart || !periodEnd) {
      return { status: 400, response: { success: false, error: 'Monat oder Monatsbereich ungültig' } };
    }
    const hourlyRate = ensureNumber(payload?.hourlyRate, -1);
    if (hourlyRate < 0) {
      return { status: 400, response: { success: false, error: 'Stunden-Gehalt fehlt oder ist ungültig' } };
    }

    const computed = await this._collectTrainingSessions(clubId, periodStart, periodEnd);
    const computedHoursTotal = computed.totalHours;

    const run = await BillingRun.create({
      clubId,
      templateId,
      name: payload?.name ? String(payload.name).trim() : `Abrechnung ${payload?.monthFrom || ''}`,
      periodStart,
      periodEnd,
      selfRecipientUserId: user.id,
      selfRecipientName: payload?.selfRecipientName ? String(payload.selfRecipientName).trim() : user.email,
      hourlyRate,
      computedHoursTotal,
      sessionLabel: payload?.sessionLabel ? String(payload.sessionLabel).trim() : null,
      iban: payload?.iban ? String(payload.iban).trim() : null,
      ibanWithoutCountry: Boolean(payload?.ibanWithoutCountry),
      sameAccountCheckbox: Boolean(payload?.sameAccountCheckbox),
      omitSelfRecipientName: Boolean(payload?.omitSelfRecipientName),
      omitIban: Boolean(payload?.omitIban),
      omitLocationText: Boolean(payload?.omitLocationText),
      omitDocumentDate: Boolean(payload?.omitDocumentDate),
      omitSessionLabel: Boolean(payload?.omitSessionLabel),
      locationText: payload?.locationText ? String(payload.locationText).trim() : null,
      documentDate: payload?.documentDate || null,
      status: 'draft',
      createdByUserId: user.id
    });
    await BillingUserSetting.upsert({
      clubId,
      userId: user.id,
      lastHourlyRate: hourlyRate,
      lastSelfRecipientName: payload?.selfRecipientName ? String(payload.selfRecipientName).trim() : user.email,
      lastLocationText: payload?.locationText ? String(payload.locationText).trim() : null
    });
    return { status: 201, response: { success: true, run } };
  }

  async listRuns(userToken, clubId) {
    await checkAccess(userToken, clubId);
    const runs = await BillingRun.findAll({
      where: { clubId },
      include: [{ model: BillingDocument, as: 'documents', required: false }],
      order: [['createdAt', 'DESC']]
    });
    return { status: 200, response: { success: true, runs } };
  }

  async getRunDetails(userToken, runId) {
    const run = await BillingRun.findByPk(runId, {
      include: [{
        model: BillingDocument,
        as: 'documents',
        required: false,
        include: [{ model: BillingDocumentValue, as: 'values', required: false }]
      }]
    });
    if (!run) {
      return { status: 404, response: { success: false, error: 'Abrechnungslauf nicht gefunden' } };
    }
    await checkAccess(userToken, run.clubId);
    return { status: 200, response: { success: true, run } };
  }

  async deleteRun(userToken, runId) {
    const run = await BillingRun.findByPk(runId);
    if (!run) {
      return { status: 404, response: { success: false, error: 'Abrechnungslauf nicht gefunden' } };
    }
    await checkAccess(userToken, run.clubId);
    const transaction = await sequelize.transaction();
    try {
      const docs = await BillingDocument.findAll({
        where: { runId: run.id },
        attributes: ['id'],
        transaction
      });
      const docIds = docs.map((d) => d.id);
      if (docIds.length) {
        await BillingDocumentValue.destroy({
          where: { billingDocumentId: { [Op.in]: docIds } },
          transaction
        });
      }
      await BillingDocument.destroy({ where: { runId: run.id }, transaction });
      await run.destroy({ transaction });
      await transaction.commit();
      return { status: 200, response: { success: true } };
    } catch (error) {
      await transaction.rollback();
      throw error;
    }
  }

  async generateRun(userToken, runId, payload = {}) {
    const run = await BillingRun.findByPk(runId, {
      include: [{ model: BillingTemplate, as: 'template', include: [{ model: BillingTemplateField, as: 'fields', required: false }] }]
    });
    if (!run) {
      return { status: 404, response: { success: false, error: 'Abrechnungslauf nicht gefunden' } };
    }
    await checkAccess(userToken, run.clubId);
    if (!run.template) {
      console.warn('[billing.generateRun] Vorlage fehlt für Run', {
        runId: run.id,
        clubId: run.clubId,
        templateId: run.templateId
      });
      return { status: 404, response: { success: false, error: 'Vorlage für diesen Abrechnungslauf nicht gefunden. Bitte neuen Abrechnungslauf mit aktueller Vorlage erstellen.' } };
    }
    const resolvedTemplatePath = this._resolveExistingFilePath(run.template?.pdfStoragePath);
    if (!resolvedTemplatePath) {
      console.warn('[billing.generateRun] Vorlagen-PDF nicht gefunden', {
        runId: run.id,
        clubId: run.clubId,
        templateId: run.templateId,
        storedPath: run.template?.pdfStoragePath,
        cwd: process.cwd()
      });
      return { status: 404, response: { success: false, error: 'Vorlagen-PDF nicht gefunden' } };
    }
    const numberLocale = this._normalizeLocale(payload?.locale || payload?.language || payload?.lang);
    const computed = await this._collectTrainingSessions(run.clubId, run.periodStart, run.periodEnd);
    const sessionLabel = run.omitSessionLabel ? '' : String(run.sessionLabel || '');
    const sessionsWithLabel = computed.sessions.map((session) => ({
      ...session,
      label: sessionLabel
    }));
    const hoursTotal = ensureNumber(computed.totalHours, 0);
    const amountTotal = Number((hoursTotal * ensureNumber(run.hourlyRate, 0)).toFixed(2));
    const generatedFilename = `${(run.name || 'abrechnung').replace(/[^\w.-]+/g, '_')}-${run.id}.pdf`;
    const generatedPath = path.resolve('uploads/billing-generated', generatedFilename);

    await this._renderBillingPdfFromTemplate(
      resolvedTemplatePath,
      generatedPath,
      run,
      sessionsWithLabel,
      hoursTotal,
      amountTotal,
      numberLocale
    );

    const transaction = await sequelize.transaction();
    try {
      const existingDoc = await BillingDocument.findOne({ where: { runId: run.id }, transaction });
      let document;
      if (existingDoc) {
        existingDoc.displayName = run.selfRecipientName;
        existingDoc.status = 'generated';
        existingDoc.pdfFilename = generatedFilename;
        existingDoc.pdfStoragePath = generatedPath;
        await existingDoc.save({ transaction });
        await BillingDocumentValue.destroy({ where: { billingDocumentId: existingDoc.id }, transaction });
        document = existingDoc;
      } else {
        document = await BillingDocument.create({
          runId: run.id,
          displayName: run.selfRecipientName,
          status: 'generated',
          pdfFilename: generatedFilename,
          pdfStoragePath: generatedPath
        }, { transaction });
      }

      const values = [
        { fieldKey: 'self_recipient_name', resolvedValue: run.selfRecipientName, resolvedSource: 'billing_run.self_recipient_name' },
        { fieldKey: 'hourly_rate', resolvedValue: String(run.hourlyRate), resolvedSource: 'billing_run.hourly_rate' },
        { fieldKey: 'hours_total', resolvedValue: String(hoursTotal), resolvedSource: 'diary_dates.sum(duration)' },
        { fieldKey: 'hours_total_2', resolvedValue: String(hoursTotal), resolvedSource: 'diary_dates.sum(duration)' },
        { fieldKey: 'iban', resolvedValue: this._normalizeIban(run.iban, run.ibanWithoutCountry), resolvedSource: 'billing_run.iban' },
        { fieldKey: 'amount_total', resolvedValue: String(amountTotal), resolvedSource: 'calculation:hours_total*hourly_rate' },
        { fieldKey: 'lessons_rows', resolvedValue: JSON.stringify(sessionsWithLabel), resolvedSource: 'diary_dates(trainingStart/trainingEnd)' },
        { fieldKey: 'lessons_count', resolvedValue: String(computed.sessions.length), resolvedSource: 'diary_dates.count' },
        { fieldKey: 'same_account_checkbox', resolvedValue: run.sameAccountCheckbox ? 'X' : '', resolvedSource: 'billing_run.same_account_checkbox' },
        { fieldKey: 'session_label', resolvedValue: sessionLabel, resolvedSource: 'billing_run.session_label' },
        { fieldKey: 'location_text', resolvedValue: run.locationText || '', resolvedSource: 'billing_run.location_text' },
        { fieldKey: 'document_date', resolvedValue: run.documentDate || '', resolvedSource: 'billing_run.document_date' }
      ];

      for (const entry of values) {
        await BillingDocumentValue.create({
          billingDocumentId: document.id,
          fieldKey: entry.fieldKey,
          resolvedValue: entry.resolvedValue,
          resolvedSource: entry.resolvedSource
        }, { transaction });
      }

      run.status = 'generated';
      run.computedHoursTotal = hoursTotal;
      await run.save({ transaction });
      await transaction.commit();

      const generated = await BillingDocument.findByPk(document.id, {
        include: [{ model: BillingDocumentValue, as: 'values', required: false }]
      });
      return { status: 200, response: { success: true, document: generated } };
    } catch (error) {
      await transaction.rollback();
      throw error;
    }
  }

  async downloadTemplatePdf(userToken, templateId) {
    const template = await BillingTemplate.findByPk(templateId);
    if (!template) return { status: 404, response: { success: false, error: 'Vorlage nicht gefunden' } };
    await checkAccess(userToken, template.clubId);
    const resolvedTemplatePath = this._resolveExistingFilePath(template.pdfStoragePath);
    if (!resolvedTemplatePath) {
      return { status: 404, response: { success: false, error: 'Datei nicht gefunden' } };
    }
    return {
      status: 200,
      file: {
        path: resolvedTemplatePath,
        name: template.pdfFilename,
        mimeType: template.pdfMimeType || 'application/pdf'
      }
    };
  }

  async downloadGeneratedRunPdf(userToken, runId) {
    const run = await BillingRun.findByPk(runId, {
      include: [{ model: BillingDocument, as: 'documents', required: false }]
    });
    if (!run) return { status: 404, response: { success: false, error: 'Abrechnungslauf nicht gefunden' } };
    await checkAccess(userToken, run.clubId);
    const docs = Array.isArray(run.documents) ? run.documents : [];
    const latestDoc = docs.slice().sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt))[0];
    if (!latestDoc?.pdfStoragePath || !fs.existsSync(latestDoc.pdfStoragePath)) {
      return { status: 404, response: { success: false, error: 'Erzeugte PDF-Datei nicht gefunden' } };
    }
    return {
      status: 200,
      file: {
        path: path.resolve(latestDoc.pdfStoragePath),
        name: latestDoc.pdfFilename || `abrechnung-${run.id}.pdf`,
        mimeType: 'application/pdf'
      }
    };
  }
}

export default new BillingService();
