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