unit frm_EnviosRecogida;

{
  frm_EnviosRecogida.pas
  Formulario modal: Solicitar Recogida a empresa de transporte desde un Pedido.

  Flujo completo (genérico — funciona con cualquier transportista configurado):
    1. Carga el pedido y los transportistas configurados para la empresa activa.
    2. El usuario selecciona el transportista deseado (cbTransportista).
    3. Se carga la configuración de ese transportista desde ge_envios_config.
    4. Verifica stock disponible en ge_stock_envasado por artículo (FIFO).
    5. El usuario configura fecha/hora de recogida, bultos, contacto, servicio.
    6. Al confirmar:
       a) TEnviosClientFactory.Create → IEnviosClient según transportista
       b) Client.Login → obtiene sesión
       c) Client.GrabaRecogida → Nº recogida
       d) Genera Albarán de Venta (ge_albaranes + ge_albaranes_lineas)
       e) Descuenta stock FIFO (ge_stock_envasado)
       f) Guarda en ge_envios_recogidas (config_id = transportista elegido)
       g) Actualiza pedido estado = 'EN PREPARACION'
       h) Genera Documento de Carga en pantalla/impresora
}

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
  System.Classes, System.UITypes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs,
  Vcl.StdCtrls, Vcl.ExtCtrls, Vcl.Grids, Vcl.ComCtrls, Vcl.Buttons,
  Vcl.Samples.Spin,
  Data.DB, FireDAC.Comp.Client, FireDAC.Stan.Param, FireDAC.Stan.Intf,
  FireDAC.Stan.Option, FireDAC.Stan.Error, FireDAC.DatS, FireDAC.Phys.Intf,
  FireDAC.DApt.Intf, FireDAC.DApt, FireDAC.Comp.DataSet, uAppTheme,
  uEnviosTypes;

type
  TLineaStockEnv = record
    IdArticulo  : Integer;
    Descripcion : string;
    CantPedida  : Double;
    StockTotal  : Double;
    StockOK     : Boolean;
  end;

  TfrmEnviosRecogida = class(TForm)
    // ── Cabecera ──────────────────────────────────────────────────────────────
    pnlHeader: TPanel;
    lblTitulo: TLabel;
    lblPedidoNum: TLabel;
    edtPedidoNum: TEdit;
    lblCliente: TLabel;
    edtCliente: TEdit;
    lblFechaPedido: TLabel;
    edtFechaPedido: TEdit;

    // ── Selector de transportista ─────────────────────────────────────────────
    pnlTransportista: TPanel;
    lblTransportista: TLabel;
    cbTransportista: TComboBox;
    lblTranspInfo: TLabel;

    // ── Verificación de Stock ─────────────────────────────────────────────────
    pnlStock: TPanel;
    lblStockTitle: TLabel;
    sgStock: TStringGrid;
    btnVerificarStock: TButton;
    lblStockStatus: TLabel;

    // ── Datos de la Recogida ──────────────────────────────────────────────────
    pnlRecogida: TPanel;
    lblRecogidaTitle: TLabel;
    lblFechaRec: TLabel;
    dtpFechaRec: TDateTimePicker;
    lblHoraIni: TLabel;
    dtpHoraIni: TDateTimePicker;
    lblHoraFin: TLabel;
    dtpHoraFin: TDateTimePicker;
    lblBultos: TLabel;
    spnBultos: TSpinEdit;
    lblPeso: TLabel;
    edtPeso: TEdit;
    lblTipoServ: TLabel;
    cbTipoServ: TComboBox;
    lblConductor: TLabel;
    edtConductor: TEdit;
    lblMatricula: TLabel;
    edtMatricula: TEdit;
    lblContacto: TLabel;
    edtContacto: TEdit;
    lblObs: TLabel;
    memObs: TMemo;

    // ── Dirección de recogida ─────────────────────────────────────────────────
    pnlDirRecogida: TPanel;
    lblDirRecTitle: TLabel;
    lblNomRec: TLabel;
    edtNomRec: TEdit;
    lblDirRec: TLabel;
    edtDirRec: TEdit;
    lblNumRec: TLabel;
    edtNumRec: TEdit;
    lblCPRec: TLabel;
    edtCPRec: TEdit;
    edtTlfRec: TEdit;

    // ── Dirección de envío ──────────────────────────────────────────────────
    pnlDirEnvio: TPanel;
    lblDirEnvTitle: TLabel;
    lblNomEnv: TLabel;
    edtNomEnv: TEdit;
    lblDirEnv: TLabel;
    edtDirEnv: TEdit;
    lblNumEnv: TLabel;
    edtNumEnv: TEdit;
    lblCPEnv: TLabel;
    edtCPEnv: TEdit;
    lblPobEnv: TLabel;
    edtPobEnv: TEdit;
    lblTlfEnv: TLabel;
    edtTlfEnv: TEdit;

    // ── Botones ───────────────────────────────────────────────────────────────
    pnlButtons: TPanel;
    btnConfirmar: TButton;
    btnCancelar: TButton;
    lblInfoResult: TLabel;

    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure cbTransportistaChange(Sender: TObject);
    procedure btnVerificarStockClick(Sender: TObject);
    procedure btnConfirmarClick(Sender: TObject);
    procedure btnCancelarClick(Sender: TObject);

  private
    FPedidoId        : Integer;
    FEmpresaId       : Integer;
    FClienteId       : Integer;
    FDestinoId       : Integer;
    FConfigActual    : TEnviosConfig;   // Config del transportista elegido
    FRecogidaId      : Integer;
    FAlbaranId       : Integer;
    FAlbaranCodigo   : string;
    FStockVerificado : Boolean;
    FLineasStock     : TArray<TLineaStockEnv>;
    FConfigIds       : TArray<Integer>; // Para mapear cbTransportista.ItemIndex → config_id

    procedure LoadPedidoData;
    procedure CargarTransportistas;
    procedure AplicarConfigTransportista(AConfigId: Integer);
    function  VerificarStock: Boolean;
    function  GenerarAlbaran: Integer;
    function  GenerarTextoDocumentoCarga(const ACodRecogida: string): string;
    procedure MostrarDocumentoCarga(const ATexto, ACodRecogida: string);
    procedure PopularServicioCombo;
  public
    property PedidoId  : Integer read FPedidoId   write FPedidoId;
    property RecogidaId: Integer read FRecogidaId;
    property AlbaranId : Integer read FAlbaranId;
  end;

var
  frmEnviosRecogida: TfrmEnviosRecogida;

implementation

uses
  System.Math, System.StrUtils, Winapi.ShellAPI,
  dmg_Main, uDbErrorHandler, uEnviosClient, uCalidadHitosService, frm_SelectImpresoraEnvio,
  uPdfRotateService;

{$R *.dfm}

procedure TfrmEnviosRecogida.FormCreate(Sender: TObject);
begin
  FPedidoId       := 0; FEmpresaId := 0;
  FClienteId      := 0; FDestinoId := 0;
  FRecogidaId     := 0; FAlbaranId := 0;
  FStockVerificado := False;
  SetLength(FLineasStock, 0);
  SetLength(FConfigIds, 0);
end;

procedure TfrmEnviosRecogida.FormDestroy(Sender: TObject);
begin
  if frmEnviosRecogida = Self then
    frmEnviosRecogida := nil;
end;

procedure TfrmEnviosRecogida.PopularServicioCombo;
var
  Qry: TFDQuery;
begin
  cbTipoServ.Items.Clear;
  if FConfigActual.TransportistaId = 0 then Exit;

  Qry := TFDQuery.Create(nil);
  try
    Qry.Connection := dmgMain.dbConn;
    Qry.SQL.Text :=
      'SELECT codigo, descripcion ' +
      'FROM ge_envios_servicios ' +
      'WHERE transportista_id = :transp AND activo = 1 ' +
      'ORDER BY codigo';
    Qry.ParamByName('transp').AsInteger := FConfigActual.TransportistaId;
    Qry.Open;
    while not Qry.Eof do
    begin
      cbTipoServ.Items.Add(Qry.FieldByName('codigo').AsString + ' - ' + Qry.FieldByName('descripcion').AsString);
      Qry.Next;
    end;
  finally
    Qry.Free;
  end;

  if cbTipoServ.Items.Count > 0 then
    cbTipoServ.ItemIndex := 0;
end;

procedure TfrmEnviosRecogida.FormShow(Sender: TObject);
begin
  TAppTheme.ApplyToForm(Self);
  PopularServicioCombo;

  dtpFechaRec.Date := Date + 1;
  dtpHoraIni.Time  := EncodeTime(9,  0, 0, 0);
  dtpHoraFin.Time  := EncodeTime(18, 0, 0, 0);
  spnBultos.Value  := 1;

  btnConfirmar.Enabled := False;
  lblStockStatus.Caption  := '';
  lblInfoResult.Caption   := '';
  lblTranspInfo.Caption   := '';

  // Configurar grid de stock
  sgStock.ColCount := 4;
  sgStock.RowCount := 2;
  sgStock.FixedRows := 1;
  sgStock.FixedCols := 0;
  sgStock.Cells[0,0] := 'Artículo';
  sgStock.Cells[1,0] := 'Cant. Pedida';
  sgStock.Cells[2,0] := 'Stock Disponible';
  sgStock.Cells[3,0] := 'Estado';
  sgStock.ColWidths[0] := 280; sgStock.ColWidths[1] := 100;
  sgStock.ColWidths[2] := 130; sgStock.ColWidths[3] := 90;

  if FPedidoId > 0 then
  begin
    LoadPedidoData;
    CargarTransportistas;
    if cbTransportista.Items.Count > 0 then
    begin
      cbTransportista.ItemIndex := 0;
      cbTransportistaChange(cbTransportista);
    end;
  end;
end;

// ---------------------------------------------------------------------------
// Carga la lista de transportistas activos configurados para la empresa
// ---------------------------------------------------------------------------
procedure TfrmEnviosRecogida.CargarTransportistas;
var
  Qry: TFDQuery;
  i  : Integer;
begin
  cbTransportista.Items.Clear;
  SetLength(FConfigIds, 0);
  i := 0;

  Qry := TFDQuery.Create(nil);
  try
    Qry.Connection := dmgMain.dbConn;
    Qry.SQL.Text :=
      'SELECT ec.id, t.nombre ' +
      'FROM ge_envios_config ec ' +
      'JOIN ge_envios_transportistas t ON t.id = ec.transportista_id ' +
      'WHERE ec.empresa_id = :emp AND ec.activo = 1 ' +
      'ORDER BY t.nombre';
    Qry.ParamByName('emp').AsInteger := FEmpresaId;
    Qry.Open;
    while not Qry.Eof do
    begin
      cbTransportista.Items.Add(Qry.FieldByName('nombre').AsString);
      SetLength(FConfigIds, i + 1);
      FConfigIds[i] := Qry.FieldByName('id').AsInteger;
      Inc(i);
      Qry.Next;
    end;
  finally
    Qry.Free;
  end;

  if cbTransportista.Items.Count = 0 then
  begin
    cbTransportista.Items.Add('— Sin transportistas configurados —');
    lblTranspInfo.Caption := '❌ Configure transportistas en Empresas → Config. Envíos';
    lblTranspInfo.Font.Color := clRed;
  end;
end;

// ---------------------------------------------------------------------------
// Cambio de transportista — recarga la configuración
// ---------------------------------------------------------------------------
procedure TfrmEnviosRecogida.cbTransportistaChange(Sender: TObject);
var
  LIdx: Integer;
begin
  LIdx := cbTransportista.ItemIndex;
  if (LIdx < 0) or (LIdx >= Length(FConfigIds)) then Exit;

  AplicarConfigTransportista(FConfigIds[LIdx]);
  btnVerificarStockClick(nil); // Comprobación automática de stock al cambiar de transportista
end;

procedure TfrmEnviosRecogida.AplicarConfigTransportista(AConfigId: Integer);
var
  LServCode: string;
  i: Integer;
begin
  if not CargarEnviosConfig(AConfigId, FConfigActual) then
  begin
    lblTranspInfo.Caption := '❌ No se pudo cargar la configuración del transportista.';
    lblTranspInfo.Font.Color := clRed;
    btnConfirmar.Enabled := False;
    Exit;
  end;

  // Mostrar info del entorno
  if FConfigActual.Entorno then
    lblTranspInfo.Caption := '⚠ Entorno de VALIDACIÓN (pruebas)'
  else
    lblTranspInfo.Caption := '✅ Entorno de Producción';
  lblTranspInfo.Font.Color := clGray;

  // Repopular combo de servicios según el transportista actual
  PopularServicioCombo;

  // Preseleccionar el servicio por defecto de este transportista
  LServCode := FConfigActual.ServDefecto;
  for i := 0 to cbTipoServ.Items.Count - 1 do
    if cbTipoServ.Items[i].StartsWith(LServCode) then
    begin
      cbTipoServ.ItemIndex := i;
      Break;
    end;

  // Rellenar dirección de recogida (origen) con el remitente/almacén de enviosconfig
  edtNomRec.Text := FConfigActual.NomRemitente;
  edtDirRec.Text := FConfigActual.DirRemitente;
  edtCPRec.Text  := FConfigActual.CPRemitente;
  edtTlfRec.Text := FConfigActual.TlfRemitente;
  edtNumRec.Text := '';

  // Resetear verificación de stock al cambiar transportista
  FStockVerificado := False;
  btnConfirmar.Enabled := False;
  lblStockStatus.Caption := '';
end;

// ---------------------------------------------------------------------------
procedure TfrmEnviosRecogida.LoadPedidoData;
var
  Qry: TFDQuery;
  LEstado: string;
begin
  Qry := TFDQuery.Create(nil);
  try
    Qry.Connection := dmgMain.dbConn;
    Qry.SQL.Text :=
      'SELECT p.numero_pedido, p.empresa_id, p.cliente_id, p.clientedestino_id, ' +
      '       p.fecha_pedido, p.estado, c.nombre_fiscal, c.telefono, ' +
      '       d.descripcion AS dest_desc, d.direccion, d.cp, d.poblacion, d.provincia ' +
      'FROM ge_pedidos p ' +
      'LEFT JOIN ge_clientes c ON c.id = p.cliente_id ' +
      'LEFT JOIN ge_clientes_destinos d ON d.id = p.clientedestino_id ' +
      'WHERE p.id = :id';
    Qry.ParamByName('id').AsInteger := FPedidoId;
    Qry.Open;
    if Qry.IsEmpty then Exit;

    FEmpresaId := Qry.FieldByName('empresa_id').AsInteger;
    FClienteId := Qry.FieldByName('cliente_id').AsInteger;
    FDestinoId := Qry.FieldByName('clientedestino_id').AsInteger;
    LEstado    := UpperCase(Trim(Qry.FieldByName('estado').AsString));

    edtPedidoNum.Text   := Qry.FieldByName('numero_pedido').AsString;
    edtCliente.Text     := Qry.FieldByName('nombre_fiscal').AsString;
    edtFechaPedido.Text := FormatDateTime('dd/mm/yyyy', Qry.FieldByName('fecha_pedido').AsDateTime);

    // Rellenar dirección de entrega del cliente (Destinatario)
    edtNomEnv.Text   := Qry.FieldByName('nombre_fiscal').AsString;
    edtDirEnv.Text   := Qry.FieldByName('direccion').AsString;
    edtNumEnv.Text   := '';
    edtCPEnv.Text    := Qry.FieldByName('cp').AsString;
    edtPobEnv.Text   := Qry.FieldByName('poblacion').AsString;
    edtTlfEnv.Text   := Qry.FieldByName('telefono').AsString;
    edtContacto.Text := Qry.FieldByName('nombre_fiscal').AsString;

    // Buscar si ya existe albarán previo
    Qry.Close;
    Qry.SQL.Text := 'SELECT id, serie, numero FROM ge_albaranes WHERE id_pedido = :id AND (anulado = 0 OR anulado IS NULL) LIMIT 1';
    Qry.ParamByName('id').AsInteger := FPedidoId;
    Qry.Open;
    if not Qry.IsEmpty then
    begin
      FAlbaranId := Qry.FieldByName('id').AsInteger;
      FAlbaranCodigo := Trim(Qry.FieldByName('serie').AsString) + '/' + IntToStr(Qry.FieldByName('numero').AsInteger);
    end;

    // Obtener la suma de los bultos reales del pedido / albarán
    Qry.Close;
    if FAlbaranId > 0 then
    begin
      Qry.SQL.Text := 'SELECT COUNT(*) AS total_bultos FROM ge_albaranes_bultos WHERE albaran_id = :alb AND (nivel = 2 OR (nivel = 3 AND bulto_padre_id IS NULL))';
      Qry.ParamByName('alb').AsInteger := FAlbaranId;
      Qry.Open;
    end;
    
    if (FAlbaranId > 0) and (not Qry.IsEmpty) and (Qry.FieldByName('total_bultos').AsInteger > 0) then
      spnBultos.Value := Qry.FieldByName('total_bultos').AsInteger
    else
    begin
      Qry.Close;
      Qry.SQL.Text := 'SELECT COALESCE(SUM(bultos), 0) AS total_bultos FROM ge_pedidos_lineas WHERE id_pedido = :id';
      Qry.ParamByName('id').AsInteger := FPedidoId;
      Qry.Open;
      if not Qry.IsEmpty and (Qry.FieldByName('total_bultos').AsInteger > 0) then
        spnBultos.Value := Qry.FieldByName('total_bultos').AsInteger
      else
        spnBultos.Value := 1;
    end;

    // Calcular el peso total del pedido basado en unidades * peso de artículo
    Qry.Close;
    Qry.SQL.Text := 
      'SELECT COALESCE(SUM(pl.cantidad * COALESCE(a.peso, 0)), 0) AS total_peso ' +
      'FROM ge_pedidos_lineas pl ' +
      'JOIN ge_articulos a ON pl.id_articulo = a.id ' +
      'WHERE pl.id_pedido = :id';
    Qry.ParamByName('id').AsInteger := FPedidoId;
    Qry.Open;
    if not Qry.IsEmpty and (Qry.FieldByName('total_peso').AsFloat > 0) then
      edtPeso.Text := FormatFloat('0.000', Qry.FieldByName('total_peso').AsFloat)
    else
      edtPeso.Text := '1.000';
  finally
    Qry.Free;
  end;
end;

// ---------------------------------------------------------------------------
function TfrmEnviosRecogida.VerificarStock: Boolean;
var
  Qry  : TFDQuery;
  i, LRow: Integer;
  LLine: TLineaStockEnv;
  LPedEstado: string;
begin
  SetLength(FLineasStock, 0);
  Qry := TFDQuery.Create(nil);
  try
    Qry.Connection := dmgMain.dbConn;
    
    // Comprobar estado del pedido
    Qry.SQL.Text := 'SELECT estado FROM ge_pedidos WHERE id = :id';
    Qry.ParamByName('id').AsInteger := FPedidoId;
    Qry.Open;
    LPedEstado := '';
    if not Qry.IsEmpty then
      LPedEstado := UpperCase(Trim(Qry.FieldByName('estado').AsString));
    Qry.Close;

    // Si el pedido ya está en estado PREPARADO, el stock ya fue verificado y preparado
    if LPedEstado = 'PREPARADO' then
    begin
      Qry.SQL.Text :=
        'SELECT pl.id_articulo, pl.descripcion, pl.cantidad ' +
        'FROM ge_pedidos_lineas pl ' +
        'WHERE pl.id_pedido = :id ' +
        'ORDER BY pl.id ASC';
      Qry.ParamByName('id').AsInteger := FPedidoId;
      Qry.Open;

      LRow := 1;
      sgStock.RowCount := Max(2, Qry.RecordCount + 1);
      i := 0;
      while not Qry.Eof do
      begin
        LLine.IdArticulo  := Qry.FieldByName('id_articulo').AsInteger;
        LLine.Descripcion := Qry.FieldByName('descripcion').AsString;
        LLine.CantPedida  := Qry.FieldByName('cantidad').AsFloat;
        LLine.StockTotal  := LLine.CantPedida;
        LLine.StockOK     := True;
        SetLength(FLineasStock, i + 1);
        FLineasStock[i] := LLine;

        sgStock.Cells[0,LRow] := LLine.Descripcion;
        sgStock.Cells[1,LRow] := FormatFloat('#,##0.##', LLine.CantPedida);
        sgStock.Cells[2,LRow] := FormatFloat('#,##0.##', LLine.StockTotal);
        sgStock.Cells[3,LRow] := ' PREPARADO';
        Inc(i); Inc(LRow);
        Qry.Next;
      end;
      Result := True;
      FStockVerificado := True;
      Exit;
    end;

    Qry.SQL.Text :=
      'SELECT pl.id_articulo, pl.descripcion, pl.cantidad, ' +
      '       COALESCE(SUM(GREATEST(0, se.cantidad_unidades - COALESCE(se.cantidad_reservada, 0))), 0) AS stock_total ' +
      'FROM ge_pedidos_lineas pl ' +
      'LEFT JOIN ge_articulos a ON a.id = pl.id_articulo ' +
      'LEFT JOIN ge_stock_envasado se ON se.id_articulo = COALESCE(NULLIF(a.id_maestro, 0), pl.id_articulo) ' +
      'WHERE pl.id_pedido = :id ' +
      'GROUP BY pl.id_articulo, pl.descripcion, pl.cantidad';
    Qry.ParamByName('id').AsInteger := FPedidoId;
    Qry.Open;

    LRow := 1;
    sgStock.RowCount := Max(2, Qry.RecordCount + 1);
    i := 0;
    while not Qry.Eof do
    begin
      LLine.IdArticulo  := Qry.FieldByName('id_articulo').AsInteger;
      LLine.Descripcion := Qry.FieldByName('descripcion').AsString;
      LLine.CantPedida  := Qry.FieldByName('cantidad').AsFloat;
      LLine.StockTotal  := Qry.FieldByName('stock_total').AsFloat;
      LLine.StockOK     := LLine.StockTotal >= LLine.CantPedida;
      SetLength(FLineasStock, i + 1);
      FLineasStock[i] := LLine;
      sgStock.Cells[0,LRow] := LLine.Descripcion;
      sgStock.Cells[1,LRow] := FormatFloat('#,##0.##', LLine.CantPedida);
      sgStock.Cells[2,LRow] := FormatFloat('#,##0.##', LLine.StockTotal);
      if LLine.StockOK then
        sgStock.Cells[3,LRow] :=' OK'
      else
        sgStock.Cells[3,LRow] :=' FALTA';
      Inc(i); Inc(LRow);
      Qry.Next;
    end;
    Result := True;
    for i := 0 to Length(FLineasStock) - 1 do
      if not FLineasStock[i].StockOK then begin Result := False; Break; end;
    FStockVerificado := True;
  finally
    Qry.Free;
  end;
end;

procedure TfrmEnviosRecogida.btnVerificarStockClick(Sender: TObject);
begin
  if cbTransportista.ItemIndex < 0 then begin ShowMessage('Seleccione un transportista primero.'); Exit; end;
  lblStockStatus.Caption := '⏳ Verificando stock...';
  lblStockStatus.Font.Color := clGray;
  Application.ProcessMessages;
  try
    if VerificarStock then
    begin
      lblStockStatus.Caption := '✅ Stock disponible para todos los artículos.';
      lblStockStatus.Font.Color := clGreen;
      btnConfirmar.Enabled := True;
    end
    else
    begin
      lblStockStatus.Caption := '❌ Stock insuficiente en algún artículo.';
      lblStockStatus.Font.Color := clRed;
      btnConfirmar.Enabled := False;
    end;
  except
    on E: Exception do begin lblStockStatus.Caption := '❌ Error: ' + E.Message; lblStockStatus.Font.Color := clRed; end;
  end;
end;

// ---------------------------------------------------------------------------
function TfrmEnviosRecogida.GenerarAlbaran: Integer;
var
  Qry, QryLines, QryStock: TFDQuery;
  LSerie: string;
  LNumero: Integer;
  LAlbId: Integer;
  LPosicion: Integer;
  LArtId, LArtStockId: Integer;
  LDesc: string;
  LPrecio: Double;
  LTipoIva: Double;
  LCantTotalReq: Double;
  LCantPendiente: Double;
  LBultosReq, LCantBulto, LBultosLinea: Double;
  LStockId: Integer;
  LDisp: Double;
  LTomar: Double;
  LLoteCode: string;
  LFabFecha: TDateTime;
  LDiasVida: Integer;
  LFechaCaducidad: TDateTime;
  LBaseLinea, LTotalLinea, LIvaLinea: Double;
  LTotalBase, LTotalIva, LTotalDoc: Double;
  LHasCaducidad: Boolean;
  LCountLineas: Integer;
  LNeedsHeader: Boolean;
  LHasSeriesRow: Boolean;
begin
  Result := 0;
  LTotalBase := 0;
  LTotalIva := 0;
  LTotalDoc := 0;
  LAlbId := 0;
  LNeedsHeader := True;
  LHasSeriesRow := False;

  Qry := TFDQuery.Create(nil);
  QryLines := TFDQuery.Create(nil);
  QryStock := TFDQuery.Create(nil);
  try
    Qry.Connection := dmgMain.dbConn;
    QryLines.Connection := dmgMain.dbConn;
    QryStock.Connection := dmgMain.dbConn;

    // 0. Comprobar si ya existe un albarán activo creado previamente para este pedido
    Qry.SQL.Text := 'SELECT id, serie, numero FROM ge_albaranes WHERE id_pedido = :ped AND (anulado = 0 OR anulado IS NULL) LIMIT 1';
    Qry.ParamByName('ped').AsInteger := FPedidoId;
    Qry.Open;
    if not Qry.IsEmpty then
    begin
      LAlbId := Qry.FieldByName('id').AsInteger;
      LSerie := Qry.FieldByName('serie').AsString;
      LNumero := Qry.FieldByName('numero').AsInteger;
      Qry.Close;

      // Comprobar si este albarán ya contiene líneas válidas
      Qry.SQL.Text := 'SELECT COUNT(*) AS total_lineas FROM ge_albaranes_lineas WHERE albaran_id = :alb';
      Qry.ParamByName('alb').AsInteger := LAlbId;
      Qry.Open;
      LCountLineas := Qry.FieldByName('total_lineas').AsInteger;
      Qry.Close;

      if LCountLineas > 0 then
      begin
        // El albarán ya existe y tiene líneas completas
        FAlbaranCodigo := Trim(LSerie) + '/' + IntToStr(LNumero);
        Result := LAlbId;
        Exit;
      end
      else
      begin
        // Existe cabecera pero sin líneas: no se necesita crear otra cabecera
        LNeedsHeader := False;
      end;
    end
    else
      Qry.Close;

    // Búsqueda de serie oficial priorizando la por defecto de la empresa
    LSerie := '';
    LNumero := 0;
    if dmgMain.GetSerieYNumeroDocumento(IntToStr(FEmpresaId), 'ALBARAN', Date, '', LSerie, LNumero) and (LSerie <> '') then
    begin
      LHasSeriesRow := True;
    end
    else
    begin
      Qry.SQL.Text :=
        'SELECT serie, sig_numero_albaran AS prox_num ' +
        'FROM ge_series ' +
        'WHERE empresa_id = :emp ' +
        '  AND activo = 1 ' +
        '  AND (es_abono = 0 OR es_abono IS NULL) ' +
        'ORDER BY por_defecto DESC, ' +
        '         (CASE WHEN (:fec >= fecha_inicio OR fecha_inicio IS NULL) AND (:fec <= fecha_fin OR fecha_fin IS NULL) THEN 1 ELSE 0 END) DESC, ' +
        '         serie ASC ' +
        'LIMIT 1';
      Qry.ParamByName('emp').AsInteger := FEmpresaId;
      Qry.ParamByName('fec').AsDate := Date;
      Qry.Open;
      if not Qry.IsEmpty and (Trim(Qry.FieldByName('serie').AsString) <> '') then
      begin
        LSerie  := Trim(Qry.FieldByName('serie').AsString);
        LNumero := Qry.FieldByName('prox_num').AsInteger;
        if LNumero <= 0 then LNumero := 1;
        LHasSeriesRow := True;
      end
      else
      begin
        LSerie  := FormatDateTime('yy', Date);
        LNumero := 1;
      end;
      Qry.Close;
    end;

    if LNeedsHeader then
    begin
      // Insertar cabecera con totales iniciales a 0
      dmgMain.qryExec.Close;
      dmgMain.qryExec.SQL.Text :=
        'INSERT INTO ge_albaranes (empresa_id, cliente_id, clientedestino_id, serie, numero, fecha,' +
        ' base_imponible, iva, total, id_pedido, id_forma_pago, referencia, observaciones1, id_user_creator, id_user_update) ' +
        'VALUES (:emp,:cli,:dest,:ser,:num,NOW(),0,0,0,:ped,' +
        ' (SELECT id_forma_pago FROM ge_clientes WHERE id = :cli),' +
        ' CONCAT(''Pedido '', (SELECT COALESCE(numero_pedido, '''') FROM ge_pedidos WHERE id = :ped)),' +
        ' (SELECT COALESCE(observaciones, '''') FROM ge_pedidos WHERE id = :ped),:user,:user)';
      dmgMain.qryExec.ParamByName('emp').AsInteger  := FEmpresaId;
      dmgMain.qryExec.ParamByName('cli').AsInteger  := FClienteId;
      dmgMain.qryExec.ParamByName('dest').DataType  := ftInteger;
      if FDestinoId > 0 then
        dmgMain.qryExec.ParamByName('dest').AsInteger := FDestinoId
      else
        dmgMain.qryExec.ParamByName('dest').Clear;
      dmgMain.qryExec.ParamByName('ser').AsString   := LSerie;
      dmgMain.qryExec.ParamByName('num').AsInteger  := LNumero;
      dmgMain.qryExec.ParamByName('ped').AsInteger  := FPedidoId;
      dmgMain.qryExec.ParamByName('user').AsString  := dmgMain.CurrentUserId;
      dmgMain.qryExec.ExecSQL;

      dmgMain.qryExec.Close;
      dmgMain.qryExec.SQL.Text := 'SELECT LAST_INSERT_ID() AS id';
      dmgMain.qryExec.Open;
      LAlbId := dmgMain.qryExec.FieldByName('id').AsInteger;
      dmgMain.qryExec.Close;

      if LAlbId <= 0 then Exit;

      if LHasSeriesRow then
      begin
        dmgMain.qryExec.SQL.Text := 'UPDATE ge_series SET sig_numero_albaran = :num + 1 WHERE empresa_id=:emp AND serie=:ser AND activo=1';
        dmgMain.qryExec.ParamByName('num').AsInteger := LNumero;
        dmgMain.qryExec.ParamByName('emp').AsInteger := FEmpresaId;
        dmgMain.qryExec.ParamByName('ser').AsString  := LSerie;
        dmgMain.qryExec.ExecSQL;
      end;
    end
    else
    begin
      // Si el albarán existente tenía serie 'A' u obsoleta, corregirla a la serie oficial
      dmgMain.qryExec.Close;
      dmgMain.qryExec.SQL.Text :=
        'UPDATE ge_albaranes SET serie = :ser, numero = :num, updated_at = NOW(), id_user_update = :user ' +
        'WHERE id = :id AND (serie = ''A'' OR serie IS NULL OR TRIM(serie) = '''')';
      dmgMain.qryExec.ParamByName('ser').AsString  := LSerie;
      dmgMain.qryExec.ParamByName('num').AsInteger := LNumero;
      dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
      dmgMain.qryExec.ParamByName('id').AsInteger  := LAlbId;
      dmgMain.qryExec.ExecSQL;
      if dmgMain.qryExec.RowsAffected > 0 then
      begin
        if LHasSeriesRow then
        begin
          dmgMain.qryExec.Close;
          dmgMain.qryExec.SQL.Text := 'UPDATE ge_series SET sig_numero_albaran = :num + 1 WHERE empresa_id=:emp AND serie=:ser AND activo=1';
          dmgMain.qryExec.ParamByName('num').AsInteger := LNumero;
          dmgMain.qryExec.ParamByName('emp').AsInteger := FEmpresaId;
          dmgMain.qryExec.ParamByName('ser').AsString  := LSerie;
          dmgMain.qryExec.ExecSQL;
        end;
      end;
    end;

    FAlbaranCodigo := Trim(LSerie) + '/' + IntToStr(LNumero);

    // Obtener líneas del pedido incluyendo bultos (cantidad_bulto es un concepto calculado)
    QryLines.SQL.Text :=
      'SELECT pl.id_articulo, pl.descripcion, pl.cantidad, pl.precio, ' +
      '       COALESCE(pl.bultos, 0) AS bultos, ' +
      '       COALESCE(ti.tipo, 10) AS tipo_iva, ' +
      '       COALESCE(NULLIF(a.id_maestro, 0), pl.id_articulo) AS id_articulo_stock ' +
      'FROM ge_pedidos_lineas pl ' +
      'LEFT JOIN ge_articulos a ON a.id = pl.id_articulo ' +
      'LEFT JOIN ge_tipos_iva ti ON ti.id = a.id_tipo_iva ' +
      'WHERE pl.id_pedido = :id ' +
      'ORDER BY pl.id ASC';
    QryLines.ParamByName('id').AsInteger := FPedidoId;
    QryLines.Open;

    LPosicion := 1;

    while not QryLines.Eof do
    begin
      LArtId        := QryLines.FieldByName('id_articulo').AsInteger;
      LArtStockId   := QryLines.FieldByName('id_articulo_stock').AsInteger;
      LDesc         := QryLines.FieldByName('descripcion').AsString;
      LCantTotalReq := QryLines.FieldByName('cantidad').AsFloat;
      LPrecio       := QryLines.FieldByName('precio').AsFloat;
      LBultosReq    := QryLines.FieldByName('bultos').AsFloat;
      LTipoIva      := QryLines.FieldByName('tipo_iva').AsFloat;
      LCantPendiente:= LCantTotalReq;

      if (LBultosReq > 0) and (LCantTotalReq > 0) then
        LCantBulto := LCantTotalReq / LBultosReq
      else
        LCantBulto := 0;

      if LCantTotalReq > 0 then
      begin
        // Buscar existencias de stock repartidas por lotes considerando stock disponible (unidades - reservadas)
        QryStock.Close;
        QryStock.SQL.Text :=
          'SELECT se.id AS stock_id, ' +
          '       GREATEST(0, se.cantidad_unidades - COALESCE(se.cantidad_reservada, 0)) AS disponible, ' +
          '       COALESCE(NULLIF(l.codigo_antiguo, ''''), l.codigo) AS lote_codigo, l.fecha AS fecha_fabricacion, ' +
          '       a.dias_vida ' +
          'FROM ge_stock_envasado se ' +
          'LEFT JOIN ge_lotes l ON se.id_lote = l.id ' +
          'LEFT JOIN ge_articulos a ON se.id_articulo = a.id ' +
          'WHERE se.id_articulo = :art ' +
          '  AND (se.cantidad_unidades - COALESCE(se.cantidad_reservada, 0)) > 0 ' +
          'ORDER BY l.fecha ASC, l.id ASC, se.id ASC';
        QryStock.ParamByName('art').AsInteger := LArtStockId;
        QryStock.Open;

        while (not QryStock.Eof) and (LCantPendiente > 0) do
        begin
          LStockId  := QryStock.FieldByName('stock_id').AsInteger;
          LDisp     := QryStock.FieldByName('disponible').AsFloat;
          LTomar    := Min(LCantPendiente, LDisp);
          LLoteCode := QryStock.FieldByName('lote_codigo').AsString;

          if LCantBulto > 0 then
            LBultosLinea := Ceil(LTomar / LCantBulto)
          else
            LBultosLinea := 0;

          // Calcular fecha de caducidad = fecha de fabricación + días de vida del artículo
          LHasCaducidad := False;
          LFechaCaducidad := 0;
          if (not QryStock.FieldByName('fecha_fabricacion').IsNull) and
             (not QryStock.FieldByName('dias_vida').IsNull) and
             (QryStock.FieldByName('dias_vida').AsInteger > 0) then
          begin
            LFabFecha := QryStock.FieldByName('fecha_fabricacion').AsDateTime;
            LDiasVida := QryStock.FieldByName('dias_vida').AsInteger;
            LFechaCaducidad := LFabFecha + LDiasVida;
            LHasCaducidad := True;
          end;

          LBaseLinea  := LTomar * LPrecio;
          LIvaLinea   := LBaseLinea * (LTipoIva / 100.0);
          LTotalLinea := LBaseLinea + LIvaLinea;

          LTotalBase := LTotalBase + LBaseLinea;
          LTotalIva  := LTotalIva + LIvaLinea;
          LTotalDoc  := LTotalDoc + LTotalLinea;

          // Crear línea de albarán por cada lote extraído arrastrando bultos y cantidad por bulto
          dmgMain.qryExec.Close;
          dmgMain.qryExec.SQL.Text :=
            'INSERT INTO ge_albaranes_lineas ' +
            '(albaran_id, articulo_id, articulo_codigo, descripcion, cantidad, bultos, cantidad_bulto, precio, ' +
            ' base, iva, total, tipo_iva, lotes, fecha_caducidad, posicion, id_user_creator, id_user_update) ' +
            'VALUES (:alb, :art, :artcod, :desc, :cant, :bultos, :cant_bulto, :precio, ' +
            '        :base, :iva_cant, :tot, :iva_tipo, :lote, :fcad, :pos, :user, :user)';
          dmgMain.qryExec.ParamByName('alb').AsInteger        := LAlbId;
          dmgMain.qryExec.ParamByName('art').AsInteger        := LArtId;
          dmgMain.qryExec.ParamByName('artcod').AsString      := IntToStr(LArtId);
          dmgMain.qryExec.ParamByName('desc').AsString        := LDesc;
          dmgMain.qryExec.ParamByName('cant').AsFloat         := LTomar;
          dmgMain.qryExec.ParamByName('bultos').AsFloat       := LBultosLinea;
          dmgMain.qryExec.ParamByName('cant_bulto').AsFloat   := LCantBulto;
          dmgMain.qryExec.ParamByName('precio').AsFloat       := LPrecio;
          dmgMain.qryExec.ParamByName('base').AsFloat         := LBaseLinea;
          dmgMain.qryExec.ParamByName('iva_cant').AsFloat     := LIvaLinea;
          dmgMain.qryExec.ParamByName('tot').AsFloat          := LTotalLinea;
          dmgMain.qryExec.ParamByName('iva_tipo').AsFloat     := LTipoIva;

          dmgMain.qryExec.ParamByName('lote').DataType        := ftString;
          if LLoteCode <> '' then
            dmgMain.qryExec.ParamByName('lote').AsString      := LLoteCode
          else
            dmgMain.qryExec.ParamByName('lote').Clear;

          dmgMain.qryExec.ParamByName('fcad').DataType        := ftDate;
          if LHasCaducidad then
            dmgMain.qryExec.ParamByName('fcad').AsDate        := LFechaCaducidad
          else
            dmgMain.qryExec.ParamByName('fcad').Clear;

          dmgMain.qryExec.ParamByName('pos').AsInteger        := LPosicion;
          dmgMain.qryExec.ParamByName('user').AsString       := dmgMain.CurrentUserId;
          dmgMain.qryExec.ExecSQL;

          // Añadir las unidades a cantidad_reservada (no se descuentan de cantidad_unidades hasta la preparación)
          dmgMain.qryExec.Close;
          dmgMain.qryExec.SQL.Text :=
            'UPDATE ge_stock_envasado SET cantidad_reservada = COALESCE(cantidad_reservada, 0) + :cant, ' +
            'updated_at = NOW(), id_user_update = :user WHERE id = :id';
          dmgMain.qryExec.ParamByName('cant').AsFloat  := LTomar;
          dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
          dmgMain.qryExec.ParamByName('id').AsInteger  := LStockId;
          dmgMain.qryExec.ExecSQL;

          LCantPendiente := LCantPendiente - LTomar;
          Inc(LPosicion);
          QryStock.Next;
        end;

        // Si quedara cantidad pendiente por no disponer de lotes de stock
        if LCantPendiente > 0 then
        begin
          if LCantBulto > 0 then
            LBultosLinea := Ceil(LCantPendiente / LCantBulto)
          else
            LBultosLinea := 0;

          LBaseLinea  := LCantPendiente * LPrecio;
          LIvaLinea   := LBaseLinea * (LTipoIva / 100.0);
          LTotalLinea := LBaseLinea + LIvaLinea;

          LTotalBase := LTotalBase + LBaseLinea;
          LTotalIva  := LTotalIva + LIvaLinea;
          LTotalDoc  := LTotalDoc + LTotalLinea;

          dmgMain.qryExec.Close;
          dmgMain.qryExec.SQL.Text :=
            'INSERT INTO ge_albaranes_lineas ' +
            '(albaran_id, articulo_id, articulo_codigo, descripcion, cantidad, bultos, cantidad_bulto, precio, ' +
            ' base, iva, total, tipo_iva, posicion, id_user_creator, id_user_update) ' +
            'VALUES (:alb, :art, :artcod, :desc, :cant, :bultos, :cant_bulto, :precio, ' +
            '        :base, :iva_cant, :tot, :iva_tipo, :pos, :user, :user)';
          dmgMain.qryExec.ParamByName('alb').AsInteger        := LAlbId;
          dmgMain.qryExec.ParamByName('art').AsInteger        := LArtId;
          dmgMain.qryExec.ParamByName('artcod').AsString      := IntToStr(LArtId);
          dmgMain.qryExec.ParamByName('desc').AsString        := LDesc;
          dmgMain.qryExec.ParamByName('cant').AsFloat         := LCantPendiente;
          dmgMain.qryExec.ParamByName('bultos').AsFloat       := LBultosLinea;
          dmgMain.qryExec.ParamByName('cant_bulto').AsFloat   := LCantBulto;
          dmgMain.qryExec.ParamByName('precio').AsFloat       := LPrecio;
          dmgMain.qryExec.ParamByName('base').AsFloat         := LBaseLinea;
          dmgMain.qryExec.ParamByName('iva_cant').AsFloat     := LIvaLinea;
          dmgMain.qryExec.ParamByName('tot').AsFloat          := LTotalLinea;
          dmgMain.qryExec.ParamByName('iva_tipo').AsFloat     := LTipoIva;
          dmgMain.qryExec.ParamByName('pos').AsInteger        := LPosicion;
          dmgMain.qryExec.ParamByName('user').AsString       := dmgMain.CurrentUserId;
          dmgMain.qryExec.ExecSQL;

          Inc(LPosicion);
        end;
      end;

      QryLines.Next;
    end;

    // Actualizar los totales calculados en la cabecera del albarán
    dmgMain.qryExec.Close;
    dmgMain.qryExec.SQL.Text :=
      'UPDATE ge_albaranes SET base_imponible = :base, iva = :iva, total = :tot WHERE id = :id';
    dmgMain.qryExec.ParamByName('base').AsFloat := LTotalBase;
    dmgMain.qryExec.ParamByName('iva').AsFloat  := LTotalIva;
    dmgMain.qryExec.ParamByName('tot').AsFloat  := LTotalDoc;
    dmgMain.qryExec.ParamByName('id').AsInteger := LAlbId;
    dmgMain.qryExec.ExecSQL;

    Result := LAlbId;
  finally
    Qry.Free;
    QryLines.Free;
    QryStock.Free;
  end;
end;


function TfrmEnviosRecogida.GenerarTextoDocumentoCarga(const ACodRecogida: string): string;
var
  LDoc: TStringList;
  LQry: TFDQuery;
begin
  LDoc := TStringList.Create;
  LQry := TFDQuery.Create(nil);
  try
    LDoc.Add('═══════════════════════════════════════════════════');
    LDoc.Add('           DOCUMENTO DE CARGA / EXPEDICIÓN         ');
    LDoc.Add('           ' + UpperCase(FConfigActual.NombreTransp));
    LDoc.Add('═══════════════════════════════════════════════════');
    LDoc.Add('Nº Recogida / Ref.        : ' + ACodRecogida);
    LDoc.Add('Transportista             : ' + FConfigActual.NombreTransp);
    LDoc.Add('Pedido                    : ' + edtPedidoNum.Text);
    LDoc.Add('Albarán generado          : ' + FAlbaranCodigo);
    LDoc.Add('Fecha recogida            : ' + FormatDateTime('dd/mm/yyyy', dtpFechaRec.Date));
    LDoc.Add('Franja horaria            : ' + FormatDateTime('hh:nn', dtpHoraIni.Time) + ' - ' + FormatDateTime('hh:nn', dtpHoraFin.Time));
    LDoc.Add('Nº de bultos              : ' + IntToStr(spnBultos.Value));
    LDoc.Add('Peso total                : ' + edtPeso.Text + ' kg');
    if Trim(edtConductor.Text) <> '' then
      LDoc.Add('Conductor / Transportista : ' + Trim(edtConductor.Text));
    if Trim(edtMatricula.Text) <> '' then
      LDoc.Add('Matrícula vehículo        : ' + Trim(edtMatricula.Text));
    LDoc.Add('───────────────────────────────────────────────────');
    LDoc.Add('DESTINATARIO:');
    LDoc.Add('  ' + edtNomEnv.Text);
    LDoc.Add('  ' + edtDirEnv.Text + ' ' + edtNumEnv.Text);
    LDoc.Add('  ' + edtCPEnv.Text + ' ' + edtPobEnv.Text);
    LDoc.Add('  Tel: ' + edtTlfEnv.Text + ' | Contacto: ' + edtContacto.Text);
    if Trim(memObs.Text) <> '' then
    begin
      LDoc.Add('───────────────────────────────────────────────────');
      LDoc.Add('OBSERVACIONES:');
      LDoc.Add('  ' + Trim(memObs.Text));
    end;
    LDoc.Add('───────────────────────────────────────────────────');
    LDoc.Add('ARTÍCULOS DEL ENVÍO:');
    LDoc.Add('');
    LQry.Connection := dmgMain.dbConn;
    LQry.SQL.Text := 'SELECT descripcion, cantidad, COALESCE(bultos, 0) AS bultos FROM ge_pedidos_lineas WHERE id_pedido=:id';
    LQry.ParamByName('id').AsInteger := FPedidoId;
    LQry.Open;
    while not LQry.Eof do
    begin
      LDoc.Add(Format('  %-40s  %8s ud. (%d bultos)', [
        LQry.FieldByName('descripcion').AsString,
        FormatFloat('#,##0.##', LQry.FieldByName('cantidad').AsFloat),
        LQry.FieldByName('bultos').AsInteger
      ]));
      LQry.Next;
    end;
    LDoc.Add('');
    LDoc.Add('═══════════════════════════════════════════════════');
    LDoc.Add('Firma del transportista / conductor:');
    LDoc.Add('');
    LDoc.Add('___________________________________________________');
    LDoc.Add('Generado: ' + FormatDateTime('dd/mm/yyyy hh:nn:ss', Now));
    Result := LDoc.Text;
  finally
    LDoc.Free;
    LQry.Free;
  end;
end;

procedure TfrmEnviosRecogida.MostrarDocumentoCarga(const ATexto, ACodRecogida: string);
var
  LDoc: TStringList;
  LSafeCod, LTemp: string;
  c: Char;
begin
  LSafeCod := ACodRecogida;
  for c in ['<', '>', ':', '"', '/', '\', '|', '?', '*'] do
    LSafeCod := LSafeCod.Replace(c, '_');
  LSafeCod := Trim(LSafeCod);

  LDoc := TStringList.Create;
  try
    LDoc.Text := ATexto;
    LTemp := GetEnvironmentVariable('TEMP') + '\DocCarga_' + LSafeCod + '_' + FormatDateTime('yyyymmddhhnnss', Now) + '.txt';
    LDoc.SaveToFile(LTemp, TEncoding.UTF8);
    ShellExecute(0, 'open', PChar(LTemp), nil, nil, SW_SHOWNORMAL);
  finally
    LDoc.Free;
  end;
end;

// ---------------------------------------------------------------------------
procedure TfrmEnviosRecogida.btnConfirmarClick(Sender: TObject);
var
  LClient : IEnviosClient;
  LParams : TEnvioParams;
  LResult : TEnvioResult;
  LError  : string;
  LCodServ: string;
  i       : Integer;
  LDocText: string;
  LTrackingCode: string;
begin
  if not FStockVerificado then begin ShowMessage('Verifique el stock antes de confirmar.'); Exit; end;
  if Trim(edtNomRec.Text) = '' then begin ShowMessage('Introduzca el nombre de contacto en la dirección de recogida.'); edtNomRec.SetFocus; Exit; end;
  if Trim(edtCPRec.Text) = '' then begin ShowMessage('Introduzca el código postal de la dirección de recogida.'); edtCPRec.SetFocus; Exit; end;
  if Trim(edtNomEnv.Text) = '' then begin ShowMessage('Introduzca el nombre del destinatario en la dirección de envío.'); edtNomEnv.SetFocus; Exit; end;
  if Trim(edtDirEnv.Text) = '' then begin ShowMessage('Introduzca la calle/vía en la dirección de envío.'); edtDirEnv.SetFocus; Exit; end;
  if Trim(edtCPEnv.Text) = '' then begin ShowMessage('Introduzca el código postal en la dirección de envío.'); edtCPEnv.SetFocus; Exit; end;
  if Trim(edtPobEnv.Text) = '' then begin ShowMessage('Introduzca la población en la dirección de envío.'); edtPobEnv.SetFocus; Exit; end;
  if FConfigActual.ConfigId = 0 then begin ShowMessage('No hay configuración de transportista cargada.'); Exit; end;

  if MessageDlg('¿Confirma la solicitud de envío y generación del albarán de carga con ' + FConfigActual.NombreTransp + '?', mtConfirmation, [mbYes, mbNo], 0) <> mrYes then Exit;

  btnConfirmar.Enabled := False;
  lblInfoResult.Caption := '⏳ Procesando...';
  Application.ProcessMessages;

  LCodServ := '48';
  if (cbTipoServ.ItemIndex >= 0) and (cbTipoServ.Items.Count > 0) then
    LCodServ := Copy(cbTipoServ.Items[cbTipoServ.ItemIndex], 1, 2);

  // Origen (Remitente): leído de los campos de pantalla (que muestran el remitente/almacén de recogida)
  LParams.NomOri        := Trim(edtNomRec.Text);
  LParams.DirOri        := Trim(edtDirRec.Text) + ' ' + Trim(edtNumRec.Text);
  LParams.PobOri        := FConfigActual.PobRemitente;
  LParams.CPOri         := Trim(edtCPRec.Text);
  LParams.TlfOri        := Trim(edtTlfRec.Text);

  // Destino (Destinatario): dirección del cliente (leído del formulario)
  LParams.NomDes        := Trim(edtNomEnv.Text);
  LParams.DirDes        := Trim(edtDirEnv.Text) + ' ' + Trim(edtNumEnv.Text);
  LParams.PobDes        := Trim(edtPobEnv.Text);
  LParams.CPDes         := Trim(edtCPEnv.Text);
  LParams.TlfDes        := Trim(edtTlfEnv.Text);
  LParams.CodPais       := 'ES'; // España por defecto

  // Datos del envío
  LParams.CodTipoServ   := LCodServ;
  LParams.NumBultos     := spnBultos.Value;
  LParams.Referencia    := edtPedidoNum.Text;
  LParams.Observaciones := Trim(memObs.Text);
  
  var LFormatSettings := TFormatSettings.Create;
  LFormatSettings.DecimalSeparator := '.';
  var LTextPeso := Trim(edtPeso.Text).Replace(',', '.');
  LParams.Peso := StrToFloatDef(LTextPeso, 1.0, LFormatSettings);

  dmgMain.dbConn.StartTransaction;
  try
    // 1. Crear cliente via factory (si tiene integración API de transportista)
    LClient := TEnviosClientFactory.Create(FConfigActual, LError);
    if Assigned(LClient) then
    begin
      lblInfoResult.Caption := '⏳ Contactando con ' + FConfigActual.NombreTransp + '...';
      Application.ProcessMessages;
      if LClient.Login then
      begin
        LResult := LClient.GrabaEnvio(LParams);
        if not LResult.OK then
          raise Exception.Create('Error del transportista: ' + LResult.Error);
        LTrackingCode := LResult.AlbaranTransportista;
      end
      else
        raise Exception.Create('Error de autenticación con el servicio web de ' + FConfigActual.NombreTransp);
    end
    else
    begin
      // Transportista sin API web o Transporte Propio
      LTrackingCode := 'CARGA-' + edtPedidoNum.Text;
      LResult.OK := True;
      LResult.AlbaranTransportista := LTrackingCode;
      LResult.GuidTransportista := '';
    end;

    // 2. Generar albarán si no existía ya
    lblInfoResult.Caption := 'Validando albarán de venta...'; Application.ProcessMessages;
    FAlbaranId := GenerarAlbaran;
    if FAlbaranId = 0 then raise Exception.Create('No se pudo generar el albarán de venta.');

    // 3. Generar texto del albarán de carga
    LDocText := GenerarTextoDocumentoCarga(LTrackingCode);

    // 4. Guardar recogida en ge_envios_recogidas
    dmgMain.qryExec.Close;
    dmgMain.qryExec.SQL.Text :=
      'INSERT INTO ge_envios_recogidas ' +
      '(pedido_id,albaran_id,empresa_id,config_id,cod_recogida,guid_transportista,' +
      ' cod_tipo_servicio,num_bultos,fecha_recogida,hora_ini,hora_fin,' +
      ' persona_contacto,conductor,matricula,referencia,observaciones,' +
      ' direccion,cp,poblacion,telefono,albaran_carga,' +
      ' estado_codigo,estado_descripcion,id_user_creator,id_user_update) ' +
      'VALUES (:ped,:alb,:emp,:cfg,:cod,:guid,' +
      '        :serv,:bul,:frec,:hini,:hfin,' +
      '        :cont,:cond,:mat,:ref,:obs,' +
      '        :dir,:cp,:pob,:tel,:doc,' +
      '        0,''Documentado'',:user,:user)';
    dmgMain.qryExec.ParamByName('ped').AsInteger  := FPedidoId;
    dmgMain.qryExec.ParamByName('alb').AsInteger  := FAlbaranId;
    dmgMain.qryExec.ParamByName('emp').AsInteger  := FEmpresaId;
    dmgMain.qryExec.ParamByName('cfg').AsInteger  := FConfigActual.ConfigId;
    dmgMain.qryExec.ParamByName('cod').AsString   := LTrackingCode;
    dmgMain.qryExec.ParamByName('guid').AsString  := LResult.GuidTransportista;
    dmgMain.qryExec.ParamByName('serv').AsString  := LCodServ;
    dmgMain.qryExec.ParamByName('bul').AsInteger  := spnBultos.Value;
    dmgMain.qryExec.ParamByName('frec').AsDate    := dtpFechaRec.Date;
    dmgMain.qryExec.ParamByName('hini').AsTime    := dtpHoraIni.Time;
    dmgMain.qryExec.ParamByName('hfin').AsTime    := dtpHoraFin.Time;
    dmgMain.qryExec.ParamByName('cont').AsString  := Trim(edtContacto.Text);
    dmgMain.qryExec.ParamByName('cond').AsString  := Trim(edtConductor.Text);
    dmgMain.qryExec.ParamByName('mat').AsString   := Trim(edtMatricula.Text);
    dmgMain.qryExec.ParamByName('ref').AsString   := edtPedidoNum.Text;
    dmgMain.qryExec.ParamByName('obs').AsString   := Trim(memObs.Text);
    dmgMain.qryExec.ParamByName('dir').AsString   := Trim(edtDirEnv.Text) + ' ' + Trim(edtNumEnv.Text);
    dmgMain.qryExec.ParamByName('cp').AsString    := Trim(edtCPEnv.Text);
    dmgMain.qryExec.ParamByName('pob').AsString   := Trim(edtPobEnv.Text);
    dmgMain.qryExec.ParamByName('tel').AsString   := Trim(edtTlfEnv.Text);
    dmgMain.qryExec.ParamByName('doc').AsString   := LDocText;
    dmgMain.qryExec.ParamByName('user').AsString  := dmgMain.CurrentUserId;
    dmgMain.qryExec.ExecSQL;

    dmgMain.qryExec.Close;
    dmgMain.qryExec.SQL.Text := 'SELECT LAST_INSERT_ID() AS id';
    dmgMain.qryExec.Open;
    FRecogidaId := dmgMain.qryExec.FieldByName('id').AsInteger;
    dmgMain.qryExec.Close;

    // 5. Actualizar estado del pedido a EN PREPARACION
    dmgMain.qryExec.SQL.Text :=
      'UPDATE ge_pedidos SET estado=''EN PREPARACION'', id_user_update=:user WHERE id=:id';
    dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
    dmgMain.qryExec.ParamByName('id').AsInteger  := FPedidoId;
    dmgMain.qryExec.ExecSQL;

    dmgMain.dbConn.Commit;

    // 5.b Disparar Hito de Calidad de Expedición y Envíos (CHK-EXPEDICION / RG-09)
    try
      TCalidadHitosService.DispararHito(
        'EXPEDICION_ENVIO',
        FRecogidaId,
        'Envío Pedido #' + edtPedidoNum.Text + ' (' + FConfigActual.NombreTransp + ')',
        tohEnvioRecogida,
        True
      );
    except
      on E: Exception do
        ShowMessage('Nota: No se pudo abrir la checklist de calidad del envío: ' + E.Message);
    end;

    lblInfoResult.Caption :=
      '✅ Envío documentado [' + FConfigActual.NombreTransp + ']. ' +
      'Ref: ' + LTrackingCode + ' | Albarán: ' + FAlbaranCodigo;
    lblInfoResult.Font.Color := clGreen;

    // 6. Imprimir etiqueta térmica (si aplica) o descargar/abrir etiqueta PDF del transportista
    if Assigned(LClient) and (MessageDlg('¿Desea imprimir o abrir la etiqueta del transportista?', mtConfirmation, [mbYes, mbNo], 0) = mrYes) then
    begin
      try
        var LImpresora: string := '';
        var LEsPDF: Boolean := False;
        var LGirar180: Boolean := True;
        if TfrmSelectImpresoraEnvio.SeleccionarDestino(Self, FConfigActual.NombreTransp, FConfigActual.ModeloImpresora, LImpresora, LEsPDF, LGirar180) then
        begin
          if not LEsPDF then
          begin
            var LErrorTermico: string := '';
            if LClient.ImprimirEtiquetaTermica(LTrackingCode, LImpresora, LErrorTermico, LGirar180) then
            begin
              lblInfoResult.Caption := lblInfoResult.Caption + ' | 🏷️ Etiqueta enviada a ' + LImpresora;
              ShowMessage('🏷️ Etiqueta enviada correctamente a la impresora "' + LImpresora + '".');
            end
            else
            begin
              if MessageDlg('No se pudo imprimir en la impresora seleccionada (' + LErrorTermico + ').' + sLineBreak + sLineBreak +
                            '¿Desea previsualizar y abrir la etiqueta en formato PDF?', mtConfirmation, [mbYes, mbNo], 0) = mrYes then
                LEsPDF := True;
            end;
          end;

          if LEsPDF then
          begin
            lblInfoResult.Caption := lblInfoResult.Caption + ' | ⏳ Descargando etiqueta PDF...';
            Application.ProcessMessages;
            var LEtiquetaBytes := LClient.GetEtiqueta(LTrackingCode, True);
            if Length(LEtiquetaBytes) > 0 then
            begin
              var LPdfFile := GetEnvironmentVariable('TEMP') + '\Etiqueta_' +
                              StringReplace(LTrackingCode, '/', '_', [rfReplaceAll]) +
                              '_' + FormatDateTime('yyyymmddhhnnss', Now) + '.pdf';
              var LFS := TFileStream.Create(LPdfFile, fmCreate);
              try
                LFS.WriteBuffer(LEtiquetaBytes[0], Length(LEtiquetaBytes));
              finally
                LFS.Free;
              end;

              if LGirar180 then
                TPdfRotateService.RotarPdfArchivo180(LPdfFile);

              ShellExecute(0, 'open', PChar(LPdfFile), nil, nil, SW_SHOWNORMAL);
              lblInfoResult.Caption := StringReplace(lblInfoResult.Caption,
                '⏳ Descargando etiqueta PDF...', '🏷️ Etiqueta PDF abierta', []);
            end;
          end;
        end;
      except
        on E: Exception do
          ShowMessage('Aviso: No se pudo imprimir/descargar la etiqueta del transportista.' + #13#10 +
                      'El envío se ha registrado correctamente.' + #13#10 +
                      'Error: ' + E.Message);
      end;
    end;

    // Mostrar el documento de carga generado
    if MessageDlg('¿Desea abrir el Documento de Carga para revisarlo o imprimirlo?', mtConfirmation, [mbYes, mbNo], 0) = mrYes then
    begin
      MostrarDocumentoCarga(LDocText, LTrackingCode);
    end;

    LClient := nil;
    ModalResult := mrOk;

  except
    on E: Exception do
    begin
      dmgMain.dbConn.Rollback;
      FAlbaranId := 0; FRecogidaId := 0;
      LClient := nil;
      lblInfoResult.Caption := '❌ Error: ' + E.Message;
      lblInfoResult.Font.Color := clRed;
      btnConfirmar.Enabled := True;
      TDbErrorHandler.HandleException(E, 'Error al confirmar envío');
    end;
  end;
end;

procedure TfrmEnviosRecogida.btnCancelarClick(Sender: TObject);
begin
  Close;
end;

end.
