unit uEdicomWsClient;

interface

uses
  System.SysUtils, System.Classes, System.Net.HttpClient, System.Net.HttpClientComponent,
  System.Net.URLClient, System.Net.Mime, System.JSON, FireDAC.Comp.Client, Data.DB;

type
  TEEdicomWsError = class(Exception);

  TEdicomEndpointConfig = record
    Datasource: string;
    BearerToken: string;
    BaseUrl: string;
    DomainId: string;
    ApplicationId: string;
    GlnEmpresa: string;
  end;

  TEdicomWsClient = class
  private
    FConfigs: TArray<TEdicomEndpointConfig>;
    FPrimaryConfig: TEdicomEndpointConfig;
    FConn: TFDConnection;
    function GetAppLogDir: string;
    procedure WriteAppLog(const ALogText: string; const ALastReceivedBody: string = ''; const ALastSentBody: string = '');
    procedure LoadConfig(const AEmpresaId: string);
  public
    constructor Create(AConn: TFDConnection; const AEmpresaId: string);

    /// <summary>
    /// Envía una factura XML a EDICOM mediante POST multipart/form-data
    /// Schema: OUT_XML_INTEGRATION_LAYOUT_INVOICE_GLOBAL_449046
    /// </summary>
    function PublicarFactura(const AFacturaId: string; const AXmlContent: string; out AResponseMsg: string): Boolean;

    /// <summary>
    /// Envía un albarán XML a EDICOM mediante POST multipart/form-data
    /// Schema: OUT_XML_INTEGRATION_LAYOUT_DESADV_GLOBAL_449046
    /// </summary>
    function PublicarAlbaran(const AlbaranId: string; const AXmlContent: string; out AResponseMsg: string): Boolean;

    /// <summary>
    /// Consulta los pedidos pendientes de recibir mediante GET iterando por cada código/datasource configurado
    /// Schema: IN_XML_INTEGRATION_LAYOUT_ORDER_GLOBAL_449046
    /// </summary>
    function ConsultarPedidosPendientes(out AResponseMsg: string): string;

    /// <summary>
    /// Confirma la recepción de pedidos mediante POST JSON array
    /// </summary>
    function ConfirmarRecepcionPedidos(const AMessageIdsJson: string; out AResponseMsg: string): Boolean;

    /// <summary>
    /// Comprueba si una empresa tiene integración activa en ge_edicom_config
    /// </summary>
    class function HasEdicomConfig(const AEmpresaId: string; AConn: TFDConnection = nil): Boolean;

    /// <summary>
    /// Consulta y descarga pedidos pendientes de EDICOM iPaaS e importa en el ERP
    /// </summary>
    class function DownloadAndImportPendingOrders(AConn: TFDConnection; const AEmpresaId: string; out ADownloadedCount: Integer; out AResponseOrErrorMsg: string): Boolean;
  end;

implementation

uses
  dmg_Main, uEdicomProcessor;

function TEdicomWsClient.GetAppLogDir: string;
var
  LAppDir: string;
begin
  LAppDir := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0)));
  Result := LAppDir + 'logs';
  if not DirectoryExists(Result) then
    ForceDirectories(Result);
end;

procedure TEdicomWsClient.WriteAppLog(const ALogText: string; const ALastReceivedBody: string = ''; const ALastSentBody: string = '');
var
  LAppDir, LLogDir, LDailyLogPath, LMainLogPath, LLastRespPath, LLastSentPath: string;
  LLogList: TStringList;
begin
  LAppDir := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0)));
  LLogDir := GetAppLogDir;
  
  LDailyLogPath := IncludeTrailingPathDelimiter(LLogDir) + 'edicom_' + FormatDateTime('yyyy-mm-dd', Now) + '.log';
  LMainLogPath := LAppDir + 'edicom_ws_debug.log';
  
  LLogList := TStringList.Create;
  try
    LLogList.Text := ALogText;
    
    // 1. Guardar en el log diario dentro de la subcarpeta logs/
    try
      if FileExists(LDailyLogPath) then
      begin
        var LExisting := TStringList.Create;
        try
          LExisting.LoadFromFile(LDailyLogPath, TEncoding.UTF8);
          LExisting.Add('');
          LExisting.Add(ALogText);
          LExisting.SaveToFile(LDailyLogPath, TEncoding.UTF8);
        finally
          LExisting.Free;
        end;
      end
      else
        LLogList.SaveToFile(LDailyLogPath, TEncoding.UTF8);
    except
    end;

    // 2. Guardar en el log acumulativo edicom_ws_debug.log en la carpeta raíz de la aplicación
    try
      if FileExists(LMainLogPath) then
      begin
        var LExisting := TStringList.Create;
        try
          LExisting.LoadFromFile(LMainLogPath, TEncoding.UTF8);
          LExisting.Add('');
          LExisting.Add(ALogText);
          LExisting.SaveToFile(LMainLogPath, TEncoding.UTF8);
        finally
          LExisting.Free;
        end;
      end
      else
        LLogList.SaveToFile(LMainLogPath, TEncoding.UTF8);
    except
    end;
    
    // 3. Si hay payload recibido, guardarlo completo en fichero específico en la carpeta de la aplicación
    if ALastReceivedBody <> '' then
    begin
      try
        LLastRespPath := LAppDir + 'edicom_last_response_body.txt';
        var LBodyFile := TStringList.Create;
        try
          LBodyFile.Text := ALastReceivedBody;
          LBodyFile.SaveToFile(LLastRespPath, TEncoding.UTF8);
        finally
          LBodyFile.Free;
        end;
      except
      end;
    end;

    // 4. Si hay payload enviado, guardarlo completo en fichero específico en la carpeta de la aplicación
    if ALastSentBody <> '' then
    begin
      try
        LLastSentPath := LAppDir + 'edicom_last_sent_body.txt';
        var LSentFile := TStringList.Create;
        try
          LSentFile.Text := ALastSentBody;
          LSentFile.SaveToFile(LLastSentPath, TEncoding.UTF8);
        finally
          LSentFile.Free;
        end;
      except
      end;
    end;
  finally
    LLogList.Free;
  end;
end;

constructor TEdicomWsClient.Create(AConn: TFDConnection; const AEmpresaId: string);
begin
  inherited Create;
  FConn := AConn;
  LoadConfig(AEmpresaId);
end;

procedure TEdicomWsClient.LoadConfig(const AEmpresaId: string);
var
  Qry: TFDQuery;
  LDefaultToken, LDefaultBaseUrl, LDefaultDomain: string;

  function IsValidJwtToken(const AToken: string): Boolean;
  begin
    Result := (Length(Trim(AToken)) > 50) and (Pos('eyJ', Trim(AToken)) = 1);
  end;

  procedure AddEndpoint(const ADatasource, AToken, ABaseUrl, AGln: string);
  var
    LCfg: TEdicomEndpointConfig;
    I: Integer;
    LDsClean, LFinalToken: string;
  begin
    LDsClean := Trim(ADatasource);
    if LDsClean = '' then
      LDsClean := 'ASPEDI109';

    // Si es un GLN puramente numérico de 13 dígitos, el DataSource de EDICOM para Raúl Asencio es ASPEDI109
    if (Length(LDsClean) = 13) and (StrToInt64Def(LDsClean, 0) > 0) then
      LDsClean := 'ASPEDI109';

    if IsValidJwtToken(AToken) then
      LFinalToken := Trim(AToken)
    else
      LFinalToken := LDefaultToken;

    // Evitar duplicados
    for I := 0 to Length(FConfigs) - 1 do
    begin
      if SameText(FConfigs[I].Datasource, LDsClean) and 
         SameText(FConfigs[I].BearerToken, LFinalToken) then
        Exit;
    end;

    LCfg.Datasource := LDsClean;
    LCfg.BearerToken := LFinalToken;

    if Trim(ABaseUrl) <> '' then
      LCfg.BaseUrl := Trim(ABaseUrl)
    else
      LCfg.BaseUrl := LDefaultBaseUrl;

    while (Length(LCfg.BaseUrl) > 0) and (LCfg.BaseUrl[Length(LCfg.BaseUrl)] = '/') do
      Delete(LCfg.BaseUrl, Length(LCfg.BaseUrl), 1);

    LCfg.DomainId := LDefaultDomain;
    LCfg.ApplicationId := LDefaultDomain;
    LCfg.GlnEmpresa := Trim(AGln);

    SetLength(FConfigs, Length(FConfigs) + 1);
    FConfigs[Length(FConfigs) - 1] := LCfg;
  end;

  procedure ParseAndAddEndpoints(const ARawDatasources, AToken, ABaseUrl, AGln: string);
  var
    LSplits: TArray<string>;
    LItem, LSanitized: string;
  begin
    if Trim(ARawDatasources) = '' then
    begin
      AddEndpoint('ASPEDI109', AToken, ABaseUrl, AGln);
      Exit;
    end;

    LSanitized := ARawDatasources;
    LSanitized := StringReplace(LSanitized, ',', '-', [rfReplaceAll]);
    LSanitized := StringReplace(LSanitized, ';', '-', [rfReplaceAll]);
    LSanitized := StringReplace(LSanitized, '/', '-', [rfReplaceAll]);
    LSanitized := StringReplace(LSanitized, #13#10, '-', [rfReplaceAll]);
    LSanitized := StringReplace(LSanitized, #10, '-', [rfReplaceAll]);

    LSplits := LSanitized.Split(['-']);
    for LItem in LSplits do
    begin
      if Trim(LItem) <> '' then
        AddEndpoint(Trim(LItem), AToken, ABaseUrl, AGln);
    end;

    if Length(LSplits) = 0 then
      AddEndpoint(Trim(ARawDatasources), AToken, ABaseUrl, AGln);
  end;

begin
  SetLength(FConfigs, 0);
  LDefaultDomain := '449046';
  LDefaultBaseUrl := 'https://ipaasgw.edicomgroup.com/api/v1';
  // Bearer JWT Token oficial extraído de la colección de Postman de EDICOM
  LDefaultToken := 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJiYXRjaGlwYWFzX3dzQDQ0OTA0NiIsImlzcyI6InNhYXNlZGlnMTA5LWVpcGFhc3NlcnZlcjAxLTc3YjZjNDVkZmMtOGpiNHIiLCJzb3VyY2UiOiJlaXBhYXMiLCJleHAiOjMyNTAzNjM2ODAwLCJpYXQiOjE3ODUzMTc5MDMsImp0aSI6IjI1ODI3OWYzLTA3ZGQt' +
                   'NDAzMC1iN2U5LTM5MTc3NGM4NzRmNCJ9.xpxpcVk9BEyj14lXL7KnzHMjjZq1GRbloFkU3Zgv1pU';

  Qry := TFDQuery.Create(nil);
  try
    Qry.Connection := FConn;
    Qry.SQL.Text := 'SELECT datasource_code, api_key, password, url_api_base, gln_empresa FROM ge_edicom_config WHERE id_empresa = :emp AND activo = 1';
    Qry.ParamByName('emp').AsString := AEmpresaId;
    Qry.Open;
    while not Qry.Eof do
    begin
      var LToken: string := Trim(Qry.FieldByName('api_key').AsString);
      if not IsValidJwtToken(LToken) then
        LToken := Trim(Qry.FieldByName('password').AsString);
      var LUrl: string := Trim(Qry.FieldByName('url_api_base').AsString);
      var LDs: string := Trim(Qry.FieldByName('datasource_code').AsString);
      var LGln: string := Trim(Qry.FieldByName('gln_empresa').AsString);

      ParseAndAddEndpoints(LDs, LToken, LUrl, LGln);
      Qry.Next;
    end;
  finally
    Qry.Free;
  end;

  // Fallback si no hay ninguna configuración en BD
  if Length(FConfigs) = 0 then
  begin
    AddEndpoint('ASPEDI109', LDefaultToken, LDefaultBaseUrl, '');
  end;

  FPrimaryConfig := FConfigs[0];
end;

function ExtractFirstMessageId(const ARawJson: string): string;
var
  LVal: TJSONValue;
  LArr: TJSONArray;
begin
  Result := '';
  if Trim(ARawJson) = '' then Exit;
  try
    LVal := TJSONObject.ParseJSONValue(ARawJson);
    if Assigned(LVal) then
    begin
      try
        if LVal is TJSONArray then
        begin
          LArr := TJSONArray(LVal);
          if LArr.Count > 0 then
            Result := LArr.Items[0].Value;
        end
        else if LVal is TJSONObject then
        begin
          if TJSONObject(LVal).GetValue('id') <> nil then
            Result := TJSONObject(LVal).GetValue('id').Value;
        end;
      finally
        LVal.Free;
      end;
    end;
  except
    Result := '';
  end;
end;

function TEdicomWsClient.PublicarFactura(const AFacturaId: string; const AXmlContent: string; out AResponseMsg: string): Boolean;
var
  HTTP: TNetHTTPClient;
  FormData: TMultipartFormData;
  URL, LSchemaName, LRawBody, LMsgId: string;
  Response: IHTTPResponse;
  XmlStream: TStringStream;
  LLog: TStringList;
begin
  Result := False;
  AResponseMsg := '';

  LSchemaName := 'OUT_XML_INTEGRATION_LAYOUT_INVOICE_GLOBAL_' + FPrimaryConfig.DomainId;
  URL := Format('%s/domain/%s/application/%s/broker/publish?schema=%s&destination=%s_EDIWIN&reference=%s&duplicates=0',
    [FPrimaryConfig.BaseUrl, FPrimaryConfig.DomainId, FPrimaryConfig.ApplicationId, LSchemaName, FPrimaryConfig.DomainId, 'FAC_' + AFacturaId]);

  LLog := TStringList.Create;
  try
    LLog.Add('================================================================');
    LLog.Add('  EDICOM iPaaS — Publicación de Factura (PUBLISH INVOICE)');
    LLog.Add('================================================================');
    LLog.Add('Fecha/Hora: ' + FormatDateTime('yyyy-mm-dd hh:nn:ss', Now));
    LLog.Add('Factura ID: ' + AFacturaId);
    LLog.Add('URL: ' + URL);
    LLog.Add('X-Eipaas-Ds: ' + FPrimaryConfig.Datasource);
    LLog.Add('Trama XML enviada:');
    LLog.Add(AXmlContent);
    LLog.Add('');

    HTTP := TNetHTTPClient.Create(nil);
    FormData := TMultipartFormData.Create;
    XmlStream := TStringStream.Create(AXmlContent, TEncoding.UTF8);
    try
      HTTP.CustomHeaders['X-Eipaas-Ds'] := FPrimaryConfig.Datasource;
      if FPrimaryConfig.BearerToken <> '' then
        HTTP.CustomHeaders['Authorization'] := 'Bearer ' + FPrimaryConfig.BearerToken;

      FormData.AddStream('files', XmlStream, 'INVOIC_' + AFacturaId + '.xml');

      try
        Response := HTTP.Post(URL, FormData);
        if Response <> nil then
        begin
          LRawBody := Response.ContentAsString(TEncoding.UTF8);
          AResponseMsg := Format('HTTP %d - %s', [Response.StatusCode, LRawBody]);
          LLog.Add('Status Respuesta: ' + IntToStr(Response.StatusCode) + ' ' + Response.StatusText);
          LLog.Add('Body Respuesta: ' + LRawBody);
        end
        else
        begin
          LRawBody := '';
          AResponseMsg := 'Sin respuesta del servidor';
          LLog.Add('Status Respuesta: Sin respuesta del servidor');
        end;

        LMsgId := ExtractFirstMessageId(LRawBody);

        if (Response <> nil) and (Response.StatusCode >= 200) and (Response.StatusCode < 300) then
        begin
          Result := True;
          // Actualizar estado en ge_facturas acumulando los Message IDs
          dmgMain.qryExec.Close;
          if LMsgId <> '' then
          begin
            dmgMain.qryExec.SQL.Text := 
              'UPDATE ge_facturas SET edi_estado_envio = ''ENVIADO'', ' +
              'edi_message_id = IF(edi_message_id IS NULL OR edi_message_id = '''', :msgid, CONCAT(edi_message_id, '', '', :msgid)), ' +
              'edi_enviado_at = CURRENT_TIMESTAMP WHERE id = :id';
            dmgMain.qryExec.ParamByName('msgid').AsString := LMsgId;
          end
          else
          begin
            dmgMain.qryExec.SQL.Text := 
              'UPDATE ge_facturas SET edi_estado_envio = ''ENVIADO'', edi_enviado_at = CURRENT_TIMESTAMP WHERE id = :id';
          end;
          dmgMain.qryExec.ParamByName('id').AsString := AFacturaId;
          dmgMain.qryExec.ExecSQL;
        end
        else
        begin
          // Registrar error acumulando el Message ID si se generó
          dmgMain.qryExec.Close;
          if LMsgId <> '' then
          begin
            dmgMain.qryExec.SQL.Text := 
              'UPDATE ge_facturas SET edi_estado_envio = ''ERROR'', ' +
              'edi_message_id = IF(edi_message_id IS NULL OR edi_message_id = '''', :msgid, CONCAT(edi_message_id, '', '', :msgid)), ' +
              'edi_error_descripcion = :err WHERE id = :id';
            dmgMain.qryExec.ParamByName('msgid').AsString := LMsgId;
          end
          else
          begin
            dmgMain.qryExec.SQL.Text := 
              'UPDATE ge_facturas SET edi_estado_envio = ''ERROR'', edi_error_descripcion = :err WHERE id = :id';
          end;
          dmgMain.qryExec.ParamByName('err').AsString := Copy(AResponseMsg, 1, 500);
          dmgMain.qryExec.ParamByName('id').AsString := AFacturaId;
          dmgMain.qryExec.ExecSQL;
        end;
      except
        on E: Exception do
        begin
          AResponseMsg := Format('[Excepción %s]: %s', [E.ClassName, E.Message]);
          LLog.Add('Excepción: ' + E.Message);
          dmgMain.qryExec.Close;
          dmgMain.qryExec.SQL.Text := 'UPDATE ge_facturas SET edi_estado_envio = ''ERROR'', edi_error_descripcion = :err WHERE id = :id';
          dmgMain.qryExec.ParamByName('err').AsString := Copy(AResponseMsg, 1, 500);
          dmgMain.qryExec.ParamByName('id').AsString := AFacturaId;
          dmgMain.qryExec.ExecSQL;
        end;
      end;
    finally
      XmlStream.Free;
      FormData.Free;
      HTTP.Free;
    end;
    LLog.Add('================================================================');
    WriteAppLog(LLog.Text, LRawBody, AXmlContent);
  finally
    LLog.Free;
  end;
end;

function TEdicomWsClient.PublicarAlbaran(const AlbaranId: string; const AXmlContent: string; out AResponseMsg: string): Boolean;
var
  HTTP: TNetHTTPClient;
  FormData: TMultipartFormData;
  URL, LSchemaName, LRawBody, LMsgId, LSerTag, LNumTag, LFileTag: string;
  Response: IHTTPResponse;
  XmlStream: TStringStream;
  QryFileTag: TFDQuery;
  LLog: TStringList;
begin
  Result := False;
  AResponseMsg := '';

  LSchemaName := 'OUT_XML_INTEGRATION_LAYOUT_DESADV_GLOBAL_' + FPrimaryConfig.DomainId;
  URL := Format('%s/domain/%s/application/%s/broker/publish?schema=%s&destination=%s_EDIWIN&reference=%s&duplicates=0',
    [FPrimaryConfig.BaseUrl, FPrimaryConfig.DomainId, FPrimaryConfig.ApplicationId, LSchemaName, FPrimaryConfig.DomainId, 'ALB_' + AlbaranId]);

  LLog := TStringList.Create;
  try
    LLog.Add('================================================================');
    LLog.Add('  EDICOM iPaaS — Publicación de Albarán DESADV (PUBLISH DESADV)');
    LLog.Add('================================================================');
    LLog.Add('Fecha/Hora: ' + FormatDateTime('yyyy-mm-dd hh:nn:ss', Now));
    LLog.Add('Albarán ID: ' + AlbaranId);
    LLog.Add('URL: ' + URL);
    LLog.Add('X-Eipaas-Ds: ' + FPrimaryConfig.Datasource);
    LLog.Add('Trama XML enviada:');
    LLog.Add(AXmlContent);
    LLog.Add('');

    HTTP := TNetHTTPClient.Create(nil);
    FormData := TMultipartFormData.Create;
    XmlStream := TStringStream.Create(AXmlContent, TEncoding.UTF8);
    try
      HTTP.CustomHeaders['X-Eipaas-Ds'] := FPrimaryConfig.Datasource;
      if FPrimaryConfig.BearerToken <> '' then
        HTTP.CustomHeaders['Authorization'] := 'Bearer ' + FPrimaryConfig.BearerToken;

      LFileTag := AlbaranId;
      QryFileTag := TFDQuery.Create(nil);
      try
        QryFileTag.Connection := FConn;
        QryFileTag.SQL.Text := 'SELECT serie, numero FROM ge_albaranes WHERE id = :id';
        QryFileTag.ParamByName('id').AsString := AlbaranId;
        QryFileTag.Open;
        if not QryFileTag.IsEmpty then
        begin
          LSerTag := Trim(QryFileTag.FieldByName('serie').AsString);
          LNumTag := Trim(QryFileTag.FieldByName('numero').AsString);
          if LSerTag <> '' then
            LFileTag := LSerTag + '_' + LNumTag
          else if LNumTag <> '' then
            LFileTag := LNumTag;
        end;
      finally
        QryFileTag.Free;
      end;

      FormData.AddStream('files', XmlStream, 'DESADV_' + LFileTag + '.xml');

      try
        Response := HTTP.Post(URL, FormData);
        if Response <> nil then
        begin
          LRawBody := Response.ContentAsString(TEncoding.UTF8);
          AResponseMsg := Format('HTTP %d - %s', [Response.StatusCode, LRawBody]);
          LLog.Add('Status Respuesta: ' + IntToStr(Response.StatusCode) + ' ' + Response.StatusText);
          LLog.Add('Body Respuesta: ' + LRawBody);
        end
        else
        begin
          LRawBody := '';
          AResponseMsg := 'Sin respuesta del servidor';
          LLog.Add('Status Respuesta: Sin respuesta del servidor');
        end;

        LMsgId := ExtractFirstMessageId(LRawBody);

        if (Response <> nil) and (Response.StatusCode >= 200) and (Response.StatusCode < 300) then
        begin
          Result := True;
          dmgMain.qryExec.Close;
          if LMsgId <> '' then
          begin
            dmgMain.qryExec.SQL.Text := 
              'UPDATE ge_albaranes SET edi_estado_envio = ''ENVIADO'', ' +
              'edi_message_id = IF(edi_message_id IS NULL OR edi_message_id = '''', :msgid, CONCAT(edi_message_id, '', '', :msgid)), ' +
              'edi_enviado_at = CURRENT_TIMESTAMP WHERE id = :id';
            dmgMain.qryExec.ParamByName('msgid').AsString := LMsgId;
          end
          else
          begin
            dmgMain.qryExec.SQL.Text := 
              'UPDATE ge_albaranes SET edi_estado_envio = ''ENVIADO'', edi_enviado_at = CURRENT_TIMESTAMP WHERE id = :id';
          end;
          dmgMain.qryExec.ParamByName('id').AsString := AlbaranId;
          dmgMain.qryExec.ExecSQL;
        end
        else
        begin
          dmgMain.qryExec.Close;
          if LMsgId <> '' then
          begin
            dmgMain.qryExec.SQL.Text := 
              'UPDATE ge_albaranes SET edi_estado_envio = ''ERROR'', ' +
              'edi_message_id = IF(edi_message_id IS NULL OR edi_message_id = '''', :msgid, CONCAT(edi_message_id, '', '', :msgid)), ' +
              'edi_error_descripcion = :err WHERE id = :id';
            dmgMain.qryExec.ParamByName('msgid').AsString := LMsgId;
          end
          else
          begin
            dmgMain.qryExec.SQL.Text := 
              'UPDATE ge_albaranes SET edi_estado_envio = ''ERROR'', edi_error_descripcion = :err WHERE id = :id';
          end;
          dmgMain.qryExec.ParamByName('err').AsString := Copy(AResponseMsg, 1, 500);
          dmgMain.qryExec.ParamByName('id').AsString := AlbaranId;
          dmgMain.qryExec.ExecSQL;
        end;
      except
        on E: Exception do
        begin
          AResponseMsg := Format('[Excepción %s]: %s', [E.ClassName, E.Message]);
          LLog.Add('Excepción: ' + E.Message);
          dmgMain.qryExec.Close;
          dmgMain.qryExec.SQL.Text := 'UPDATE ge_albaranes SET edi_estado_envio = ''ERROR'', edi_error_descripcion = :err WHERE id = :id';
          dmgMain.qryExec.ParamByName('err').AsString := Copy(AResponseMsg, 1, 500);
          dmgMain.qryExec.ParamByName('id').AsString := AlbaranId;
          dmgMain.qryExec.ExecSQL;
        end;
      end;
    finally
      XmlStream.Free;
      FormData.Free;
      HTTP.Free;
    end;
    LLog.Add('================================================================');
    WriteAppLog(LLog.Text, LRawBody, AXmlContent);
  finally
    LLog.Free;
  end;
end;

function TEdicomWsClient.ConsultarPedidosPendientes(out AResponseMsg: string): string;
var
  HTTP: THTTPClient;
  URL, LSchemaName, LBody, LCombinedBody: string;
  Response: IHTTPResponse;
  LLog: TStringList;
  LRespStream: TMemoryStream;
  LRespBytes: TBytes;
  I: Integer;
  LCfg: TEdicomEndpointConfig;
  LResultMsgs: TStringList;
  LHasSuccess: Boolean;
begin
  Result := '';
  AResponseMsg := '';
  LCombinedBody := '';
  LHasSuccess := False;
  LResultMsgs := TStringList.Create;

  LLog := TStringList.Create;
  try
    LLog.Add('================================================================');
    LLog.Add('  EDICOM iPaaS — Diagnóstico de Petición SUBSCRIBE ORDERS');
    LLog.Add('  (Consultando ' + IntToStr(Length(FConfigs)) + ' código(s) / datasource(s) secuencialmente)');
    LLog.Add('================================================================');
    LLog.Add('Fecha/Hora: ' + FormatDateTime('yyyy-mm-dd hh:nn:ss', Now));
    LLog.Add('');

    for I := 0 to Length(FConfigs) - 1 do
    begin
      LCfg := FConfigs[I];

      LSchemaName := 'IN_XML_INTEGRATION_LAYOUT_ORDER_GLOBAL_' + LCfg.DomainId;
      URL := Format('%s/domain/%s/application/%s/broker/subscription?domain=%s&application=%s&schema=%s&destination=%s',
        [LCfg.BaseUrl, LCfg.DomainId, LCfg.ApplicationId, LCfg.DomainId, LCfg.ApplicationId, LSchemaName, LCfg.DomainId]);

      LLog.Add(Format('------------------ INTENTO [%d/%d] - Datasource: %s ------------------', [I + 1, Length(FConfigs), LCfg.Datasource]));
      LLog.Add('Método: GET');
      LLog.Add('URL: ' + URL);
      LLog.Add('X-Eipaas-Ds: ' + LCfg.Datasource);
      LLog.Add('Authorization: Bearer ' + LCfg.BearerToken);
      LLog.Add('');

      HTTP := THTTPClient.Create;
      LRespStream := TMemoryStream.Create;
      try
        HTTP.CustomHeaders['X-Eipaas-Ds'] := LCfg.Datasource;
        HTTP.CustomHeaders['Content-Type'] := 'application/json';
        HTTP.ContentType := 'application/json';
        HTTP.Accept := '*/*';
        if LCfg.BearerToken <> '' then
          HTTP.CustomHeaders['Authorization'] := 'Bearer ' + LCfg.BearerToken;

        try
          Response := HTTP.Get(URL, LRespStream);

          LLog.Add('Status Respuesta: ' + IntToStr(Response.StatusCode) + ' ' + Response.StatusText);
          LLog.Add('Content-Type: ' + Response.MimeType);
          LLog.Add('Stream Size: ' + IntToStr(LRespStream.Size) + ' bytes');

          if LRespStream.Size > 0 then
          begin
            LRespStream.Position := 0;
            SetLength(LRespBytes, LRespStream.Size);
            LRespStream.ReadBuffer(LRespBytes[0], LRespStream.Size);
            LBody := TEncoding.UTF8.GetString(LRespBytes);
          end
          else
            LBody := '';

          LLog.Add('Body (longitud: ' + IntToStr(Length(LBody)) + ' chars):');
          if Length(LBody) > 0 then
            LLog.Add(LBody)
          else
            LLog.Add('  (vacío)');
          LLog.Add('');

          if (Response.StatusCode >= 200) and (Response.StatusCode < 300) then
          begin
            if Trim(LBody) <> '' then
            begin
              LHasSuccess := True;
              if LCombinedBody <> '' then
                LCombinedBody := LCombinedBody + sLineBreak + sLineBreak + LBody
              else
                LCombinedBody := LBody;

              LResultMsgs.Add(Format('[%s]: 200 OK con contenido (%d bytes)', [LCfg.Datasource, Length(LBody)]));
            end
            else
            begin
              LResultMsgs.Add(Format('[%s]: 200 OK (sin pedidos pendientes)', [LCfg.Datasource]));
            end;
          end
          else
          begin
            LResultMsgs.Add(Format('[%s]: HTTP %d %s', [LCfg.Datasource, Response.StatusCode, Response.StatusText]));
          end;
        except
          on E: Exception do
          begin
            LLog.Add('Excepción: ' + E.Message);
            LResultMsgs.Add(Format('[%s]: Excepción %s', [LCfg.Datasource, E.Message]));
          end;
        end;
      finally
        LRespStream.Free;
        HTTP.Free;
      end;
    end;

    LLog.Add('======================== RESUMEN ========================');
    LLog.Add(LResultMsgs.Text);
    LLog.Add('================================================================');
    WriteAppLog(LLog.Text, LCombinedBody, '');

    if LHasSuccess or (Trim(LCombinedBody) <> '') then
    begin
      Result := LCombinedBody;
      AResponseMsg := 'Descarga completada:' + sLineBreak + LResultMsgs.Text;
    end
    else
    begin
      Result := '';
      AResponseMsg := LResultMsgs.Text;
    end;
  finally
    LResultMsgs.Free;
    LLog.Free;
  end;
end;

function TEdicomWsClient.ConfirmarRecepcionPedidos(const AMessageIdsJson: string; out AResponseMsg: string): Boolean;
var
  HTTP: THTTPClient;
  URL, LRawBody: string;
  Response: IHTTPResponse;
  JsonStream: TStringStream;
  LLog: TStringList;
  I: Integer;
  LCfg: TEdicomEndpointConfig;
  LAnySuccess: Boolean;
begin
  Result := False;
  AResponseMsg := '';
  LAnySuccess := False;

  LLog := TStringList.Create;
  try
    LLog.Add('');
    LLog.Add('================================================================');
    LLog.Add('  EDICOM iPaaS — Confirmación de Recepción (CONFIRM ORDERS)');
    LLog.Add('================================================================');
    LLog.Add('Fecha/Hora: ' + FormatDateTime('yyyy-mm-dd hh:nn:ss', Now));
    LLog.Add('Payload JSON enviado: ' + AMessageIdsJson);
    LLog.Add('');

    for I := 0 to Length(FConfigs) - 1 do
    begin
      LCfg := FConfigs[I];
      URL := Format('%s/domain/%s/application/%s/broker/subscription/confirm', [LCfg.BaseUrl, LCfg.DomainId, LCfg.ApplicationId]);

      HTTP := THTTPClient.Create;
      JsonStream := TStringStream.Create(AMessageIdsJson, TEncoding.UTF8);
      try
        HTTP.CustomHeaders['X-Eipaas-Ds'] := LCfg.Datasource;
        HTTP.CustomHeaders['Content-Type'] := 'application/json';
        HTTP.ContentType := 'application/json';
        HTTP.Accept := 'application/json, */*';
        if LCfg.BearerToken <> '' then
          HTTP.CustomHeaders['Authorization'] := 'Bearer ' + LCfg.BearerToken;

        try
          Response := HTTP.Post(URL, JsonStream);
          if Assigned(Response) then
          begin
            LRawBody := Response.ContentAsString(TEncoding.UTF8);
            LLog.Add(Format('[%s] Status: %d %s - Body: %s', [LCfg.Datasource, Response.StatusCode, Response.StatusText, LRawBody]));

            if (Response.StatusCode >= 200) and (Response.StatusCode < 300) then
            begin
              LAnySuccess := True;
              AResponseMsg := Format('HTTP %d %s - %s', [Response.StatusCode, Response.StatusText, LRawBody]);
            end;
          end;
        except
          on E: Exception do
            LLog.Add(Format('[%s] Excepción: %s', [LCfg.Datasource, E.Message]));
        end;
      finally
        JsonStream.Free;
        HTTP.Free;
      end;
    end;

    Result := LAnySuccess;
    LLog.Add('================================================================');
    WriteAppLog(LLog.Text, '', AMessageIdsJson);
  finally
    LLog.Free;
  end;
end;

class function TEdicomWsClient.HasEdicomConfig(const AEmpresaId: string; AConn: TFDConnection = nil): Boolean;
var
  Qry: TFDQuery;
  LConn: TFDConnection;
begin
  Result := False;
  if Trim(AEmpresaId) = '' then Exit;

  LConn := AConn;
  if LConn = nil then
    LConn := dmgMain.dbConn;

  if (LConn = nil) or not LConn.Connected then Exit;

  Qry := TFDQuery.Create(nil);
  try
    Qry.Connection := LConn;
    Qry.SQL.Text := 'SELECT id FROM ge_edicom_config WHERE id_empresa = :emp AND activo = 1 LIMIT 1';
    Qry.ParamByName('emp').AsString := AEmpresaId;
    Qry.Open;
    Result := not Qry.IsEmpty;
  except
    Result := False;
  end;
  Qry.Free;
end;

class function TEdicomWsClient.DownloadAndImportPendingOrders(AConn: TFDConnection; const AEmpresaId: string; out ADownloadedCount: Integer; out AResponseOrErrorMsg: string): Boolean;
var
  LClient: TEdicomWsClient;
  LXmlContent, LErrorMsg, LConfirmMsg, LMsgIdJson, LLastResponseMsg: string;
  LMessageIds: TArray<string>;
  I, LBatchCount, LLoopCount: Integer;
begin
  ADownloadedCount := 0;
  AResponseOrErrorMsg := '';
  Result := False;

  LClient := TEdicomWsClient.Create(AConn, AEmpresaId);
  try
    LLoopCount := 0;
    while LLoopCount < 50 do
    begin
      Inc(LLoopCount);
      LLastResponseMsg := '';
      LXmlContent := LClient.ConsultarPedidosPendientes(LLastResponseMsg);
      if Trim(LXmlContent) = '' then
      begin
        // Cola vacía o sin más pedidos pendientes en este ciclo
        Break;
      end;

      LMessageIds := nil;
      LErrorMsg := '';
      LBatchCount := TEdicomProcessor.ImportOrdersFromXmlContent(LXmlContent, StrToIntDef(AEmpresaId, 1), LErrorMsg, LMessageIds);
      if LBatchCount > 0 then
      begin
        Inc(ADownloadedCount, LBatchCount);
        // Confirmar recepción ante EDICOM iPaaS si se obtuvieron message IDs
        if Length(LMessageIds) > 0 then
        begin
          LMsgIdJson := '[';
          for I := 0 to Length(LMessageIds) - 1 do
          begin
            if I > 0 then LMsgIdJson := LMsgIdJson + ',';
            LMsgIdJson := LMsgIdJson + '"' + LMessageIds[I] + '"';
          end;
          LMsgIdJson := LMsgIdJson + ']';
          LClient.ConfirmarRecepcionPedidos(LMsgIdJson, LConfirmMsg);
        end;
        Result := True;
      end
      else if LErrorMsg <> '' then
      begin
        // Error de parseo o BD en este lote; salir para evitar bucle infinito
        AResponseOrErrorMsg := LErrorMsg;
        Break;
      end
      else
      begin
        // Conectó pero el XML no contenía pedidos nuevos. Confirmar messageIds si los hubo para desatascar la cola
        if Length(LMessageIds) > 0 then
        begin
          LMsgIdJson := '[';
          for I := 0 to Length(LMessageIds) - 1 do
          begin
            if I > 0 then LMsgIdJson := LMsgIdJson + ',';
            LMsgIdJson := LMsgIdJson + '"' + LMessageIds[I] + '"';
          end;
          LMsgIdJson := LMsgIdJson + ']';
          LClient.ConfirmarRecepcionPedidos(LMsgIdJson, LConfirmMsg);
        end;
        Break;
      end;
    end;

    if (ADownloadedCount > 0) or (AResponseOrErrorMsg = '') then
      Result := True;

    if (ADownloadedCount = 0) and (AResponseOrErrorMsg = '') then
      AResponseOrErrorMsg := LLastResponseMsg;
  finally
    LClient.Free;
  end;
end;

end.
