unit uEnviosNacexClient;

{
  uEnviosNacexClient.pas
  Implementación Nacex del cliente de envíos genérico.

  Descripción:
    Implementa la interfaz de negocio para el transportista Nacex usando
    su API Web Service (Servlet ws) a través de peticiones HTTP POST.
    Las credenciales del cliente se configuran combinando el usuario y el número
    de abonado en el campo CodCliente de ge_envios_config usando el formato:
    "USUARIO/NUM_ABONADO" (ej. "RAULASENCIO/10045").
}

interface

uses
  System.SysUtils, System.Classes, System.Net.HttpClient, System.Net.URLClient,
  System.NetEncoding, uEnviosTypes;

type
  TNacexEnviosClient = class
  private
    FConfig          : TEnviosConfig;
    FURLBase         : string;
    FCachedLabelRef  : string;
    FCachedLabelBytes: TBytes;

    function PostRequest(const AMethod, AData: string): string;
    function CleanResponse(const AResponse: string): string;
    function ParseNacexDate(const ADateStr: string): TDateTime;
    function GetPoblacionPorCP(const ACP: string): string;
    function MapNacexEstadoToGeneric(const AEstadoText: string): Integer;
    function URLEncode(const AStr: string): string;
    procedure DoValidateCertificate(const Sender: TObject; const ARequest: TURLRequest; const Certificate: TCertificate; var AResult: Boolean);
  public
    constructor Create(const AConfig: TEnviosConfig);

    function Login: Boolean;
    function GrabaEnvio(const AParams: TEnvioParams): TEnvioResult;
    function GetEtiqueta(const ARef: string; APDF: Boolean): TBytes;
    function GetEstados(const ARef: string): TArray<TEstadoEnvio>;
    function GrabaRecogida(const AParams: TRecogidaParams): TRecogidaResult;
    function AnulaEnvio(const ARef: string; out AMsg: string): Boolean;
    function ImprimirEtiquetaIoNA(const ARef: string; const AImpresora: string; out AError: string; AGirar180: Boolean): Boolean; overload;
    function ImprimirEtiquetaIoNA(const ARef: string; const AImpresora: string; out AError: string): Boolean; overload;
    function ImprimirEtiquetaIoNA(const ARef: string; out AError: string): Boolean; overload;
  end;

implementation

uses
  Winapi.Windows, Winapi.ShellAPI, Vcl.Dialogs, System.IOUtils, uPdfRotateService;

// ---------------------------------------------------------------------------
procedure TNacexEnviosClient.DoValidateCertificate(const Sender: TObject;
  const ARequest: TURLRequest; const Certificate: TCertificate;
  var AResult: Boolean);
begin
  AResult := True;  // Aceptar certificado autofirmado de ioNA local
end;

// ---------------------------------------------------------------------------
function TNacexEnviosClient.URLEncode(const AStr: string): string;
begin
  Result := TNetEncoding.URL.Encode(AStr).Replace('+', '%20');
end;

// ---------------------------------------------------------------------------
constructor TNacexEnviosClient.Create(const AConfig: TEnviosConfig);
var
  LBaseURL: string;
begin
  inherited Create;
  FConfig := AConfig;

  if AConfig.Entorno then
    LBaseURL := AConfig.UrlPruebas
  else
    LBaseURL := AConfig.UrlProduccion;

  LBaseURL := Trim(LBaseURL);

  // Fallback si no está configurada la URL en BD
  if LBaseURL = '' then
    LBaseURL := 'https://pda.nacex.com/nacex_ws/ws'
  else
  begin
    // Auto-corregir URL si falta el sufijo /ws del servlet
    if not LBaseURL.EndsWith('/ws', True) and not LBaseURL.EndsWith('ws', True) then
    begin
      if LBaseURL.EndsWith('/') then
        LBaseURL := LBaseURL + 'ws'
      else
        LBaseURL := LBaseURL + '/ws';
    end;
  end;

  FURLBase := LBaseURL;
  FCachedLabelRef := '';
  SetLength(FCachedLabelBytes, 0);
end;

// ---------------------------------------------------------------------------
function TNacexEnviosClient.CleanResponse(const AResponse: string): string;
var
  LParts: TArray<string>;
begin
  LParts := AResponse.Split(['|']);
  if Length(LParts) >= 3 then
    Result := Format('%s (Código: %s)', [LParts[1], LParts[2]])
  else if Length(LParts) >= 2 then
    Result := LParts[1]
  else
    Result := AResponse;
end;

// ---------------------------------------------------------------------------
function TNacexEnviosClient.ParseNacexDate(const ADateStr: string): TDateTime;
var
  LParts: TArray<string>;
  LDay, LMonth, LYear: Word;
  IDay, IMonth, IYear: Integer;
begin
  Result := 0;
  LParts := ADateStr.Split(['/']);
  if Length(LParts) = 3 then
  begin
    if TryStrToInt(LParts[0], IDay) and
       TryStrToInt(LParts[1], IMonth) and
       TryStrToInt(LParts[2], IYear) then
    begin
      LDay   := Word(IDay);
      LMonth := Word(IMonth);
      LYear  := Word(IYear);
      try
        Result := EncodeDate(LYear, LMonth, LDay);
      except
        Result := 0;
      end;
    end;
  end;
end;

function DecodificarNacexBase64(const AStr: string): TBytes;
var
  LClean: string;
  LPad: Integer;
  LTokens: TArray<string>;
  LTok: string;
begin
  LClean := Trim(AStr);
  if Pos('|', LClean) > 0 then
  begin
    LTokens := LClean.Split(['|']);
    for LTok in LTokens do
    begin
      if (Length(Trim(LTok)) > Length(LClean) div 2) or (Trim(LTok).StartsWith('JVBERi')) then
      begin
        LClean := Trim(LTok);
        Break;
      end;
    end;
  end;

  if LClean.EndsWith('*') then
    LClean := Copy(LClean, 1, Length(LClean) - 1);
  if LClean.StartsWith('|') then
    LClean := Copy(LClean, 2, Length(LClean) - 1);

  // Quitar retornos de carro, saltos de línea y espacios
  LClean := StringReplace(LClean, #13, '', [rfReplaceAll]);
  LClean := StringReplace(LClean, #10, '', [rfReplaceAll]);
  LClean := StringReplace(LClean, ' ', '', [rfReplaceAll]);

  // Convertir URL-safe Base64 a Base64 estándar (- -> +, _ -> /)
  LClean := StringReplace(LClean, '-', '+', [rfReplaceAll]);
  LClean := StringReplace(LClean, '_', '/', [rfReplaceAll]);

  // Asegurar padding de múltiplos de 4
  LPad := Length(LClean) mod 4;
  if LPad > 0 then
    LClean := LClean + StringOfChar('=', 4 - LPad);

  try
    Result := TNetEncoding.Base64.DecodeStringToBytes(LClean);
  except
    SetLength(Result, 0);
  end;
end;

// ---------------------------------------------------------------------------
function TNacexEnviosClient.PostRequest(const AMethod, AData: string): string;
var
  LClient: THTTPClient;
  LBodyStream: TStringStream;
  LResponse: IHTTPResponse;
  LParts: TArray<string>;
  LWSUser: string;
  LBody: string;
begin
  Result := '';

  // Extraer el usuario del Webservice del campo CodCliente (formato: USUARIO/NUM_ABONADO)
  LParts := FConfig.CodCliente.Split(['/']);
  if Length(LParts) >= 2 then
    LWSUser := LParts[0]
  else
    LWSUser := FConfig.CodCliente;

  if (Trim(FConfig.CodAgencia) = '') or (Trim(LWSUser) = '') or (Trim(FConfig.Password) = '') then
    raise Exception.Create('Las credenciales de conexión con Nacex para el entorno seleccionado (Producción/Pruebas) están vacías en la configuración.');

  LClient := THTTPClient.Create;
  try
    LClient.ConnectionTimeout := 15000;
    LClient.ResponseTimeout   := 30000;
    LClient.ContentType := 'application/x-www-form-urlencoded;charset=utf-8';

    // Construir el cuerpo manualmente para evitar doble codificación URL de los valores en AData
    LBody := 'method=' + TNetEncoding.URL.Encode(AMethod) +
             '&user=' + TNetEncoding.URL.Encode(LWSUser) +
             '&pass=' + TNetEncoding.URL.Encode(FConfig.Password) +
             '&data=' + AData;

    LBodyStream := TStringStream.Create(LBody, TEncoding.UTF8);
    try
      try
        LResponse := LClient.Post(FURLBase, LBodyStream);
        if (LResponse.StatusCode < 200) or (LResponse.StatusCode >= 300) then
          raise Exception.CreateFmt('Error HTTP %d: %s', [LResponse.StatusCode, LResponse.StatusText]);
        Result := LResponse.ContentAsString;
      except
        on E: Exception do
          raise Exception.CreateFmt('Error de conexión con Nacex (método %s): %s', [AMethod, E.Message]);
      end;
    finally
      LBodyStream.Free;
    end;

    // Verificar si la respuesta es una página HTML en lugar de una respuesta del WS
    if Result.Trim.StartsWith('<html', True) or Result.Trim.StartsWith('<!doctype', True) then
      raise Exception.Create('El servidor de Nacex devolvió una página HTML en lugar de una respuesta del Web Service. ' +
                             'Por favor, verifique que la URL de conexión configurada sea correcta (debe terminar en "/ws").');

    // Verificar si hay errores informados por el servidor Nacex
    if Result.StartsWith('ERROR|') then
    begin
      raise Exception.Create(Format(
        'Error devuelto por Nacex: %s' + #13#10 +
        'URL: %s' + #13#10 +
        'Método: %s' + #13#10 +
        'Datos (data): %s' + #13#10 +
        'Respuesta completa: "%s"',
        [CleanResponse(Result), FURLBase, AMethod, AData, Result]));
    end;
  finally
    LClient.Free;
  end;
end;

// ---------------------------------------------------------------------------
function TNacexEnviosClient.GetPoblacionPorCP(const ACP: string): string;
var
  LResponse: string;
  LParts: TArray<string>;
begin
  Result := '';
  try
    LResponse := PostRequest('getPueblos', 'cp=' + ACP + '|inc_cp=N');
    if LResponse <> '' then
    begin
      LParts := LResponse.Split(['|', #13, #10]);
      if Length(LParts) > 0 then
        Result := Trim(LParts[0]);
    end;
  except
    Result := ''; // Silencioso, usaremos fallback
  end;
end;

// ---------------------------------------------------------------------------
function TNacexEnviosClient.MapNacexEstadoToGeneric(const AEstadoText: string): Integer;
var
  LUpper: string;
begin
  LUpper := UpperCase(AEstadoText);
  if Pos('ENTREGADO', LUpper) > 0 then Result := ENV_EST_ENTREGADO
  else if Pos('OK', LUpper) > 0 then Result := ENV_EST_ENTREGADO
  else if Pos('REPARTO', LUpper) > 0 then Result := ENV_EST_REPARTO
  else if Pos('TRANSITO', LUpper) > 0 then Result := ENV_EST_TRANSITO
  else if Pos('RECOGIDO', LUpper) > 0 then Result := ENV_EST_TRANSITO
  else if Pos('RECOGIDA', LUpper) > 0 then Result := ENV_EST_TRANSITO
  else if Pos('SOLICITADA', LUpper) > 0 then Result := ENV_EST_DOCUMENTADO
  else if Pos('NOTIFICADO', LUpper) > 0 then Result := ENV_EST_DOCUMENTADO
  else if Pos('DOCUMENTADO', LUpper) > 0 then Result := ENV_EST_DOCUMENTADO
  else if Pos('INCIDENCIA', LUpper) > 0 then Result := ENV_EST_INCIDENCIA
  else if Pos('DEVUELTO', LUpper) > 0 then Result := ENV_EST_DEVUELTO
  else if Pos('ANULADA', LUpper) > 0 then Result := ENV_EST_DEVUELTO
  else if Pos('SOL SIN OK', LUpper) > 0 then Result := ENV_EST_DEVUELTO
  else Result := ENV_EST_TRANSITO;
end;

// ---------------------------------------------------------------------------
function TNacexEnviosClient.Login: Boolean;
var
  LResponse: string;
begin
  Result := False;
  try
    // getAgencia sirve como método ligero y seguro para verificar la autenticación
    LResponse := PostRequest('getAgencia', '03660');
    Result := (LResponse <> '') and not LResponse.StartsWith('ERROR');
  except
    Result := False;
  end;
end;

// ---------------------------------------------------------------------------
function TNacexEnviosClient.GrabaEnvio(const AParams: TEnvioParams): TEnvioResult;
var
  LData: string;
  LResponse: string;
  LParts: TArray<string>;
  LWSUser, LNumCliente: string;
  LServDefecto: string;
  LModeloImp: string;
begin
  Result := Default(TEnvioResult);
  Result.OK := False;

  // Extraer credenciales (formato: USUARIO/NUM_ABONADO)
  LParts := FConfig.CodCliente.Split(['/']);
  if Length(LParts) >= 2 then
  begin
    LWSUser := LParts[0];
    LNumCliente := LParts[1];
  end
  else
  begin
    LWSUser := FConfig.CodCliente;
    LNumCliente := FConfig.CodCliente;
  end;

  LServDefecto := AParams.CodTipoServ;
  if Trim(LServDefecto) = '' then
    LServDefecto := FConfig.ServDefecto;
  if Trim(LServDefecto) = '' then
    LServDefecto := '27'; // NACEX 19:00H por defecto

  LModeloImp := Trim(FConfig.ModeloImpresora);
  if LModeloImp = '' then
    LModeloImp := 'PDF_B';

  var LFormatSettings := TFormatSettings.Create;
  LFormatSettings.DecimalSeparator := '.';
  var LPesoEnvio := AParams.Peso;
  if LPesoEnvio <= 0 then
    LPesoEnvio := 1.0;

  var LObs, LObs1, LObs2, LObs3, LObs4: string;
  LObs := Copy(Trim(AParams.Observaciones), 1, 120);
  LObs1 := Copy(LObs, 1, 30);
  LObs2 := Copy(LObs, 31, 30);
  LObs3 := Copy(LObs, 61, 30);
  LObs4 := Copy(LObs, 91, 30);

  LData := 'del_cli=' + FConfig.CodAgencia +
           '|num_cli=' + LNumCliente +
           '|tip_ser=' + LServDefecto +
           '|tip_cob=O' + // Por defecto cobro en Origen (O)
           '|ref_cli=' + URLEncode(AParams.Referencia) +
           '|tip_env=2' + // Caja / Paquete ordinario
           '|bul=' + IntToStr(AParams.NumBultos) +
           '|kil=' + FormatFloat('0.000', LPesoEnvio, LFormatSettings) +
           '|nom_ent=' + URLEncode(AParams.NomDes) +
           '|dir_ent=' + URLEncode(AParams.DirDes) +
           '|cp_ent=' + URLEncode(AParams.CPDes) +
           '|pob_ent=' + URLEncode(AParams.PobDes) +
           '|pais_ent=' + URLEncode(AParams.CodPais) +
           '|tel_ent=' + URLEncode(AParams.TlfDes);

  if LObs1 <> '' then LData := LData + '|obs1=' + URLEncode(LObs1);
  if LObs2 <> '' then LData := LData + '|obs2=' + URLEncode(LObs2);
  if LObs3 <> '' then LData := LData + '|obs3=' + URLEncode(LObs3);
  if LObs4 <> '' then LData := LData + '|obs4=' + URLEncode(LObs4);

  LData := LData + '|etiqueta=S' +
           '|modelo=' + LModeloImp +
           '|seguimiento=S';

  LResponse := PostRequest('putExpedicion', LData);

  LParts := LResponse.Split(['|']);
  if (Length(LParts) >= 2) and (LParts[1] <> '') then
  begin
    Result.AlbaranTransportista := LParts[1]; // Formato: agencia/albaran
    Result.GuidTransportista    := LParts[0]; // Código único de expedición
    Result.OK := True;

    if Length(LParts) >= 5 then
      Result.CodAgeDestino := LParts[4];

    if Length(LParts) >= 11 then
      Result.FechaEntrega := ParseNacexDate(LParts[10]);

    // Guardar en caché el PDF de la etiqueta devuelta en la creación
    for var i := High(LParts) downto 0 do
    begin
      var LTrimPart := Trim(LParts[i]);
      if (Length(LTrimPart) > 100) and
         (LTrimPart.StartsWith('JVBERi') or (Pos('PDF', LTrimPart) > 0) or (Length(LTrimPart) > 500)) then
      begin
        try
          FCachedLabelBytes := DecodificarNacexBase64(LTrimPart);
          if Length(FCachedLabelBytes) > 100 then
          begin
            FCachedLabelRef := Result.AlbaranTransportista;
            Break;
          end;
        except
          SetLength(FCachedLabelBytes, 0);
          FCachedLabelRef := '';
        end;
      end;
    end;
  end
  else
  begin
    Result.Error := 'Respuesta del servidor Nacex no válida: ' + LResponse;
  end;
end;

// ---------------------------------------------------------------------------
function TNacexEnviosClient.GetEtiqueta(const ARef: string; APDF: Boolean): TBytes;
var
  LData: string;
  LResponse: string;
  LParts: TArray<string>;
  LModelo: string;
  LEsTermico: Boolean;
  LMethod: string;
begin
  // Devolver de caché si coincide con el albarán guardado en la creación
  if (FCachedLabelRef <> '') and
     (SameText(FCachedLabelRef, ARef) or (Pos(ARef, FCachedLabelRef) > 0) or (Pos(FCachedLabelRef, ARef) > 0)) and
     (Length(FCachedLabelBytes) > 0) then
  begin
    Result := FCachedLabelBytes;
    Exit;
  end;

  if APDF then
    LModelo := 'PDF_B'
  else
  begin
    LModelo := Trim(FConfig.ModeloImpresora);
    if LModelo = '' then
      LModelo := 'ZEBRA_B';
  end;

  LEsTermico := not SameText(LModelo, 'PDF_B') and not SameText(LModelo, 'IMAGEN_B');

  LParts := ARef.Split(['/']);
  if Length(LParts) >= 2 then
  begin
    // Si viene como agencia/numero (ej: 0336/10396397), el método oficial de Nacex es getEtiquetaBultos2
    LMethod := 'getEtiquetaBultos2';
    LData := 'agencia=' + LParts[0] + '|numero=' + LParts[1] + '|bultoIni=1|bultoFin=1|modelo=' + LModelo;
  end
  else
  begin
    // Si viene como código único de expedición (ej: 245076189)
    LMethod := 'getEtiqueta';
    LData := 'codExp=' + ARef + '|modelo=' + LModelo;
  end;

  LResponse := PostRequest(LMethod, LData);

  // Fallback si falla getEtiquetaBultos2 con formato agencia/numero
  if (Length(LParts) >= 2) and (LResponse.StartsWith('ERROR|') or (LResponse = '')) then
  begin
    var LFallbackResp := PostRequest('getEtiqueta', 'codExp=' + LParts[1] + '|modelo=' + LModelo);
    if not LFallbackResp.StartsWith('ERROR|') and (LFallbackResp <> '') then
      LResponse := LFallbackResp;
  end;

  if LResponse <> '' then
  begin
    if not LEsTermico then
    begin
      try
        Result := DecodificarNacexBase64(LResponse);
        if (Length(Result) < 10) or (Result[0] <> Ord('%')) or (Result[1] <> Ord('P')) or (Result[2] <> Ord('D')) or (Result[3] <> Ord('F')) then
        begin
          if LResponse.StartsWith('ERROR|') then
            raise Exception.Create('Error devuelto por Nacex: ' + CleanResponse(LResponse))
          else
            raise Exception.Create('El documento devuelto por Nacex no tiene un formato PDF válido.');
        end;
      except
        on E: Exception do
          raise Exception.CreateFmt('Error decodificando la etiqueta PDF de Nacex: %s', [E.Message]);
      end;
    end
    else
    begin
      // Para ZPL / térmico, retorna texto plano
      Result := TEncoding.UTF8.GetBytes(LResponse);
    end;
  end
  else
    raise Exception.Create('No se ha recibido contenido de etiqueta desde Nacex.');
end;

// ---------------------------------------------------------------------------
function TNacexEnviosClient.ImprimirEtiquetaIoNA(const ARef: string; out AError: string): Boolean;
begin
  Result := ImprimirEtiquetaIoNA(ARef, '', AError, True);
end;

// ---------------------------------------------------------------------------
function TNacexEnviosClient.ImprimirEtiquetaIoNA(const ARef: string; const AImpresora: string; out AError: string): Boolean;
begin
  Result := ImprimirEtiquetaIoNA(ARef, AImpresora, AError, True);
end;

// ---------------------------------------------------------------------------
function TNacexEnviosClient.ImprimirEtiquetaIoNA(const ARef: string; const AImpresora: string; out AError: string; AGirar180: Boolean): Boolean;
var
  LClient: THTTPClient;
  LBodyStream: TStringStream;
  LResponse: IHTTPResponse;
  LBody, LChosenPrinter, LPrintersResp, LLine: string;
  LPrinterList: TArray<string>;
  LUpper: string;
  LRespText: string;
  LEtiquetaBytes: TBytes;
  LPdfPath, LBase64: string;
  LFS: TFileStream;
  LRet: NativeUInt;
begin
  Result := False;
  AError := '';
  LChosenPrinter := Trim(AImpresora);

  // 1. Obtener los bytes de la etiqueta oficial en PDF
  try
    LEtiquetaBytes := GetEtiqueta(ARef, True);
  except
    on E: Exception do
    begin
      AError := 'Error al descargar la etiqueta de Nacex: ' + E.Message;
      Exit;
    end;
  end;

  if Length(LEtiquetaBytes) = 0 then
  begin
    AError := 'Nacex no devolvió el contenido binario de la etiqueta.';
    Exit;
  end;

  // 2. Guardar archivo PDF temporal para impresión directa de Windows
  LPdfPath := GetEnvironmentVariable('TEMP') + '\Etiqueta_' +
              StringReplace(ARef, '/', '_', [rfReplaceAll]) + '_' +
              FormatDateTime('yyyymmddhhnnss', Now) + '.pdf';
  try
    LFS := TFileStream.Create(LPdfPath, fmCreate);
    try
      LFS.WriteBuffer(LEtiquetaBytes[0], Length(LEtiquetaBytes));
    finally
      LFS.Free;
    end;
  except
    on E: Exception do
    begin
      AError := 'Error creando archivo PDF temporal: ' + E.Message;
      Exit;
    end;
  end;

  // Si se solicita giro de 180° para no solapar con el logo preimpreso
  if AGirar180 then
  begin
    TPdfRotateService.RotarPdfArchivo180(LPdfPath);
    try
      LEtiquetaBytes := TFile.ReadAllBytes(LPdfPath);
    except
      // Si falla recargar bytes para ioNA, se continuará con los bytes originales en ioNA
    end;
  end;

  // 3. Si se especificó una impresora (Honeywell, Zebra, etc.), intentar impresión directa de Windows
  if LChosenPrinter <> '' then
  begin
    LRet := ShellExecute(0, 'printto', PChar(LPdfPath), PChar('"' + LChosenPrinter + '"'), nil, SW_HIDE);
    if LRet > 32 then
    begin
      Result := True;
      Exit;
    end;
  end;

  // 4. Intentar servicio local ioNA si está activo en 127.0.0.1:8000
  LClient := THTTPClient.Create;
  try
    LClient.ConnectionTimeout := 2000;
    LClient.ResponseTimeout   := 5000;
    LClient.ContentType       := 'application/x-www-form-urlencoded';
    LClient.OnValidateServerCertificate := DoValidateCertificate;

    // Si no tenemos impresora elegida, consultar impresoras a ioNA
    if LChosenPrinter = '' then
    begin
      try
        LResponse := LClient.Get('https://127.0.0.1:8000/printers');
        if (LResponse.StatusCode >= 200) and (LResponse.StatusCode < 300) then
        begin
          LPrintersResp := LResponse.ContentAsString(TEncoding.UTF8);
          LPrinterList := LPrintersResp.Split([#13#10, #10, #13]);
          for LLine in LPrinterList do
          begin
            LUpper := UpperCase(Trim(LLine));
            if (LUpper <> '') and (
               (Pos('ZEBRA', LUpper) > 0) or
               (Pos('ZDESIGNER', LUpper) > 0) or
               (Pos('HONEYWELL', LUpper) > 0) or
               (Pos('INTERMEC', LUpper) > 0) or
               (Pos('TEC', LUpper) > 0) or
               (Pos('TOSHIBA', LUpper) > 0) or
               (Pos('TSC', LUpper) > 0) or
               (Pos('CITIZEN', LUpper) > 0) or
               (Pos('GODEX', LUpper) > 0) or
               (Pos('DATAMAX', LUpper) > 0) or
               (Pos('THERMAL', LUpper) > 0) or
               (Pos('ETIQUETA', LUpper) > 0) or
               (Pos('LABEL', LUpper) > 0) or
               (Pos('DIRECT', LUpper) > 0) or
               (Pos('NACEX', LUpper) > 0)
            ) then
            begin
              LChosenPrinter := Trim(LLine);
              Break;
            end;
          end;
        end;
      except
        // Ignorar si ioNA no está en ejecución
      end;
    end;

    // Construir petición con contenido PDF Base64 directo para ioNA
    LBase64 := TNetEncoding.Base64.EncodeBytesToString(LEtiquetaBytes);
    LBody := 'eti=' + TNetEncoding.URL.Encode(LBase64) +
             '&nombreDoc=' + TNetEncoding.URL.Encode('Etiqueta_' + StringReplace(ARef, '/', '_', [rfReplaceAll])) +
             '&etiquetas=1';

    if LChosenPrinter <> '' then
    begin
      LBody := LBody + '&impresora=' + TNetEncoding.URL.Encode(LChosenPrinter) +
                       '&init_string=' + TNetEncoding.URL.Encode(LChosenPrinter + '|||S|N|1') +
                       '&SHOW_PRINTER_DIALOG=N';
    end
    else
    begin
      LBody := LBody + '&SHOW_PRINTER_DIALOG=S';
    end;

    LBodyStream := TStringStream.Create(LBody, TEncoding.UTF8);
    try
      try
        LResponse := LClient.Post('https://127.0.0.1:8000/print', LBodyStream);
        if (LResponse.StatusCode >= 200) and (LResponse.StatusCode < 300) then
        begin
          LRespText := LResponse.ContentAsString(TEncoding.UTF8);
          if Pos('cancelada', LowerCase(LRespText)) = 0 then
          begin
            Result := True;
            Exit;
          end;
        end;
      except
        on E: Exception do
          AError := E.Message;
      end;
    finally
      LBodyStream.Free;
    end;
  finally
    LClient.Free;
  end;

  if not Result and (AError = '') then
  begin
    if LChosenPrinter <> '' then
      AError := 'No se pudo enviar el documento a la impresora "' + LChosenPrinter + '" ni al servicio ioNA.'
    else
      AError := 'No se pudo iniciar el servicio de impresión de etiquetas.';
  end;
end;

// ---------------------------------------------------------------------------
function TNacexEnviosClient.GetEstados(const ARef: string): TArray<TEstadoEnvio>;
var
  LData: string;
  LResponse: string;
  LLines: TArray<string>;
  LParts: TArray<string>;
  LFields: TArray<string>;
  LCount: Integer;
  LEstado: TEstadoEnvio;
  i: Integer;
  LDateStr, LTimeStr, LDesc, LAgency: string;
  LStateCode: Integer;
begin
  SetLength(Result, 0);

  LParts := ARef.Split(['/']);
  if Length(LParts) >= 2 then
    LData := 'ag=' + LParts[0] + '|numalb=' + LParts[1]
  else
    LData := 'codexp=' + ARef;

  try
    LResponse := PostRequest('getHistoricoExpedicion', LData);
  except
    on E: Exception do
    begin
      // Si no existe tracking de expedición en el servidor de Nacex, devolvemos estado inicial
      if Pos('No existe', E.Message) > 0 then
      begin
        SetLength(Result, 1);
        Result[0].IdEstado := 0;
        Result[0].Codigo := ENV_EST_DOCUMENTADO;
        Result[0].Descripcion := 'Documentado (Pre-registro Nacex)';
        Result[0].FechaHora := Now;
        Result[0].CodUsuario := 'SYSTEM';
        Exit;
      end;
      raise;
    end;
  end;

  LLines := LResponse.Split(['|', #13, #10]);
  LCount := 0;

  for i := 0 to Length(LLines) - 1 do
  begin
    if Trim(LLines[i]) = '' then Continue;

    LFields := LLines[i].Split(['~']);
    if Length(LFields) >= 3 then
    begin
      LDateStr := LFields[0];
      LTimeStr := LFields[1];
      LDesc    := LFields[2];
      LAgency  := '';
      if Length(LFields) >= 4 then
        LAgency := LFields[3];

      LEstado.IdEstado   := LCount;
      LEstado.CodUsuario := LAgency;

      try
        LEstado.FechaHora := ParseNacexDate(LDateStr) + StrToTime(LTimeStr);
      except
        LEstado.FechaHora := Now;
      end;

      LStateCode := MapNacexEstadoToGeneric(LDesc);
      LEstado.Codigo := LStateCode;

      if LAgency <> '' then
        LEstado.Descripcion := LDesc + ' (' + LAgency + ')'
      else
        LEstado.Descripcion := LDesc;

      SetLength(Result, LCount + 1);
      Result[LCount] := LEstado;
      Inc(LCount);
    end;
  end;
end;

// ---------------------------------------------------------------------------
function TNacexEnviosClient.GrabaRecogida(const AParams: TRecogidaParams): TRecogidaResult;
var
  LData: string;
  LResponse: string;
  LParts: TArray<string>;
  LWSUser, LNumCliente: string;
  LPobRec: string;
  LServDefecto: string;
  LTimeStart, LTimeEnd: string;
  LEmailSolicitante, LModeloImp: string;
begin
  Result := Default(TRecogidaResult);
  Result.OK := False;

  LParts := FConfig.CodCliente.Split(['/']);
  if Length(LParts) >= 2 then
  begin
    LWSUser := LParts[0];
    LNumCliente := LParts[1];
  end
  else
  begin
    LWSUser := FConfig.CodCliente;
    LNumCliente := FConfig.CodCliente;
  end;

  // Nacex requiere la población de recogida obligatoriamente
  LPobRec := GetPoblacionPorCP(AParams.CPOri);
  if LPobRec = '' then
    LPobRec := 'CENTRAL'; // Fallback por si falla el WS

  LServDefecto := AParams.CodTipoServ;
  if Trim(LServDefecto) = '' then
    LServDefecto := FConfig.ServDefecto;
  if Trim(LServDefecto) = '' then
    LServDefecto := '08';

  LEmailSolicitante := Trim(FConfig.EmailRemitente);
  if LEmailSolicitante = '' then
    LEmailSolicitante := 'cliente@nacex.es';

  LModeloImp := Trim(FConfig.ModeloImpresora);
  if LModeloImp = '' then
    LModeloImp := 'PDF_B';

  // Formatear horarios de recogida hh:mm
  if AParams.HoraIni > 0 then
    LTimeStart := FormatDateTime('hh:nn', AParams.HoraIni)
  else
    LTimeStart := '09:00';

  if AParams.HoraFin > 0 then
    LTimeEnd := FormatDateTime('hh:nn', AParams.HoraFin)
  else
    LTimeEnd := '19:00';

  var LObs, LObs1, LObs2, LObs3, LObs4: string;
  LObs := Copy(Trim(AParams.Observaciones), 1, 120);
  LObs1 := Copy(LObs, 1, 30);
  LObs2 := Copy(LObs, 31, 30);
  LObs3 := Copy(LObs, 61, 30);
  LObs4 := Copy(LObs, 91, 30);

  LData := 'delcli=' + FConfig.CodAgencia +
           '|numcli=' + LNumCliente +
           '|fecha=' + FormatDateTime('dd/MM/yyyy', AParams.FechaRecogida) +
           '|cobro=D' + // Cobro en Destino
           '|servicio=' + LServDefecto +
           '|referencia=' + URLEncode(AParams.Referencia) +
           '|envase=2' + // Caja por defecto
           '|bultos=' + IntToStr(AParams.NumBultos) +
           '|peso=1.0' + // Obligatorio
           '|nomrec=' + URLEncode(AParams.NomOri) +
           '|dirrec=' + URLEncode(AParams.TipoViaOri + ' ' + AParams.DirOri + ', ' + AParams.NumOri) +
           '|cprec=' + URLEncode(AParams.CPOri) +
           '|pobrec=' + URLEncode(LPobRec) +
           '|telrec=' + URLEncode(AParams.TlfOri) +
           '|noment=' + URLEncode(AParams.NomDes) +
           '|dirent=' + URLEncode(AParams.TipoViaDes + ' ' + AParams.DirDes + ', ' + AParams.NumDes) +
           '|cpent=' + URLEncode(AParams.CPDes) +
           '|pobent=' + URLEncode(AParams.PobDes) +
           '|telent=' + URLEncode(AParams.TlfDes);

  if LObs1 <> '' then LData := LData + '|obs1=' + URLEncode(LObs1);
  if LObs2 <> '' then LData := LData + '|obs2=' + URLEncode(LObs2);
  if LObs3 <> '' then LData := LData + '|obs3=' + URLEncode(LObs3);
  if LObs4 <> '' then LData := LData + '|obs4=' + URLEncode(LObs4);

  LData := LData + '|obs1_e=' + URLEncode(AParams.ObsDes) +
           '|solicitante=' + URLEncode(FConfig.NomRemitente) +
           '|email_solicitante=' + URLEncode(LEmailSolicitante) +
           '|hora_ini1=' + LTimeStart +
           '|hora_fin1=' + LTimeEnd +
           '|vehiculo=C' + // Coche
           '|contenido=' + URLEncode(AParams.Contenido) +
           '|etiqueta=S' +
           '|modelo=' + LModeloImp +
           '|seguimiento=S';

  ShowMessage('Llamada API Nacex (putRecogida):' + #13#10 +
              'URL: ' + FURLBase + #13#10 +
              'Parámetros:' + #13#10 +
              '  method=putRecogida' + #13#10 +
              '  user=' + LWSUser + #13#10 +
              '  pass=' + StringOfChar('*', Length(FConfig.Password)) + #13#10 +
              '  data=' + #13#10 + '    ' + LData.Replace('|', #13#10 + '    '));

  LResponse := PostRequest('putRecogida', LData);

  LParts := LResponse.Split(['|']);
  if (Length(LParts) >= 2) and (LParts[1] <> '') then
  begin
    Result.CodRecogida       := LParts[1]; // Formato: agencia/numero_solicitud
    Result.GuidTransportista := LParts[1];
    Result.CodAgeOrigen      := LParts[1].Split(['/'])[0];
    Result.FechaAlta         := Now;
    Result.OK                := True;
  end
  else
  begin
    Result.Error := Format(
      'Respuesta no válida del servidor Nacex.' + #13#10 +
      'URL: %s' + #13#10 +
      'User WS: "%s"' + #13#10 +
      'Pass MD5 (Longitud): %d' + #13#10 +
      'Agencia (delcli): "%s" | Abonado (numcli): "%s"' + #13#10 +
      'Datos enviados (data): %s' + #13#10 +
      'Respuesta recibida del servidor: "%s"',
      [FURLBase, LWSUser, Length(FConfig.Password), FConfig.CodAgencia, LNumCliente, LData, LResponse]);
  end;
end;

// ---------------------------------------------------------------------------
function TNacexEnviosClient.AnulaEnvio(const ARef: string; out AMsg: string): Boolean;
var
  LData: string;
  LResponse: string;
  LParts: TArray<string>;
begin
  Result := False;
  AMsg := '';

  LParts := ARef.Split(['/']);
  if Length(LParts) >= 2 then
    LData := 'origen=' + LParts[0] + '|albaran=' + LParts[1]
  else
    LData := 'expe_codigo=' + ARef;

  try
    LResponse := PostRequest('cancelExpedicion', LData);
    if (LResponse <> '') and not LResponse.StartsWith('ERROR') then
    begin
      AMsg := LResponse;
      Result := True;
    end
    else
    begin
      AMsg := 'Error al cancelar la expedición: ' + LResponse;
    end;
  except
    on E: Exception do
    begin
      AMsg := E.Message;
    end;
  end;
end;

end.
