const DocumentTemplateService = require('../../services/template/DocumentTemplateService');
const ContractUploadService = require('../../services/contracts/ContractUploadService');
const { S3Client, PutObjectCommand, GetObjectCommand, ListObjectsV2Command, CopyObjectCommand, DeleteObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
const PDFDocument = require('pdfkit');
const stream = require('stream');
const puppeteer = require('puppeteer');
const fs = require('fs');
const path = require('path');
const DEBUG_PDF_FILES = !!process.env.DEBUG_PDF_FILES;
const db = require('../../database/database');
const UnitOfWork = require('../../database/UnitOfWork');
const { logger } = require('../../middleware/logger');
const CompanyStampService = require('../../services/stamp/company/CompanyStampService');
const I18nPreferencesRepository = require('../../repositories/settings/I18nPreferencesRepository');
const { s3: sharedExoscaleClient } = require('../../utils/exoscaleUploader');
const { normalizeLanguageCode, resolveDocumentTemplateStorageFolder } = require('../../utils/languageUtils');

const i18nPreferencesRepository = new I18nPreferencesRepository();

// Ensure debug directory exists and helper to save files
function ensureDebugDir() {
  const debugDir = path.join(__dirname, '../../debug-pdf');
  if (!fs.existsSync(debugDir)) fs.mkdirSync(debugDir, { recursive: true });
  return debugDir;
}
function saveDebugFile(filename, data, encoding = 'utf8') {
  if (!DEBUG_PDF_FILES) return;
  const debugDir = ensureDebugDir();
  const p = path.join(debugDir, filename);
  try {
    let folderStructureWarning = null;
    if (Buffer.isBuffer(data)) fs.writeFileSync(p, data);
    else fs.writeFileSync(p, data, { encoding });
    logger.debug(`[DEBUG] Wrote debug file: ${p}`);
  } catch (e) {
    logger.error('[DEBUG] Failed to write debug file', p, e);
  }
}

function normalizeInvoiceTaxMode(rawValue) {
  const normalized = rawValue !== undefined && rawValue !== null
    ? String(rawValue).trim().toLowerCase()
    : rawValue;
  return ['standard', 'reverse_charge', 'both'].includes(normalized) ? normalized : 'both';
}

async function resolveSupportedTemplateLanguage(rawValue) {
  const normalized = normalizeLanguageCode(rawValue);
  if (!normalized) return null;

  const languages = await i18nPreferencesRepository.listLanguages({ enabledOnly: true });
  return languages.some((entry) => entry.languageCode === normalized) ? normalized : null;
}

function getRequestedTemplateLanguage(req) {
  return normalizeLanguageCode(
    req?.query?.lang
      || req?.query?.language
      || req?.user?.lang
      || req?.user?.language
      || '',
  ) || null;
}

function buildTemplateStorageKey(lang, originalName) {
  const langFolder = resolveDocumentTemplateStorageFolder(lang);
  return `DocumentTemplates/${langFolder}/${Date.now()}_${originalName}`;
}

// Helper to remove/empty placeholders except allow-list
// Updated: match any content inside {{ ... }} (not only \w+) so placeholders like {{company.name}} are sanitized.
// allowList contains exact placeholder names to preserve (e.g. 'companyStamp', 'companyLogo', 'profitplanetSignature').
function sanitizePlaceholders(html, allowList = ['currentDate','companyStamp','companyStampInline','companyStampSmall','companyLogo','profitplanetSignature']) {
  if (!html || typeof html !== 'string') return html;
  const allowSet = new Set((allowList || []).map(s => String(s).trim()).filter(Boolean));

  let total = 0;
  let preserved = 0;
  const cleaned = html.replace(/{{\s*([^}]+?)\s*}}/g, (match, inner) => {
    total++;
    const name = String(inner || '').trim();
    // preserve exact allow-list matches
    if (allowSet.has(name)) {
      preserved++;
      return match;
    }
    // preserve allow-list items that are prefixes (e.g. allow 'companyStamp' to keep 'companyStampInline' variants)
    for (const a of allowSet) {
      if (name.startsWith(a)) {
        preserved++;
        return match;
      }
    }
    // otherwise remove/empty
    return '';
  });

  logger.debug('[sanitizePlaceholders] placeholders cleaned', { total, preserved, removed: total - preserved });
  return cleaned;
}

// Helper to get signed URLs and raw HTML from S3
// NOTE: added optional serverBaseUrl parameter. When provided, previewUrl for contract templates will point
// to a backend preview endpoint (avoids direct S3 CORS issues).
async function enrichTemplate(template, s3, serverBaseUrl = null) {
  let previewUrl = null;
  let fileUrl = null;
  let html = null;
  try {
    logger.debug(`[enrichTemplate] template.id=${template.id} storageKey=${template.storageKey} lang=${template.lang} version=${template.version}`);
    const command = new GetObjectCommand({
      Bucket: process.env.EXOSCALE_BUCKET,
      Key: template.storageKey
    });
    // original signed URL (useful as fileUrl)
    try {
      fileUrl = await getSignedUrl(s3, command, { expiresIn: 600 }); // 10 min
    } catch (e) {
      logger.warn('[enrichTemplate] failed to create signed URL for original file', e && e.message);
      fileUrl = null;
    }

    // Fetch raw HTML content
    const fileObj = await s3.send(command);
    if (fileObj.Body) {
      html = await streamToString(fileObj.Body, template.id);
      logger.debug(`[enrichTemplate] fetched html length=${html ? html.length : 0}`);
      // Save full raw HTML for offline inspection when debug enabled
      if (DEBUG_PDF_FILES && html) {
        saveDebugFile(`template_${template.id}_html_full.html`, html);
      }
    } else {
      logger.warn(`[enrichTemplate] S3 returned empty Body for key=${template.storageKey}`);
    }

    // NEW: Do NOT expose previews for active contract templates (remove preview for "aktive contracts")
    if (template.type === 'contract' && template.state === 'active') {
      previewUrl = null; // explicitly hide preview
      logger.debug('[enrichTemplate] hiding previewUrl for active contract', { id: template.id });
    } else if (template.type === 'contract' && html) {
      // previous sanitized-preview behavior for non-active contracts (kept)
      try {
        const sanitized = sanitizePlaceholders(html);
        if (DEBUG_PDF_FILES) saveDebugFile(`template_${template.id}_sanitized_preview.html`, sanitized);

        // optional: upload sanitized preview to S3 (keeps earlier behavior for offline debugging)
        try {
          const langFolder = resolveDocumentTemplateStorageFolder(template.lang);
          const previewKey = `DocumentTemplates/previews/${langFolder}/template_${template.id}_preview_${Date.now()}.html`;
          const putCmd = new PutObjectCommand({
            Bucket: process.env.EXOSCALE_BUCKET,
            Key: previewKey,
            Body: sanitized,
            ContentType: 'text/html; charset=utf-8'
          });
          await s3.send(putCmd);
          logger.debug('[enrichTemplate] uploaded sanitized preview to S3', { previewKey });
        } catch (eUpload) {
          logger.warn('[enrichTemplate] failed to upload sanitized preview to S3 (continue)', eUpload && eUpload.message);
        }

        if (serverBaseUrl) {
          // CHANGED: route path must match getRoutes.js
          previewUrl = `${serverBaseUrl}/document-templates/${template.id}/preview`;
        } else {
          previewUrl = fileUrl;
        }
        logger.debug('[enrichTemplate] previewUrl assigned', { previewUrl: !!previewUrl });
      } catch (e) {
        logger.warn('[enrichTemplate] failed to produce sanitized preview', e && e.message);
        previewUrl = fileUrl;
      }
    } else {
      // non-contract or other cases: use original signed URL as preview
      previewUrl = fileUrl;
    }
  } catch (e) {
    logger.error(`[enrichTemplate] Error fetching S3 object for key=${template.storageKey}`, e && e.stack ? e.stack : e);
    previewUrl = previewUrl || null;
    fileUrl = fileUrl || null;
    html = html || null;
  }
  return {
    ...template,
    previewUrl,
    fileUrl,
    html,
    version: template.version
  };
}

// Hardened reader for S3 bodies (Node stream, async iterable, web stream, or Buffer)
async function s3BodyToBuffer(body) {
  if (!body) return null;
  if (typeof body.transformToByteArray === 'function') {
    const arr = await body.transformToByteArray();
    return Buffer.from(arr);
  }
  if (typeof body.getReader === 'function') {
    const reader = body.getReader();
    const chunks = [];
    // eslint-disable-next-line no-constant-condition
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      chunks.push(Buffer.from(value));
    }
    return Buffer.concat(chunks);
  }
  if (body[Symbol.asyncIterator]) {
    const chunks = [];
    for await (const chunk of body) {
      chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
    }
    return Buffer.concat(chunks);
  }
  if (typeof body.on === 'function') {
    return new Promise((resolve, reject) => {
      const chunks = [];
      body.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
      body.on('end', () => resolve(Buffer.concat(chunks)));
      body.on('error', reject);
    });
  }
  if (Buffer.isBuffer(body)) return body;
  if (ArrayBuffer.isView(body)) return Buffer.from(body.buffer, body.byteOffset, body.byteLength);
  if (body instanceof ArrayBuffer) return Buffer.from(body);
  throw new Error('Unsupported S3 Body type');
}

// Helper to convert S3 stream to string (hardened + debug)
function streamToString(s3BodyStream, templateId) {
  return new Promise(async (resolve, reject) => {
    try {
      const chunks = [];
      let totalBytes = 0;
      let chunkCount = 0;

      // Support async iterator (some AWS SDK bodies are async iterable)
      if (s3BodyStream[Symbol.asyncIterator]) {
        for await (const chunk of s3BodyStream) {
          const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
          chunks.push(buf);
          totalBytes += buf.length;
          chunkCount++;
        }
      } else if (typeof s3BodyStream.on === 'function') {
        s3BodyStream.on('data', (chunk) => {
          const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
          chunks.push(buf);
          totalBytes += buf.length;
          chunkCount++;
        });
        s3BodyStream.on('error', (err) => {
          logger.error(`[streamToString] stream error for template=${templateId}`, err && err.stack ? err.stack : err);
          reject(err);
        });
        s3BodyStream.on('end', () => {
          const buffer = Buffer.concat(chunks);
          logger.debug(`[streamToString] template=${templateId} chunks=${chunkCount} bytes=${totalBytes}`);
          // Save raw buffer when DEBUG or tiny
          if (DEBUG_PDF_FILES || totalBytes < 32) {
            saveDebugFile(`template_${templateId}_html_raw.bin`, buffer);
          }
          resolve(buffer.toString('utf-8'));
        });
        return;
      } else {
        // Fallback: try to read as buffer
        const data = await s3BodyStream.arrayBuffer?.();
        const buffer = data ? Buffer.from(data) : Buffer.alloc(0);
        logger.debug(`[streamToString] fallback read template=${templateId} bytes=${buffer.length}`);
        if (DEBUG_PDF_FILES || buffer.length < 32) saveDebugFile(`template_${templateId}_html_raw.bin`, buffer);
        return resolve(buffer.toString('utf-8'));
      }

      // If we reached here (async iterator path), finalize
      const buffer = Buffer.concat(chunks);
      logger.debug(`[streamToString] template=${templateId} chunks=${chunkCount} bytes=${totalBytes}`);
      if (DEBUG_PDF_FILES || totalBytes < 32) saveDebugFile(`template_${templateId}_html_raw.bin`, buffer);
      resolve(buffer.toString('utf-8'));
    } catch (err) {
      logger.error(`[streamToString] Error reading stream for template=${templateId}`, err && err.stack ? err.stack : err);
      reject(err);
    }
  });
}

async function listAllKeys(s3Client, prefix) {
  const keys = [];
  let token = undefined;
  do {
    const res = await s3Client.send(new ListObjectsV2Command({
      Bucket: process.env.EXOSCALE_BUCKET,
      Prefix: prefix,
      ContinuationToken: token
    }));
    const batch = (res && res.Contents ? res.Contents : [])
      .map(item => item && item.Key)
      .filter(Boolean);
    keys.push(...batch);
    token = res && res.IsTruncated ? res.NextContinuationToken : undefined;
  } while (token);
  return keys;
}

function getTimestamp() {
  const d = new Date();
  const pad = (n) => String(n).padStart(2, '0');
  return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
}

function appendTimestampToKey(key) {
  const ext = path.posix.extname(key);
  const base = ext ? key.slice(0, -ext.length) : key;
  return `${base}-${getTimestamp()}${ext}`;
}

async function objectExists(s3Client, key) {
  try {
    const { HeadObjectCommand } = require('@aws-sdk/client-s3');
    await s3Client.send(new HeadObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: key }));
    return true;
  } catch (e) {
    const status = e && e.$metadata && e.$metadata.httpStatusCode;
    if (status === 404) return false;
    throw e;
  }
}

function formatS3Error(err) {
  if (!err) return null;
  if (typeof err === 'string') return { message: err };
  return {
    name: err.name,
    message: err.message,
    code: err.code,
    stack: err.stack,
    metadata: err.$metadata,
    cause: err.cause && (err.cause.message || err.cause)
  };
}

// Ensure HTML is a valid document
function ensureHtmlDocument(html) {
  // If it already looks like a full HTML doc, return as is
  if (/<html[\s\S]*?>[\s\S]*<\/html>/i.test(html)) return html;
  // Otherwise, wrap it
  return `
    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8">
        <title>Contract PDF</title>
        <style>
          body { font-family: Arial, sans-serif; margin: 40px; }
          h1, h2, h3 { color: #222; }
          img { display: block; margin-top: 20px; }
        </style>
      </head>
      <body>
        ${html}
      </body>
    </html>
  `;
}

function escapeRegExp(str) {
  return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

// Raw preview helper: fetch latest active contract template HTML (placeholders intact)
async function fetchLatestActiveContractTemplateHtml({ userType, contractType }) {
  const allowedContractTypes = ['contract', 'gdpr', 'abo'];
  const safeContractType = allowedContractTypes.includes(String(contractType || '').toLowerCase())
    ? String(contractType).toLowerCase()
    : 'contract';

  const lang = arguments[0]?.lang || null;
  const latest = await DocumentTemplateService.getLatestActiveForUserType(userType, 'contract', safeContractType, null, lang);
  if (!latest) return { latest: null, html: '' };

  const s3 = new S3Client({
    region: process.env.EXOSCALE_REGION,
    endpoint: process.env.EXOSCALE_ENDPOINT,
    credentials: {
      accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
      secretAccessKey: process.env.EXOSCALE_SECRET_KEY
    }
  });
  const command = new GetObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: latest.storageKey });
  const fileObj = await s3.send(command);
  if (!fileObj.Body) {
    logger.warn('[fetchLatestActiveContractTemplateHtml] empty S3 body', { id: latest.id, key: latest.storageKey });
    return { latest, html: '' };
  }
  const html = await streamToString(fileObj.Body, latest.id);
  return { latest, html: ensureHtmlDocument(html) };
}

async function renderLatestActiveContractHtmlForUser({ targetUserId, userType, contractType, req = null, lang = null }) {
  const allowedContractTypes = ['contract', 'gdpr', 'abo'];
  const safeContractType = allowedContractTypes.includes(String(contractType || '').toLowerCase())
    ? String(contractType).toLowerCase()
    : 'contract';

  const latest = await DocumentTemplateService.getLatestActiveForUserType(userType, 'contract', safeContractType, null, lang);
  if (!latest) return { latest: null, html: '' };

  const s3 = new S3Client({
    region: process.env.EXOSCALE_REGION,
    endpoint: process.env.EXOSCALE_ENDPOINT,
    credentials: {
      accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
      secretAccessKey: process.env.EXOSCALE_SECRET_KEY
    }
  });
  const command = new GetObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: latest.storageKey });
  const fileObj = await s3.send(command);
  if (!fileObj.Body) {
    const err = new Error('Template file not available');
    err.status = 404;
    throw err;
  }
  let html = await streamToString(fileObj.Body, latest.id);

  const uow = new UnitOfWork();
  let vars;
  try {
    await uow.start();
    vars = await ContractUploadService.buildTemplateVars({
      userId: targetUserId,
      user_type: userType,
      contractData: {},
      unitOfWork: uow
    });
    await uow.commit();
  } catch (e) {
    try { await uow.rollback(e); } catch (_) {}
    throw e;
  }

  Object.entries(vars).forEach(([k, v]) => {
    const pattern = new RegExp(`{{\\s*${escapeRegExp(k)}\\s*}}`, 'g');
    html = html.replace(pattern, String(v ?? ''));
  });

  html = html.replace(/{{\s*signatureImage\s*}}/g, 'Your signature will appear here');
  html = sanitizePlaceholders(html, ['companyStamp','companyStampInline','companyStampSmall','companyLogo','profitplanetSignature']);

  const reqForStamp = (req && req.user) ? req : { user: { id: targetUserId, user_type: userType } };
  try { html = await applyCompanyStampPlaceholders(html, reqForStamp); } catch (e) {}
  try { html = await applyProfitPlanetSignature(html); } catch (e) {}

  return { latest, html: ensureHtmlDocument(html) };
}

exports.listTemplates = async (req, res) => {
  const templates = await DocumentTemplateService.listTemplates();
  const s3 = new S3Client({
    region: process.env.EXOSCALE_REGION,
    endpoint: process.env.EXOSCALE_ENDPOINT,
    credentials: {
      accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
      secretAccessKey: process.env.EXOSCALE_SECRET_KEY
    }
  });

  const serverBaseUrl = `${req.protocol}://${req.get('host')}`;

  // Generate signed preview/file URLs and fetch HTML for each template
  const templatesWithPreview = await Promise.all(
    templates.map(t => enrichTemplate(t, s3, serverBaseUrl))
  );

  res.json(templatesWithPreview);
};

// Public dashboard endpoint (admin only)
exports.listTemplatesPublic = async (req, res) => {
  // Same as listTemplates, but for /api/document-templates-public
  const templates = await DocumentTemplateService.listTemplates();
  const s3 = new S3Client({
    region: process.env.EXOSCALE_REGION,
    endpoint: process.env.EXOSCALE_ENDPOINT,
    credentials: {
      accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
      secretAccessKey: process.env.EXOSCALE_SECRET_KEY
    }
  });

  const serverBaseUrl = `${req.protocol}://${req.get('host')}`;

  const templatesWithPreview = await Promise.all(
    templates.map(t => enrichTemplate(t, s3, serverBaseUrl))
  );

  res.json(templatesWithPreview);
};

exports.uploadTemplate = async (req, res) => {
  const { name, type, description, lang } = req.body;
  const rawContractType = req.body.contract_type || req.body.contractType;
  const rawUserType = req.body.user_type || req.body.userType;
  const rawTaxMode = req.body.tax_mode || req.body.taxMode;
  const allowed = ['personal','company','both'];
  const user_type = allowed.includes(rawUserType) ? rawUserType : 'both';
  const file = req.file;
  if (!file) return res.status(400).json({ error: 'No file uploaded' });
  const supportedLang = await resolveSupportedTemplateLanguage(lang);
  if (!supportedLang) return res.status(400).json({ error: 'Invalid or missing language' });

  const allowedContractTypes = ['contract', 'gdpr', 'abo'];
  const normalizedContractType = rawContractType !== undefined && rawContractType !== null
    ? String(rawContractType).trim().toLowerCase()
    : rawContractType;
  const contract_type = (type === 'contract' && allowedContractTypes.includes(normalizedContractType))
    ? normalizedContractType
    : (type === 'contract' ? 'contract' : null);
  const tax_mode = type === 'invoice' ? normalizeInvoiceTaxMode(rawTaxMode) : null;

  const key = buildTemplateStorageKey(supportedLang, file.originalname);

  const s3 = new S3Client({
    region: process.env.EXOSCALE_REGION,
    endpoint: process.env.EXOSCALE_ENDPOINT,
    credentials: {
      accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
      secretAccessKey: process.env.EXOSCALE_SECRET_KEY
    }
  });
  await s3.send(new PutObjectCommand({
    Bucket: process.env.EXOSCALE_BUCKET,
    Key: key,
    Body: file.buffer,
    ContentType: file.mimetype
  }));

  const template = await DocumentTemplateService.uploadTemplate({ name, type, contract_type, storageKey: key, description, lang: supportedLang, version: 1, user_type, tax_mode });
  // Enrich with previewUrl, fileUrl, html
  const enriched = await enrichTemplate(template, s3);
  res.status(201).json(enriched);
};

exports.getTemplate = async (req, res) => {
  const template = await DocumentTemplateService.getTemplate(req.params.id);
  if (!template) return res.status(404).json({ error: 'Not found' });
  const s3 = new S3Client({
    region: process.env.EXOSCALE_REGION,
    endpoint: process.env.EXOSCALE_ENDPOINT,
    credentials: {
      accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
      secretAccessKey: process.env.EXOSCALE_SECRET_KEY
    }
  });
  const enriched = await enrichTemplate(template, s3);
  res.json(enriched);
};

exports.deleteTemplate = async (req, res) => {
  await DocumentTemplateService.deleteTemplate(req.params.id);
  res.status(204).end();
};

exports.updateTemplate = async (req, res) => {
  const id = req.params.id;
  const { name, type, description, lang } = req.body;
  const rawUserType = req.body.user_type || req.body.userType;
  const rawContractType = req.body.contract_type || req.body.contractType;
  const rawTaxMode = req.body.tax_mode || req.body.taxMode;
  const allowed = ['personal','company','both'];
  const user_type = allowed.includes(rawUserType) ? rawUserType : undefined;
  let storageKey;
  let file = req.file;

  const current = await DocumentTemplateService.getTemplate(id);
  if (!current) return res.status(404).json({ error: 'Template not found' });

  let nextLang = normalizeLanguageCode(current.lang) || current.lang;
  if (lang !== undefined) {
    nextLang = await resolveSupportedTemplateLanguage(lang);
    if (!nextLang) return res.status(400).json({ error: 'Invalid or missing language' });
  }

  const nextType = type !== undefined ? type : current.type;
  const allowedContractTypes = ['contract', 'gdpr', 'abo'];
  let contract_type = null;
  if (nextType === 'contract') {
    const candidate = rawContractType !== undefined ? rawContractType : current.contract_type;
    const normalizedCandidate = candidate !== undefined && candidate !== null ? String(candidate).trim().toLowerCase() : candidate;
    if (normalizedCandidate && allowedContractTypes.includes(normalizedCandidate)) {
      contract_type = normalizedCandidate;
    } else {
      contract_type = 'contract';
    }
  }
  const tax_mode = nextType === 'invoice'
    ? normalizeInvoiceTaxMode(rawTaxMode !== undefined ? rawTaxMode : current.tax_mode)
    : null;

  if (file) {
    storageKey = buildTemplateStorageKey(nextLang, file.originalname);
    const s3 = new S3Client({
      region: process.env.EXOSCALE_REGION,
      endpoint: process.env.EXOSCALE_ENDPOINT,
      credentials: {
        accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
        secretAccessKey: process.env.EXOSCALE_SECRET_KEY
      }
    });
    await s3.send(new PutObjectCommand({
      Bucket: process.env.EXOSCALE_BUCKET,
      Key: storageKey,
      Body: file.buffer,
      ContentType: file.mimetype
    }));
  }

  const updateData = {
    name: name !== undefined ? name : current.name,
    type: nextType,
    contract_type,
    tax_mode,
    description: description !== undefined ? description : current.description,
    lang: nextLang,
    storageKey: storageKey || current.storageKey,
    ...(user_type !== undefined ? { user_type } : {})
  };

  const updated = await DocumentTemplateService.updateTemplate(id, updateData);
  // Enrich with previewUrl, fileUrl, html
  const s3 = new S3Client({
    region: process.env.EXOSCALE_REGION,
    endpoint: process.env.EXOSCALE_ENDPOINT,
    credentials: {
      accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
      secretAccessKey: process.env.EXOSCALE_SECRET_KEY
    }
  });
  const enriched = await enrichTemplate(updated, s3);
  res.json(enriched);
};

// NEW: revise a template by creating a NEW template record (version bump) and deactivating the previous one.
// Route suggestion: POST /document-templates/:id/revise (multipart/form-data with 'file')
exports.reviseTemplate = async (req, res) => {
  const previousId = req.params.id;
  const { name, type, description, lang } = req.body;
  const rawUserType = req.body.user_type || req.body.userType;
  const rawContractType = req.body.contract_type || req.body.contractType;
  const rawTaxMode = req.body.tax_mode || req.body.taxMode;
  const requestedState = req.body.state; // optional: 'active' | 'inactive'

  const file = req.file;
  if (!file) return res.status(400).json({ error: 'No file uploaded' });

  const previous = await DocumentTemplateService.getTemplate(previousId);
  if (!previous) return res.status(404).json({ error: 'Template not found' });

  const nextType = type !== undefined ? type : previous.type;
  const nextLang = lang !== undefined
    ? await resolveSupportedTemplateLanguage(lang)
    : (normalizeLanguageCode(previous.lang) || previous.lang);
  if (!nextLang) return res.status(400).json({ error: 'Invalid or missing language' });

  const allowedUserTypes = ['personal', 'company', 'both'];
  const user_type = allowedUserTypes.includes(rawUserType)
    ? rawUserType
    : (previous.user_type || 'both');

  const allowedContractTypes = ['contract', 'gdpr', 'abo'];
  let contract_type = null;
  if (nextType === 'contract') {
    const candidate = rawContractType !== undefined ? rawContractType : previous.contract_type;
    const normalizedCandidate = candidate !== undefined && candidate !== null ? String(candidate).trim().toLowerCase() : candidate;
    contract_type = allowedContractTypes.includes(normalizedCandidate) ? normalizedCandidate : 'contract';
  }
  const tax_mode = nextType === 'invoice'
    ? normalizeInvoiceTaxMode(rawTaxMode !== undefined ? rawTaxMode : previous.tax_mode)
    : null;

  const storageKey = buildTemplateStorageKey(nextLang, file.originalname);

  const s3 = new S3Client({
    region: process.env.EXOSCALE_REGION,
    endpoint: process.env.EXOSCALE_ENDPOINT,
    credentials: {
      accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
      secretAccessKey: process.env.EXOSCALE_SECRET_KEY
    }
  });
  await s3.send(new PutObjectCommand({
    Bucket: process.env.EXOSCALE_BUCKET,
    Key: storageKey,
    Body: file.buffer,
    ContentType: file.mimetype
  }));

  const payload = {
    name: name !== undefined ? name : previous.name,
    type: nextType,
    contract_type,
    tax_mode,
    storageKey,
    description: description !== undefined ? description : previous.description,
    lang: nextLang,
    user_type,
    ...(requestedState !== undefined ? { state: requestedState } : {})
  };

  const created = await DocumentTemplateService.reviseTemplate(previousId, payload);
  if (!created) return res.status(404).json({ error: 'Template not found' });
  const enriched = await enrichTemplate(created, s3);
  res.status(201).json(enriched);
};

exports.updateTemplateState = async (req, res) => {
  const id = req.params.id;
  const { state } = req.body;
  if (!['active', 'inactive'].includes(state)) {
    return res.status(400).json({ error: 'Invalid state value' });
  }
  const updated = await DocumentTemplateService.updateTemplateState(id, state);
  if (!updated) return res.status(404).json({ error: 'Template not found' });
  // Enrich with previewUrl, fileUrl, html
  const s3 = new S3Client({
    region: process.env.EXOSCALE_REGION,
    endpoint: process.env.EXOSCALE_ENDPOINT,
    credentials: {
      accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
      secretAccessKey: process.env.EXOSCALE_SECRET_KEY
    }
  });
  const enriched = await enrichTemplate(updated, s3);
  res.json(enriched);
};

exports.listTemplatesFiltered = async (req, res) => {
  const { state } = req.query;
  const userTypeFilter = req.query.user_type || req.query.userType;
  let templates = await DocumentTemplateService.listTemplates();
  if (state) templates = templates.filter(t => t.state === state);
  if (userTypeFilter) templates = templates.filter(t => t.user_type === userTypeFilter);
  const s3 = new S3Client({
    region: process.env.EXOSCALE_REGION,
    endpoint: process.env.EXOSCALE_ENDPOINT,
    credentials: {
      accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
      secretAccessKey: process.env.EXOSCALE_SECRET_KEY
    }
  });
  const enriched = await Promise.all(templates.map(t => enrichTemplate(t, s3)));
  res.json(enriched);
};

// New: list only active templates relevant to authenticated user (suggest route: GET /api/document-templates/active/mine)
exports.listMyActiveTemplates = async (req, res) => {
  const userType = req.user?.user_type || req.user?.userType;
  if (!userType) return res.status(401).json({ error: 'Unauthorized: missing user type' });
  const templateType = req.query.type || null; // optional ?type=contract
  const templates = await DocumentTemplateService.getActiveTemplatesForUserType(userType, templateType);
  const s3 = new S3Client({
    region: process.env.EXOSCALE_REGION,
    endpoint: process.env.EXOSCALE_ENDPOINT,
    credentials: {
      accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
      secretAccessKey: process.env.EXOSCALE_SECRET_KEY
    }
  });
  const enriched = await Promise.all(templates.map(t => enrichTemplate(t, s3)));
  res.json(enriched);
};

// small helper: log and send PDF buffer safely
function sendPdfBuffer(res, pdfBuffer, filename, debugTag) {
  try {
    // Log small head/tail of buffer for debugging
    const head = pdfBuffer.slice(0, 16).toString('hex');
    const tail = pdfBuffer.slice(-16).toString('hex');
    logger.debug(`[SEND-PDF][${debugTag}] filename=${filename} bytes=${pdfBuffer.length} head=${head} tail=${tail}`);

    // Ensure binary-friendly headers
    res.setHeader('Content-Type', 'application/pdf');
    res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
    res.setHeader('Content-Transfer-Encoding', 'binary');
    res.setHeader('Cache-Control', 'no-store');
    res.setHeader('Content-Length', pdfBuffer.length);

    // Log headers that will be sent
    logger.debug(`[SEND-PDF][${debugTag}] Response headers prepared: Content-Length=${res.getHeader('Content-Length')}`);

    // Attach finish/close listeners
    res.on('finish', () => {
      logger.debug(`[SEND-PDF][${debugTag}] res.finish for ${filename} bytesSent=${pdfBuffer.length}`);
    });
    res.on('close', () => {
      logger.debug(`[SEND-PDF][${debugTag}] res.close for ${filename}`);
    });

    // Send binary. use end with buffer to avoid any transformations.
    res.end(pdfBuffer);
  } catch (e) {
    logger.error('[SEND-PDF] failed to send pdfBuffer', e && e.stack ? e.stack : e);
    try { res.status(500).json({ error: 'Failed to send PDF' }); } catch (e2) {}
  }
}

// Helper: format date -> dd.mm.yyyy HH:MM:SS
function formatDateTime(dateLike) {
  const d = dateLike ? new Date(dateLike) : new Date();
  const pad = n => String(n).padStart(2,'0');
  return `${pad(d.getDate())}.${pad(d.getMonth()+1)}.${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}

// Generate PDF from template HTML (styled)
exports.generatePdf = async (req, res) => {
  const id = req.params.id;
  logger.info(`[generatePdf] Start for templateId=${id}`);
  const template = await DocumentTemplateService.getTemplate(id);
  if (!template) {
    logger.error(`[generatePdf] Template not found for id=${id}`);
    return res.status(404).json({ error: 'Template not found' });
  }
  const userType = req.user?.user_type || req.user?.userType;
  if (!userType) {
    logger.warn('[generatePdf] unauthorized_missing_user', { id });
    return res.status(401).json({ error: 'Unauthorized' });
  }
  if (template.state !== 'active') {
    logger.warn('[generatePdf] template_inactive', { id, state: template.state });
    return res.status(403).json({ error: 'Template inactive' });
  }
  if (template.user_type !== 'both' && template.user_type !== userType) {
    logger.warn('[generatePdf] template_user_type_mismatch', { id, templateUserType: template.user_type, userType });
    return res.status(403).json({ error: 'Template not allowed for this user type' });
  }

  // Get HTML content from S3
  const s3 = new S3Client({
    region: process.env.EXOSCALE_REGION,
    endpoint: process.env.EXOSCALE_ENDPOINT,
    credentials: {
      accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
      secretAccessKey: process.env.EXOSCALE_SECRET_KEY
    }
  });
  let html = '';
  try {
    logger.debug(`[generatePdf] Fetching HTML from S3: key=${template.storageKey} id=${id} lang=${template.lang} version=${template.version}`);
    const command = new GetObjectCommand({
      Bucket: process.env.EXOSCALE_BUCKET,
      Key: template.storageKey
    });
    const fileObj = await s3.send(command);
    if (fileObj.Body) {
      html = await streamToString(fileObj.Body, id);
      logger.debug(`[generatePdf] HTML fetched, length=${html.length}`);
      if (DEBUG_PDF_FILES) saveDebugFile(`template_${id}_generate_html.html`, ensureHtmlDocument(html));
    } else {
      logger.error(`[generatePdf] S3 fileObj.Body is empty for key=${template.storageKey}`);
    }
  } catch (e) {
    logger.error(`[generatePdf] Error fetching template file from S3:`, e && e.stack ? e.stack : e);
    return res.status(500).json({ error: 'Failed to fetch template file' });
  }

  // If client requested a preview/sanitized PDF, and this is a contract, sanitize placeholders (opt-in)
  const previewFlag = req.query && (req.query.preview === '1' || req.query.sanitize === '1');
  if (previewFlag && template.type === 'contract') {
    logger.debug('[generatePdf] preview flag detected - sanitizing placeholders before rendering PDF', { id });
    html = sanitizePlaceholders(html);
    // Still replace currentDate into sanitized output so previews show a date
    try {
      html = html.replace(/{{\s*currentDate\s*}}/g, formatDateTime());
    } catch (e) { /* ignore */ }
    if (DEBUG_PDF_FILES) saveDebugFile(`template_${id}_generate_html_sanitized.html`, ensureHtmlDocument(html));
  } else {
    // NEW: replace {{currentDate}} also for unsigned PDFs (existing behavior)
    html = html.replace(/{{\s*currentDate\s*}}/g, formatDateTime());
  }

  // NEW: apply company stamp & Profit Planet signature (stamp insertion still safe after sanitization)
  html = await applyCompanyStampPlaceholders(html, req);
  html = await applyProfitPlanetSignature(html);

  // Use puppeteer to render HTML and generate PDF
  try {
    const browser = await puppeteer.launch({
      headless: "new",
      args: ['--no-sandbox', '--disable-setuid-sandbox']
    });
    const page = await browser.newPage();

    // Instrument page console and errors
    page.on('console', msg => {
      try {
        logger.debug(`[PUPPETEER][console][template:${id}] ${msg.type()}: ${msg.text()}`);
      } catch (e) {
        logger.debug('[PUPPETEER][console] error reading message', e);
      }
    });
    page.on('pageerror', err => logger.error(`[PUPPETEER][pageerror][template:${id}]`, err && err.stack ? err.stack : err));
    page.on('requestfailed', req => logger.warn(`[PUPPETEER][requestfailed][template:${id}] url=${req.url()} failure=${req.failure()?.errorText}`));

    const htmlDoc = ensureHtmlDocument(html);
    logger.debug(`[generatePdf] Final HTML length=${htmlDoc.length} headSnippet=${htmlDoc.slice(0,200).replace(/\n/g,' ')}`);
    await page.setContent(htmlDoc, { waitUntil: 'networkidle0', timeout: 60000 });

    // Save a rendered screenshot for inspection
    if (DEBUG_PDF_FILES) {
      const screenshotPath = `template_${id}_render.png`;
      await page.screenshot({ path: path.join(ensureDebugDir(), screenshotPath), fullPage: true });
      logger.debug(`[generatePdf] Saved screenshot ${screenshotPath}`);
    }

    const pdfBuffer = await page.pdf({ format: 'A4' });
    await browser.close();

    logger.info(`[generatePdf] Generated PDF for template ${id} size=${pdfBuffer.length} bytes`);
    if (DEBUG_PDF_FILES) saveDebugFile(`template_${id}.pdf`, pdfBuffer, null);

    // Log incoming request headers (helpful to troubleshoot transport)
    try {
      logger.debug(`[generatePdf] request headers: Accept=${req.get('accept')} Referer=${req.get('referer')} Origin=${req.get('origin')}`);
    } catch (e) {}

    // Send using helper
    return sendPdfBuffer(res, pdfBuffer, `template_${id}.pdf`, `template_${id}`);
  } catch (err) {
    logger.error(`[generatePdf] Error generating PDF:`, err && err.stack ? err.stack : err);
    res.status(500).json({ error: 'Fehler beim Generieren des Vertrags.' });
  }
};

// Generate PDF with signature inserted (styled HTML)
exports.generatePdfWithSignature = async (req, res) => {
  const id = req.params.id;
  logger.info(`[generatePdfWithSignature] Start for templateId=${id}`);

  // Accept multiple incoming shapes from frontend:
  // - req.body.userData (preferred)
  // - req.body.user or req.body.context (some clients send 'user' + 'context')
  // - authenticated user in req.user
  const rawBody = req.body || {};
  // signature image may be named signatureImage, signature or signatureData on the client
  const signatureImage = rawBody.signatureImage || rawBody.signature || rawBody.signatureData || null;

  // Merge/normalize candidate sources for variables
  const rawUserData = rawBody.userData || rawBody.user || rawBody.context || {};
  const authUser = req.user || {};

  // small helper: try multiple possible keys and return the first defined/non-empty one
  const pick = (src, keys) => {
    if (!src) return undefined;
    for (const k of keys) {
      if (src[k] !== undefined && src[k] !== null && String(src[k]).trim() !== '') return src[k];
    }
    return undefined;
  };

  // Format Date/ISO -> "dd.mm.yyyy HH:MM:SS" (24h)
  const formatDateTime = (input) => {
    const d = input ? new Date(input) : new Date();
    if (!d || isNaN(d.getTime())) return '';
    const pad = n => String(n).padStart(2, '0');
    const day = pad(d.getDate());
    const month = pad(d.getMonth() + 1);
    const year = d.getFullYear();
    const hours = pad(d.getHours());
    const minutes = pad(d.getMinutes());
    const seconds = pad(d.getSeconds());
    return `${day}.${month}.${year} ${hours}:${minutes}:${seconds}`;
  };

  // prefer explicit context/currentDate (ISO), otherwise use now formatted
  const rawDate = pick(rawBody, ['currentDate']) || pick(rawBody.context || {}, ['currentDate']);
  const formattedCurrentDate = rawDate ? formatDateTime(rawDate) : formatDateTime();

  // normalized map used to replace template placeholders
  const userData = {
     companyName: pick(rawUserData, ['companyName','company_name','company','name']) ||
                  pick(authUser, ['companyName','company_name','company','companyName','company']) || '',
     registrationNumber: pick(rawUserData, ['registrationNumber','registration_number','registrationNo','registration']) ||
                        pick(authUser, ['registrationNumber','registration_number','registrationNo']) || '',
     contactPersonName: pick(rawUserData, ['contactPersonName','contact_person_name','contactPerson','contactName','contact']) ||
                        pick(authUser, ['contactPersonName','name','firstName','fullName']) || '',
     companyAddress: pick(rawUserData, ['companyAddress','company_address','address','streetAddress']) ||
                    pick(authUser, ['companyAddress','address']) || '',
     email: pick(rawUserData, ['email','userEmail']) || authUser.email || '',
     // ensure these exist so replacement step can touch them even if empty
     contactPersonPhone: '',
     country: '',
     // ADDED: ensure company placeholders exist
     companyFullAddress: '',
     companyEmail: '',
     companyPhone: ''
   };

  // Map company-prefixed placeholders from raw request early (helps when no company_profile row exists)
  const rawCompanyName = pick(rawUserData, ['companyCompanyName','companyName','company_name','company']);
  const rawCompanyReg  = pick(rawUserData, ['companyRegistrationNumber','registrationNumber','registration_number','registrationNo']);
  const rawCompanyAddr = pick(rawUserData, ['companyAddress','company_address','address','streetAddress']);
  const rawCompanyZip  = pick(rawUserData, ['companyZipCode','company_zip','zip','zip_code','postalCode','postal_code']);
  const rawCompanyCity = pick(rawUserData, ['companyCity','city','town']);
  const rawCompanyEmail = pick(rawUserData, ['companyEmail','company_email','contact_email','email']);
  const rawCompanyPhone = pick(rawUserData, ['companyPhone','company_phone','contactPhone','contact_phone','phone','telephone','mobile']);
  const rawCompanyContactName = pick(rawUserData, ['companyContactPersonName','contactPersonName','contact_person_name','contactPerson','contactName','contact']);
  const rawCompanyContactPhone = pick(rawUserData, ['companyContactPersonPhone','contactPersonPhone','contact_person_phone','contactPhone','contact_phone','phone','telephone','mobile']);

  userData.companyCompanyName = userData.companyCompanyName || rawCompanyName || userData.companyName || '';
  userData.companyRegistrationNumber = userData.companyRegistrationNumber || rawCompanyReg || userData.registrationNumber || '';
  userData.companyAddress = userData.companyAddress || rawCompanyAddr || userData.address || '';
  userData.companyZipCode = userData.companyZipCode || rawCompanyZip || userData.zip_code || '';
  userData.companyCity = userData.companyCity || rawCompanyCity || userData.city || '';
  userData.companyEmail = userData.companyEmail || rawCompanyEmail || userData.email || '';
  userData.companyPhone = userData.companyPhone || rawCompanyPhone || userData.contactPersonPhone || userData.phone || '';
  userData.companyContactPersonName = userData.companyContactPersonName || rawCompanyContactName || userData.contactPersonName || '';
  userData.companyContactPersonPhone = userData.companyContactPersonPhone || rawCompanyContactPhone || userData.contactPersonPhone || '';

  // Add personal-user specific fallbacks: fullName and personalAddress and phone
  const rawFull = pick(rawUserData, ['fullName','full_name','name']) ||
                   ((pick(rawUserData, ['firstName','first_name']) && pick(rawUserData, ['lastName','last_name'])) ? `${pick(rawUserData, ['firstName','first_name'])} ${pick(rawUserData, ['lastName','last_name'])}` : undefined);
  const authFull = pick(authUser, ['fullName','full_name','name']) ||
                    ((pick(authUser, ['firstName','first_name']) && pick(authUser, ['lastName','last_name'])) ? `${pick(authUser, ['firstName','first_name'])} ${pick(authUser, ['lastName','last_name'])}` : undefined);
  userData.fullName = rawFull || authFull || '';

  // street/address part
  userData.address = pick(rawUserData, ['address','streetAddress','street','personalAddress','personal_address','addr']) ||
                     pick(authUser, ['address','streetAddress','street','personalAddress','personal_address']) || '';

  // postal code / zip
  userData.zip_code = pick(rawUserData, ['zip','zip_code','postalCode','postal_code','postcode']) ||
                      pick(authUser, ['zip','zip_code','postalCode','postal_code','postcode']) || '';

  // city/town
  userData.city = pick(rawUserData, ['city','town','municipality']) ||
                  pick(authUser, ['city','town']) || '';

  // legacy single-field address still supported as personalAddress
  userData.personalAddress = pick(rawUserData, ['personalAddress','personal_address']) ||
                            pick(authUser, ['personalAddress','personal_address']) || '';

  userData.phone = pick(rawUserData, ['phone','telephone','mobile','contactPhone','contact_phone']) ||
                    pick(authUser, ['phone','telephone','mobile','contactPhone','contact_person_phone']) || '';

  // add formatted currentDate into normalized map
  userData.currentDate = formattedCurrentDate;

  // Derive fullName from company contact if still missing (helps company contracts)
  if (!userData.fullName) {
    const contactName = rawCompanyContactName || rawCompanyName || userData.contactPersonName || userData.companyContactPersonName;
    if (contactName) userData.fullName = contactName;
  }

  // If frontend didn't send personalAddress/phone, try to fetch from personal_profiles in DB (authenticated user)
  try {
    // Build candidate identifiers: various authUser/rawUserData id/email shapes
    const idCandidates = [
      authUser && (authUser.userId || authUser.id || authUser.user_id),
      rawUserData && (rawUserData.userId || rawUserData.id || rawUserData.user_id)
    ].flat().filter(Boolean);
    const emailCandidates = [
      authUser && (authUser.email || authUser.userEmail || authUser.username),
      rawUserData && (rawUserData.email || rawUserData.userEmail)
    ].flat().filter(Boolean);

    // helper to normalize db.execute result (works with mysql2/other adapters)
    const readRows = (res) => {
      if (!res) return [];
      if (Array.isArray(res) && Array.isArray(res[0])) return res[0];
      if (Array.isArray(res)) return res;
      return res;
    };

    // --- personal_profiles lookup (existing behavior) ---
    let prof = null;
    for (const uid of idCandidates) {
      try {
        const result = await db.execute('SELECT * FROM personal_profiles WHERE user_id = ? LIMIT 1', [uid]);
        const rows = readRows(result);
        if (rows && rows[0]) { prof = rows[0]; logger.debug(`[generatePdfWithSignature] Found personal_profiles by user_id=${uid}`); break; }
      } catch (ignored) { /* continue to next */ }
    }
    if (!prof) {
      for (const em of emailCandidates) {
        try {
          const result = await db.execute('SELECT * FROM personal_profiles WHERE email = ? LIMIT 1', [em]);
          const rows = readRows(result);
          if (rows && rows[0]) { prof = rows[0]; logger.debug(`[generatePdfWithSignature] Found personal_profiles by email=${em}`); break; }
        } catch (ignored) { /* continue */ }
      }
    }
    if (prof) {
      // Use DB values as canonical when present
      userData.address = pick(prof, ['address','street','street_address','streetAddress','address_line1','addressLine1','addr','personal_address']) || userData.address || '';
      userData.zip_code = pick(prof, ['zip','zip_code','postal_code','postalCode']) || userData.zip_code || '';
      userData.city = pick(prof, ['city','town','municipality']) || userData.city || '';
      userData.personalAddress = pick(prof, ['personal_address','personalAddress']) || userData.personalAddress || '';
      userData.phone = pick(prof, ['phone','telephone','mobile','contact_phone','contactPhone','phone_secondary']) || userData.phone || '';
      const fn = pick(prof, ['full_name','fullName','name','first_name','firstName']);
      const ln = pick(prof, ['last_name','lastName']);
      if (fn || ln) userData.fullName = `${fn || ''} ${ln || ''}`.trim();
      if (!userData.fullName) userData.fullName = pick(prof, ['full_name','fullName','name']) || userData.fullName || '';

      // Compose full address primarily from DB columns
      const addrParts = [];
      if (userData.address) addrParts.push(userData.address);
      const zipCity = [userData.zip_code, userData.city].filter(Boolean).join(' ');
      if (zipCity) addrParts.push(zipCity);
      userData.fullAddress = addrParts.join(', ') || userData.fullAddress || userData.personalAddress || '';

      logger.debug(`[generatePdfWithSignature] Applied personal_profiles data id=${prof.id || prof.user_id || 'unknown'}`);
    }

    // --- company_profiles lookup (UnitOfWork) for company-related placeholders if still missing ---
    const companyFieldsMissing = (
      !userData.companyName ||
      !userData.registrationNumber ||
      !userData.companyAddress ||
      !userData.address ||            // also check generic address used in templates
      !userData.zip_code ||
      !userData.city ||
      !userData.country ||
      !userData.contactPersonName ||
      !userData.contactPersonPhone
    );
    if (companyFieldsMissing && (idCandidates.length || emailCandidates.length)) {
      let uow;
      try {
        uow = new UnitOfWork();
        await uow.start();
        let comp = null;
        // Try by id candidates
        for (const uid of idCandidates) {
          try {
            const [rows] = await uow.connection.execute('SELECT * FROM company_profiles WHERE user_id = ? OR company_id = ? LIMIT 1', [uid, uid]);
            if (rows && rows[0]) { comp = rows[0]; logger.debug(`[generatePdfWithSignature] Found company_profiles by id=${uid}`); break; }
          } catch (ignored) { /* continue */ }
        }
        // Try by email if not found
        if (!comp) {
          for (const em of emailCandidates) {
            try {
              // 1) Try join: company_profiles matched to users by users.email (best case)
              try {
                const [joinRows] = await uow.connection.execute(
                  'SELECT cp.* FROM company_profiles cp JOIN users u ON cp.user_id = u.id WHERE u.email = ? LIMIT 1',
                  [em]
                );
                if (joinRows && joinRows[0]) {
                  comp = joinRows[0];
                  logger.debug(`[generatePdfWithSignature] Found company_profiles by join users.email=${em}`);
                  break;
                }
              } catch (eJoin) {
                // non-fatal, continue to next strategy
                // console.debug(`[generatePdfWithSignature] join lookup failed for ${em}`, eJoin && eJoin.message);
              }

              // 2) Resolve users.id by email, then find company_profiles by user_id/company_id
              try {
                const [uRows] = await uow.connection.execute('SELECT id FROM users WHERE email = ? LIMIT 1', [em]);
                const userRow = (uRows && uRows[0]) ? uRows[0] : null;
                if (userRow && userRow.id) {
                  const [rows] = await uow.connection.execute(
                    'SELECT * FROM company_profiles WHERE user_id = ? OR company_id = ? LIMIT 1',
                    [userRow.id, userRow.id]
                  );
                  if (rows && rows[0]) {
                    comp = rows[0];
                    logger.debug(`[generatePdfWithSignature] Found company_profiles by users.id=${userRow.id} (email=${em})`);
                    break;
                  }
                }
              } catch (eResolve) {
                // non-fatal
                // console.warn('[generatePdfWithSignature] resolve user id failed', eResolve && eResolve.message);
              }

              // 3) Fallback: try common email columns on company_profiles (safe tries inside try/catch)
              const fallbackCols = ['company_email','contact_email','email'];
              for (const col of fallbackCols) {
                try {
                  const q = `SELECT * FROM company_profiles WHERE ${col} = ? LIMIT 1`;
                  const [rows2] = await uow.connection.execute(q, [em]);
                  if (rows2 && rows2[0]) {
                    comp = rows2[0];
                    logger.debug(`[generatePdfWithSignature] Found company_profiles by ${col}=${em}`);
                    break;
                  }
                } catch (ignored2) {
                  // column might not exist or other minor issue - continue to next fallback
                }
              }
              if (comp) break;
            } catch (ignored) {
              // continue to next email candidate
            }
          }
        }
        if (comp) {
          // Prefer DB values directly
          const compName = pick(comp, ['company_name']);
          const compReg  = pick(comp, ['registration_number']);
          const compAddr = pick(comp, ['address']);
          const compZip  = pick(comp, ['zip_code']);
          const compCity = pick(comp, ['city']);
          const compCountry = pick(comp, ['country']);
          const compContactName = pick(comp, ['contact_person_name']);
          const compContactPhone = pick(comp, ['contact_person_phone']);
          const compPhone = pick(comp, ['phone']);

          if (compName) { userData.companyName = compName; userData.companyCompanyName = compName; }
          if (compReg) { userData.registrationNumber = compReg; userData.companyRegistrationNumber = compReg; }
          if (compAddr) { userData.companyAddress = compAddr; userData.address = userData.address || compAddr; }
          if (compZip) { userData.companyZipCode = compZip; userData.zip_code = userData.zip_code || compZip; }
          if (compCity) { userData.companyCity = compCity; userData.city = userData.city || compCity; }
          if (compCountry) { userData.companyCountry = compCountry; userData.country = userData.country || compCountry; }
          if (compContactName) { userData.companyContactPersonName = compContactName; userData.contactPersonName = userData.contactPersonName || compContactName; }
          if (compContactPhone) { userData.companyContactPersonPhone = compContactPhone; userData.contactPersonPhone = userData.contactPersonPhone || compContactPhone; }
          if (compPhone) { userData.companyPhone = compPhone; userData.phone = userData.phone || compPhone; }

          // Compose company full address from DB fields
          const addrParts = [];
          if (compAddr) addrParts.push(compAddr);
          const zipCity = [compZip, compCity].filter(Boolean).join(' ');
          if (zipCity) addrParts.push(zipCity);
          const composed = addrParts.join(', ');
          if (composed) userData.companyFullAddress = composed;

          // Keep email fallback behavior (no email column in company_profiles)

          logger.debug(`[generatePdfWithSignature] Applied company_profiles data id=${comp.id || comp.company_id || 'unknown'}`);
        }
        await uow.commit();
      } catch (errU) {
        if (uow) {
          try { await uow.rollback(errU); } catch (e) { logger.error('[generatePdfWithSignature] rollback error', e); }
        }
        logger.error('[generatePdfWithSignature] Error fetching company_profiles via UnitOfWork', errU && errU.stack ? errU.stack : errU);
      }
    }
  } catch (err) {
    logger.error('[generatePdfWithSignature] Error fetching personal_profiles/company_profiles from DB', err && err.stack ? err.stack : err);
  }

  logger.debug(`[generatePdfWithSignature] Resolved userData keys=${Object.keys(userData).join(',')} values=${JSON.stringify(userData)}`);
  logger.debug(`[generatePdfWithSignature] Received signatureImage length=${signatureImage ? String(signatureImage).length : 0}`);
   const template = await DocumentTemplateService.getTemplate(id);
   if (!template) {
     logger.error(`[generatePdfWithSignature] Template not found for id=${id}`);
     return res.status(404).json({ error: 'Template not found' });
   }
   const userType = req.user?.user_type || req.user?.userType;
   if (!userType) {
     logger.warn('[generatePdfWithSignature] unauthorized_missing_user', { id });
     return res.status(401).json({ error: 'Unauthorized' });
   }
   if (template.state !== 'active') {
     logger.warn('[generatePdfWithSignature] template_inactive', { id, state: template.state });
     return res.status(403).json({ error: 'Template inactive' });
   }
   if (template.user_type !== 'both' && template.user_type !== userType) {
     logger.warn('[generatePdfWithSignature] template_user_type_mismatch', { id, templateUserType: template.user_type, userType });
     return res.status(403).json({ error: 'Template not allowed for this user type' });
   }

   // Get HTML content from S3
   const s3 = new S3Client({
     region: process.env.EXOSCALE_REGION,
     endpoint: process.env.EXOSCALE_ENDPOINT,
     credentials: {
       accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
       secretAccessKey: process.env.EXOSCALE_SECRET_KEY
     }
   });
   let html = '';
   try {
     logger.debug(`[generatePdfWithSignature] Fetching HTML from S3: key=${template.storageKey}`);
     const command = new GetObjectCommand({
       Bucket: process.env.EXOSCALE_BUCKET,
       Key: template.storageKey
     });
     const fileObj = await s3.send(command);
     if (fileObj.Body) {
       html = await streamToString(fileObj.Body, id);
       logger.debug(`[generatePdfWithSignature] HTML fetched, length=${html.length}`);
     } else {
       logger.error(`[generatePdfWithSignature] S3 fileObj.Body is empty`);
     }
   } catch (e) {
     logger.error(`[generatePdfWithSignature] Error fetching template file from S3:`, e && e.stack ? e.stack : e);
     return res.status(500).json({ error: 'Failed to fetch template file' });
   }

   // If client requested a preview/sanitized PDF, and this is a contract, sanitize placeholders (opt-in)
   const previewFlagSigned = req.query && (req.query.preview === '1' || req.query.sanitize === '1');
   if (previewFlagSigned && template.type === 'contract') {
     logger.debug('[generatePdfWithSignature] preview flag detected - sanitizing placeholders before rendering signed PDF', { id });
     html = sanitizePlaceholders(html);
     // Still replace currentDate into sanitized output so previews show a date
     try {
       html = html.replace(/{{\s*currentDate\s*}}/g, formattedCurrentDate || formatDateTime());
     } catch (e) { /* ignore */ }
   }

  // Final fallbacks for composed address fields (still prefer DB values set above)
  if (!userData.fullAddress) {
    const addrParts = [];
    if (userData.address) addrParts.push(userData.address);
    const zipCity = [userData.zip_code, userData.city].filter(Boolean).join(' ');
    if (zipCity) addrParts.push(zipCity);
    userData.fullAddress = addrParts.join(', ');
  }
  if (!userData.companyFullAddress) {
    const addrParts = [];
    if (userData.companyAddress) addrParts.push(userData.companyAddress);
    const zipCity = [userData.companyZipCode, userData.companyCity].filter(Boolean).join(' ');
    if (zipCity) addrParts.push(zipCity);
    userData.companyFullAddress = addrParts.join(', ');
  }

  // Insert userData into HTML (simple replace, adjust as needed)
  if (userData && typeof userData === 'object') {
    Object.entries(userData).forEach(([key, value]) => {
      const pattern = new RegExp(`{{\\s*${escapeRegExp(key)}\\s*}}`, 'g');
      const beforeCount = (html.match(pattern) || []).length;
      html = html.replace(pattern, String(value ?? ''));
      const afterCount = (html.match(pattern) || []).length;
      if (beforeCount || afterCount) {
        logger.debug(`[generatePdfWithSignature] replaced ${key}: before=${beforeCount} after=${afterCount}`);
      }
    });
    const remainingPlaceholders = html.match(/{{\s*([^}\s]+)\s*}}/g) || [];
    if (remainingPlaceholders.length) {
      logger.debug('[generatePdfWithSignature] Unreplaced placeholders detected', { count: remainingPlaceholders.length, samples: remainingPlaceholders.slice(0, 5) });
    }
    logger.debug(`[generatePdfWithSignature] User data replaced in HTML`);
  }

  // Replace {{signatureImage}} with <img src="...">
  if (signatureImage) {
    let imgSrc = signatureImage;
    if (!String(imgSrc).startsWith('data:image/')) {
      // assume raw base64 -> wrap
      imgSrc = `data:image/png;base64,${imgSrc}`;
    }
    logger.debug(`[generatePdfWithSignature] signature image validated length=${imgSrc.length}`);
    html = html.replace(/{{\s*signatureImage\s*}}/g, `<img src="${imgSrc}" style="max-width:200px;max-height:100px;">`);
    logger.debug(`[generatePdfWithSignature] Signature image injected into HTML`);
  } else {
    html = html.replace(/{{\s*signatureImage\s*}}/g, '');
    logger.debug(`[generatePdfWithSignature] No signature image provided`);
  }

  // NEW: apply company stamp placeholders
  html = await applyCompanyStampPlaceholders(html, req);
  html = await applyProfitPlanetSignature(html); // NEW

  if (DEBUG_PDF_FILES) {
    saveDebugFile(`template_${id}_signed_html.html`, ensureHtmlDocument(html));
    logger.debug(`[generatePdfWithSignature] Signed HTML saved for inspection`);
  }

  // Use puppeteer to render HTML and generate PDF
  try {
    const browser = await puppeteer.launch({
      headless: "new",
      args: ['--no-sandbox', '--disable-setuid-sandbox']
    });
    const page = await browser.newPage();

    // Instrument page
    page.on('console', msg => {
      try { logger.debug(`[PUPPETEER][console][signed:${id}] ${msg.type()}: ${msg.text()}`); } catch (e) {}
    });
    page.on('pageerror', err => logger.error(`[PUPPETEER][pageerror][signed:${id}]`, err && err.stack ? err.stack : err));
    page.on('requestfailed', req => logger.warn(`[PUPPETEER][requestfailed][signed:${id}] url=${req.url()} failure=${req.failure()?.errorText}`));

    const htmlDoc = ensureHtmlDocument(html);
    logger.debug(`[generatePdfWithSignature] Final signed HTML length=${htmlDoc.length} headSnippet=${htmlDoc.slice(0,200).replace(/\n/g,' ')}`);
    await page.setContent(htmlDoc, { waitUntil: 'networkidle0', timeout: 60000 });

    if (DEBUG_PDF_FILES) {
      await page.screenshot({ path: path.join(ensureDebugDir(), `template_${id}_signed_render.png`), fullPage: true });
      logger.debug(`[generatePdfWithSignature] Saved signed render screenshot`);
    }

    const pdfBuffer = await page.pdf({ format: 'A4' });
    await browser.close();

    logger.info(`[generatePdfWithSignature] Generated signed PDF size=${pdfBuffer.length} bytes`);
    if (DEBUG_PDF_FILES) saveDebugFile(`template_${id}_signed.pdf`, pdfBuffer, null);

    // Log incoming request headers (helpful to troubleshoot transport)
    try {
      logger.debug(`[generatePdfWithSignature] request headers: Accept=${req.get('accept')} Referer=${req.get('referer')} Origin=${req.get('origin')}`);
    } catch (e) {}

    // Send using helper
    return sendPdfBuffer(res, pdfBuffer, `template_${id}_signed.pdf`, `template_${id}_signed`);
  } catch (err) {
    logger.error(`[generatePdfWithSignature] Error generating PDF:`, err && err.stack ? err.stack : err);
    res.status(500).json({ error: 'Fehler beim Generieren des Vertrags.' });
  }
};

// UPDATED helper: now delegates to service (queries removed from controller) + deep logs
async function applyProfitPlanetSignature(html) {
  if (!html) {
    logger.debug('[applyProfitPlanetSignature] html empty/undefined');
    return html;
  }
  const placeholderMatches = html.match(/{{\s*profitplanetSignature\s*}}/gi) || [];
  if (!placeholderMatches.length) {
    logger.debug('[applyProfitPlanetSignature] no placeholder found');
    return html;
  }
  logger.debug('[applyProfitPlanetSignature] placeholder(s) detected', { count: placeholderMatches.length });

  const companyIdEnv = parseInt(process.env.PROFITPLANET_COMPANY_ID || '0', 10);
  const companyId = Number.isFinite(companyIdEnv) && companyIdEnv > 0 ? companyIdEnv : null;

  try {
    const svcStart = Date.now();
    const { tag, reason } = await DocumentTemplateService.getProfitPlanetSignatureTag({
      companyId,
      maxW: 300,
      maxH: 300
    });
    const svcMs = Date.now() - svcStart;
    logger.debug('[applyProfitPlanetSignature] service response', {
      reason,
      companyId: companyId || 'n/a',
      tagLen: tag ? tag.length : 0,
      tagHead: tag ? tag.slice(0, 60) : ''
    });

    let newHtml;
    if (!tag) {
      newHtml = html.replace(/{{\s*profitplanetSignature\s*}}/gi, '');
      const remaining = (newHtml.match(/{{\s*profitplanetSignature\s*}}/gi) || []).length;
      logger.debug('[applyProfitPlanetSignature] removed placeholder(s)', { reason, remaining, service_ms: svcMs });
      return newHtml;
    }

    newHtml = html.replace(/{{\s*profitplanetSignature\s*}}/gi, tag);
    const remaining = (newHtml.match(/{{\s*profitplanetSignature\s*}}/gi) || []).length;
    logger.debug('[applyProfitPlanetSignature] replaced placeholder(s)', { insertedLen: tag.length, remaining, service_ms: svcMs });
    return newHtml;
  } catch (e) {
    logger.error('[applyProfitPlanetSignature] service error', { msg: e.message, stack: e.stack?.split('\n')[0] });
    const cleaned = html.replace(/{{\s*profitplanetSignature\s*}}/gi, '');
    const remaining = (cleaned.match(/{{\s*profitplanetSignature\s*}}/gi) || []).length;
    logger.debug('[applyProfitPlanetSignature] fallback removal after error', { remaining });
    return cleaned;
  }
}

// NEW helper: apply company stamp placeholders (single definition)
async function applyCompanyStampPlaceholders(html, req) {
  try {
    if (!html) return html;
    if (!/{{\s*companyStamp(?:Inline|Small)?\s*}}/i.test(html)) return html;
    const user = req.user;
    if (!user || user.user_type !== 'company') {
      logger.debug('[applyCompanyStampPlaceholders] user not company or missing user -> removing');
      return html.replace(/{{\s*companyStamp(?:Inline|Small)?\s*}}/gi, '');
    }
    const companyId = user.id || user.userId;
    const stamp = await CompanyStampService.getActiveStampForCompany(companyId);
    if (!stamp) {
      logger.debug('[applyCompanyStampPlaceholders] no active stamp', { companyId });
      return html.replace(/{{\s*companyStamp(?:Inline|Small)?\s*}}/gi, '');
    }
    logger.debug('[applyCompanyStampPlaceholders] stamp found', {
      companyId,
      mime: stamp.mime_type,
      imgLen: stamp.image_base64 ? stamp.image_base64.length : 0
    });
    const dataUri = `data:${stamp.mime_type};base64,${stamp.image_base64}`;
    const mainTag = `<img src="${dataUri}" style="max-width:300px;max-height:300px;">`;
    const inlineTag = `<img src="${dataUri}" style="height:60px;display:inline-block;vertical-align:middle;">`;
    const smallTag = `<img src="${dataUri}" style="height:120px;max-width:120px;">`;
    const replaced = html
      .replace(/{{\s*companyStamp\s*}}/gi, mainTag)
      .replace(/{{\s*companyStampInline\s*}}/gi, inlineTag)
      .replace(/{{\s*companyStampSmall\s*}}/gi, smallTag);
    const remaining = (replaced.match(/{{\s*companyStamp(?:Inline|Small)?\s*}}/gi) || []).length;
    logger.debug('[applyCompanyStampPlaceholders] replacement done', { remaining });
    return replaced;
  } catch (e) {
    logger.error('[applyCompanyStampPlaceholders] error', { msg: e.message });
    return html.replace(/{{\s*companyStamp(?:Inline|Small)?\s*}}/gi, '');
  }
}

// NEW endpoint: serve a sanitized preview HTML from backend (avoids S3 CORS). Uses same sanitize logic and will
// inject allowed stamps/signature if available for the requesting user.
exports.previewTemplate = async (req, res) => {
  const id = req.params.id;
  try {
    const template = await DocumentTemplateService.getTemplate(id);
    if (!template) return res.status(404).send('Not found');

    const s3 = new S3Client({
      region: process.env.EXOSCALE_REGION,
      endpoint: process.env.EXOSCALE_ENDPOINT,
      credentials: {
        accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
        secretAccessKey: process.env.EXOSCALE_SECRET_KEY
      }
    });

    const command = new GetObjectCommand({
      Bucket: process.env.EXOSCALE_BUCKET,
      Key: template.storageKey
    });
    const fileObj = await s3.send(command);
    let html = '';
    if (fileObj.Body) {
      html = await streamToString(fileObj.Body, id);
    } else {
      logger.warn('[previewTemplate] S3 returned empty body', { id, key: template.storageKey });
      return res.status(404).send('Preview not available');
    }

    // sanitize for contract templates (opt-out: non-contract keep placeholders)
    if (template.type === 'contract') {
      html = sanitizePlaceholders(html);
      // keep current date for preview UX
      html = html.replace(/{{\s*currentDate\s*}}/g, formatDateTime());
    }

    // Apply company stamp & profitplanet signature for richer preview when possible
    try { html = await applyCompanyStampPlaceholders(html, req); } catch (e) { logger.warn('[previewTemplate] applyCompanyStampPlaceholders failed', e && e.message); }
    try { html = await applyProfitPlanetSignature(html); } catch (e) { logger.warn('[previewTemplate] applyProfitPlanetSignature failed', e && e.message); }

    // Return sanitized HTML with correct content-type (CORS already handled by your server middleware)
    res.setHeader('Content-Type', 'text/html; charset=utf-8');
    return res.send(ensureHtmlDocument(html));
  } catch (err) {
    logger.error('[previewTemplate] error', { err: err && err.stack ? err.stack : err });
    return res.status(500).send('Preview error');
  }
};

// NEW endpoint: render sanitized preview as PDF and return binary
exports.previewPdf = async (req, res) => {
  const id = req.params.id;
  logger.info(`[previewPdf] Start for templateId=${id}`);
  try {
    const template = await DocumentTemplateService.getTemplate(id);
    if (!template) return res.status(404).json({ error: 'Template not found' });

    // Fetch HTML from S3
    const s3 = new S3Client({
      region: process.env.EXOSCALE_REGION,
      endpoint: process.env.EXOSCALE_ENDPOINT,
      credentials: {
        accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
        secretAccessKey: process.env.EXOSCALE_SECRET_KEY
      }
    });

    const command = new GetObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: template.storageKey });
    const fileObj = await s3.send(command);
    if (!fileObj.Body) {
      logger.warn('[previewPdf] empty S3 body', { id, key: template.storageKey });
      return res.status(404).json({ error: 'Preview not available' });
    }
    let html = await streamToString(fileObj.Body, id);

    // Sanitize for contract previews (opt-in policy used for listing previews)
    if (template.type === 'contract') {
      html = sanitizePlaceholders(html);
      html = html.replace(/{{\s*currentDate\s*}}/g, formatDateTime());
    }

    // Apply stamps/signature where appropriate (harmless for sanitized content)
    try { html = await applyCompanyStampPlaceholders(html, req); } catch (e) { logger.warn('[previewPdf] applyCompanyStampPlaceholders failed', e && e.message); }
    try { html = await applyProfitPlanetSignature(html); } catch (e) { logger.warn('[previewPdf] applyProfitPlanetSignature failed', e && e.message); }

    if (DEBUG_PDF_FILES) saveDebugFile(`template_${id}_preview_html.html`, ensureHtmlDocument(html));

    // Render PDF server-side
    const browser = await puppeteer.launch({ headless: "new", args: ['--no-sandbox', '--disable-setuid-sandbox'] });
    const page = await browser.newPage();
    page.on('console', msg => { try { logger.debug(`[PUPPETEER][preview:${id}] ${msg.type()}: ${msg.text()}`); } catch (e) {} });
    await page.setContent(ensureHtmlDocument(html), { waitUntil: 'networkidle0', timeout: 60000 });
    const pdfBuffer = await page.pdf({ format: 'A4' });
    await browser.close();

    if (DEBUG_PDF_FILES) saveDebugFile(`template_${id}_preview.pdf`, pdfBuffer, null);
    logger.info(`[previewPdf] Generated preview PDF for template ${id} size=${pdfBuffer.length} bytes`);

    return sendPdfBuffer(res, pdfBuffer, `template_${id}_preview.pdf`, `template_${id}_preview`);
  } catch (err) {
    logger.error('[previewPdf] error', err && err.stack ? err.stack : err);
    return res.status(500).json({ error: 'Failed to generate preview PDF' });
  }
};

// NEW: Admin-only endpoint to preview the user's uploaded/signed contract (contract or gdpr) from object storage only
// GET /api/admin/contracts/:id/preview?userType=personal|company&contract_type=contract|gdpr|abo
exports.previewLatestForUser = async (req, res) => {
  const targetUserId = parseInt(req.params.id, 10);
  const contractTypeParam = (req.query.contract_type || req.query.contractType || '').toString().toLowerCase();
  const allowedContractTypes = ['contract', 'gdpr', 'abo'];
  const contractType = allowedContractTypes.includes(contractTypeParam) ? contractTypeParam : 'contract';

  const documentId = parseInt((req.query.documentId || req.query.document_id || '').toString(), 10);
  const objectKeyParam = (req.query.objectKey || req.query.object_key || '').toString();

  let folderStructureWarning = null;

  logger.info('[previewLatestForUser] start', { targetUserId, contractType, requestId: req.id });

  if (!req.user || !['admin', 'super_admin'].includes(req.user.role)) {
    return res.status(403).json({ error: 'Forbidden: Admins only' });
  }
  if (!Number.isFinite(targetUserId) || targetUserId <= 0) {
    return res.status(400).json({ error: 'Invalid user id' });
  }

  try {
    // Resolve target user (only for logging and email fallback if needed)
    let userRow = null;
    try {
      const [uRows] = await db.execute('SELECT id, email, user_type, role FROM users WHERE id = ? LIMIT 1', [targetUserId]);
      userRow = (uRows && uRows[0]) ? uRows[0] : null;
    } catch (e) {
      logger.error('[previewLatestForUser] failed to load users row', e && e.message);
    }

    const userTypeParam = (req.query.userType || req.query.user_type || '').toString().toLowerCase();
    const userType = ['personal', 'company'].includes(userTypeParam)
      ? userTypeParam
      : ((userRow && userRow.user_type) ? String(userRow.user_type).toLowerCase() : 'personal');
    const contractCategory = userType === 'company' ? 'company' : 'personal';

    // Check if there are files in the user's Exoscale contracts folder but no contract/gdpr subfolders.
    try {
      const s3ForList = sharedExoscaleClient || new S3Client({
        region: process.env.EXOSCALE_REGION,
        endpoint: process.env.EXOSCALE_ENDPOINT,
        forcePathStyle: true,
        credentials: {
          accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
          secretAccessKey: process.env.EXOSCALE_SECRET_KEY
        }
      });
      const basePrefix = `contracts/${contractCategory}/${targetUserId}/`;
      const listResp = await s3ForList.send(new ListObjectsV2Command({
        Bucket: process.env.EXOSCALE_BUCKET,
        Prefix: basePrefix,
        MaxKeys: 200
      }));
      const keys = (listResp && listResp.Contents ? listResp.Contents : [])
        .map(item => item && item.Key)
        .filter(Boolean);
      if (keys.length > 0) {
        const hasContractFolder = keys.some(k => k.startsWith(`${basePrefix}contract/`));
        const hasGdprFolder = keys.some(k => k.startsWith(`${basePrefix}gdpr/`));
        const hasAboFolder = keys.some(k => k.startsWith(`${basePrefix}abo/`));
        if (!hasContractFolder && !hasGdprFolder && !hasAboFolder) {
          folderStructureWarning = 'Admin user has to clean up and move the files in exoscale folder';
          logger.error('[previewLatestForUser] contract folder structure invalid: Admin user has to clean up and move the files in exoscale folder', {
            userId: targetUserId,
            contractCategory,
            prefix: basePrefix,
            sampleKeys: keys.slice(0, 10)
          });
        }
      }
    } catch (e) {
      logger.warn('[previewLatestForUser] contract folder structure check failed', e && (e.stack || e.message));
    }

    const jsonWithWarning = (payload) => {
      if (!folderStructureWarning) return payload;
      return { ...payload, warning: folderStructureWarning };
    };

    // Fetch a specific document by id (if provided), by objectKey, otherwise latest by contractType
    let doc = null;
    let resolvedContractType = contractType;
    if (Number.isFinite(documentId) && documentId > 0) {
      try {
        const [rows] = await db.execute(
          `SELECT id, object_storage_id, contract_type
             FROM user_documents
            WHERE id = ? AND user_id = ?
            LIMIT 1`,
          [documentId, targetUserId]
        );
        const arr = Array.isArray(rows) ? rows : (rows ? [rows] : []);
        doc = arr[0] || null;
        if (doc && doc.contract_type && allowedContractTypes.includes(String(doc.contract_type).toLowerCase())) {
          resolvedContractType = String(doc.contract_type).toLowerCase();
        } else if (doc && doc.object_storage_id && /\/gdpr\//i.test(doc.object_storage_id)) {
          resolvedContractType = 'gdpr';
        }
        logger.info('[previewLatestForUser] user_documents lookup by documentId', { targetUserId, documentId, resolvedContractType, found: !!doc });
      } catch (e) {
        logger.warn('[previewLatestForUser] user_documents lookup by documentId failed', e && (e.stack || e.message));
      }
    } else if (objectKeyParam) {
      const key = objectKeyParam;
      const basePrefix = `contracts/${contractCategory}/${targetUserId}/`;
      if (!key.startsWith(basePrefix)) {
        return res.status(400).json({ error: 'Invalid object key' });
      }
      if (key.includes('/gdpr/')) resolvedContractType = 'gdpr';
      else if (key.includes('/contract/')) resolvedContractType = 'contract';
      else if (key.includes('/abo/')) resolvedContractType = 'abo';
      doc = { object_storage_id: key };
      logger.info('[previewLatestForUser] using objectKey directly', { targetUserId, resolvedContractType, key });
    } else {
      // Choose document_type set based on contractType (aligned with ContractUploadService paths)
      // uploadContract stores under contracts/<category>/<userId>/<contract_type> with document_type 'contract'
      // so use contract_type column to disambiguate between contract vs gdpr
      const docTypesMap = {
        contract: ['contract', 'signed_contract', 'contract_pdf', 'signed_contract_pdf'],
        gdpr: ['contract', 'signed_contract', 'contract_pdf', 'signed_contract_pdf'],
        abo: ['contract', 'signed_contract', 'contract_pdf', 'signed_contract_pdf']
      };
      const docTypes = docTypesMap[contractType] || docTypesMap.contract;
      const placeholders = docTypes.map(() => '?').join(',');

      try {
        const [rows] = await db.execute(
          `SELECT object_storage_id
             FROM user_documents
            WHERE user_id = ?
              AND document_type IN (${placeholders})
              AND object_storage_id IS NOT NULL
              AND (contract_type = ? OR (contract_type IS NULL AND ? = 'contract'))
            ORDER BY upload_at DESC, id DESC
            LIMIT 1`,
          [targetUserId, ...docTypes, contractType, contractType]
        );
        const arr = Array.isArray(rows) ? rows : (rows ? [rows] : []);
        doc = arr[0] || null;
        logger.info('[previewLatestForUser] user_documents lookup', { targetUserId, contractType, count: arr.length });
      } catch (e) {
        logger.warn('[previewLatestForUser] user_documents lookup failed', e && (e.stack || e.message));
      }
    }

    const activeContractType = resolvedContractType || contractType;

    if (!doc || !doc.object_storage_id) {
      if (folderStructureWarning) res.setHeader('X-Contract-Preview-Warning', folderStructureWarning);
      return res.status(404).json(jsonWithWarning({ message: `No uploaded ${activeContractType.toUpperCase()} file found for this user` }));
    }

    try {
      const createClient = () => sharedExoscaleClient || new S3Client({
        region: process.env.EXOSCALE_REGION,
        endpoint: process.env.EXOSCALE_ENDPOINT,
        forcePathStyle: true,
        credentials: {
          accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
          secretAccessKey: process.env.EXOSCALE_SECRET_KEY
        }
      });
      let s3File = createClient();
      logger.info('[previewLatestForUser] attempting S3 fetch', {
        bucket: process.env.EXOSCALE_BUCKET,
        key: doc.object_storage_id,
        userId: targetUserId,
        contractType: activeContractType
      });
      const cmd = new GetObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: doc.object_storage_id });
      let fileObj;
      try {
        fileObj = await s3File.send(cmd);
      } catch (firstErr) {
        logger.warn('[previewLatestForUser] S3 fetch failed (shared client), retrying with fresh client', {
          key: doc.object_storage_id,
          userId: targetUserId,
          contractType,
          error: formatS3Error(firstErr)
        });
        s3File = new S3Client({
          region: process.env.EXOSCALE_REGION,
          endpoint: process.env.EXOSCALE_ENDPOINT,
          forcePathStyle: true,
          credentials: {
            accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
            secretAccessKey: process.env.EXOSCALE_SECRET_KEY
          }
        });
        fileObj = await s3File.send(cmd);
      }
      const pdfBuffer = await s3BodyToBuffer(fileObj.Body);
      if (!pdfBuffer || !pdfBuffer.length) {
        logger.warn('[previewLatestForUser] S3 returned empty Body', { key: doc.object_storage_id, userId: targetUserId, contractType: activeContractType });
        if (folderStructureWarning) res.setHeader('X-Contract-Preview-Warning', folderStructureWarning);
        return res.status(404).json(jsonWithWarning({ message: `${activeContractType.toUpperCase()} file not available` }));
      }

      const b64 = pdfBuffer.toString('base64');
      const html = ensureHtmlDocument(`
        <html><head><meta charset="utf-8" /><title>${resolvedContractType.toUpperCase()} Preview</title></head>
        <body style="margin:0;padding:0;height:100vh;background:#0f172a;">
          <embed src="data:application/pdf;base64,${b64}" type="application/pdf" style="width:100%;height:100%;border:none;" />
        </body></html>
      `);
      res.setHeader('Content-Type', 'text/html; charset=utf-8');
      if (folderStructureWarning) res.setHeader('X-Contract-Preview-Warning', folderStructureWarning);
      return res.send(html);
    } catch (e) {
      if (e && (e.name === 'NoSuchKey' || (e.$metadata && e.$metadata.httpStatusCode === 404))) {
        logger.warn('[previewLatestForUser] object missing in storage', { key: doc.object_storage_id, userId: targetUserId, contractType: activeContractType });
        if (folderStructureWarning) res.setHeader('X-Contract-Preview-Warning', folderStructureWarning);
        return res.status(404).json(jsonWithWarning({ message: `${activeContractType.toUpperCase()} file not available` }));
      }
      logger.error('[previewLatestForUser] S3 fetch failed', {
        key: doc.object_storage_id,
        userId: targetUserId,
        contractType: activeContractType,
        error: formatS3Error(e)
      });
      if (folderStructureWarning) res.setHeader('X-Contract-Preview-Warning', folderStructureWarning);
      return res.status(500).json(jsonWithWarning({ message: 'Failed to load user document' }));
    }
  } catch (err) {
    logger.error('[previewLatestForUser] error', err && err.stack ? err.stack : err);
    return res.status(500).json({ error: 'Failed to render preview' });
  }
};

// NEW: Admin-only endpoint to list contract files from object storage for a user
// GET /api/admin/contracts/:id/files?userType=personal|company
exports.listUserContractFiles = async (req, res) => {
  const targetUserId = parseInt(req.params.id, 10);
  if (!req.user || !['admin', 'super_admin'].includes(req.user.role)) {
    return res.status(403).json({ error: 'Forbidden: Admins only' });
  }
  if (!Number.isFinite(targetUserId) || targetUserId <= 0) {
    return res.status(400).json({ error: 'Invalid user id' });
  }

  try {
    const [uRows] = await db.execute('SELECT id, user_type FROM users WHERE id = ? LIMIT 1', [targetUserId]);
    const userRow = (uRows && uRows[0]) ? uRows[0] : null;
    const userType = (userRow && userRow.user_type) ? String(userRow.user_type).toLowerCase() : null;
    const contractCategory = userType === 'company' ? 'company' : 'personal';

    const s3Client = sharedExoscaleClient || new S3Client({
      region: process.env.EXOSCALE_REGION,
      endpoint: process.env.EXOSCALE_ENDPOINT,
      forcePathStyle: true,
      credentials: {
        accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
        secretAccessKey: process.env.EXOSCALE_SECRET_KEY
      }
    });

    const basePrefix = `contracts/${contractCategory}/${targetUserId}/`;
    const contractPrefix = `${basePrefix}contract/`;
    const gdprPrefix = `${basePrefix}gdpr/`;

    const [contractKeysRaw, gdprKeysRaw, docRows] = await Promise.all([
      listAllKeys(s3Client, contractPrefix),
      listAllKeys(s3Client, gdprPrefix),
      db.execute(
        `SELECT id, object_storage_id, contract_type
           FROM user_documents
          WHERE user_id = ? AND document_type = 'contract'`,
        [targetUserId]
      )
    ]);
    const normalizeKeys = (keys) => (Array.isArray(keys) ? keys : [])
      .filter(k => !!k && !k.endsWith('/'))
      .filter(k => /\.pdf$/i.test(String(k)));
    const contractKeys = normalizeKeys(contractKeysRaw);
    const gdprKeys = normalizeKeys(gdprKeysRaw);
    const rows = Array.isArray(docRows && docRows[0]) ? docRows[0] : [];
    const idMap = new Map(rows.map(r => [String(r.object_storage_id), r]));

    const toItem = (key) => {
      const doc = idMap.get(String(key));
      return {
        key,
        filename: path.posix.basename(key),
        documentId: doc ? doc.id : null,
        contract_type: doc ? doc.contract_type : null
      };
    };

    return res.json({
      userType,
      contract: contractKeys.map(toItem),
      gdpr: gdprKeys.map(toItem)
    });
  } catch (err) {
    logger.error('[listUserContractFiles] error', err && err.stack ? err.stack : err);
    return res.status(500).json({ error: 'Failed to list user contract files' });
  }
};

// NEW: Admin-only endpoint to list contract documents for a user
// GET /api/admin/contracts/:id/documents?userType=personal|company
exports.listUserContractDocuments = async (req, res) => {
  const targetUserId = parseInt(req.params.id, 10);
  if (!req.user || !['admin', 'super_admin'].includes(req.user.role)) {
    return res.status(403).json({ error: 'Forbidden: Admins only' });
  }
  if (!Number.isFinite(targetUserId) || targetUserId <= 0) {
    return res.status(400).json({ error: 'Invalid user id' });
  }

  try {
    let userRow = null;
    try {
      const [uRows] = await db.execute('SELECT id, email, user_type FROM users WHERE id = ? LIMIT 1', [targetUserId]);
      userRow = (uRows && uRows[0]) ? uRows[0] : null;
    } catch (e) {
      logger.warn('[listUserContractDocuments] failed to load users row', e && e.message);
    }

    const [rows] = await db.execute(
      `SELECT id, user_id, document_type, contract_type, object_storage_id, original_filename, file_size, mime_type, upload_at
         FROM user_documents
        WHERE user_id = ? AND document_type = 'contract'
        ORDER BY upload_at DESC, id DESC`,
      [targetUserId]
    );

    const docs = (Array.isArray(rows) ? rows : []).map((d) => {
      const key = d.object_storage_id || '';
      let folderType = 'loose';
      if (/\/gdpr\//i.test(key)) folderType = 'gdpr';
      else if (/\/contract\//i.test(key)) folderType = 'contract';
      else if (/\/abo\//i.test(key)) folderType = 'abo';
      return {
        ...d,
        folderType
      };
    });

    return res.json({
      userType: userRow ? userRow.user_type : null,
      documents: docs
    });
  } catch (err) {
    logger.error('[listUserContractDocuments] error', err && err.stack ? err.stack : err);
    return res.status(500).json({ error: 'Failed to list user contract documents' });
  }
};

// NEW: Admin-only endpoint to move a contract document between /contract and /gdpr
// POST /api/admin/contracts/:id/move { documentId, targetType }
exports.moveUserContractDocument = async (req, res) => {
  const targetUserId = parseInt(req.params.id, 10);
  const { documentId, targetType, objectKey } = req.body || {};
  const allowedTypes = ['contract', 'gdpr', 'abo'];
  const target = (targetType || '').toString().toLowerCase();

  if (!req.user || !['admin', 'super_admin'].includes(req.user.role)) {
    return res.status(403).json({ error: 'Forbidden: Admins only' });
  }
  if (!Number.isFinite(targetUserId) || targetUserId <= 0) {
    return res.status(400).json({ error: 'Invalid user id' });
  }
  if ((!Number.isFinite(documentId) || documentId <= 0) && !objectKey) {
    return res.status(400).json({ error: 'Invalid document id' });
  }
  if (!allowedTypes.includes(target)) {
    return res.status(400).json({ error: 'Invalid target type' });
  }

  try {
    const [uRows] = await db.execute('SELECT id, user_type FROM users WHERE id = ? LIMIT 1', [targetUserId]);
    const userRow = (uRows && uRows[0]) ? uRows[0] : null;
    const userType = (userRow && userRow.user_type) ? String(userRow.user_type).toLowerCase() : null;
    const contractCategory = userType === 'company' ? 'company' : 'personal';

    let currentKey = null;
    if (objectKey) {
      const key = String(objectKey);
      const basePrefix = `contracts/${contractCategory}/${targetUserId}/`;
      if (!key.startsWith(basePrefix) || (!key.includes('/contract/') && !key.includes('/gdpr/') && !key.includes('/abo/'))) {
        return res.status(400).json({ error: 'Invalid object key' });
      }
      currentKey = key;
    } else {
      const [rows] = await db.execute(
        `SELECT id, object_storage_id, contract_type
           FROM user_documents
          WHERE id = ? AND user_id = ?
          LIMIT 1`,
        [documentId, targetUserId]
      );
      const doc = (rows && rows[0]) ? rows[0] : null;
      if (!doc || !doc.object_storage_id) {
        return res.status(404).json({ error: 'Document not found' });
      }
      currentKey = String(doc.object_storage_id);
    }
    const fileName = path.posix.basename(currentKey);
    const basePrefix = `contracts/${contractCategory}/${targetUserId}/`;
    let destKey = `${basePrefix}${target}/${fileName}`;

    const s3Client = sharedExoscaleClient || new S3Client({
      region: process.env.EXOSCALE_REGION,
      endpoint: process.env.EXOSCALE_ENDPOINT,
      forcePathStyle: true,
      credentials: {
        accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
        secretAccessKey: process.env.EXOSCALE_SECRET_KEY
      }
    });

    if (currentKey !== destKey) {
      try {
        if (await objectExists(s3Client, destKey)) {
          destKey = appendTimestampToKey(destKey);
        }
      } catch (e) {
        logger.warn('[moveUserContractDocument] objectExists failed', { destKey, error: formatS3Error(e) });
      }

      const copySource = `${process.env.EXOSCALE_BUCKET}/${encodeURIComponent(currentKey)}`;
      await s3Client.send(new CopyObjectCommand({
        Bucket: process.env.EXOSCALE_BUCKET,
        CopySource: copySource,
        Key: destKey
      }));
      await s3Client.send(new DeleteObjectCommand({
        Bucket: process.env.EXOSCALE_BUCKET,
        Key: currentKey
      }));
    }

    await db.execute(
      `UPDATE user_documents SET object_storage_id = ?, contract_type = ? WHERE object_storage_id = ?`,
      [destKey, target, currentKey]
    );

    return res.json({
      success: true,
      documentId,
      from: currentKey,
      to: destKey,
      contract_type: target
    });
  } catch (err) {
    logger.error('[moveUserContractDocument] error', err && err.stack ? err.stack : err);
    return res.status(500).json({ error: 'Failed to move document' });
  }
};

// Authenticated user endpoint to preview latest active contract template (raw HTML; placeholders intact)
// GET /api/contracts/preview/latest
exports.previewLatestForMe = async (req, res) => {
  if (!req.user) return res.status(401).json({ error: 'Unauthorized' });
  const targetUserId = req.user.id || req.user.userId;
  const userType = (req.user.user_type || req.user.userType || '').toString().toLowerCase();
  if (!targetUserId || !userType) return res.status(400).json({ error: 'Invalid authenticated user' });

  const contractTypeParam = (req.query.contract_type || req.query.contractType || '').toString().toLowerCase();
  const allowedContractTypes = ['contract', 'gdpr', 'abo'];
  const contractType = allowedContractTypes.includes(contractTypeParam) ? contractTypeParam : 'contract';
    const lang = getRequestedTemplateLanguage(req);

  try {
    const { latest, html } = await fetchLatestActiveContractTemplateHtml({ userType, contractType, lang });
    if (!latest) {
      logger.info('[previewLatestForMe] no active template', { userId: targetUserId, userType, contractType });
      res.setHeader('Content-Type', 'text/html; charset=utf-8');
      return res.status(200).send('');
    }
    res.setHeader('Content-Type', 'text/html; charset=utf-8');
    return res.send(html);
  } catch (err) {
    logger.error('[previewLatestForMe] error', err && err.stack ? err.stack : err);
    return res.status(500).json({ error: 'Failed to render preview' });
  }
};

// User: convenience endpoint to preview latest active ABO contract (HTML) for authenticated user
// GET /api/contracts/abo/active
exports.previewLatestAboForMe = async (req, res) => {
  if (!req.user) return res.status(401).json({ success: false, message: 'Unauthorized' });
  const targetUserId = req.user.id || req.user.userId;
  const userType = (req.user.user_type || req.user.userType || '').toString().toLowerCase();
  const lang = getRequestedTemplateLanguage(req);
  if (!targetUserId || !userType) return res.status(400).json({ success: false, message: 'Invalid authenticated user' });

  try {
    const { latest, html } = await fetchLatestActiveContractTemplateHtml({ userType, contractType: 'abo', lang });
    if (!latest) {
      logger.info('[previewLatestAboForMe] no active template', { userId: targetUserId, userType, contractType: 'abo' });
      res.setHeader('Content-Type', 'text/html; charset=utf-8');
      return res.status(200).send('');
    }
    res.setHeader('Content-Type', 'text/html; charset=utf-8');
    return res.status(200).send(html || '');
  } catch (err) {
    logger.error('[previewLatestAboForMe] error', err && err.stack ? err.stack : err);
    const status = err && err.status ? err.status : 500;
    return res.status(status).json({ success: false, message: 'Failed to render abo contract' });
  }
};

// NEW controller: download sanitized PDF (variables emptied)
// Keeps stamp/signature placeholders so company stamps or profitplanetSignature can still be applied if available.
exports.downloadPdf = async (req, res) => {
  const id = req.params.id;
  logger.info(`[downloadPdf] Start for templateId=${id}`);
  try {
    const template = await DocumentTemplateService.getTemplate(id);
    if (!template) {
      logger.warn(`[downloadPdf] Template not found id=${id}`);
      return res.status(404).json({ error: 'Template not found' });
    }

    const userType = req.user?.user_type || req.user?.userType;
    if (!userType) {
      logger.warn('[downloadPdf] unauthorized_missing_user', { id });
      return res.status(401).json({ error: 'Unauthorized' });
    }
    if (template.state !== 'active') {
      logger.warn('[downloadPdf] template_inactive', { id, state: template.state });
      return res.status(403).json({ error: 'Template inactive' });
    }
    if (template.user_type !== 'both' && template.user_type !== userType) {
      logger.warn('[downloadPdf] template_user_type_mismatch', { id, templateUserType: template.user_type, userType });
      return res.status(403).json({ error: 'Template not allowed for this user type' });
    }

    // Fetch HTML from S3
    const s3 = new S3Client({
      region: process.env.EXOSCALE_REGION,
      endpoint: process.env.EXOSCALE_ENDPOINT,
      credentials: {
        accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
        secretAccessKey: process.env.EXOSCALE_SECRET_KEY
      }
    });
    const command = new GetObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: template.storageKey });
    const fileObj = await s3.send(command);
    if (!fileObj.Body) {
      logger.warn('[downloadPdf] empty S3 body', { id, key: template.storageKey });
      return res.status(404).json({ error: 'Template file not available' });
    }
    let html = await streamToString(fileObj.Body, id);

    // SANITIZE: remove variables for downloaded PDF.
    // Allow only stamp/signature placeholders to remain so images can still be injected.
    html = sanitizePlaceholders(html, ['companyStamp','companyStampInline','companyStampSmall','companyLogo','profitplanetSignature']);
    // Do NOT keep currentDate for download (user requested variables emptied)

    // Apply company stamp & profitplanet signature (these placeholders were preserved above)
    try { html = await applyCompanyStampPlaceholders(html, req); } catch (e) { logger.warn('[downloadPdf] applyCompanyStampPlaceholders failed', e && e.message); }
    try { html = await applyProfitPlanetSignature(html); } catch (e) { logger.warn('[downloadPdf] applyProfitPlanetSignature failed', e && e.message); }

    if (DEBUG_PDF_FILES) saveDebugFile(`template_${id}_download_html.html`, ensureHtmlDocument(html));

    // Render to PDF server-side
    const browser = await puppeteer.launch({ headless: "new", args: ['--no-sandbox', '--disable-setuid-sandbox'] });
    const page = await browser.newPage();
    page.on('console', msg => { try { logger.debug(`[PUPPETEER][download:${id}] ${msg.type()}: ${msg.text()}`); } catch (e) {} });
    await page.setContent(ensureHtmlDocument(html), { waitUntil: 'networkidle0', timeout: 60000 });
    const pdfBuffer = await page.pdf({ format: 'A4' });
    await browser.close();

    if (DEBUG_PDF_FILES) saveDebugFile(`template_${id}_download.pdf`, pdfBuffer, null);
    logger.info(`[downloadPdf] Generated download PDF for template ${id} size=${pdfBuffer.length} bytes`);

    return sendPdfBuffer(res, pdfBuffer, `template_${id}_download.pdf`, `template_${id}_download`);
  } catch (err) {
    logger.error('[downloadPdf] error', err && err.stack ? err.stack : err);
    return res.status(500).json({ error: 'Failed to generate download PDF' });
  }
};
