pispas_modules/
printsrvc.rs

1
2// ===== IMPORTS CORE (Todas las plataformas) =====
3use crate::{
4    pdf_manager::PDFManager,
5    send_message,
6    service::{Service, WebSocketWrite},
7};
8
9use async_trait::async_trait;
10use base64::{engine::general_purpose, Engine};
11use easy_trace::prelude::{debug, error, info};
12use futures_util::SinkExt;
13use lazy_static::lazy_static;
14use md5;
15use regex::{Regex, RegexBuilder};
16use serde::{Deserialize, Serialize};
17use serde_json::{json, Value};
18use std::{
19    collections::HashMap,
20    fs::File,
21    io::Write,
22    path::Path,
23    process::{Command, Stdio},
24    sync::Arc,
25};
26use printers::common::base::job::PrinterJobOptions;
27use tokio::sync::Mutex;
28use tokio::task;
29use tokio::time::timeout;
30use tokio::time::{sleep, Duration};
31
32// ===== IMPORTS ESPECÍFICOS DE WINDOWS =====
33#[cfg(target_os = "windows")]
34use {
35    std::{
36        os::windows::{
37            process::CommandExt,
38        },
39    },
40};
41
42
43
44use printers::common::base::printer::Printer;
45use sharing::paths::WKHTMLTOPDF_PATH;
46use sharing::utils::ConfigEnv;
47
48type BoxError = Box<dyn std::error::Error + Send + Sync>;
49
50lazy_static! {
51    /// Global PDF Manager to handle cached PDF files.
52    static ref PDF_MANAGER: Mutex<PDFManager> = Mutex::new(PDFManager::new());
53}
54
55/// Version of the PrintService module.
56pub const PRINT_VERSION: &str = "1.0.0";
57/// Predefined buffer for opening cash drawers.
58pub const BUFFER_OPEN_DRAWER: &[u8] = b"\x1B\x70\x00\x64\xC8";
59/// Name of the `CommandViewer` pseudo-printer. It's not a real OS device —
60/// when clients print to it, the job is routed to the command-viewer module
61/// which renders it into the local diagnostics web UI instead of paper.
62/// Fuente única de verdad para las comparaciones del print path en este crate
63/// (`new()`, `get_print_list()`, el router de jobs y las comprobaciones de
64/// disponibilidad). Nota: el valor por defecto de `LIST_PRINTERS` en la
65/// plantilla `.env` (packages/sharing) es un literal aparte.
66pub const COMMAND_VIEWER_NAME: &str = "CommandViewer";
67
68/// Struct to manage the printing service, including printer handling and job processing.
69pub struct PrintService {
70    printers: Arc<Mutex<Vec<Printer>>>, // List of available printers.
71    list_printers: Arc<Mutex<Vec<String>>>, // List of printer names.
72    config: ConfigEnv,                     // Configuration for the service.
73    command_viewer: bool, // Flag to indicate if CommandViewer is enabled.
74}
75
76impl Clone for PrintService {
77    fn clone(&self) -> Self {
78        PrintService {
79            printers: Arc::clone(&self.printers),
80            list_printers: Arc::clone(&self.list_printers),
81            config: self.config.clone(),
82            command_viewer: self.command_viewer,
83        }
84    }
85}
86
87/// Enum defining the various actions supported by the PrintService.
88#[derive(Deserialize, Serialize, Debug, Clone)]
89#[serde(rename_all = "PascalCase")]
90pub enum PrintAction {
91    Print {
92        content: String,
93        printer_name: Option<String>,
94        copies: Option<u32>,
95        open: bool,
96    },
97    OpenDrawer {
98        printer_name: String,
99    },
100    Check,
101    ListPrinters,
102    Unknown,
103}
104
105impl PrintService {
106    /// Creates a new instance of the PrintService.
107    pub async fn new(mut config: ConfigEnv) -> Self {
108        let devices = PrintService::list_printers().await;
109        println!("PrintService initialized with {} printers", devices.len());
110
111        // Log printer details (solo para debug)
112        let simplified_devices: Vec<String> = devices
113            .iter()
114            .map(|p| format!("name: {}, driver_name: {}", p.name, p.driver_name))
115            .collect();
116        debug!("PrintService initialized with printers: {:?}", simplified_devices);
117
118        // 1) Partimos de las impresoras detectadas en el sistema
119        let mut names: std::collections::HashSet<String> = devices.iter().map(|p| p.name.clone()).collect();
120
121        // 2) Añadimos CommandViewer si está habilitado
122        info!("modules configured: {:?}", config.modules);
123        let command_viewer = config.modules.iter().any(|m| m == "commandViewer");
124        if command_viewer {
125            names.insert(COMMAND_VIEWER_NAME.to_string());
126        }
127        info!("CommandViewer enabled: {}", command_viewer);
128
129        // 3) Unimos con las que ya hubiera en config.list_printers
130        if let Some(cfg_list) = &config.list_printers {
131            for p in cfg_list {
132                names.insert(p.clone());
133            }
134        } else {
135            info!("No printers configured in config, using detected ones");
136        }
137
138        // 4) Convertimos a Vec ordenado (opcional, para determinismo)
139        let mut merged: Vec<String> = names.into_iter().collect();
140        merged.sort();
141
142        // 5) Escribimos la unión de vuelta al config (¡aquí está la clave!)
143        config.list_printers = Some(merged.clone());
144
145        info!("PRINTERS NAMES (merged): {:?}", merged);
146
147        config.save(); // descomenta si tu tipo lo soporta aquí
148
149        PrintService {
150            printers: Arc::new(Mutex::new(devices)),
151            list_printers: Arc::new(Mutex::new(merged)),
152            config,
153            command_viewer,
154        }
155    }
156
157    async fn add_printer_string(&mut self, printer_name: &str) {
158        info!("LIST_PRINTERS BEFORE: {:?}", self.config.list_printers);
159        let mut printers = self.list_printers.lock().await;
160        if !printers.contains(&printer_name.to_string()) {
161            printers.push(printer_name.to_string());
162        }
163        self.config.list_printers
164            .get_or_insert_with(Vec::new)
165            .push(printer_name.to_string());
166        info!("list_printers updated: {:?}", self.config.list_printers);
167        self.config.save();
168    }
169
170    /// Returns the list of printer names the Pispas client should see in the
171    /// config dropdown.
172    ///
173    /// Called on demand when a `getPrinters` WebSocket action arrives. Every
174    /// call re-enumerates the OS printers via `list_printers()` so a printer
175    /// plugged in AFTER the service started shows up without the operator
176    /// having to restart `pispas-modules.exe`. The refresh only happens at
177    /// request time, there is no background polling.
178    ///
179    /// The result is merged with:
180    /// - the `CommandViewer` pseudo-printer (if the module is enabled), and
181    /// - any extra names kept in `config.list_printers` (user-added entries
182    ///   from the configurator).
183    ///
184    /// Both caches (`self.printers` with the rich `Printer` struct used by
185    /// the print path, and `self.list_printers` with the plain names used by
186    /// the WS handler) are updated so subsequent print jobs can resolve the
187    /// newly discovered device without waiting for the fallback refresh in
188    /// the print error path.
189    async fn get_print_list(&self) -> Vec<String> {
190        let devices = PrintService::list_printers().await;
191
192        // Empezamos por las impresoras del SO recién enumeradas, más el
193        // pseudo-printer CommandViewer y las entradas de config. Nada de esto
194        // necesita el lock de list_printers todavía.
195        let mut names: std::collections::HashSet<String> =
196            devices.iter().map(|p| p.name.clone()).collect();
197        if self.command_viewer {
198            names.insert(COMMAND_VIEWER_NAME.to_string());
199        }
200        if let Some(cfg_list) = &self.config.list_printers {
201            for p in cfg_list {
202                names.insert(p.clone());
203            }
204        }
205
206        // Fusionamos las entradas que ya vivían en la caché y reescribimos BAJO
207        // EL MISMO lock, sin soltarlo entre leer y escribir, para no perder
208        // nombres que otra tarea (p.ej. add_printer_string) añada en medio.
209        let merged: Vec<String> = {
210            let mut current = self.list_printers.lock().await;
211            for name in current.iter() {
212                names.insert(name.clone());
213            }
214            let mut merged: Vec<String> = names.into_iter().collect();
215            merged.sort();
216            *current = merged.clone();
217            merged
218        };
219
220        // Refresca la otra caché (Printer completos) usada por el print path.
221        *self.printers.lock().await = devices;
222
223        merged
224    }
225
226
227    /// Extracts CSS properties like margins and page dimensions from the HTML content.
228    ///
229    /// # Arguments
230    /// - `html`: HTML content as a string.
231    fn extract_css_from_html(&self, html: &str) -> HashMap<String, String> {
232        let mut extracted_css = HashMap::new();
233
234        extracted_css.insert("margin-top".to_string(), "0mm".to_string());
235        extracted_css.insert("margin-right".to_string(), "0mm".to_string());
236        extracted_css.insert("margin-bottom".to_string(), "0mm".to_string());
237        extracted_css.insert("margin-left".to_string(), "0mm".to_string());
238        extracted_css.insert("page-width".to_string(), "72mm".to_string());
239        extracted_css.insert("page-height".to_string(), "297mm".to_string());
240
241        let page_re = RegexBuilder::new(r"@page\s*\{\s*([^}]*)\s*\}")
242            .dot_matches_new_line(true)
243            .build();
244
245        match page_re {
246            Ok(page_re) => {
247                // Search for the @page block
248                if let Some(page_match) = page_re.captures(html) {
249                    let page_css = page_match.get(1).map_or("", |m| m.as_str());
250
251                    // Regex to capture page size (size)
252                    if let Ok(size_re) = Regex::new(r"size:\s*([\d.]+mm)\s+([\d.]+mm)(?:\s+\w+)?;")
253                    {
254                        if let Some(size_match) = size_re.captures(page_css) {
255                            let page_width = size_match.get(1).map_or("", |m| m.as_str());
256                            let page_height = size_match.get(2).map_or("", |m| m.as_str());
257                            extracted_css.insert("page-width".to_string(), page_width.to_string());
258                            extracted_css
259                                .insert("page-height".to_string(), page_height.to_string());
260                        }
261                    }
262
263                    // Regex to capture margins
264                    if let Ok(margin_re) =
265                        Regex::new(r"margin-(top|right|bottom|left):\s*([\d.]+[a-z]+);")
266                    {
267                        for margin_match in margin_re.captures_iter(page_css) {
268                            let margin_name = margin_match.get(1).map_or("", |m| m.as_str());
269                            let margin_value = margin_match.get(2).map_or("", |m| m.as_str());
270                            extracted_css.insert(
271                                format!("margin-{}", margin_name),
272                                margin_value.to_string(),
273                            );
274                        }
275                    }
276                }
277            }
278            Err(e) => {
279                error!("Error to create regex: {}", e);
280            }
281        }
282
283        extracted_css
284    }
285
286    /// Processes a given print action and executes the respective logic.
287    async fn run_action(&mut self, action: PrintAction) -> (i32, String) {
288        match action {
289            PrintAction::Print {
290                content,
291                printer_name,
292                copies,
293                open,
294            } => {
295                info!("Printing content");
296
297                // Llamamos a `save_and_print_pdf` para manejar la impresión
298                let print_result = self
299                    .save_and_print_pdf(&content, printer_name.as_deref(), copies.unwrap_or(1))
300                    .await;
301
302                match print_result {
303                    Ok(_) => {
304                        info!("Print job completed successfully.");
305
306                        if open {
307                            if let Some(printer_name) = printer_name {
308                                info!("Opening drawer for printer: {}", printer_name);
309                                let (status, message) = self.open_drawer(&printer_name).await;
310                                if status != 0 {
311                                    error!("Failed to open drawer: {}", message);
312                                } else {
313                                    info!("Drawer opened successfully");
314                                }
315                            } else {
316                                error!("Printer name is not provided for opening drawer");
317                            }
318                        }
319
320                        (0, "print ok".to_string())
321                    }
322                    Err(e) => {
323                        error!("Failed to print: {}", e);
324                        (1, "print failed".to_string())
325                    }
326                }
327            }
328            PrintAction::OpenDrawer { printer_name } => {
329                info!("Opening drawer for printer: {}", printer_name);
330                self.open_drawer(&printer_name).await
331            }
332            PrintAction::Check => {
333                info!("Performing check action");
334                (0, "check ok".to_string())
335            }
336            PrintAction::ListPrinters => {
337                let printers = self.printers.lock();
338                let printer_names = printers
339                    .await
340                    .iter()
341                    .map(|p| p.name.clone())
342                    .collect::<Vec<String>>();
343                (0, serde_json::to_string(&printer_names).unwrap())
344            }
345            PrintAction::Unknown => {
346                error!("Unknown action");
347                (1, "unknown action".to_string())
348            }
349        }
350    }
351
352    async fn send_html_to_kitchen(
353        &self,
354        decoded_html: String,
355        _id: &str,
356        print_name: &str,
357    ) -> Result<(), BoxError> {
358        use serde_json::json;
359        use tokio_tungstenite::{connect_async, tungstenite::Message};
360
361        let url = "ws://127.0.0.1:9001"; // WebSocket del módulo de cocina
362        let (mut socket, _) = connect_async(url).await?;
363        //nuevo uuid v4
364        let uuid = uuid::Uuid::new_v4();
365        // Crear un mensaje JSON para agregar la comanda
366        let command_message = json!({
367            "action": "addCommand",
368            "data": {
369                "id": format!("{}", uuid), // Generar un ID único
370                "html": decoded_html,
371                "order_no": "",
372                "archived": false,
373                "printer": print_name,
374            }
375        });
376
377        // Enviar el mensaje
378        socket
379            .send(Message::Text(command_message.to_string()))
380            .await?;
381        // debug!("HTML sent to kitchen: {:?}", decoded_html);
382        info!("HTML sent to kitchen");
383        Ok(())
384    }
385
386    /// Saves the given HTML content as a PDF and sends it to the specified printer.
387    ///
388    /// # Arguments
389    /// - `content`: The base64-encoded HTML content to be printed.
390    /// - `printer_name`: The name of the printer to which the job should be sent.
391    /// - `copies`: Number of copies to print.
392    ///
393    /// # Returns
394    /// - `Ok(())` if the job was successfully processed.
395    /// - `Err` with an appropriate error message otherwise.
396
397    #[cfg(not(target_os = "windows"))]
398    /// Prints a PDF file using the printers crate (cross-platform)
399    async fn print_pdf(
400        &self,
401        file_path: &Path,
402        printer_name: Option<&str>,
403        copies: u32,
404    ) -> Result<(), BoxError> {
405        info!("Printing with printers crate: {:?} printer {:?} copies {}",
406          file_path, printer_name, copies);
407
408        // Verificar que el archivo existe
409        if !file_path.exists() {
410            error!("PDF file not found: {:?}", file_path);
411            return Err("PDF file not found".into());
412        }
413
414        let printer_name = printer_name.unwrap_or("Default");
415
416        // Skip printing if it's CommandViewer
417        if printer_name == COMMAND_VIEWER_NAME {
418            info!("Skipping print for CommandViewer");
419            return Ok(());
420        }
421
422        // Obtener la impresora usando la librería printers
423        let printer = if printer_name == "Default" {
424            printers::get_default_printer()
425        } else {
426            printers::get_printer_by_name(printer_name)
427        };
428
429        let printer = match printer {
430            Some(p) => p,
431            None => {
432                // Actualizar lista de impresoras y reintentar
433                let new_printers = PrintService::list_printers().await;
434                let mut printers_cache = self.printers.lock().await;
435                *printers_cache = new_printers;
436
437                return Err(format!("Printer '{}' not found", printer_name).into());
438            }
439        };
440
441        // Imprimir el PDF el número de copias especificado
442        for copy in 1..=copies {
443            let job_name = format!("Print Job {} (copy {}/{})",
444                                   file_path.file_name().unwrap_or_default().to_string_lossy(),
445                                   copy, copies);
446
447            let options = PrinterJobOptions {
448                name: Some(&job_name),
449                raw_properties: &[],
450            };
451
452            match printer.print_file(file_path.to_str().unwrap(), options) {
453                Ok(_) => {
454                    info!("Print job {} sent successfully", job_name);
455                }
456                Err(e) => {
457                    error!("Failed to print copy {}: {:?}", copy, e);
458                    return Err(format!("Failed to print: {:?}", e).into());
459                }
460            }
461        }
462
463        info!("All {} copies printed successfully", copies);
464        Ok(())
465    }
466
467    /// Envía datos raw a la impresora usando printer.print() directamente
468    pub async fn send_to_printer(&self, printer_name: &str, data: &[u8]) -> Result<(), BoxError> {
469        info!("Sending raw data to printer: {} ({} bytes)", printer_name, data.len());
470
471        // Buscar la impresora (asumimos que ya existe porque se validó antes)
472        let printers = self.printers.lock().await;
473        let printer = printers.iter()
474            .find(|p| p.name == printer_name)
475            .ok_or_else(|| format!("Printer '{}' not found", printer_name))?
476            .clone();
477
478        drop(printers); // Liberar el lock inmediatamente
479
480        info!("Found printer: {} (driver: {})", printer.name, printer.driver_name);
481
482        // Enviar datos raw usando print() directamente
483        let result = tokio::task::spawn_blocking({
484            let data = data.to_vec();
485            move || {
486                let options = PrinterJobOptions {
487                    name: Some("Open Drawer Command"),  
488                    raw_properties: &[],             
489                };
490
491                printer.print(&data, options)
492            }
493        }).await;
494
495        match result {
496            Ok(Ok(job)) => {
497                info!("Raw data sent successfully. Job: {:?}", job);
498                Ok(())
499            }
500            Ok(Err(e)) => {
501                error!("Failed to print raw data: {:?}", e);
502                Err(format!("Print failed: {:?}", e).into())
503            }
504            Err(e) => {
505                error!("Task spawn failed: {}", e);
506                Err(format!("Task spawn failed: {}", e).into())
507            }
508        }
509    }
510
511    /// Función específica para abrir cajón (wrapper más semántico)
512    pub async fn open_drawer(&self, printer_name: &str) -> (i32, String) {
513        info!("Opening drawer for printer: {}", printer_name);
514
515        match self.send_to_printer(printer_name, BUFFER_OPEN_DRAWER).await {
516            Ok(_) => {
517                info!("Drawer opened successfully");
518                (0, "drawer opened".to_string())
519            }
520            Err(e) => {
521                error!("Failed to open drawer: {}", e);
522                (1, "drawer failed".to_string())
523            }
524        }
525    }
526
527
528    async fn save_and_print_pdf(
529        &mut self,
530        content: &str,
531        printer_name: Option<&str>,
532        copies: u32,
533    ) -> Result<(), BoxError> {
534        // Ensure the jobs directory exists
535        let job_dir = sharing::paths::jobs_dir();
536        if !job_dir.exists() {
537            match std::fs::create_dir_all(&job_dir) {
538                Ok(_) => {
539                    info!("Created jobs directory");
540                }
541                Err(e) => {
542                    error!("Failed to create jobs directory: {}", e);
543                    return Err(e.into());
544                }
545            }
546        }
547
548        // Decode the base64 content
549        let decoded_html = match general_purpose::STANDARD.decode(content) {
550            Ok(decoded) => decoded,
551            Err(e) => {
552                error!("Failed to decode content: {}", e);
553                return Err(e.into());
554            }
555        };
556
557        // Convert the Vec<u8> to a String (assuming the content is valid UTF-8)
558        let decoded_html_str = String::from_utf8(decoded_html.clone())
559            .map_err(|e| format!("Failed to convert decoded content to string: {}", e))?;
560
561        // Extract CSS properties
562        let css_properties = self.extract_css_from_html(&decoded_html_str);
563
564        // Clonar las propiedades CSS necesarias
565        let page_width = css_properties
566            .get("page-width")
567            .unwrap_or(&"72mm".to_string())
568            .clone();
569        let page_height = css_properties
570            .get("page-height")
571            .unwrap_or(&"297mm".to_string())
572            .clone();
573        let margin_top = css_properties
574            .get("margin-top")
575            .unwrap_or(&"0mm".to_string())
576            .clone();
577        let margin_right = css_properties
578            .get("margin-right")
579            .unwrap_or(&"0mm".to_string())
580            .clone();
581        let margin_bottom = css_properties
582            .get("margin-bottom")
583            .unwrap_or(&"0mm".to_string())
584            .clone();
585        let margin_left = css_properties
586            .get("margin-left")
587            .unwrap_or(&"0mm".to_string())
588            .clone();
589
590        let file_md5 = format!("{:x}", md5::compute(decoded_html_str.as_bytes()));
591        let name = printer_name.unwrap_or("POS-80C");
592
593        if self.command_viewer{
594            if let Err(e) = self
595                .send_html_to_kitchen(decoded_html_str.clone(), &file_md5.clone(), name)
596                .await
597            {
598                error!("Failed to send HTML to kitchen: {}", e);
599            }
600        }
601
602        // Build the file path using the hash
603        let pdf_filename = format!("{}_{}.pdf", self.config.service_name, file_md5);
604        let pdf_path = job_dir.join(pdf_filename);
605        info!("PDF path: {:?}", pdf_path);
606
607        //if pdf_filename exists, call print)
608        if !pdf_path.exists() {
609            // Guardamos el HTML como archivo temporal
610            let html_filename = format!("{}_{}.html", self.config.service_name, file_md5);
611            let html_path = job_dir.join(html_filename);
612            let mut file = match File::create(&html_path) {
613                Ok(f) => f,
614                Err(e) => {
615                    error!("Failed to create HTML file: {}", e);
616                    return Err(e.into());
617                }
618            };
619            match file.write_all(&decoded_html.clone()) {
620                Ok(_) => {
621                    info!("HTML content saved to file: {:?}", html_path);
622                }
623                Err(e) => {
624                    error!("Failed to write HTML content to file: {}", e);
625                    return Err(e.into());
626                }
627            }
628
629
630            // Verificar si wkhtmltopdf está disponible
631            if !Path::new(sharing::paths::WKHTMLTOPDF_PATH.as_str()).exists() {
632                info!("WKHTMLTOPDF_PATH: {:?}", sharing::paths::WKHTMLTOPDF_PATH);
633                return Err("wkhtmltopdf executable not found".into());
634            }
635            let wkhtmltopdf_path = WKHTMLTOPDF_PATH.as_str().to_string();
636            // Clone pdf_path to pass to the async task
637            let pdf_path_clone = pdf_path.clone();
638            let html_path_clone = html_path.clone();
639
640            info!("Converting HTML to PDF");
641            let result = timeout(
642                Duration::from_secs(10),
643                task::spawn_blocking(move || {
644                    let mut command = Command::new(&wkhtmltopdf_path);
645                    command
646                        .arg("--page-width")
647                        .arg(page_width)
648                        .arg("--page-height")
649                        .arg(page_height)
650                        .arg("--margin-top")
651                        .arg(margin_top)
652                        .arg("--margin-right")
653                        .arg(margin_right)
654                        .arg("--margin-bottom")
655                        .arg(margin_bottom)
656                        .arg("--margin-left")
657                        .arg(margin_left)
658                        .arg("--print-media-type")
659                        .arg("--no-pdf-compression")           // Evita compresión que puede causar problemas
660                        .arg("--image-quality")
661                        .arg("100")
662                        .arg(&html_path_clone)
663                        .arg(&pdf_path_clone)
664                        .stdout(Stdio::null())
665                        .stderr(Stdio::null());
666
667                    // ✅ Solo aplicar creation_flags en Windows
668                    #[cfg(target_os = "windows")]
669                    command.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW);
670
671                    let status = command
672                        .spawn()
673                        .map_err(|e| format!("Failed to execute wkhtmltopdf: {}", e))?
674                        .wait()
675                        .map_err(|e| format!("Failed to wait for wkhtmltopdf: {}", e))?;
676
677                    if status.success() {
678                        Ok(())
679                    } else {
680                        Err(format!("wkhtmltopdf failed with exit code: {:?}", status))
681                    }
682                }),
683            )
684                .await??;
685            match result {
686                Ok(_) => {
687                    info!("HTML successfully converted to PDF.");
688                }
689                Err(e) => {
690                    error!("Failed to convert HTML to PDF: {}", e);
691                    return Err(e.into());
692                }
693            }
694
695
696            //delete html file
697            match std::fs::remove_file(&html_path) {
698                Ok(_) => {
699                    info!("HTML file deleted: {:?}", html_path);
700                }
701                Err(e) => {
702                    error!("Failed to delete HTML file: {}", e);
703                    return Err(e.into());
704                }
705            }
706        }
707
708        info!("PDF file generated: {:?}", pdf_path);
709            //return if printer is CommandViewer (comparación exacta: una
710            // impresora real cuyo nombre contenga "CommandViewer" no debe colar)
711            if name == COMMAND_VIEWER_NAME {
712                //if printer_name not in list add list_printers
713                if !self.list_printers.lock().await.contains(&name.to_string()) {
714                    info!("Adding CommandViewer to printer list");
715                    self.add_printer_string(name).await;
716                }
717                PDF_MANAGER.lock().await.add_pdf_file(pdf_path);
718                return Ok(());
719            }
720        //if not windows call print_pdf_ else print_with_sumatra
721        #[cfg(target_os = "windows")]
722        {
723            match self.print_with_sumatra(pdf_path.as_path(), printer_name, copies).await {
724                Ok(_) => {
725                    info!("Print job sent successfully");
726                }
727                Err(e) => {
728                    error!("Failed to send print job: {}", e);
729                    PDF_MANAGER.lock().await.add_pdf_file(pdf_path);
730                    return Err(e.into());
731                }
732            }
733        }
734        #[cfg(not(target_os = "windows"))]
735        {
736            match self.print_pdf(pdf_path.as_path(), printer_name, copies).await {
737                Ok(_) => {
738                    info!("Print job sent successfully");
739                }
740                Err(e) => {
741                    error!("Failed to send print job: {}", e);
742                    crate::printsrvc::PDF_MANAGER.lock().await.add_pdf_file(pdf_path);
743                    return Err(e.into());
744                }
745            }
746        }
747
748        PDF_MANAGER.lock().await.add_pdf_file(pdf_path);
749    Ok(())
750    }
751
752
753    #[cfg(target_os = "windows")]
754    /// Prints the given PDF file using the SumatraPDF application.
755    ///
756    /// # Arguments
757    /// - `file_path`: Path to the PDF file to print.
758    /// - `printer_name`: The target printer's name.
759    /// - `copies`: Number of copies to print.
760    ///
761    /// # Returns
762    /// - `Ok(())` if the print job was successfully sent.
763    /// - `Err` with an appropriate error message if the job failed.
764    async fn print_with_sumatra(
765        &self,
766        file_path: &Path,
767        printer_name: Option<&str>,
768        copies: u32,
769    ) -> Result<(), BoxError> {
770        info!(
771            "Printing with SumatraPDF file {:?} printer {:?} copies {}",
772            file_path, printer_name, copies
773        );
774        if !Path::new(sharing::paths::SUMATRA_PATH.as_str()).exists() {
775            return Err("SumatraPDF executable not found".into());
776        }
777        let name = printer_name.unwrap_or("Default Printer");
778
779        // Verifica si la impresora está disponible
780        let printer_exists = {
781            let printers = self.printers.lock().await; // Bloquea el mutex aquí
782            printers.iter().any(|p| p.name == name) || name == COMMAND_VIEWER_NAME
783        };
784
785        // Check if the printer is available
786        if !printer_exists {
787            let new_printers = PrintService::list_printers().await;
788            let mut printers = self.printers.lock().await; // Bloquea nuevamente para actualizar
789            if !new_printers.iter().any(|p| p.name == name) {
790                return Err("Printer not found".into());
791            }
792            *printers = new_printers;
793        }
794
795        for _ in 0..copies {
796            let file_path = file_path.to_path_buf(); // Clona el Path en un PathBuf
797            let name = name.to_string(); // Clona el nombre para el hilo
798
799            let result = timeout(
800                Duration::from_secs(5),
801                task::spawn_blocking(move || {
802                    Command::new(sharing::paths::SUMATRA_PATH.as_str())
803                        .arg("-print-to")
804                        .arg(name)
805                        .arg("-print-settings")
806                        .arg("noscale") // Separar correctamente la opción y su valor
807                        .arg(file_path)
808                        .stdout(Stdio::null())
809                        .stderr(Stdio::null())
810                        .status()
811                        .expect("Failed to execute print command")
812                }),
813            )
814                .await;
815
816            match result {
817                Ok(Ok(status)) => {
818                    if !status.success() {
819                        error!("º command failed: {:?}", status);
820                        return Err("Print command failed".into());
821                    }
822                }
823                Ok(Err(e)) => {
824                    error!("Task spawn failed: {:?}", e);
825                    return Err("Task spawn failed".into());
826                }
827                Err(_) => {
828                    error!("Print command timed out");
829                    return Err("Print command timed out".into());
830                }
831            }
832        }
833
834        info!("Print job sent successfully");
835        Ok(())
836    }
837
838    /// Finds a printer by name from the cached list of printers.
839    ///
840    /// # Arguments
841    /// - `printer_name`: Name of the printer to locate.
842    ///
843    /// # Returns
844    /// - `Option<Printer>` containing the printer details if found.
845    /// - `None` if the printer does not exist in the cached list.
846    pub async fn _find_printer(&self, printer_name: &str) -> Option<Printer> {
847        self.printers
848            .lock()
849            .await
850            .iter()
851            .find(|p| p.name == printer_name)
852            .cloned()
853    }
854
855
856
857    /// Retrieves the list of available printers.
858    ///
859    /// # Returns
860    /// - A vector of `Printer` objects representing the available printers.
861    async fn list_printers() -> Vec<Printer> {
862        // iOS no expone impresoras del SO por esta vía.
863        #[cfg(target_os = "ios")]
864        {
865            Vec::new()
866        }
867
868        // `printers::get_printers()` es una llamada SÍNCRONA al SO que puede
869        // tardar; se ejecuta en `spawn_blocking` para no bloquear el executor
870        // async (esta función se invoca en cada `getPrinters`).
871        #[cfg(not(target_os = "ios"))]
872        {
873            match tokio::task::spawn_blocking(|| printers::get_printers()).await {
874                Ok(list) => list,
875                Err(e) => {
876                    error!("Error enumerando impresoras del SO: {}", e);
877                    Vec::new()
878                }
879            }
880        }
881    }
882
883    /// Processes the given JSON action object and executes the respective operations.
884    ///
885    /// # Arguments
886    /// - `action`: A JSON `Value` containing the action details.
887    ///
888    /// # Returns
889    /// - `(0, String)` if the actions were processed successfully.
890    /// - `(1, String)` if an error occurred during processing.
891    async fn process_action(&mut self, action: Value, write: WebSocketWrite) -> (i32, String) {
892        let action_type = match action.get("ACTION").and_then(Value::as_str) {
893            Some(action_str) => action_str,
894            None => {
895                error!("Missing or invalid ACTION field");
896                return (1, "Missing ACTION field".to_string());
897            }
898        };
899
900        if action_type == "getPrinters" {
901            info!("Processing getPrinters action");
902            let printers = self.get_print_list().await;
903
904            let uuid = match action.get("UUIDV4") {
905                Some(Value::String(uuid)) => uuid,
906                _ => {
907                    error!("Missing or invalid MESSAGE_UUID field");
908                    return (1, "Missing MESSAGE_UUID field".to_string());
909                }
910            };
911            let response = json!({
912            "SERVICE_NAME": "PRINT",
913            "SERVICE_VERS": PRINT_VERSION,
914            "MESSAGE_TYPE": "RESPONSE",
915            "MESSAGE_EXEC": "SUCCESS",
916            "MESSAGE_UUID": uuid,
917            "MESSAGE_DATA": printers,
918        }).to_string();
919
920            send_message(&write, response).await;
921            return (0, "Printers list sent".to_string());
922        }
923
924        let action_map = match action.get("ACTION") {
925            Some(Value::String(action_str)) => {
926                match serde_json::from_str::<serde_json::Map<String, Value>>(action_str) {
927                    Ok(map) => map,
928                    Err(e) => {
929                        error!("Failed to parse ACTION as JSON object: {}", e);
930                        return (1, format!("Invalid ACTION format: {}", e));
931                    }
932                }
933            }
934            _ => {
935                error!("Missing or invalid ACTION field");
936                return (1, "Missing ACTION field".to_string());
937            }
938        };
939
940        for (device_name, device_actions) in action_map {
941            info!("Processing actions for device: {}", device_name);
942
943            if device_actions.is_null() {
944                return (0, "Device is reachable".to_string());
945            }
946
947            // ✅ MANEJO CORRECTO SEGÚN EL TIPO DE DEVICE_ACTIONS
948            match &device_actions {
949                Value::Object(actions) => {
950                    debug!("Device actions is object: {:?}", actions);
951
952                    // Check if the "print" field exists and is not null
953                    if let Some(Value::String(print_content)) = actions.get("print") {
954                        info!("Found print content, length: {}", print_content.len());
955
956                        if let Some(print_action) = self.parse_print_action(&device_name, &actions) {
957                            let result = self.run_action(print_action).await;
958                            if result.0 != 0 {
959                                return result;
960                            }
961                        }
962                    } else {
963                        // Process as drawer open if not a valid print
964                        if let Some(open_action) = self.parse_open_action(&device_name, &actions) {
965                            info!("open_action {:?}", open_action);
966                            let result = self.run_action(open_action).await;
967                            if result.0 != 0 {
968                                return result;
969                            }
970                        }
971                    }
972                }
973                Value::String(content) => {
974                    // 🔥 SI ES UN STRING, ASUMIR QUE ES CONTENIDO BASE64 DIRECTO
975                    info!("Device actions is string (assuming base64), length: {}", content.len());
976
977                    let print_action = PrintAction::Print {
978                        content: content.clone(),
979                        printer_name: Some(device_name.clone()),
980                        copies: Some(1),
981                        open: false,
982                    };
983
984                    let result = self.run_action(print_action).await;
985                    if result.0 != 0 {
986                        return result;
987                    }
988                }
989                _ => {
990                    let error_msg = format!("Actions for '{}' must be a JSON object or string, got: {:?}",
991                                            device_name, device_actions);
992                    error!("{}", error_msg);
993                    return (1, error_msg);
994                }
995            }
996        }
997
998        (0, "All actions processed successfully".to_string())
999    }
1000
1001    /// Parses a print action from a JSON object.
1002    ///
1003    /// # Arguments
1004    /// - `device`: Name of the printer.
1005    /// - `actions`: JSON object containing the action details.
1006    ///
1007    /// # Returns
1008    /// - `Option<PrintAction>` if the action could be parsed successfully.
1009    /// - `None` if the action is invalid or unsupported.
1010    fn parse_print_action(
1011        &self,
1012        device: &str,
1013        actions: &serde_json::Map<String, Value>,
1014    ) -> Option<PrintAction> {
1015        if let Some(print_content) = actions.get("print") {
1016            let content = match print_content {
1017                Value::String(s) => {
1018                    info!("Print content for device {}: {} chars", device, s.len());
1019                    s.clone()
1020                }
1021                _ => {
1022                    error!("Print content for device {} is not a string: {:?}", device, print_content);
1023                    return None;
1024                }
1025            };
1026
1027            let copies = actions
1028                .get("copies")
1029                .and_then(Value::as_u64)
1030                .map(|c| c as u32)
1031                .unwrap_or(1);
1032
1033            let open = actions
1034                .get("open")
1035                .and_then(Value::as_bool)
1036                .unwrap_or(false);
1037
1038            info!("Parsed print action - device: {}, copies: {}, open: {}", device, copies, open);
1039
1040            Some(PrintAction::Print {
1041                content,
1042                printer_name: Some(device.to_string()),
1043                copies: Some(copies),
1044                open,
1045            })
1046        } else {
1047            debug!("No 'print' field found in actions for device: {}", device);
1048            None
1049        }
1050    }
1051
1052    /// Parses an open drawer action from a JSON object.
1053    ///
1054    /// # Arguments
1055    /// - `device`: Name of the printer.
1056    /// - `actions`: JSON object containing the action details.
1057    ///
1058    /// # Returns
1059    /// - `Option<PrintAction>` if the action could be parsed successfully.
1060    /// - `None` if the action is invalid or unsupported.
1061    fn parse_open_action(
1062        &self,
1063        device: &str,
1064        actions: &serde_json::Map<String, Value>,
1065    ) -> Option<PrintAction> {
1066        if let Some(open_drawer) = actions.get("open") {
1067            if open_drawer.as_bool().unwrap_or(false) {
1068                return Some(PrintAction::OpenDrawer {
1069                    printer_name: device.to_string(),
1070                });
1071            }
1072        }
1073        None
1074    }
1075}
1076
1077#[async_trait]
1078impl Service for PrintService {
1079    /// Executes a print service action based on the provided JSON input.
1080    ///
1081    /// # Arguments
1082    ///
1083    /// * `action` - A JSON value containing the action details.
1084    /// * `_write` - A `WebSocketWrite` (unused in this implementation).
1085    ///
1086    /// # Returns
1087    ///
1088    /// A tuple containing:
1089    /// * `i32` - Status code (0 for success, 1 for failure).
1090    /// * `String` - A descriptive message about the action result. If the action takes longer than 2 seconds, returns an asynchronous processing message.
1091    async fn run(&self, action: Value, _write: WebSocketWrite) -> (i32, String) {
1092        let deserialized_action = if action.is_string() {
1093            match serde_json::from_str::<Value>(action.as_str().unwrap_or("")) {
1094                Ok(val) => val,
1095                Err(err) => {
1096                    error!("Failed to parse string JSON: {}", err);
1097                    return (1, format!("Invalid action format: {}", err));
1098                }
1099            }
1100        } else {
1101            action
1102        };
1103
1104        let result: Arc<Mutex<Option<(i32, String)>>> = Arc::new(Mutex::new(None)); // Explicit type annotation
1105        let result_clone = Arc::clone(&result);
1106        let mut self_clone = self.clone();
1107
1108        // Spawn the background task
1109        tokio::spawn(async move {
1110            let process_result = self_clone.process_action(deserialized_action, _write).await;
1111            let mut lock = result_clone.lock().await;
1112            *lock = Some(process_result);
1113        });
1114
1115        // Active wait for up to 2 seconds
1116        let start = tokio::time::Instant::now();
1117        while start.elapsed() < Duration::from_secs(2) {
1118            {
1119                let lock = result.lock().await;
1120                if let Some((code, msg)) = &*lock {
1121                    return (*code, msg.clone()); // If result is ready, return it
1122                }
1123            }
1124            sleep(Duration::from_millis(100)).await; // Small delay to avoid busy waiting
1125        }
1126
1127        // If no result within 2 seconds, return async message but continue processing
1128        (0, "Action is being processed asynchronously".to_string())
1129    }
1130
1131    /// Converts the service instance into a `dyn Any` reference.
1132    ///
1133    /// # Returns
1134    ///
1135    /// A reference to `dyn Any` for dynamic type checks.
1136    fn as_any(&self) -> &dyn std::any::Any {
1137        self
1138    }
1139
1140    /// Stops the print service, performing any necessary cleanup tasks.
1141    fn stop_service(&self) {
1142        info!("Stopping PrintService...");
1143    }
1144
1145    /// Retrieves the current version of the PrintService.
1146    ///
1147    /// # Returns
1148    ///
1149    /// A `String` containing the version of the service.
1150    fn get_version(&self) -> String {
1151        PRINT_VERSION.to_string()
1152    }
1153}