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