unit frm_PlantaPesaje;

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.ComCtrls, Vcl.Buttons, Data.DB, FireDAC.Comp.Client,
  uScaleSerial, uScaleConfig, dm_LocalData;

type
  TIngredienteItem = record
    IdServidor: Integer;
    ProcesoId: Integer;
    IdMateriaPrima: Integer;
    NombreMateriaPrima: string;
    CantidadBase: Double;
    Unidad: string;
    CantidadObjetivo: Double;
    LoteSeleccionado: string;
    LoteEntradaId: Integer;
    FechaCaducidad: string;
    StockDisponible: Double;
  end;

  TfrmPlantaPesaje = class(TForm)
    pnlTop: TPanel;
    lblTituloElab: TLabel;
    lblInfoMasa: TLabel;
    pnlCenter: TPanel;
    pnlIngrediente: TPanel;
    lblPasoIngrediente: TLabel;
    lblNombreIngrediente: TLabel;
    lblPesoObjetivo: TLabel;
    pnlBasculaDisplay: TPanel;
    lblPesoBalanza: TLabel;
    lblEstadoBalanza: TLabel;
    pbProgreso: TProgressBar;
    pnlLote: TPanel;
    lblTituloLote: TLabel;
    lblInfoLote: TLabel;
    btnCambiarLote: TSpeedButton;
    pnlAcciones: TPanel;
    btnConfirmar: TButton;
    btnTara: TButton;
    btnEntradaManual: TButton;
    btnCancelar: TButton;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure btnTaraClick(Sender: TObject);
    procedure btnEntradaManualClick(Sender: TObject);
    procedure btnCambiarLoteClick(Sender: TObject);
    procedure btnConfirmarClick(Sender: TObject);
    procedure btnCancelarClick(Sender: TObject);
  private
    FScaleReader: TScaleSerialReader;
    FElabGuid: string;
    FRecetaId: Integer;
    FRecetaNombre: string;
    FNumeroMasas: Integer;
    FMultiplicador: Double;
    FMasaActual: Integer;

    FIngredientes: array of TIngredienteItem;
    FCurrentIndex: Integer;
    FCurrentWeight: Double;
    FCurrentIsStable: Boolean;

    procedure OnScaleWeight(Sender: TObject; AWeightGrams: Double; AIsStable: Boolean; const ARawData: string);
    procedure CargarIngredientesReceta;
    procedure BuscarLotePorDefecto(var AItem: TIngredienteItem);
    procedure MostrarIngredienteActual;
    procedure FinalizarPesajeMasa;
  public
    class function IniciarPesaje(
      const AElabGuid: string;
      ARecetaId: Integer;
      const ARecetaNombre: string;
      ANumeroMasas: Integer;
      AMultiplicador: Double;
      AMasaActual: Integer;
      AScaleReader: TScaleSerialReader
    ): Boolean;
  end;

implementation

{$R *.dfm}

class function TfrmPlantaPesaje.IniciarPesaje(
  const AElabGuid: string;
  ARecetaId: Integer;
  const ARecetaNombre: string;
  ANumeroMasas: Integer;
  AMultiplicador: Double;
  AMasaActual: Integer;
  AScaleReader: TScaleSerialReader
): Boolean;
var
  LFrm: TfrmPlantaPesaje;
begin
  LFrm := TfrmPlantaPesaje.Create(Application);
  try
    LFrm.FElabGuid := AElabGuid;
    LFrm.FRecetaId := ARecetaId;
    LFrm.FRecetaNombre := ARecetaNombre;
    LFrm.FNumeroMasas := ANumeroMasas;
    LFrm.FMultiplicador := AMultiplicador;
    LFrm.FMasaActual := AMasaActual;
    LFrm.FScaleReader := AScaleReader;

    LFrm.lblTituloElab.Caption := Format('ELABORACIÓN: %s', [ARecetaNombre]);
    LFrm.lblInfoMasa.Caption := Format('MASA %d DE %d (Factor: x%.2f)', [AMasaActual, ANumeroMasas, AMultiplicador]);

    LFrm.CargarIngredientesReceta;
    Result := (LFrm.ShowModal = mrOk);
  finally
    LFrm.Free;
  end;
end;

procedure TfrmPlantaPesaje.FormCreate(Sender: TObject);
begin
  Tag := 999;
  FCurrentIndex := 0;
  FCurrentWeight := 0.0;
  FCurrentIsStable := False;
end;

procedure TfrmPlantaPesaje.FormDestroy(Sender: TObject);
begin
  if Assigned(FScaleReader) then
    FScaleReader.OnWeight := nil;
end;

procedure TfrmPlantaPesaje.CargarIngredientesReceta;
var
  LQry: TFDQuery;
  LItem: TIngredienteItem;
begin
  SetLength(FIngredientes, 0);
  LQry := dmLocalData.OpenLocalQuery(
    'SELECT pi.* FROM local_procesos_ingredientes pi ' +
    'JOIN local_recetas_procesos rp ON pi.proceso_id = rp.id_servidor ' +
    'WHERE rp.receta_id = ? ' +
    'ORDER BY rp.orden, pi.id_servidor',
    [FRecetaId]
  );
  try
    while not LQry.Eof do
    begin
      LItem.IdServidor := LQry.FieldByName('id_servidor').AsInteger;
      LItem.ProcesoId := LQry.FieldByName('proceso_id').AsInteger;
      LItem.IdMateriaPrima := LQry.FieldByName('id_materia_prima').AsInteger;
      LItem.NombreMateriaPrima := LQry.FieldByName('materia_prima_nombre').AsString;
      LItem.CantidadBase := LQry.FieldByName('cantidad_base').AsFloat;
      LItem.Unidad := LQry.FieldByName('unidad').AsString;
      LItem.CantidadObjetivo := LItem.CantidadBase * FMultiplicador;

      BuscarLotePorDefecto(LItem);

      SetLength(FIngredientes, Length(FIngredientes) + 1);
      FIngredientes[High(FIngredientes)] := LItem;
      LQry.Next;
    end;
  finally
    LQry.Free;
  end;

  if Length(FIngredientes) = 0 then
  begin
    ShowMessage('Esta receta no tiene ingredientes definidos.');
    ModalResult := mrCancel;
    Exit;
  end;

  FCurrentIndex := 0;
  MostrarIngredienteActual;

  if Assigned(FScaleReader) then
  begin
    FScaleReader.OnWeight := OnScaleWeight;
    FScaleReader.Zero;
  end;
end;

procedure TfrmPlantaPesaje.BuscarLotePorDefecto(var AItem: TIngredienteItem);
var
  LQry: TFDQuery;
begin
  AItem.LoteSeleccionado := 'SIN LOTE';
  AItem.LoteEntradaId := 0;
  AItem.FechaCaducidad := '-';
  AItem.StockDisponible := 0.0;

  LQry := dmLocalData.OpenLocalQuery(
    'SELECT id_entrada, lote, fecha_caducidad, stock_restante ' +
    'FROM local_lotes_disponibles ' +
    'WHERE id_materia_prima = ? AND stock_restante > 0 ' +
    'ORDER BY fecha_caducidad ASC LIMIT 1',
    [AItem.IdMateriaPrima]
  );
  try
    if not LQry.IsEmpty then
    begin
      AItem.LoteEntradaId := LQry.FieldByName('id_entrada').AsInteger;
      AItem.LoteSeleccionado := LQry.FieldByName('lote').AsString;
      AItem.FechaCaducidad := LQry.FieldByName('fecha_caducidad').AsString;
      AItem.StockDisponible := LQry.FieldByName('stock_restante').AsFloat;
    end;
  finally
    LQry.Free;
  end;
end;

procedure TfrmPlantaPesaje.MostrarIngredienteActual;
var
  LItem: TIngredienteItem;
begin
  if (FCurrentIndex < 0) or (FCurrentIndex > High(FIngredientes)) then Exit;
  LItem := FIngredientes[FCurrentIndex];

  lblPasoIngrediente.Caption := Format('Ingrediente %d de %d', [FCurrentIndex + 1, Length(FIngredientes)]);
  lblNombreIngrediente.Caption := LItem.NombreMateriaPrima;
  lblPesoObjetivo.Caption := Format('OBJETIVO: %.1f %s', [LItem.CantidadObjetivo, LItem.Unidad]);

  lblInfoLote.Caption := Format(
    'Lote: %s   |   Caducidad: %s   |   Stock Disp: %.1f %s',
    [LItem.LoteSeleccionado, LItem.FechaCaducidad, LItem.StockDisponible, LItem.Unidad]
  );

  pbProgreso.Max := Round(LItem.CantidadObjetivo);
  pbProgreso.Position := 0;

  lblPesoBalanza.Caption := '0.0 g';
  lblEstadoBalanza.Caption := 'Coloque el ingrediente en la báscula';
  pnlBasculaDisplay.Color := clWhite;
  btnConfirmar.Enabled := True;
end;

procedure TfrmPlantaPesaje.OnScaleWeight(Sender: TObject; AWeightGrams: Double; AIsStable: Boolean; const ARawData: string);
var
  LItem: TIngredienteItem;
  LDiff: Double;
  LTol: Double;
begin
  FCurrentWeight := AWeightGrams;
  FCurrentIsStable := AIsStable;

  if (FCurrentIndex < 0) or (FCurrentIndex > High(FIngredientes)) then Exit;
  LItem := FIngredientes[FCurrentIndex];

  lblPesoBalanza.Caption := Format('%.1f g', [AWeightGrams]);
  pbProgreso.Position := Round(AWeightGrams);

  LTol := 2.0;
  if Assigned(FScaleReader) then
    LTol := FScaleReader.Config.ToleranceGrams;

  LDiff := AWeightGrams - LItem.CantidadObjetivo;

  if Abs(LDiff) <= LTol then
  begin
    pnlBasculaDisplay.Color := $00D4FCD4; // Verde pastel suave
    if AIsStable then
      lblEstadoBalanza.Caption := '✓ PESO EXACTO Y ESTABLE'
    else
      lblEstadoBalanza.Caption := '⏳ Peso objetivo alcanzado (estabilizando...)';
  end
  else if LDiff < -LTol then
  begin
    pnlBasculaDisplay.Color := $00FFF3D6; // Azul/amarillo suave
    lblEstadoBalanza.Caption := Format('Faltan %.1f g', [Abs(LDiff)]);
  end
  else
  begin
    pnlBasculaDisplay.Color := $00D6D6FF; // Rojo pastel suave
    lblEstadoBalanza.Caption := Format('Exceso de %.1f g', [LDiff]);
  end;
end;

procedure TfrmPlantaPesaje.btnTaraClick(Sender: TObject);
begin
  if Assigned(FScaleReader) then
    FScaleReader.Tare;
end;

procedure TfrmPlantaPesaje.btnEntradaManualClick(Sender: TObject);
var
  LRes: string;
  LVal: Double;
begin
  LRes := InputBox(
    'Entrada Manual de Peso',
    Format('Ingrese el peso real en gramos para "%s":', [FIngredientes[FCurrentIndex].NombreMateriaPrima]),
    FloatToStr(FIngredientes[FCurrentIndex].CantidadObjetivo)
  );

  LRes := StringReplace(LRes, ',', '.', [rfReplaceAll]);
  if TryStrToFloat(LRes, LVal) then
  begin
    FCurrentWeight := LVal;
    FCurrentIsStable := True;
    if Assigned(FScaleReader) and FScaleReader.Config.Simulated then
      FScaleReader.SetSimulatedWeight(LVal);
    OnScaleWeight(Self, LVal, True, 'MANUAL');
  end;
end;

procedure TfrmPlantaPesaje.btnCambiarLoteClick(Sender: TObject);
var
  LQry: TFDQuery;
  LLotesStr: string;
  LRes: string;
  I: Integer;
  LItem: TIngredienteItem;
begin
  LItem := FIngredientes[FCurrentIndex];
  LQry := dmLocalData.OpenLocalQuery(
    'SELECT id_entrada, lote, fecha_caducidad, stock_restante ' +
    'FROM local_lotes_disponibles ' +
    'WHERE id_materia_prima = ? ' +
    'ORDER BY fecha_caducidad ASC',
    [LItem.IdMateriaPrima]
  );
  try
    if LQry.IsEmpty then
    begin
      // Permitir escribir lote manual si no hay en stock registrado
      LRes := InputBox('Lote no registrado', 'No hay lotes con stock en catálogo. Ingrese el código de lote del envase:', LItem.LoteSeleccionado);
      if Trim(LRes) <> '' then
      begin
        FIngredientes[FCurrentIndex].LoteSeleccionado := Trim(LRes);
        FIngredientes[FCurrentIndex].LoteEntradaId := 0;
        MostrarIngredienteActual;
      end;
      Exit;
    end;

    LLotesStr := 'Lotes disponibles para ' + LItem.NombreMateriaPrima + sLineBreak;
    I := 1;
    while not LQry.Eof do
    begin
      LLotesStr := LLotesStr + Format('[%d] Lote: %s (Cad: %s, Stock: %.1f)' + sLineBreak,
        [I, LQry.FieldByName('lote').AsString, LQry.FieldByName('fecha_caducidad').AsString,
         LQry.FieldByName('stock_restante').AsFloat]);
      Inc(I);
      LQry.Next;
    end;

    LRes := InputBox('Selección de Lote', LLotesStr + sLineBreak + 'Escriba el lote deseado:', LItem.LoteSeleccionado);
    if Trim(LRes) <> '' then
    begin
      FIngredientes[FCurrentIndex].LoteSeleccionado := Trim(LRes);
      // Buscar si coincide con alguno registrado
      LQry.First;
      while not LQry.Eof do
      begin
        if SameText(LQry.FieldByName('lote').AsString, Trim(LRes)) then
        begin
          FIngredientes[FCurrentIndex].LoteEntradaId := LQry.FieldByName('id_entrada').AsInteger;
          FIngredientes[FCurrentIndex].FechaCaducidad := LQry.FieldByName('fecha_caducidad').AsString;
          FIngredientes[FCurrentIndex].StockDisponible := LQry.FieldByName('stock_restante').AsFloat;
          Break;
        end;
        LQry.Next;
      end;
      MostrarIngredienteActual;
    end;
  finally
    LQry.Free;
  end;
end;

procedure TfrmPlantaPesaje.btnConfirmarClick(Sender: TObject);
var
  LItem: TIngredienteItem;
  LConsumoGuid: string;
begin
  if (FCurrentIndex < 0) or (FCurrentIndex > High(FIngredientes)) then Exit;
  LItem := FIngredientes[FCurrentIndex];

  if FCurrentWeight <= 0 then
  begin
    if MessageDlg('El peso registrado es 0. ¿Desea confirmar de todas formas?', mtConfirmation, [mbYes, mbNo], 0) <> mrYes then
      Exit;
  end;

  // Registrar consumo local
  LConsumoGuid := dmLocalData.NewGuidString;
  dmLocalData.ExecLocalSQL(
    'INSERT INTO local_lotes_consumidos (' +
    '  local_guid, elaboracion_guid, server_elaboracion_id, id_materia_prima, ' +
    '  materia_prima_nombre, lote_texto, lote_entrada_id, cantidad, unidad, ' +
    '  masa_numero, sync_status, created_at' +
    ') VALUES (?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ''PENDING'', ?)',
    [LConsumoGuid, FElabGuid, LItem.IdMateriaPrima, LItem.NombreMateriaPrima,
     LItem.LoteSeleccionado, LItem.LoteEntradaId, FCurrentWeight, LItem.Unidad,
     FMasaActual, FormatDateTime('yyyy-mm-dd hh:nn:ss', Now)]
  );

  // Descontar del catálogo local de lotes
  if LItem.LoteEntradaId > 0 then
  begin
    dmLocalData.ExecLocalSQL(
      'UPDATE local_lotes_disponibles SET stock_restante = MAX(0, stock_restante - ?) WHERE id_entrada = ?',
      [FCurrentWeight, LItem.LoteEntradaId]
    );
  end;

  // Avanzar al siguiente ingrediente
  Inc(FCurrentIndex);
  if FCurrentIndex <= High(FIngredientes) then
  begin
    FCurrentWeight := 0;
    if Assigned(FScaleReader) then
      FScaleReader.Zero;
    MostrarIngredienteActual;
  end
  else
  begin
    FinalizarPesajeMasa;
  end;
end;

procedure TfrmPlantaPesaje.FinalizarPesajeMasa;
begin
  if FMasaActual < FNumeroMasas then
  begin
    if MessageDlg(
      Format('¡Pesaje de la Masa %d completado!' + sLineBreak + sLineBreak +
             '¿Desea preparar y pesar ahora la Masa %d de %d?',
             [FMasaActual, FMasaActual + 1, FNumeroMasas]),
      mtConfirmation, [mbYes, mbNo], 0) = mrYes then
    begin
      Inc(FMasaActual);
      dmLocalData.ExecLocalSQL(
        'UPDATE local_elaboraciones SET masa_actual = ? WHERE local_guid = ?',
        [FMasaActual, FElabGuid]
      );
      lblInfoMasa.Caption := Format('MASA %d DE %d (Factor: x%.2f)', [FMasaActual, FNumeroMasas, FMultiplicador]);
      FCurrentIndex := 0;
      FCurrentWeight := 0;
      if Assigned(FScaleReader) then
        FScaleReader.Zero;
      MostrarIngredienteActual;
      Exit;
    end;
  end;

  // Todas las masas pesadas -> Pasa a fase de amasado
  dmLocalData.ExecLocalSQL(
    'UPDATE local_elaboraciones SET estado = ''EN_AMASADO'' WHERE local_guid = ?',
    [FElabGuid]
  );

  ShowMessage('¡Todos los ingredientes han sido pesados y registrados correctamente!' + sLineBreak +
              'Pase a la fase de amasado y elaboración.');
  ModalResult := mrOk;
end;

procedure TfrmPlantaPesaje.btnCancelarClick(Sender: TObject);
begin
  if MessageDlg('¿Está seguro de que desea cancelar el pesaje en curso?', mtConfirmation, [mbYes, mbNo], 0) = mrYes then
    ModalResult := mrCancel;
end;

end.
