unit uSyncEngine;

interface

uses
  System.SysUtils, System.Classes, System.SyncObjs, Data.DB,
  FireDAC.Stan.Intf, FireDAC.Stan.Option, FireDAC.Stan.Error, FireDAC.UI.Intf,
  FireDAC.Phys.Intf, FireDAC.Stan.Def, FireDAC.Stan.Pool, FireDAC.Stan.Async,
  FireDAC.Phys, FireDAC.Phys.MySQL, FireDAC.Phys.MySQLDef, FireDAC.Comp.Client,
  FireDAC.Stan.Param, uScaleConfig, dm_LocalData;

type
  TNetStatus = (nsOnline, nsOffline, nsSyncing);

  TOnSyncStatusEvent = procedure(Sender: TObject; AStatus: TNetStatus; APendingCount: Integer; const AMessage: string) of object;

  TSyncWorkerThread = class(TThread)
  private
    FOwner: TObject;
    FConfig: TScaleConfig;
    FStopEvent: TEvent;
    FServerConn: TFDConnection;
    FMySQLDriver: TFDPhysMySQLDriverLink;
    FNetStatus: TNetStatus;
    FPendingCount: Integer;
    FLastMessage: string;

    function TestAndConnectServer: Boolean;
    procedure DisconnectServer;
    function CountPendingLocalRecords: Integer;
    procedure ProcessUploadSync;
    procedure ProcessDownloadSync;
    procedure NotifyStatus(AStatus: TNetStatus; APending: Integer; const AMsg: string);
    procedure LogSync(const AOp, ADet, ARes: string);
  protected
    procedure Execute; override;
  public
    constructor Create(AOwner: TObject; const AConfig: TScaleConfig);
    destructor Destroy; override;
    procedure Stop;
  end;

  TSyncEngine = class
  private
    FConfig: TScaleConfig;
    FWorker: TSyncWorkerThread;
    FNetStatus: TNetStatus;
    FPendingCount: Integer;
    FLastMessage: string;
    FOnSyncStatus: TOnSyncStatusEvent;
  public
    constructor Create(const AConfig: TScaleConfig);
    destructor Destroy; override;

    procedure Start;
    procedure Stop;
    procedure TriggerSync;

    property NetStatus: TNetStatus read FNetStatus;
    property PendingCount: Integer read FPendingCount;
    property LastMessage: string read FLastMessage;
    property OnSyncStatus: TOnSyncStatusEvent read FOnSyncStatus write FOnSyncStatus;
  end;

implementation

uses
  System.Variants, System.DateUtils;

{ TSyncWorkerThread }

constructor TSyncWorkerThread.Create(AOwner: TObject; const AConfig: TScaleConfig);
begin
  inherited Create(True);
  FOwner := AOwner;
  FConfig := AConfig;
  FStopEvent := TEvent.Create(nil, True, False, '');
  FNetStatus := nsOffline;
  FPendingCount := 0;
  FLastMessage := 'Iniciando motor de sincronización...';

  FServerConn := TFDConnection.Create(nil);
  FServerConn.LoginPrompt := False;
  FMySQLDriver := TFDPhysMySQLDriverLink.Create(nil);

  FreeOnTerminate := False;
end;

destructor TSyncWorkerThread.Destroy;
begin
  Stop;
  DisconnectServer;
  FMySQLDriver.Free;
  FServerConn.Free;
  FStopEvent.Free;
  inherited Destroy;
end;

procedure TSyncWorkerThread.Stop;
begin
  Terminate;
  if Assigned(FStopEvent) then
    FStopEvent.SetEvent;
  WaitFor;
end;

function TSyncWorkerThread.TestAndConnectServer: Boolean;
begin
  Result := False;
  try
    if not FServerConn.Connected then
    begin
      FServerConn.Params.Clear;
      FServerConn.Params.DriverID := 'MySQL';
      FServerConn.Params.Add('Server=' + FConfig.ServerHost);
      FServerConn.Params.Add('Port=' + IntToStr(FConfig.ServerPort));
      FServerConn.Params.Add('Database=' + FConfig.ServerDatabase);
      FServerConn.Params.Add('User_Name=' + FConfig.ServerUser);
      FServerConn.Params.Add('Password=' + FConfig.ServerPass);
      FServerConn.Params.Add('CharacterSet=utf8mb4');
      FServerConn.Params.Add('ReadTimeout=3000');
      FServerConn.Params.Add('WriteTimeout=3000');
      FServerConn.Connected := True;
    end;

    // Comprobación rápida de ping
    FServerConn.ExecSQL('SELECT 1');
    Result := True;
  except
    on E: Exception do
    begin
      DisconnectServer;
      Result := False;
    end;
  end;
end;

procedure TSyncWorkerThread.DisconnectServer;
begin
  try
    if FServerConn.Connected then
      FServerConn.Connected := False;
  except
    // Ignorar errores al desconectar
  end;
end;

function TSyncWorkerThread.CountPendingLocalRecords: Integer;
var
  LQry: TFDQuery;
begin
  Result := 0;
  try
    LQry := dmLocalData.OpenLocalQuery(
      'SELECT (SELECT COUNT(*) FROM local_elaboraciones WHERE sync_status = ''PENDING'') + ' +
      '       (SELECT COUNT(*) FROM local_lotes_consumidos WHERE sync_status = ''PENDING'') AS Total'
    );
    try
      if not LQry.IsEmpty then
        Result := LQry.FieldByName('Total').AsInteger;
    finally
      LQry.Free;
    end;
  except
    Result := 0;
  end;
end;

procedure TSyncWorkerThread.LogSync(const AOp, ADet, ARes: string);
begin
  try
    dmLocalData.ExecLocalSQL(
      'INSERT INTO local_sync_log (fecha, operacion, detalles, resultado) VALUES (?, ?, ?, ?)',
      [FormatDateTime('yyyy-mm-dd hh:nn:ss', Now), AOp, ADet, ARes]
    );
  except
    // no-op en logs
  end;
end;

procedure TSyncWorkerThread.ProcessUploadSync;
var
  LQryLocalElab, LQryLocalCons: TFDQuery;
  LServerQry: TFDQuery;
  LLocalGuid: string;
  LServerId: Integer;
begin
  LServerQry := TFDQuery.Create(nil);
  try
    LServerQry.Connection := FServerConn;

    // 1. Subir elaboraciones finalizadas o en curso pendientes
    LQryLocalElab := dmLocalData.OpenLocalQuery(
      'SELECT * FROM local_elaboraciones WHERE sync_status = ''PENDING'''
    );
    try
      while not LQryLocalElab.Eof do
      begin
        LLocalGuid := LQryLocalElab.FieldByName('local_guid').AsString;
        LServerId := LQryLocalElab.FieldByName('server_id').AsInteger;

        if LServerId = 0 then
        begin
          // Nueva orden creada en local -> INSERT en ob_elaboraciones
          FServerConn.StartTransaction;
          try
            LServerQry.SQL.Text :=
              'INSERT INTO ob_elaboraciones (' +
              '  tipo, fecha, id_obrador, id_receta, lote_id, multiplicador, numero_masas, ' +
              '  hora_inicio, hora_fin, temp_obrador, temp_masa_final, tiempo_real_minutos, ' +
              '  ok_visual, observaciones' +
              ') VALUES (' +
              '  ''primer_empaste'', :fecha, :id_obrador, :id_receta, 1, :multiplicador, :numero_masas, ' +
              '  :hora_inicio, :hora_fin, :temp_obrador, :temp_masa_final, :tiempo_real, ' +
              '  :ok_visual, :observaciones' +
              ')';

            LServerQry.ParamByName('fecha').AsDate := Date;
            LServerQry.ParamByName('id_obrador').AsInteger := LQryLocalElab.FieldByName('id_obrador').AsInteger;
            LServerQry.ParamByName('id_receta').AsInteger := LQryLocalElab.FieldByName('id_receta').AsInteger;
            LServerQry.ParamByName('multiplicador').AsFloat := LQryLocalElab.FieldByName('multiplicador').AsFloat;
            LServerQry.ParamByName('numero_masas').AsInteger := LQryLocalElab.FieldByName('numero_masas').AsInteger;

            if LQryLocalElab.FieldByName('hora_inicio').AsString <> '' then
              LServerQry.ParamByName('hora_inicio').AsString := LQryLocalElab.FieldByName('hora_inicio').AsString
            else
              LServerQry.ParamByName('hora_inicio').Clear;

            if LQryLocalElab.FieldByName('hora_fin').AsString <> '' then
              LServerQry.ParamByName('hora_fin').AsString := LQryLocalElab.FieldByName('hora_fin').AsString
            else
              LServerQry.ParamByName('hora_fin').Clear;

            LServerQry.ParamByName('temp_obrador').AsFloat := LQryLocalElab.FieldByName('temp_obrador').AsFloat;
            LServerQry.ParamByName('temp_masa_final').AsFloat := LQryLocalElab.FieldByName('temp_masa_final').AsFloat;
            LServerQry.ParamByName('tiempo_real').AsInteger := LQryLocalElab.FieldByName('tiempo_real_minutos').AsInteger;
            LServerQry.ParamByName('ok_visual').AsInteger := LQryLocalElab.FieldByName('ok_visual').AsInteger;
            LServerQry.ParamByName('observaciones').AsString := LQryLocalElab.FieldByName('observaciones').AsString;
            LServerQry.ExecSQL;

            LServerQry.SQL.Text := 'SELECT LAST_INSERT_ID() AS id';
            LServerQry.Open;
            LServerId := LServerQry.FieldByName('id').AsInteger;
            LServerQry.Close;

            FServerConn.Commit;

            // Actualizar local
            dmLocalData.ExecLocalSQL(
              'UPDATE local_elaboraciones SET server_id = ?, sync_status = ''SYNCED'' WHERE local_guid = ?',
              [LServerId, LLocalGuid]
            );

            // Actualizar referencias en lotes consumidos
            dmLocalData.ExecLocalSQL(
              'UPDATE local_lotes_consumidos SET server_elaboracion_id = ? WHERE elaboracion_guid = ?',
              [LServerId, LLocalGuid]
            );

            LogSync('UPLOAD_ELAB', Format('GUID %s -> ServerId %d', [LLocalGuid, LServerId]), 'OK');
          except
            on E: Exception do
            begin
              if FServerConn.InTransaction then
                FServerConn.Rollback;
              LogSync('UPLOAD_ELAB', Format('GUID %s Error: %s', [LLocalGuid, E.Message]), 'ERROR');
            end;
          end;
        end
        else
        begin
          // Actualización de estado / horas de orden existente
          try
            LServerQry.SQL.Text :=
              'UPDATE ob_elaboraciones SET ' +
              '  hora_inicio = COALESCE(:hora_inicio, hora_inicio), ' +
              '  hora_fin = :hora_fin, ' +
              '  temp_obrador = :temp_obrador, ' +
              '  temp_masa_final = :temp_masa_final, ' +
              '  tiempo_real_minutos = :tiempo_real, ' +
              '  ok_visual = :ok_visual, ' +
              '  observaciones = :observaciones ' +
              'WHERE id = :id';

            LServerQry.ParamByName('id').AsInteger := LServerId;
            if LQryLocalElab.FieldByName('hora_inicio').AsString <> '' then
              LServerQry.ParamByName('hora_inicio').AsString := LQryLocalElab.FieldByName('hora_inicio').AsString
            else
              LServerQry.ParamByName('hora_inicio').Clear;

            if LQryLocalElab.FieldByName('hora_fin').AsString <> '' then
              LServerQry.ParamByName('hora_fin').AsString := LQryLocalElab.FieldByName('hora_fin').AsString
            else
              LServerQry.ParamByName('hora_fin').Clear;

            LServerQry.ParamByName('temp_obrador').AsFloat := LQryLocalElab.FieldByName('temp_obrador').AsFloat;
            LServerQry.ParamByName('temp_masa_final').AsFloat := LQryLocalElab.FieldByName('temp_masa_final').AsFloat;
            LServerQry.ParamByName('tiempo_real').AsInteger := LQryLocalElab.FieldByName('tiempo_real_minutos').AsInteger;
            LServerQry.ParamByName('ok_visual').AsInteger := LQryLocalElab.FieldByName('ok_visual').AsInteger;
            LServerQry.ParamByName('observaciones').AsString := LQryLocalElab.FieldByName('observaciones').AsString;
            LServerQry.ExecSQL;

            dmLocalData.ExecLocalSQL(
              'UPDATE local_elaboraciones SET sync_status = ''SYNCED'' WHERE local_guid = ?',
              [LLocalGuid]
            );
            LogSync('UPDATE_ELAB', Format('ServerId %d', [LServerId]), 'OK');
          except
            on E: Exception do
              LogSync('UPDATE_ELAB', Format('ServerId %d Error: %s', [LServerId, E.Message]), 'ERROR');
          end;
        end;

        LQryLocalElab.Next;
      end;
    finally
      LQryLocalElab.Free;
    end;

    // 2. Subir consumos de lotes pendientes
    LQryLocalCons := dmLocalData.OpenLocalQuery(
      'SELECT * FROM local_lotes_consumidos WHERE sync_status = ''PENDING'' AND server_elaboracion_id > 0'
    );
    try
      while not LQryLocalCons.Eof do
      begin
        LLocalGuid := LQryLocalCons.FieldByName('local_guid').AsString;
        LServerId := LQryLocalCons.FieldByName('server_elaboracion_id').AsInteger;

        FServerConn.StartTransaction;
        try
          LServerQry.SQL.Text :=
            'INSERT INTO ob_lotes_consumidos (elaboracion_id, lote_consumido_id, cantidad, unidad) ' +
            'VALUES (:elab_id, :lote_id, :cantidad, :unidad)';
          LServerQry.ParamByName('elab_id').AsInteger := LServerId;
          LServerQry.ParamByName('lote_id').AsInteger := LQryLocalCons.FieldByName('lote_entrada_id').AsInteger;
          LServerQry.ParamByName('cantidad').AsFloat := LQryLocalCons.FieldByName('cantidad').AsFloat;
          LServerQry.ParamByName('unidad').AsString := LQryLocalCons.FieldByName('unidad').AsString;
          LServerQry.ExecSQL;

          // Descontar stock restante de la entrada de materia prima si aplica
          if LQryLocalCons.FieldByName('lote_entrada_id').AsInteger > 0 then
          begin
            LServerQry.SQL.Text :=
              'UPDATE ge_entradas_materias_primas ' +
              'SET stock_restante = GREATEST(0, stock_restante - :cant) ' +
              'WHERE id = :id';
            LServerQry.ParamByName('cant').AsFloat := LQryLocalCons.FieldByName('cantidad').AsFloat;
            LServerQry.ParamByName('id').AsInteger := LQryLocalCons.FieldByName('lote_entrada_id').AsInteger;
            LServerQry.ExecSQL;
          end;

          FServerConn.Commit;

          dmLocalData.ExecLocalSQL(
            'UPDATE local_lotes_consumidos SET sync_status = ''SYNCED'' WHERE local_guid = ?',
            [LLocalGuid]
          );
          LogSync('UPLOAD_CONSUMO', Format('Consumo GUID %s subido a Elab %d', [LLocalGuid, LServerId]), 'OK');
        except
          on E: Exception do
          begin
            if FServerConn.InTransaction then
              FServerConn.Rollback;
            LogSync('UPLOAD_CONSUMO', Format('Consumo GUID %s Error: %s', [LLocalGuid, E.Message]), 'ERROR');
          end;
        end;

        LQryLocalCons.Next;
      end;
    finally
      LQryLocalCons.Free;
    end;

  finally
    LServerQry.Free;
  end;
end;

procedure TSyncWorkerThread.ProcessDownloadSync;
var
  LQry: TFDQuery;
begin
  LQry := TFDQuery.Create(nil);
  try
    LQry.Connection := FServerConn;

    // 1. Descargar Recetas Activas
    LQry.SQL.Text := 'SELECT id, descripcion, version, activo FROM ob_recetas WHERE activo = 1';
    LQry.Open;
    dmLocalData.localConn.StartTransaction;
    try
      while not LQry.Eof do
      begin
        dmLocalData.ExecLocalSQL(
          'INSERT OR REPLACE INTO local_recetas (id_servidor, descripcion, version, activo) ' +
          'VALUES (?, ?, ?, ?)',
          [LQry.FieldByName('id').AsInteger, LQry.FieldByName('descripcion').AsString,
           LQry.FieldByName('version').AsString, LQry.FieldByName('activo').AsInteger]
        );
        LQry.Next;
      end;
      dmLocalData.localConn.Commit;
    except
      dmLocalData.localConn.Rollback;
    end;
    LQry.Close;

    // 2. Descargar Procesos de Recetas
    LQry.SQL.Text :=
      'SELECT id, receta_id, orden, nombre, duracion_minutos, temperatura, ph, proceso_final, id_articulo_resultado ' +
      'FROM ob_recetas_procesos ORDER BY receta_id, orden';
    LQry.Open;
    dmLocalData.localConn.StartTransaction;
    try
      while not LQry.Eof do
      begin
        dmLocalData.ExecLocalSQL(
          'INSERT OR REPLACE INTO local_recetas_procesos (' +
          '  id_servidor, receta_id, orden, nombre, duracion_minutos, temperatura, ph, proceso_final, id_articulo_resultado' +
          ') VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
          [LQry.FieldByName('id').AsInteger, LQry.FieldByName('receta_id').AsInteger,
           LQry.FieldByName('orden').AsInteger, LQry.FieldByName('nombre').AsString,
           LQry.FieldByName('duracion_minutos').AsInteger, LQry.FieldByName('temperatura').AsFloat,
           LQry.FieldByName('ph').AsFloat, LQry.FieldByName('proceso_final').AsInteger,
           LQry.FieldByName('id_articulo_resultado').AsInteger]
        );
        LQry.Next;
      end;
      dmLocalData.localConn.Commit;
    except
      dmLocalData.localConn.Rollback;
    end;
    LQry.Close;

    // 3. Descargar Ingredientes por Proceso
    LQry.SQL.Text :=
      'SELECT pi.id, pi.proceso_id, pi.id_materia_prima, mp.descripcion AS materia_nombre, ' +
      '       pi.cantidad_base, pi.unidad ' +
      'FROM ob_procesos_ingredientes pi ' +
      'JOIN ge_materias_primas mp ON pi.id_materia_prima = mp.id';
    LQry.Open;
    dmLocalData.localConn.StartTransaction;
    try
      while not LQry.Eof do
      begin
        dmLocalData.ExecLocalSQL(
          'INSERT OR REPLACE INTO local_procesos_ingredientes (' +
          '  id_servidor, proceso_id, id_materia_prima, materia_prima_nombre, cantidad_base, unidad' +
          ') VALUES (?, ?, ?, ?, ?, ?)',
          [LQry.FieldByName('id').AsInteger, LQry.FieldByName('proceso_id').AsInteger,
           LQry.FieldByName('id_materia_prima').AsInteger, LQry.FieldByName('materia_nombre').AsString,
           LQry.FieldByName('cantidad_base').AsFloat, LQry.FieldByName('unidad').AsString]
        );
        LQry.Next;
      end;
      dmLocalData.localConn.Commit;
    except
      dmLocalData.localConn.Rollback;
    end;
    LQry.Close;

    // 4. Descargar Materias Primas
    LQry.SQL.Text := 'SELECT id, descripcion, codigo_barras FROM ge_materias_primas WHERE activo = 1';
    LQry.Open;
    dmLocalData.localConn.StartTransaction;
    try
      while not LQry.Eof do
      begin
        dmLocalData.ExecLocalSQL(
          'INSERT OR REPLACE INTO local_materias_primas (id_servidor, descripcion, codigo_barras) ' +
          'VALUES (?, ?, ?)',
          [LQry.FieldByName('id').AsInteger, LQry.FieldByName('descripcion').AsString,
           LQry.FieldByName('codigo_barras').AsString]
        );
        LQry.Next;
      end;
      dmLocalData.localConn.Commit;
    except
      dmLocalData.localConn.Rollback;
    end;
    LQry.Close;

    // 5. Descargar Lotes Disponibles con Stock de Materias Primas
    LQry.SQL.Text :=
      'SELECT id, id_materia_prima, lote, fecha_caducidad, stock_restante ' +
      'FROM ge_entradas_materias_primas ' +
      'WHERE stock_restante > 0 ' +
      'ORDER BY id_materia_prima, fecha_caducidad ASC';
    LQry.Open;
    dmLocalData.localConn.StartTransaction;
    try
      // Limpiar y refrescar catálogo de lotes vigentes
      dmLocalData.ExecLocalSQL('DELETE FROM local_lotes_disponibles', []);
      while not LQry.Eof do
      begin
        dmLocalData.ExecLocalSQL(
          'INSERT INTO local_lotes_disponibles (id_entrada, id_materia_prima, lote, fecha_caducidad, stock_restante) ' +
          'VALUES (?, ?, ?, ?, ?)',
          [LQry.FieldByName('id').AsInteger, LQry.FieldByName('id_materia_prima').AsInteger,
           LQry.FieldByName('lote').AsString, LQry.FieldByName('fecha_caducidad').AsString,
           LQry.FieldByName('stock_restante').AsFloat]
        );
        LQry.Next;
      end;
      dmLocalData.localConn.Commit;
    except
      dmLocalData.localConn.Rollback;
    end;
    LQry.Close;

    // 6. Descargar Órdenes de Fabricación enviadas desde oficina para hoy y pendientes
    LQry.SQL.Text :=
      'SELECT e.id, e.id_obrador, e.id_receta, r.descripcion AS receta_nombre, ' +
      '       e.numero_masas, e.multiplicador, e.hora_inicio, e.hora_fin, e.observaciones ' +
      'FROM ob_elaboraciones e ' +
      'JOIN ob_recetas r ON e.id_receta = r.id ' +
      'WHERE e.id_obrador = :obrador AND e.fecha = CURRENT_DATE() AND e.hora_inicio IS NULL';
    LQry.ParamByName('obrador').AsInteger := FConfig.DefaultObradorId;
    LQry.Open;
    dmLocalData.localConn.StartTransaction;
    try
      while not LQry.Eof do
      begin
        // Solo insertar si no existe ya localmente con ese server_id
        dmLocalData.ExecLocalSQL(
          'INSERT OR IGNORE INTO local_elaboraciones (' +
          '  local_guid, server_id, id_obrador, id_receta, receta_nombre, ' +
          '  numero_masas, multiplicador, masa_actual, estado, sync_status, created_at' +
          ') VALUES (?, ?, ?, ?, ?, ?, ?, 1, ''PENDIENTE'', ''SYNCED'', ?)',
          [dmLocalData.NewGuidString, LQry.FieldByName('id').AsInteger,
           LQry.FieldByName('id_obrador').AsInteger, LQry.FieldByName('id_receta').AsInteger,
           LQry.FieldByName('receta_nombre').AsString, LQry.FieldByName('numero_masas').AsInteger,
           LQry.FieldByName('multiplicador').AsFloat, FormatDateTime('yyyy-mm-dd hh:nn:ss', Now)]
        );
        LQry.Next;
      end;
      dmLocalData.localConn.Commit;
    except
      dmLocalData.localConn.Rollback;
    end;
    LQry.Close;

    // 7. Descargar Empresas
    LQry.SQL.Text := 'SELECT id, nombre FROM ge_empresas WHERE activo = 1 ORDER BY nombre';
    LQry.Open;
    dmLocalData.localConn.StartTransaction;
    try
      while not LQry.Eof do
      begin
        dmLocalData.ExecLocalSQL(
          'INSERT OR REPLACE INTO local_empresas (id_servidor, nombre) VALUES (?, ?)',
          [LQry.FieldByName('id').AsInteger, LQry.FieldByName('nombre').AsString]
        );
        LQry.Next;
      end;
      dmLocalData.localConn.Commit;
    except
      dmLocalData.localConn.Rollback;
    end;
    LQry.Close;

    // 8. Descargar Obradores
    LQry.SQL.Text := 'SELECT id, id_empresa, nombre FROM ge_obradores WHERE activo = 1 ORDER BY nombre';
    LQry.Open;
    dmLocalData.localConn.StartTransaction;
    try
      while not LQry.Eof do
      begin
        dmLocalData.ExecLocalSQL(
          'INSERT OR REPLACE INTO local_obradores (id_servidor, id_empresa, nombre) VALUES (?, ?, ?)',
          [LQry.FieldByName('id').AsInteger, LQry.FieldByName('id_empresa').AsInteger,
           LQry.FieldByName('nombre').AsString]
        );
        LQry.Next;
      end;
      dmLocalData.localConn.Commit;
    except
      dmLocalData.localConn.Rollback;
    end;
    LQry.Close;

  finally
    LQry.Free;
  end;
end;

procedure TSyncWorkerThread.NotifyStatus(AStatus: TNetStatus; APending: Integer; const AMsg: string);
var
  LEngine: TSyncEngine;
begin
  FNetStatus := AStatus;
  FPendingCount := APending;
  FLastMessage := AMsg;
  LEngine := TSyncEngine(FOwner);

  TThread.Queue(nil,
    procedure
    begin
      if Assigned(LEngine) then
      begin
        LEngine.FNetStatus := AStatus;
        LEngine.FPendingCount := APending;
        LEngine.FLastMessage := AMsg;
        if Assigned(LEngine.FOnSyncStatus) then
          LEngine.FOnSyncStatus(LEngine, AStatus, APending, AMsg);
      end;
    end);
end;

procedure TSyncWorkerThread.Execute;
var
  LPending: Integer;
begin
  while not Terminated do
  begin
    LPending := CountPendingLocalRecords;

    if TestAndConnectServer then
    begin
      try
        if LPending > 0 then
        begin
          NotifyStatus(nsSyncing, LPending, Format('Sincronizando %d transacciones locales con el servidor...', [LPending]));
          ProcessUploadSync;
          LPending := CountPendingLocalRecords;
        end;

        ProcessDownloadSync;
        NotifyStatus(nsOnline, LPending, 'En línea. Servidor y catálogo local sincronizados.');
      except
        on E: Exception do
        begin
          NotifyStatus(nsOffline, LPending, Format('Conexión perdida durante sincronización: %s', [E.Message]));
          DisconnectServer;
        end;
      end;
    end
    else
    begin
      NotifyStatus(nsOffline, LPending, 'Modo Desconectado (Offline). Operando sobre almacenamiento local SQLite.');
    end;

    // Esperar 5 segundos o hasta cancelación
    if FStopEvent.WaitFor(5000) = wrSignaled then
      Break;
  end;

  DisconnectServer;
end;

{ TSyncEngine }

constructor TSyncEngine.Create(const AConfig: TScaleConfig);
begin
  inherited Create;
  FConfig := AConfig;
  FNetStatus := nsOffline;
  FPendingCount := 0;
  FLastMessage := 'Inicializado';
end;

destructor TSyncEngine.Destroy;
begin
  Stop;
  inherited Destroy;
end;

procedure TSyncEngine.Start;
begin
  Stop;
  FWorker := TSyncWorkerThread.Create(Self, FConfig);
  FWorker.Start;
end;

procedure TSyncEngine.Stop;
begin
  if Assigned(FWorker) then
  begin
    FWorker.Stop;
    FreeAndNil(FWorker);
  end;
end;

procedure TSyncEngine.TriggerSync;
begin
  // El hilo ya corre periódicamente cada 5s
end;

end.
