import { XMLParser, XMLBuilder } from 'fast-xml-parser';
import { createHash, randomUUID } from 'crypto';
import type { Credential } from '@nodecfdi/credentials/node';

const SAT_AUTH_URL = 'https://cfdidescargamasivasolicitud.clouda.sat.gob.mx/Autenticacion/Autenticacion.svc';

interface SatToken {
  token: string;
  expiresAt: Date;
}

/**
 * Genera el timestamp para la solicitud SOAP
 */
function createTimestamp(): { created: string; expires: string } {
  const now = new Date();
  const created = now.toISOString();
  const expires = new Date(now.getTime() + 5 * 60 * 1000).toISOString(); // 5 minutos
  return { created, expires };
}

/**
 * Construye el XML de solicitud de autenticación
 */
function buildAuthRequest(credential: Credential): string {
  const timestamp = createTimestamp();
  const uuid = randomUUID();

  const certificate = credential.certificate();
  // El PEM ya contiene el certificado en base64, solo quitamos headers y newlines
  const cerB64 = certificate.pem().replace(/-----.*-----/g, '').replace(/\s/g, '');

  // Canonicalizar y firmar
  const toDigestXml = `<u:Timestamp xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" u:Id="_0">` +
    `<u:Created>${timestamp.created}</u:Created>` +
    `<u:Expires>${timestamp.expires}</u:Expires>` +
    `</u:Timestamp>`;

  const digestValue = createHash('sha1').update(toDigestXml).digest('base64');

  const signedInfoXml = `<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">` +
    `<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>` +
    `<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>` +
    `<Reference URI="#_0">` +
    `<Transforms>` +
    `<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>` +
    `</Transforms>` +
    `<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>` +
    `<DigestValue>${digestValue}</DigestValue>` +
    `</Reference>` +
    `</SignedInfo>`;

  // Firmar con la llave privada (sign retorna binary string, convertir a base64)
  const signatureBinary = credential.sign(signedInfoXml, 'sha1');
  const signatureValue = Buffer.from(signatureBinary, 'binary').toString('base64');

  const soapEnvelope = `<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
  <s:Header>
    <o:Security xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" s:mustUnderstand="1">
      <u:Timestamp u:Id="_0">
        <u:Created>${timestamp.created}</u:Created>
        <u:Expires>${timestamp.expires}</u:Expires>
      </u:Timestamp>
      <o:BinarySecurityToken u:Id="uuid-${uuid}-1" ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">${cerB64}</o:BinarySecurityToken>
      <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
        ${signedInfoXml}
        <SignatureValue>${signatureValue}</SignatureValue>
        <KeyInfo>
          <o:SecurityTokenReference>
            <o:Reference URI="#uuid-${uuid}-1" ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3"/>
          </o:SecurityTokenReference>
        </KeyInfo>
      </Signature>
    </o:Security>
  </s:Header>
  <s:Body>
    <Autentica xmlns="http://DescargaMasivaTerceros.gob.mx"/>
  </s:Body>
</s:Envelope>`;

  return soapEnvelope;
}

/**
 * Extrae el token de la respuesta SOAP
 */
function parseAuthResponse(responseXml: string): SatToken {
  const parser = new XMLParser({
    ignoreAttributes: false,
    removeNSPrefix: true,
  });

  const result = parser.parse(responseXml);

  // Navegar la estructura de respuesta SOAP
  const envelope = result.Envelope || result['s:Envelope'];
  if (!envelope) {
    throw new Error('Respuesta SOAP inválida');
  }

  const body = envelope.Body || envelope['s:Body'];
  if (!body) {
    throw new Error('No se encontró el cuerpo de la respuesta');
  }

  const autenticaResponse = body.AutenticaResponse;
  if (!autenticaResponse) {
    throw new Error('No se encontró AutenticaResponse');
  }

  const autenticaResult = autenticaResponse.AutenticaResult;
  if (!autenticaResult) {
    throw new Error('No se obtuvo token de autenticación');
  }

  // El token es un SAML assertion en base64
  const token = autenticaResult;

  // El token expira en 5 minutos según documentación SAT
  const expiresAt = new Date(Date.now() + 5 * 60 * 1000);

  return { token, expiresAt };
}

/**
 * Autentica con el SAT usando la FIEL y obtiene un token
 */
export async function authenticate(credential: Credential): Promise<SatToken> {
  const soapRequest = buildAuthRequest(credential);

  try {
    const response = await fetch(SAT_AUTH_URL, {
      method: 'POST',
      headers: {
        'Content-Type': 'text/xml;charset=UTF-8',
        'SOAPAction': 'http://DescargaMasivaTerceros.gob.mx/IAutenticacion/Autentica',
      },
      body: soapRequest,
    });

    if (!response.ok) {
      const errorText = await response.text();
      throw new Error(`Error HTTP ${response.status}: ${errorText}`);
    }

    const responseXml = await response.text();
    return parseAuthResponse(responseXml);
  } catch (error: any) {
    console.error('[SAT Auth Error]', error);
    throw new Error(`Error al autenticar con el SAT: ${error.message}`);
  }
}

/**
 * Verifica si un token está vigente
 */
export function isTokenValid(token: SatToken): boolean {
  return new Date() < token.expiresAt;
}
