import crypto from 'crypto';
import { CFDIConfig, PACProvider } from '../entities/cfdi-config.entity';

export interface StampResult {
  success: boolean;
  uuid?: string;
  fechaTimbrado?: string;
  xml?: string;
  acuseRecepcion?: string;
  errorCode?: string;
  errorMessage?: string;
}

export interface CancelResult {
  success: boolean;
  acuse?: string;
  status?: string;
  errorCode?: string;
  errorMessage?: string;
}

export interface VerifyStatusResult {
  success: boolean;
  status: 'valid' | 'cancelled' | 'not_found' | 'error';
  cancellationStatus?: string;
  cancellationDate?: string;
  errorCode?: string;
  errorMessage?: string;
}

interface PACEndpoints {
  stamp: string;
  cancel: string;
  status: string;
}

const PAC_ENDPOINTS: Record<PACProvider, { sandbox: PACEndpoints; production: PACEndpoints }> = {
  [PACProvider.FINKOK]: {
    sandbox: {
      stamp: 'https://demo-facturacion.finkok.com/servicios/soap/stamp.wsdl',
      cancel: 'https://demo-facturacion.finkok.com/servicios/soap/cancel.wsdl',
      status: 'https://demo-facturacion.finkok.com/servicios/soap/utilities.wsdl',
    },
    production: {
      stamp: 'https://facturacion.finkok.com/servicios/soap/stamp.wsdl',
      cancel: 'https://facturacion.finkok.com/servicios/soap/cancel.wsdl',
      status: 'https://facturacion.finkok.com/servicios/soap/utilities.wsdl',
    },
  },
  [PACProvider.FACTURAPI]: {
    sandbox: {
      stamp: 'https://www.facturapi.io/v2/invoices',
      cancel: 'https://www.facturapi.io/v2/invoices',
      status: 'https://www.facturapi.io/v2/invoices',
    },
    production: {
      stamp: 'https://www.facturapi.io/v2/invoices',
      cancel: 'https://www.facturapi.io/v2/invoices',
      status: 'https://www.facturapi.io/v2/invoices',
    },
  },
  [PACProvider.SW_SAPIEN]: {
    sandbox: {
      stamp: 'https://services.test.sw.com.mx/cfdi33/stamp/v4',
      cancel: 'https://services.test.sw.com.mx/cfdi33/cancel',
      status: 'https://services.test.sw.com.mx/cfdi/status',
    },
    production: {
      stamp: 'https://services.sw.com.mx/cfdi33/stamp/v4',
      cancel: 'https://services.sw.com.mx/cfdi33/cancel',
      status: 'https://services.sw.com.mx/cfdi/status',
    },
  },
  [PACProvider.SOLUCIONES_FISCALES]: {
    sandbox: {
      stamp: 'https://pruebas.solucionesfiscales.com.mx/ws/timbrar',
      cancel: 'https://pruebas.solucionesfiscales.com.mx/ws/cancelar',
      status: 'https://pruebas.solucionesfiscales.com.mx/ws/estatus',
    },
    production: {
      stamp: 'https://www.solucionesfiscales.com.mx/ws/timbrar',
      cancel: 'https://www.solucionesfiscales.com.mx/ws/cancelar',
      status: 'https://www.solucionesfiscales.com.mx/ws/estatus',
    },
  },
  [PACProvider.DIGISAT]: {
    sandbox: {
      stamp: 'https://sandbox.digisat.com/timbrado',
      cancel: 'https://sandbox.digisat.com/cancelacion',
      status: 'https://sandbox.digisat.com/estatus',
    },
    production: {
      stamp: 'https://api.digisat.com/timbrado',
      cancel: 'https://api.digisat.com/cancelacion',
      status: 'https://api.digisat.com/estatus',
    },
  },
  [PACProvider.OTHER]: {
    sandbox: {
      stamp: '',
      cancel: '',
      status: '',
    },
    production: {
      stamp: '',
      cancel: '',
      status: '',
    },
  },
};

// SAT cancellation reason codes
export const CANCELLATION_REASONS = {
  '01': 'Comprobante emitido con errores con relación',
  '02': 'Comprobante emitido con errores sin relación',
  '03': 'No se llevó a cabo la operación',
  '04': 'Operación nominativa relacionada en una factura global',
} as const;

export class PACService {
  private encryptionKey: Buffer;

  constructor() {
    // Use environment variable for encryption key
    const key = process.env.PAC_ENCRYPTION_KEY || 'default-key-change-in-production';
    this.encryptionKey = crypto.scryptSync(key, 'salt', 32);
  }

  /**
   * Stamp (timbrar) a CFDI with the PAC
   */
  async stampCFDI(config: CFDIConfig, signedXml: string): Promise<StampResult> {
    const provider = config.pacProvider;
    const environment = config.pacEnvironment;

    try {
      switch (provider) {
        case PACProvider.FINKOK:
          return await this.stampWithFinkok(config, signedXml);
        case PACProvider.FACTURAPI:
          return await this.stampWithFacturapi(config, signedXml);
        case PACProvider.SW_SAPIEN:
          return await this.stampWithSWSapien(config, signedXml);
        default:
          return await this.stampGeneric(config, signedXml);
      }
    } catch (error) {
      return {
        success: false,
        errorCode: 'PAC_ERROR',
        errorMessage: error instanceof Error ? error.message : 'Unknown PAC error',
      };
    }
  }

  /**
   * Cancel a stamped CFDI
   */
  async cancelCFDI(
    config: CFDIConfig,
    uuid: string,
    reason: keyof typeof CANCELLATION_REASONS,
    substituteUuid?: string
  ): Promise<CancelResult> {
    const provider = config.pacProvider;

    try {
      switch (provider) {
        case PACProvider.FINKOK:
          return await this.cancelWithFinkok(config, uuid, reason, substituteUuid);
        case PACProvider.FACTURAPI:
          return await this.cancelWithFacturapi(config, uuid, reason, substituteUuid);
        case PACProvider.SW_SAPIEN:
          return await this.cancelWithSWSapien(config, uuid, reason, substituteUuid);
        default:
          return await this.cancelGeneric(config, uuid, reason, substituteUuid);
      }
    } catch (error) {
      return {
        success: false,
        errorCode: 'PAC_CANCEL_ERROR',
        errorMessage: error instanceof Error ? error.message : 'Unknown cancellation error',
      };
    }
  }

  /**
   * Verify CFDI status with SAT
   */
  async verifyCFDIStatus(
    config: CFDIConfig,
    emisorRfc: string,
    receptorRfc: string,
    total: number,
    uuid: string
  ): Promise<VerifyStatusResult> {
    const provider = config.pacProvider;

    try {
      switch (provider) {
        case PACProvider.FINKOK:
          return await this.verifyWithFinkok(config, emisorRfc, receptorRfc, total, uuid);
        default:
          return await this.verifyGeneric(config, emisorRfc, receptorRfc, total, uuid);
      }
    } catch (error) {
      return {
        success: false,
        status: 'error',
        errorCode: 'PAC_VERIFY_ERROR',
        errorMessage: error instanceof Error ? error.message : 'Unknown verification error',
      };
    }
  }

  // ==================== FINKOK Implementation ====================

  private async stampWithFinkok(config: CFDIConfig, signedXml: string): Promise<StampResult> {
    const credentials = this.decryptCredentials(config);
    const endpoint = this.getEndpoint(config, 'stamp');

    // Build SOAP request for Finkok stamp service
    const soapRequest = this.buildFinkokStampRequest(
      credentials.username,
      credentials.password,
      signedXml
    );

    const response = await fetch(endpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'text/xml; charset=utf-8',
        'SOAPAction': 'stamp',
      },
      body: soapRequest,
    });

    if (!response.ok) {
      throw new Error(`Finkok API error: ${response.status} ${response.statusText}`);
    }

    const responseText = await response.text();
    return this.parseFinkokStampResponse(responseText);
  }

  private buildFinkokStampRequest(username: string, password: string, xml: string): string {
    const xmlBase64 = Buffer.from(xml).toString('base64');

    return `<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
                  xmlns:stam="http://facturacion.finkok.com/stamp">
   <soapenv:Header/>
   <soapenv:Body>
      <stam:stamp>
         <stam:xml>${xmlBase64}</stam:xml>
         <stam:username>${username}</stam:username>
         <stam:password>${password}</stam:password>
      </stam:stamp>
   </soapenv:Body>
</soapenv:Envelope>`;
  }

  private parseFinkokStampResponse(responseXml: string): StampResult {
    // Check for Incidencias (errors)
    const incidenciaMatch = responseXml.match(/<CodigoError>([^<]+)<\/CodigoError>[\s\S]*?<MensajeIncidencia>([^<]+)<\/MensajeIncidencia>/);
    if (incidenciaMatch) {
      return {
        success: false,
        errorCode: incidenciaMatch[1],
        errorMessage: incidenciaMatch[2],
      };
    }

    // Extract stamped XML
    const xmlMatch = responseXml.match(/<xml>([^<]+)<\/xml>/);
    const uuidMatch = responseXml.match(/<UUID>([^<]+)<\/UUID>/);
    const fechaMatch = responseXml.match(/<Fecha>([^<]+)<\/Fecha>/);
    const acuseMatch = responseXml.match(/<Acuse>([^<]+)<\/Acuse>/);

    if (!xmlMatch || !uuidMatch) {
      return {
        success: false,
        errorCode: 'PARSE_ERROR',
        errorMessage: 'Could not parse Finkok response',
      };
    }

    const stampedXml = Buffer.from(xmlMatch[1], 'base64').toString('utf-8');

    return {
      success: true,
      uuid: uuidMatch[1],
      fechaTimbrado: fechaMatch?.[1],
      xml: stampedXml,
      acuseRecepcion: acuseMatch?.[1],
    };
  }

  private async cancelWithFinkok(
    config: CFDIConfig,
    uuid: string,
    reason: string,
    substituteUuid?: string
  ): Promise<CancelResult> {
    const credentials = this.decryptCredentials(config);
    const endpoint = this.getEndpoint(config, 'cancel');

    const soapRequest = this.buildFinkokCancelRequest(
      credentials.username,
      credentials.password,
      config.rfc,
      uuid,
      reason,
      substituteUuid
    );

    const response = await fetch(endpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'text/xml; charset=utf-8',
        'SOAPAction': 'cancel',
      },
      body: soapRequest,
    });

    if (!response.ok) {
      throw new Error(`Finkok cancel error: ${response.status}`);
    }

    const responseText = await response.text();
    return this.parseFinkokCancelResponse(responseText);
  }

  private buildFinkokCancelRequest(
    username: string,
    password: string,
    rfc: string,
    uuid: string,
    reason: string,
    substituteUuid?: string
  ): string {
    const folioSustitucion = substituteUuid || '';

    return `<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
                  xmlns:can="http://facturacion.finkok.com/cancel">
   <soapenv:Header/>
   <soapenv:Body>
      <can:cancel>
         <can:UUIDS>
            <can:string>${uuid}</can:string>
         </can:UUIDS>
         <can:username>${username}</can:username>
         <can:password>${password}</can:password>
         <can:taxpayer_id>${rfc}</can:taxpayer_id>
         <can:motivo>${reason}</can:motivo>
         <can:folio_sustitucion>${folioSustitucion}</can:folio_sustitucion>
      </can:cancel>
   </soapenv:Body>
</soapenv:Envelope>`;
  }

  private parseFinkokCancelResponse(responseXml: string): CancelResult {
    const statusMatch = responseXml.match(/<EstatusUUID>([^<]+)<\/EstatusUUID>/);
    const acuseMatch = responseXml.match(/<Acuse>([^<]+)<\/Acuse>/);
    const errorMatch = responseXml.match(/<CodEstatus>([^<]+)<\/CodEstatus>/);

    if (statusMatch?.[1] === '201' || statusMatch?.[1] === '202') {
      return {
        success: true,
        status: statusMatch[1] === '201' ? 'cancelled' : 'pending',
        acuse: acuseMatch?.[1],
      };
    }

    return {
      success: false,
      status: statusMatch?.[1],
      errorCode: errorMatch?.[1] || 'UNKNOWN',
      errorMessage: 'Cancellation failed',
    };
  }

  private async verifyWithFinkok(
    config: CFDIConfig,
    emisorRfc: string,
    receptorRfc: string,
    total: number,
    uuid: string
  ): Promise<VerifyStatusResult> {
    const credentials = this.decryptCredentials(config);
    const endpoint = this.getEndpoint(config, 'status');

    const soapRequest = `<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
                  xmlns:uti="http://facturacion.finkok.com/utilities">
   <soapenv:Header/>
   <soapenv:Body>
      <uti:get_sat_status>
         <uti:username>${credentials.username}</uti:username>
         <uti:password>${credentials.password}</uti:password>
         <uti:taxpayer_id>${emisorRfc}</uti:taxpayer_id>
         <uti:rtaxpayer_id>${receptorRfc}</uti:rtaxpayer_id>
         <uti:uuid>${uuid}</uti:uuid>
         <uti:total>${total.toFixed(2)}</uti:total>
      </uti:get_sat_status>
   </soapenv:Body>
</soapenv:Envelope>`;

    const response = await fetch(endpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'text/xml; charset=utf-8',
        'SOAPAction': 'get_sat_status',
      },
      body: soapRequest,
    });

    if (!response.ok) {
      throw new Error(`Finkok status error: ${response.status}`);
    }

    const responseText = await response.text();
    return this.parseFinkokStatusResponse(responseText);
  }

  private parseFinkokStatusResponse(responseXml: string): VerifyStatusResult {
    const estadoMatch = responseXml.match(/<Estado>([^<]+)<\/Estado>/);
    const cancelableMatch = responseXml.match(/<EsCancelable>([^<]+)<\/EsCancelable>/);
    const estatusCancelacionMatch = responseXml.match(/<EstatusCancelacion>([^<]+)<\/EstatusCancelacion>/);

    if (!estadoMatch) {
      return {
        success: false,
        status: 'error',
        errorCode: 'PARSE_ERROR',
        errorMessage: 'Could not parse status response',
      };
    }

    let status: VerifyStatusResult['status'] = 'valid';
    if (estadoMatch[1] === 'Cancelado') {
      status = 'cancelled';
    } else if (estadoMatch[1] === 'No Encontrado') {
      status = 'not_found';
    }

    return {
      success: true,
      status,
      cancellationStatus: estatusCancelacionMatch?.[1],
    };
  }

  // ==================== Facturapi Implementation ====================

  private async stampWithFacturapi(config: CFDIConfig, signedXml: string): Promise<StampResult> {
    const credentials = this.decryptCredentials(config);
    const endpoint = this.getEndpoint(config, 'stamp');

    const response = await fetch(`${endpoint}/stamp`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${credentials.password}`,
        'Content-Type': 'application/xml',
      },
      body: signedXml,
    });

    if (!response.ok) {
      const error = await response.json();
      return {
        success: false,
        errorCode: error.code,
        errorMessage: error.message,
      };
    }

    const result = await response.json();
    return {
      success: true,
      uuid: result.uuid,
      fechaTimbrado: result.stamp_date,
      xml: result.xml,
    };
  }

  private async cancelWithFacturapi(
    config: CFDIConfig,
    uuid: string,
    reason: string,
    substituteUuid?: string
  ): Promise<CancelResult> {
    const credentials = this.decryptCredentials(config);
    const endpoint = this.getEndpoint(config, 'cancel');

    const response = await fetch(`${endpoint}/${uuid}`, {
      method: 'DELETE',
      headers: {
        'Authorization': `Bearer ${credentials.password}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        motive: reason,
        substitute: substituteUuid,
      }),
    });

    if (!response.ok) {
      const error = await response.json();
      return {
        success: false,
        errorCode: error.code,
        errorMessage: error.message,
      };
    }

    const result = await response.json();
    return {
      success: true,
      status: result.status,
      acuse: result.acuse,
    };
  }

  // ==================== SW Sapien Implementation ====================

  private async stampWithSWSapien(config: CFDIConfig, signedXml: string): Promise<StampResult> {
    const credentials = this.decryptCredentials(config);
    const endpoint = this.getEndpoint(config, 'stamp');

    // First get auth token
    const authResponse = await fetch('https://services.sw.com.mx/security/authenticate', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        user: credentials.username,
        password: credentials.password,
      }),
    });

    if (!authResponse.ok) {
      throw new Error('SW Sapien authentication failed');
    }

    const authResult = await authResponse.json();
    const token = authResult.data.token;

    // Stamp with token
    const xmlBase64 = Buffer.from(signedXml).toString('base64');
    const stampResponse = await fetch(endpoint, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ xml: xmlBase64 }),
    });

    if (!stampResponse.ok) {
      const error = await stampResponse.json();
      return {
        success: false,
        errorCode: error.messageDetail,
        errorMessage: error.message,
      };
    }

    const result = await stampResponse.json();
    return {
      success: true,
      uuid: result.data.uuid,
      fechaTimbrado: result.data.fechaTimbrado,
      xml: Buffer.from(result.data.cfdi, 'base64').toString('utf-8'),
    };
  }

  private async cancelWithSWSapien(
    config: CFDIConfig,
    uuid: string,
    reason: string,
    substituteUuid?: string
  ): Promise<CancelResult> {
    // Similar implementation with SW Sapien API
    return {
      success: false,
      errorCode: 'NOT_IMPLEMENTED',
      errorMessage: 'SW Sapien cancellation not yet implemented',
    };
  }

  // ==================== Generic Implementations ====================

  private async stampGeneric(config: CFDIConfig, signedXml: string): Promise<StampResult> {
    if (!config.pacApiUrl) {
      return {
        success: false,
        errorCode: 'NO_API_URL',
        errorMessage: 'No PAC API URL configured',
      };
    }

    const credentials = this.decryptCredentials(config);

    const response = await fetch(config.pacApiUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/xml',
        'Authorization': `Basic ${Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64')}`,
      },
      body: signedXml,
    });

    if (!response.ok) {
      return {
        success: false,
        errorCode: 'API_ERROR',
        errorMessage: `API returned ${response.status}`,
      };
    }

    const xml = await response.text();
    return {
      success: true,
      xml,
    };
  }

  private async cancelGeneric(
    config: CFDIConfig,
    uuid: string,
    reason: string,
    substituteUuid?: string
  ): Promise<CancelResult> {
    return {
      success: false,
      errorCode: 'NOT_IMPLEMENTED',
      errorMessage: 'Generic cancellation not implemented. Configure a specific PAC provider.',
    };
  }

  private async verifyGeneric(
    config: CFDIConfig,
    emisorRfc: string,
    receptorRfc: string,
    total: number,
    uuid: string
  ): Promise<VerifyStatusResult> {
    return {
      success: false,
      status: 'error',
      errorCode: 'NOT_IMPLEMENTED',
      errorMessage: 'Generic verification not implemented. Configure a specific PAC provider.',
    };
  }

  // ==================== Helpers ====================

  private getEndpoint(config: CFDIConfig, type: keyof PACEndpoints): string {
    const provider = config.pacProvider;
    const environment = config.pacEnvironment;
    const endpoints = PAC_ENDPOINTS[provider];

    if (!endpoints) {
      throw new Error(`Unknown PAC provider: ${provider}`);
    }

    return endpoints[environment][type];
  }

  private decryptCredentials(config: CFDIConfig): { username: string; password: string } {
    return {
      username: config.pacUsername || '',
      password: config.pacPasswordEncrypted
        ? this.decrypt(config.pacPasswordEncrypted)
        : '',
    };
  }

  /**
   * Encrypt sensitive data
   */
  encrypt(text: string): string {
    const iv = crypto.randomBytes(16);
    const cipher = crypto.createCipheriv('aes-256-cbc', this.encryptionKey, iv);
    let encrypted = cipher.update(text, 'utf8', 'hex');
    encrypted += cipher.final('hex');
    return iv.toString('hex') + ':' + encrypted;
  }

  /**
   * Decrypt sensitive data
   */
  decrypt(encryptedText: string): string {
    const [ivHex, encrypted] = encryptedText.split(':');
    const iv = Buffer.from(ivHex, 'hex');
    const decipher = crypto.createDecipheriv('aes-256-cbc', this.encryptionKey, iv);
    let decrypted = decipher.update(encrypted, 'hex', 'utf8');
    decrypted += decipher.final('utf8');
    return decrypted;
  }

  /**
   * Decrypt and load certificate
   */
  async loadCertificate(config: CFDIConfig): Promise<{
    certificate: Buffer;
    privateKey: Buffer;
    password: string;
  }> {
    if (!config.certificateEncrypted || !config.privateKeyEncrypted) {
      throw new Error('Certificate not configured');
    }

    return {
      certificate: Buffer.from(this.decrypt(config.certificateEncrypted), 'base64'),
      privateKey: Buffer.from(this.decrypt(config.privateKeyEncrypted), 'base64'),
      password: config.certificatePasswordEncrypted
        ? this.decrypt(config.certificatePasswordEncrypted)
        : '',
    };
  }

  /**
   * Sign data with private key (for CFDI sello)
   */
  async signData(data: string, privateKey: Buffer, password: string): Promise<string> {
    const sign = crypto.createSign('RSA-SHA256');
    sign.update(data, 'utf8');
    const signature = sign.sign({
      key: privateKey,
      passphrase: password,
    });
    return signature.toString('base64');
  }

  /**
   * Get certificate as base64
   */
  getCertificateBase64(certificate: Buffer): string {
    // Convert DER to PEM format if needed, then extract base64
    const pemContent = certificate.toString('utf8');
    if (pemContent.includes('-----BEGIN CERTIFICATE-----')) {
      // Already PEM, extract base64 content
      return pemContent
        .replace('-----BEGIN CERTIFICATE-----', '')
        .replace('-----END CERTIFICATE-----', '')
        .replace(/\s+/g, '');
    }
    // Already raw/DER format
    return certificate.toString('base64');
  }
}
