import { PDFDocument } from 'pdf-lib';
import * as asn1js from 'asn1js';
import {
  Certificate,
  SignedData,
  ContentInfo,
  IssuerAndSerialNumber,
  Attribute,
  AlgorithmIdentifier,
  EncapsulatedContentInfo,
  SignerInfo,
  SignedAndUnsignedAttributes
} from 'pkijs';
import crypto from 'node:crypto';
import { Buffer } from 'node:buffer';

// pkijs setup (webcrypto global) - utilisation de l'API Web Crypto native de Node.js 18
if (typeof globalThis.crypto === 'undefined') {
  globalThis.crypto = crypto.webcrypto;
}

// =====================================================
// PDF helpers — PAdES incremental update avec /Sig + /ByteRange
// =====================================================
export async function preparePdfWithPlaceholder(pdfBytes) {
  const originalPdf = Buffer.from(pdfBytes);
  const pdfStructure = parsePdfStructure(originalPdf);
  
  // Placeholders pour ByteRange et Contents (tailles fixes pour remplacement facile)
  const byteRangePlaceholder = '[0000000000 0000000000 0000000000 0000000000]'; // 51 caractères
  const contentsPlaceholder = '<' + '0'.repeat(65536) + '>'; // 32KB pour la signature CMS
  
  // Générer le timestamp UNE SEULE FOIS pour éviter les différences entre digest et finalisation
  const signingTime = new Date().toISOString().replace(/[-:T.Z]/g, '').slice(0, 14);
  
  const incrementalUpdate = buildIncrementalUpdate(
    pdfStructure,
    byteRangePlaceholder,
    contentsPlaceholder,
    signingTime
  );
  
  return { originalPdf, pdfStructure, incrementalUpdate, signingTime };
}

// Parser le PDF pour extraire la structure nécessaire à la révision incrémentale
function parsePdfStructure(pdfBytes) {
  const pdfStr = pdfBytes.toString('latin1');
  
  // 1. Trouver le dernier startxref
  const startxrefMatch = pdfStr.match(/startxref\s+(\d+)\s+%%EOF\s*$/);
  if (!startxrefMatch) throw new Error('startxref non trouvé dans le PDF');
  const prevStartxref = parseInt(startxrefMatch[1], 10);
  
  // 2. Trouver le dernier numéro d'objet utilisé
  const objRegex = /(\d+)\s+\d+\s+obj/g;
  let maxObjNum = 0;
  let match;
  while ((match = objRegex.exec(pdfStr)) !== null) {
    const objNum = parseInt(match[1], 10);
    if (objNum > maxObjNum) maxObjNum = objNum;
  }
  
  // 3. Extraire le trailer pour trouver /Root et /Info
  const trailerMatch = pdfStr.match(/trailer\s*<<([^>]*)>>/s);
  let rootRef = null;
  let infoRef = null;
  let sizeNum = maxObjNum + 1;
  let pagesRef = null;
  
  if (trailerMatch) {
    const trailerDict = trailerMatch[1];
    const rootMatch = trailerDict.match(/\/Root\s+(\d+)\s+\d+\s+R/);
    if (rootMatch) rootRef = parseInt(rootMatch[1], 10);
    
    const infoMatch = trailerDict.match(/\/Info\s+(\d+)\s+\d+\s+R/);
    if (infoMatch) infoRef = parseInt(infoMatch[1], 10);
    
    const sizeMatch = trailerDict.match(/\/Size\s+(\d+)/);
    if (sizeMatch) sizeNum = parseInt(sizeMatch[1], 10);
  }
  
  // 4. Chercher /AcroForm et /Pages dans le catalog
  let acroFormRef = null;
  if (rootRef) {
    const catalogMatch = pdfStr.match(new RegExp(`${rootRef}\\s+\\d+\\s+obj\\s*<<([^>]*(?:>>.*?<<)*)>>`, 's'));
    if (catalogMatch) {
      const catalogDict = catalogMatch[1];
      const acroMatch = catalogDict.match(/\/AcroForm\s+(\d+)\s+\d+\s+R/);
      if (acroMatch) acroFormRef = parseInt(acroMatch[1], 10);
      
      const pagesMatch = catalogDict.match(/\/Pages\s+(\d+)\s+\d+\s+R/);
      if (pagesMatch) pagesRef = parseInt(pagesMatch[1], 10);
    }
  }
  
  // 5. Trouver la première page (pour y attacher le widget)
  let firstPageRef = null;
  if (pagesRef) {
    // Lire l'objet Pages
    const pagesObjMatch = pdfStr.match(new RegExp(`${pagesRef}\\s+\\d+\\s+obj\\s*<<([^>]*(?:>>.*?<<)*)>>`, 's'));
    if (pagesObjMatch) {
      // Chercher /Kids [...]
      const kidsMatch = pagesObjMatch[1].match(/\/Kids\s*\[\s*(\d+)\s+\d+\s+R/);
      if (kidsMatch) firstPageRef = parseInt(kidsMatch[1], 10);
    }
  }
  
  return {
    prevStartxref,
    nextObjNum: maxObjNum + 1,
    rootRef,
    infoRef,
    acroFormRef,
    firstPageRef,
    pagesRef,
    sizeNum
  };
}

// Construire la révision incrémentale PDF avec /Sig, /ByteRange, /Contents
function buildIncrementalUpdate(pdfStructure, cmsHex, signingTime) {
  const {
    prevStartxref,
    nextObjNum,
    rootRef,
    acroFormRef,
    firstPageRef,
    pagesRef,
    sizeNum
  } = pdfStructure;
  
  let objNum = nextObjNum;
  const newObjects = [];
  
  // Taille du placeholder pour /Contents (doit être suffisant pour le CMS hex)
  const contentsPlaceholderSize = 65536; // 32KB * 2 (hex)
  const contentsPlaceholder = '<' + '0'.repeat(contentsPlaceholderSize) + '>';
  
  // Placeholder ByteRange : sera calculé plus tard mais doit avoir une taille fixe
  // Format: [0 AAAAAAAAAA BBBBBBBBBB CCCCCCCCCC] avec des chiffres, pas d'espaces variables
  const byteRangePlaceholder = '[0000000000 0000000000 0000000000 0000000000]';
  
  // 1. Créer le dictionnaire /TransformParams pour DocMDP level 1 (verrouillage total)
  const transformParamsObjNum = objNum++;
  const transformParamsObj = `${transformParamsObjNum} 0 obj
<<
/Type /TransformParams
/V /1.2
/P 1
>>
endobj
`;
  newObjects.push(transformParamsObj);
  
  // 2. Créer le dictionnaire /Sig avec /Reference pour DocMDP
  const sigObjNum = objNum++;
  const sigObj = `${sigObjNum} 0 obj
<<
/Type /Sig
/Filter /Adobe.PPKLite
/SubFilter /ETSI.CAdES.detached
/ByteRange ${byteRangePlaceholder}
/Contents ${contentsPlaceholder}
/M (D:${signingTime})
/Reference [<<
  /Type /SigRef
  /TransformMethod /DocMDP
  /TransformParams ${transformParamsObjNum} 0 R
>>]
>>
endobj
`;
  newObjects.push(sigObj);
  
  // 3. Créer le widget de signature (annotation)
  const widgetObjNum = objNum++;
  const widgetObj = `${widgetObjNum} 0 obj
<<
/Type /Annot
/Subtype /Widget
/FT /Sig
/T (Signature1)
/V ${sigObjNum} 0 R
/P ${firstPageRef} 0 R
/Rect [0 0 0 0]
/F 132
>>
endobj
`;
  newObjects.push(widgetObj);
  
  // 4. Créer /AcroForm
  const acroFormObjNum = objNum++;
  const acroFormObj = `${acroFormObjNum} 0 obj
<<
/Fields [${widgetObjNum} 0 R]
/SigFlags 3
>>
endobj
`;
  newObjects.push(acroFormObj);
  
  // 5. Créer /Perms pour verrouiller le document (DocMDP level 1)
  const permsObjNum = objNum++;
  const permsObj = `${permsObjNum} 0 obj
<<
/DocMDP ${sigObjNum} 0 R
>>
endobj
`;
  newObjects.push(permsObj);
  
  // 6. Mettre à jour le Catalog pour référencer /AcroForm, /Pages et /Perms
  const catalogObjNum = objNum++;
  const catalogObj = `${catalogObjNum} 0 obj
<<
/Type /Catalog
/Pages ${pagesRef} 0 R
/AcroForm ${acroFormObjNum} 0 R
/Perms ${permsObjNum} 0 R
>>
endobj
`;
  newObjects.push(catalogObj);
  
  // 7. Mettre à jour la première page pour ajouter le widget aux /Annots
  if (firstPageRef) {
    const pageObjNum = objNum++;
    const pageObj = `${pageObjNum} 0 obj
<<
/Type /Page
/Annots [${widgetObjNum} 0 R]
>>
endobj
`;
    newObjects.push(pageObj);
  }
  
  return {
    sigObjNum,
    widgetObjNum,
    acroFormObjNum,
    catalogObjNum,
    newObjects,
    nextObjNum: objNum,
    contentsPlaceholder,
    contentsPlaceholderSize,
    byteRangePlaceholder
  };
}

export async function finalizePdfWithCms(pdfWithPlaceholder, byteRangeInfo, pdfStructure, incrementalUpdate, cmsDer, tempPdfWithRevision, contentsStart, contentsEnd) {
  const { signatureSize } = byteRangeInfo;
  
  // Convertir le CMS en hex
  const cmsHex = cmsDer.toString('hex').toUpperCase();
  if (cmsHex.length > signatureSize * 2) {
    throw new Error(`CMS trop grand pour le placeholder (${cmsHex.length / 2} > ${signatureSize})`);
  }
  
  // Utiliser le PDF temporaire déjà assemblé
  let finalPdfStr = tempPdfWithRevision.toString('latin1');
  
  // Calculer le /ByteRange final
  const byteRange = [0, contentsStart, contentsEnd, tempPdfWithRevision.length - contentsEnd];
  
  console.log('[finalizePdfWithCms] ByteRange:', byteRange);
  console.log('[finalizePdfWithCms] CMS hex length:', cmsHex.length);
  
  // Remplacer le placeholder /ByteRange (padding avec des 0 pour garder longueur identique)
  const byteRangeStr = `[${String(byteRange[0]).padStart(10, '0')} ${String(byteRange[1]).padStart(10, '0')} ${String(byteRange[2]).padStart(10, '0')} ${String(byteRange[3]).padStart(10, '0')}]`;
  
  finalPdfStr = finalPdfStr.replace(
    '[0000000000 0000000000 0000000000 0000000000]',
    byteRangeStr
  );
  
  // Remplacer le placeholder /Contents avec le CMS hex (padded)
  const cmsPadded = cmsHex + '0'.repeat(65536 - cmsHex.length);
  finalPdfStr = finalPdfStr.replace(
    /\/Contents <0+>/,
    `/Contents <${cmsPadded}>`
  );
  
  const finalBuffer = Buffer.from(finalPdfStr, 'latin1');
  
  // VALIDATION finale
  const validationPart1 = finalBuffer.slice(byteRange[0], byteRange[0] + byteRange[1]);
  const validationPart2 = finalBuffer.slice(byteRange[2], byteRange[2] + byteRange[3]);
  const validationDigest = crypto.createHash('sha256').update(validationPart1).update(validationPart2).digest();
  console.log('[finalizePdfWithCms] VALIDATION - PDF digest recalculé:', validationDigest.toString('hex'));
  
  return finalBuffer;
}

// =====================================================
// SignedAttributes (DER) + digest SHA-256 pour KMS
// =====================================================
// OIDs utiles
const OID_ID_DATA = '1.2.840.113549.1.7.1'; // id-data (ContentInfo)
const OID_ATTR_CONTENT_TYPE = '1.2.840.113549.1.9.3';
const OID_ATTR_SIGNING_TIME = '1.2.840.113549.1.9.5';
const OID_ATTR_MESSAGE_DIGEST = '1.2.840.113549.1.9.4';

export async function buildSignedAttributesDigest(pdfWithPlaceholder, byteRangeInfo, pdfStructure, incrementalUpdate) {
  // Construire le PDF final avec placeholder pour trouver le vrai ByteRange
  const { originalPdfLength } = byteRangeInfo;
  const originalPdf = pdfWithPlaceholder.slice(0, originalPdfLength);
  
  // Assembler temporairement avec placeholder
  const tempPdfWithRevision = assemblePdfWithRevision(originalPdf, pdfStructure, incrementalUpdate);
  
  // Trouver position du /Contents dans ce PDF
  const tempStr = tempPdfWithRevision.toString('latin1');
  const contentsMatch = tempStr.match(/\/Contents <(0+)>/);
  if (!contentsMatch) throw new Error('Placeholder /Contents non trouvé');
  
  const contentsStart = contentsMatch.index + '/Contents <'.length;
  const contentsEnd = contentsStart + contentsMatch[1].length;
  
  console.log('[buildSignedAttributesDigest] ByteRange calculé:', [0, contentsStart, contentsEnd, tempPdfWithRevision.length - contentsEnd]);
  
  // Calculer digest sur [0...contentsStart] + [contentsEnd...EOF]
  const part1 = tempPdfWithRevision.slice(0, contentsStart);
  const part2 = tempPdfWithRevision.slice(contentsEnd);
  
  const pdfDigest = crypto.createHash('sha256').update(part1).update(part2).digest();
  console.log('[buildSignedAttributesDigest] PDF digest (SHA256):', pdfDigest.toString('hex'));
  
  const { signedAttributesDer, signedAttributesDigest } =
    buildSignedAttributesDigestFromPdfDigest(pdfDigest);
  
  return { signedAttributesDer, signedAttributesDigest, tempPdfWithRevision, contentsStart, contentsEnd };
}

// Helper pour assembler le PDF avec révision
function assemblePdfWithRevision(originalPdf, pdfStructure, incrementalUpdate) {
  let currentOffset = originalPdf.length;
  const objectsWithOffsets = [Buffer.from('\n', 'latin1')];
  currentOffset += 1;
  
  const xrefEntries = [];
  for (let i = 0; i < incrementalUpdate.newObjects.length; i++) {
    const objStr = incrementalUpdate.newObjects[i];
    xrefEntries.push({ objNum: pdfStructure.nextObjNum + i, offset: currentOffset, gen: 0 });
    objectsWithOffsets.push(Buffer.from(objStr, 'latin1'));
    currentOffset += Buffer.byteLength(objStr, 'latin1');
  }
  
  const xrefOffset = currentOffset;
  let xrefTable = 'xref\n0 1\n0000000000 65535 f \n';
  xrefTable += `${pdfStructure.nextObjNum} ${xrefEntries.length}\n`;
  
  for (const entry of xrefEntries) {
    xrefTable += `${String(entry.offset).padStart(10, '0')} ${String(entry.gen).padStart(5, '0')} n \n`;
  }
  
  let trailer = `trailer\n<<\n/Size ${pdfStructure.nextObjNum + xrefEntries.length}\n/Prev ${pdfStructure.prevStartxref}\n/Root ${incrementalUpdate.catalogObjNum} 0 R\n`;
  if (pdfStructure.infoRef) {
    trailer += `/Info ${pdfStructure.infoRef} 0 R\n`;
  }
  trailer += `>>\nstartxref\n${xrefOffset}\n%%EOF\n`;
  
  return Buffer.concat([originalPdf, ...objectsWithOffsets, Buffer.from(xrefTable + trailer, 'latin1')]);
}

export function buildSignedAttributesDigestFromPdfDigest(pdfMessageDigest) {
  console.log('[buildSignedAttributesDigest] pdfMessageDigest:', pdfMessageDigest.toString('hex'));
  
  // Attribute ::= SEQUENCE { attrType OBJECT IDENTIFIER, attrValues SET OF ANY }
  const attrContentType = new asn1js.Sequence({
    value: [
      new asn1js.ObjectIdentifier({ value: OID_ATTR_CONTENT_TYPE }),
      new asn1js.Set({ value: [ new asn1js.ObjectIdentifier({ value: OID_ID_DATA }) ] })
    ]
  });

  const attrSigningTime = new asn1js.Sequence({
    value: [
      new asn1js.ObjectIdentifier({ value: OID_ATTR_SIGNING_TIME }),
      new asn1js.Set({ value: [ new asn1js.GeneralizedTime({ valueDate: new Date() }) ] })
    ]
  });

  const attrMessageDigest = new asn1js.Sequence({
    value: [
      new asn1js.ObjectIdentifier({ value: OID_ATTR_MESSAGE_DIGEST }),
      new asn1js.Set({ value: [ new asn1js.OctetString({ valueHex: pdfMessageDigest }) ] })
    ]
  });

  // SET OF Attributes — l'ordre DER (par tag/valeur) sera appliqué par asn1js
  const signedAttrsSet = new asn1js.Set({ value: [attrContentType, attrSigningTime, attrMessageDigest] });
  const signedAttributesDer = Buffer.from(signedAttrsSet.toBER(false));

  // Le digest à signer par KMS est SHA-256( DER(SignedAttributes) )
  const signedAttributesDigest = crypto.createHash('sha256').update(signedAttributesDer).digest();
  console.log('[buildSignedAttributesDigest] signedAttributesDigest (pour KMS):', signedAttributesDigest.toString('hex'));
  
  return { signedAttributesDer, signedAttributesDigest };
}

// =====================================================
// PEM -> pkijs.Certificate(s)
// =====================================================
export function parsePemChainToPkijsCerts(chainData) {
  try {
    if (Buffer.isBuffer(chainData)) {
      const previewHex = chainData.slice(0, 16).toString('hex');
      console.log('[chain raw] length=', chainData.length, ' headHex=', previewHex);
    }
  } catch {}

  let pemStr = null;
  let derBuf = null;

  // Normalisation des entrées (Buffer ou string)
  if (Buffer.isBuffer(chainData)) {
    derBuf = chainData; // binaire tel quel, peut être PEM en bytes ou DER
    try { pemStr = chainData.toString('utf8'); } catch {}
  } else if (typeof chainData === 'string') {
    pemStr = chainData;
  }

  // Tentative 1 : parse PEM (headers BEGIN/END)
  if (typeof pemStr === 'string' && pemStr.length > 0) {
    try {
      // strip BOM éventuel + normalisation des fins de ligne
      if (pemStr.charCodeAt(0) === 0xFEFF) pemStr = pemStr.slice(1);
      pemStr = pemStr.replace(/\r\n?/g, '\n');
      const preview = String(pemStr).slice(0, 160);
      console.log('[chain.pem preview]', preview.replace(/\n/g, '\\n'));

      const blocks = splitPemBlocks(pemStr)
        .filter(b => b.type === 'CERTIFICATE')
        .map(b => Buffer.from(b.body, 'base64'));

      if (blocks.length > 0) {
        const certsPkijs = blocks.map(der => {
          const asn1 = asn1js.fromBER(der.buffer.slice(der.byteOffset, der.byteOffset + der.byteLength));
          if (asn1.offset === -1) throw new Error('ASN.1 parse error on cert (PEM)');
          return new Certificate({ schema: asn1.result });
        });
        return { certsPkijs, signerCert: certsPkijs[0] };
      }
    } catch (e) {
      console.log('[parsePemChainToPkijsCerts] PEM parse error, trying DER/base64:', String(e));
    }
  }

  // Tentative 2 : DER brut (essayé même si le 1er octet n'est pas 0x30, avec logs)
  if (derBuf && derBuf.length >= 4) {
    try {
      const asn1Any = asn1js.fromBER(derBuf.buffer.slice(derBuf.byteOffset || 0, (derBuf.byteOffset || 0) + derBuf.byteLength));
      if (asn1Any.offset !== -1) {
        // Si c'est un Certificate
        try {
          const cert = new Certificate({ schema: asn1Any.result });
          if (cert && cert.serialNumber) {
            console.log('[DER] Parsed as X.509 Certificate');
            return { certsPkijs: [cert], signerCert: cert };
          }
        } catch {}
        // Si c'est un ContentInfo/SignedData qui contient des certs
        try {
          const ci = new ContentInfo({ schema: asn1Any.result });
          if (ci && ci.contentType === '1.2.840.113549.1.7.2') { // signedData
            const sd = new SignedData({ schema: ci.content });
            if (Array.isArray(sd.certificates) && sd.certificates.length) {
              console.log(`[DER] Parsed PKCS#7 with ${sd.certificates.length} cert(s)`);
              const certsPkijs = sd.certificates;
              return { certsPkijs, signerCert: certsPkijs[0] };
            }
          }
        } catch {}
      }
    } catch (e) {
      console.log('[DER parse] error:', String(e));
    }
  }

  // Tentative 3 : base64 sans entêtes
  if (typeof pemStr === 'string' && pemStr.length > 0) {
    try {
      const b64 = pemStr.replace(/[^A-Za-z0-9+/=]/g, '');
      if (b64.length >= 128) {
        const buf = Buffer.from(b64, 'base64');
        const asn1 = asn1js.fromBER(buf.buffer.slice(buf.byteOffset || 0, (buf.byteOffset || 0) + buf.byteLength));
        if (asn1.offset !== -1) {
          const cert = new Certificate({ schema: asn1.result });
          return { certsPkijs: [cert], signerCert: cert };
        }
      }
    } catch (e) {
      console.log('[parsePemChainToPkijsCerts] base64 parse error:', String(e));
    }
  }

  throw new Error('chain.pem vide ou invalide');
}

function splitPemBlocks(pem) {
  try {
    if (pem && pem.charCodeAt && pem.charCodeAt(0) === 0xFEFF) pem = pem.slice(1); // strip BOM
    pem = String(pem);
  } catch {}

  // Normalise les fins de ligne et log de debug
  pem = pem.replace(/\r\n?/g, '\n');
  console.log('[splitPemBlocks] input length =', pem.length);
  console.log('[splitPemBlocks] head =', pem.slice(0, 80).replace(/\n/g, '\\n'));
  if (/BEGIN CERTIFICATE/.test(pem) === false) {
    console.log('[splitPemBlocks] Aucun header PEM détecté dans le texte');
  }

  // Regex principale (ultra permissive)
  const re = /-+BEGIN\s+([^\-\n\r]+)-+\s*([\s\S]*?)\s*-+END\s+\1-+/gi;
  const blocks = [];
  let m;
  while ((m = re.exec(pem)) !== null) {
    let type = m[1].trim().toUpperCase();
    type = type.replace(/-+$/g, ''); // nettoie d'éventuels tirets résiduels
    const body = m[2].replace(/\s+/g, '');
    blocks.push({ type, body });
  }
  if (blocks.length > 0) {
    console.log(`[splitPemBlocks] regex -> ${blocks.length} bloc(s): ` + blocks.map(b => b.type).join(', '));
    return blocks;
  }

  // --- Fallback robuste par lecture ligne à ligne ---
  console.log('[splitPemBlocks] regex a échoué, tentative fallback ligne-à-ligne');
  const out = [];
  const lines = pem.split('\n');
  let i = 0;
  while (i < lines.length) {
    let line = lines[i].trim();
    if (/^-+BEGIN /.test(line) && /-+$/.test(line)) {
      const type = line.replace(/^-+BEGIN\s+/, '').replace(/-+$/, '').trim().toUpperCase();
      const buf = [];
      i++;
      while (i < lines.length) {
        const l = lines[i].trim();
        if (new RegExp(`^-+END\\s+${type}-+$`, 'i').test(l)) {
          break;
        }
        // ignore lignes vides et espaces, concaténer base64 brut
        if (l.length) buf.push(l);
        i++;
      }
      if (buf.length) out.push({ type, body: buf.join('') });
    }
    i++;
  }
  if (out.length === 0) {
    console.log('[splitPemBlocks] fallback: aucun bloc détecté');
  } else {
    console.log(`[splitPemBlocks] fallback -> ${out.length} bloc(s): ` + out.map(b => b.type).join(', '));
  }
  return out;
}

// =====================================================
// CMS/PKCS#7 (SignedData) — construction complete (sans TSA pour l’instant)
// =====================================================
export async function buildCmsSignedData(signedAttributesDer, signatureBytes, chainData) {
  const { certsPkijs, signerCert } = parsePemChainToPkijsCerts(chainData);

  // EncapsulatedContentInfo (detached): eContentType = id-data, pas de eContent
  const encap = new EncapsulatedContentInfo({
    eContentType: OID_ID_DATA
    // eContent absent pour une signature détachée
  });

  const signedData = new SignedData({
    version: 1,
    encapContentInfo: encap
  });

  // Chaîne de certificats (sans la root)
  signedData.certificates = certsPkijs;

  // SignerInfo (sid = IssuerAndSerialNumber)
  const issuerAndSerial = new IssuerAndSerialNumber({
    issuer: signerCert.issuer,
    serialNumber: signerCert.serialNumber
  });

  // digestAlgorithm = SHA-256
  const digestAlgorithm = new AlgorithmIdentifier({ algorithmId: '2.16.840.1.101.3.4.2.1' });

  // signatureAlgorithm = RSASSA-PSS avec SHA-256 / MGF1-SHA256 / saltLen=32
  const rsassaPssParams = new asn1js.Sequence({
    value: [
      // hashAlgorithm (sha256)
      new asn1js.Constructed({
        idBlock: { tagClass: 3, tagNumber: 0 }, // [0]
        value: [ new AlgorithmIdentifier({ algorithmId: '2.16.840.1.101.3.4.2.1' }).toSchema() ]
      }),
      // maskGenAlgorithm (mgf1 with sha256)
      new asn1js.Constructed({
        idBlock: { tagClass: 3, tagNumber: 1 }, // [1]
        value: [ new AlgorithmIdentifier({
          algorithmId: '1.2.840.113549.1.1.8', // mgf1
          algorithmParams: new AlgorithmIdentifier({ algorithmId: '2.16.840.1.101.3.4.2.1' }).toSchema()
        }).toSchema() ]
      }),
      // saltLength INTEGER 32
      new asn1js.Primitive({ idBlock: { tagClass: 3, tagNumber: 2 }, valueHex: new asn1js.Integer({ value: 32 }).toBER(false) })
      // trailerField [3] default 1 — omis
    ]
  });

  const signatureAlgorithm = new AlgorithmIdentifier({ algorithmId: '1.2.840.113549.1.1.10' }); // rsassaPss
  signatureAlgorithm.algorithmParams = rsassaPssParams;

  // Recréer les SignedAttributes comme objets pkijs à partir du DER fourni (pour cohérence DER)
  const signedAttrsSet = parseSignedAttributesDerToPkijsSignedSet(signedAttributesDer);

  const signerInfo = new SignerInfo({
    version: 1,
    sid: issuerAndSerial,
    digestAlgorithm,
    signatureAlgorithm,
    signedAttrs: signedAttrsSet,
    signature: new asn1js.OctetString({
      valueHex: signatureBytes.buffer.slice(
        signatureBytes.byteOffset || 0,
        (signatureBytes.byteOffset || 0) + (signatureBytes.byteLength || signatureBytes.length)
      )
    })
  });
  signedData.signerInfos.push(signerInfo);

  // ContentInfo enveloppe
  const cms = new ContentInfo({ contentType: '1.2.840.113549.1.7.2', content: signedData.toSchema(true) });
  const cmsDer = Buffer.from(cms.toSchema().toBER(false));
  return cmsDer;
}

function parseSignedAttributesDerToPkijsSignedSet(signedAttributesDer) {
  const view = signedAttributesDer instanceof Buffer ? new Uint8Array(signedAttributesDer) : signedAttributesDer;
  const asn1 = asn1js.fromBER(view.buffer.slice(view.byteOffset || 0, (view.byteOffset || 0) + (view.byteLength || view.length)));
  if (asn1.offset === -1 || !(asn1.result instanceof asn1js.Set)) {
    throw new Error('SignedAttributes DER invalide');
  }
  const attrs = [];
  for (const el of asn1.result.valueBlock.value) {
    // SEQUENCE { type OBJECT IDENTIFIER, values SET OF ANY }
    const seq = el; // asn1js.Sequence
    const typeOid = seq.valueBlock.value[0];
    const valuesSet = seq.valueBlock.value[1];
    const type = typeOid.valueBlock.toString();
    const values = valuesSet.valueBlock.value.map(v => v);
    attrs.push(new Attribute({ type, values }));
  }
  // pkijs attend un SignedAndUnsignedAttributes pour signedAttrs (type=0)
  const signedAttrs = new SignedAndUnsignedAttributes({ type: 0 });
  signedAttrs.attributes = attrs;
  return signedAttrs;
}