//! Plugin de impressão ESC/POS via USB para TM-T20 e compatíveis

use serde::{Deserialize, Serialize};
use std::sync::Mutex;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum PrinterError {
    #[error("Impressora não encontrada. Verifique se está conectada via USB e ligada.")]
    NotFound,
    #[error("Falha ao abrir impressora: {0}")]
    OpenFailed(String),
    #[error("Falha ao enviar dados: {0}")]
    WriteFailed(String),
    #[error("Configuração inválida: {0}")]
    ConfigError(String),
}

impl serde::Serialize for PrinterError {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(&self.to_string())
    }
}

// ── Comandos ESC/POS via plugins::escpos ────────────────────────────────────
use crate::plugins::escpos;

// ── Struct de dados do recibo ────────────────────────────────────────────────

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ReciboData {
    pub loja: String,
    pub data: String,
    pub operador: String,
    pub turno: String,
    pub saldo_troco: f64,
    pub fechamento: f64,
    pub saldo_esperado: f64,
    pub diferenca: f64,
    pub sangrias: f64,
    pub despesas: f64,
    pub vales: f64,
    pub areceber: f64,
    pub credito: f64,
    pub debito: f64,
    pub alimentacao: f64,
    pub voucher: f64,
    pub pixcnpj: f64,
    pub cancelamentos: f64,
}

impl ReciboData {
    pub fn fmt_money(&self, v: f64) -> String {
        format!("R$ {:.2}", v).replace('.', ",")
    }

    /// Gera todos os bytes ESC/POS do recibo 80mm
    pub fn to_escpos(&self) -> Vec<u8> {
        let mut out = Vec::new();

        // Inicializa
        out.extend(escpos::INIT);

        // Cabeçalho
        out.extend(escpos::blank_lines(1));
        out.extend(escpos::title(&self.loja));
        out.extend(escpos::line_center("Fechamento de Caixa"));
        out.extend(escpos::blank_lines(1));
        out.extend(escpos::divider());

        // Info básica
        out.extend(escpos::kv("Data:", &self.data));
        out.extend(escpos::kv("Operador:", &self.operador));
        out.extend(escpos::kv("Turno:", &self.turno));
        out.extend(escpos::divider());

        // Entradas
        out.extend(escpos::line_bold("ENTRADAS"));
        out.extend(escpos::kv("Saldo Troco:", &self.fmt_money(self.saldo_troco)));
        out.extend(escpos::kv("Fechamento:", &self.fmt_money(self.fechamento)));
        out.extend(escpos::kv("Saldo Esperado:", &self.fmt_money(self.saldo_esperado)));
        out.extend(escpos::divider());

        // Débitos
        out.extend(escpos::line_bold("DEBITOS"));
        out.extend(escpos::kv("Sangrias:", &self.fmt_money(self.sangrias)));
        out.extend(escpos::kv("Despesas:", &self.fmt_money(self.despesas)));
        out.extend(escpos::kv("Vales:", &self.fmt_money(self.vales)));
        out.extend(escpos::kv("A Receber:", &self.fmt_money(self.areceber)));
        out.extend(escpos::kv("PIX CNPJ:", &self.fmt_money(self.pixcnpj)));
        out.extend(escpos::divider());

        // Cartões
        out.extend(escpos::line_bold("CARTÕES"));
        out.extend(escpos::kv("Credito:", &self.fmt_money(self.credito)));
        out.extend(escpos::kv("Debito:", &self.fmt_money(self.debito)));
        out.extend(escpos::kv("Alimentacao:", &self.fmt_money(self.alimentacao)));
        out.extend(escpos::kv("Voucher:", &self.fmt_money(self.voucher)));
        out.extend(escpos::kv("Cancelamentos:", &self.fmt_money(self.cancelamentos)));
        out.extend(escpos::divider());

        // Diferença
        let diff_str = if self.diferenca >= 0.0 {
            format!("+{}", self.fmt_money(self.diferenca))
        } else {
            self.fmt_money(self.diferenca)
        };
        out.extend(escpos::line_bold("DIFERENCA:"));
        out.extend(escpos::title(&diff_str));
        out.extend(escpos::blank_lines(2));

        // Rodapé
        out.extend(escpos::divider());
        out.extend(escpos::line_center(
            &chrono::Local::now().format("%d/%m/%Y %H:%M").to_string(),
        ));
        out.extend(escpos::line_center("Obrigado!"));
        out.extend(escpos::blank_lines(4));

        // Corta papel
        out.extend(escpos::FEED_CUT);

        out
    }
}

// ── Printer Manager ─────────────────────────────────────────────────────────

pub struct PrinterManager {
    device_path: Mutex<Option<String>>,
    /// Modo teste: grava bytes num arquivo ao invés de enviar pra USB
    test_mode: Mutex<bool>,
}

impl Default for PrinterManager {
    fn default() -> Self {
        Self {
            device_path: Mutex::new(None),
            test_mode: Mutex::new(false),
        }
    }
}

impl PrinterManager {
    /// Detecta impressora USB conectada
    pub fn detectar(&self) -> Result<String, String> {
        #[cfg(windows)]
        {
            // Tenta encontrar porta USB virtual via WMI/registry
            // Common TM-T20 USB port names on Windows
            let candidates = [
                r"\\.\USB001",  // TM-T20 USB Printing Support
                r"\\.\USB002",
                r"\\.\GPR1",
                r"\\.\COM3",
                r"\\.\COM4",
            ];
            for port in &candidates {
                if std::path::Path::new(port).exists() || self.test_device(port) {
                    *self.device_path.lock().unwrap() = Some(port.to_string());
                    return Ok(port.to_string());
                }
            }
            Err("Nenhuma impressora USB encontrada. Verifique o cabo e se a impressora está ligada.".into())
        }
        #[cfg(not(windows))]
        {
            Err("Deteccao USB s-para apenas no Windows.".into())
        }
    }

    #[cfg(windows)]
    fn test_device(&self, path: &str) -> bool {
        use std::process::Command;
        let out = Command::new("cmd")
            .args(["/C", &format!("type {} < nul 2>&1", path)])
            .output();
        out.map(|o| o.status.success()).unwrap_or(false)
    }

    pub fn configurar(&self, path: String) -> Result<(), String> {
        if path.is_empty() {
            return Err("Caminho vazio".into());
        }
        *self.device_path.lock().unwrap() = Some(path.clone());
        log::info!("Impressora configurada: {}", path);
        Ok(())
    }

    /// Ativa modo teste: grava os bytes ESC/POS num arquivo em vez de imprimir
    pub fn set_test_mode(&self, enabled: bool) {
        *self.test_mode.lock().unwrap() = enabled;
        log::info!("Printer test mode: {}", enabled);
    }

    pub fn imprimir_bytes(&self, data: &[u8]) -> Result<usize, String> {
        // Modo teste: grava arquivo
        if *self.test_mode.lock().unwrap() {
            let test_file = std::env::temp_dir().join("fechamento_recibo_escpos.bin");
            std::fs::write(&test_file, data)
                .map_err(|e| format!("Falha ao gravar arquivo de teste: {}", e))?;
            log::info!("Bytes ESC/POS gravados em: {:?}", test_file);
            return Ok(data.len());
        }

        let path = self
            .device_path
            .lock()
            .unwrap()
            .clone()
            .ok_or_else(|| "Impressora nao configurada. Use o botao de detectar ou configure manualmente.".to_string())?;

        #[cfg(windows)]
        {
            self.windows_write(&path, data)
        }
        #[cfg(not(windows))]
        {
            let _ = data;
            Err("Impressao USB disponivel apenas no Windows.".into())
        }
    }

    #[cfg(windows)]
    fn windows_write(&self, path: &str, data: &[u8]) -> Result<usize, String> {
        use std::io::Write;

        // Tenta via File primeiro (mais simples)
        {
            let file = std::fs::OpenOptions::new()
                .write(true)
                .create(false)
                .open(path);
            if let Ok(mut f) = file {
                let r = f.write_all(data);
                let _ = f.flush();
                if r.is_ok() {
                    return Ok(data.len());
                }
            }
        }

        // Fallback: usa API Windows direta via std::process::Command + > arquivo
        // Isso绕过 problemas de locking do OpenOptions
        let tmp = std::env::temp_dir().join("escpos_tmp.bin");
        std::fs::write(&tmp, data)
            .map_err(|e| format!("Falha ao criar arquivo temporario: {}", e))?;

        let status = std::process::Command::new("cmd")
            .args(["/C", &format!("copy /B {} {} /Y > nul 2>&1", tmp.display(), path)])
            .status();

        let _ = std::fs::remove_file(&tmp);

        if status.map(|s| s.success()).unwrap_or(false) {
            log::info!("Escritos {} bytes para {}", data.len(), path);
            Ok(data.len())
        } else {
            Err(format!(
                "Falha ao enviar dados para {}. Verifique se a impressora USB está \
                 conectada e ligada. Tente configurar manualmente em Config > Impressora.",
                path
            ))
        }
    }
}

// ── Tauri Commands ───────────────────────────────────────────────────────────

static PRINTER: PrinterManager = PrinterManager {
    device_path: Mutex::new(None),
    test_mode: Mutex::new(false),
};

#[tauri::command]
pub fn detectar_impressora() -> Result<String, String> {
    PRINTER.detectar()
}

#[tauri::command]
pub fn configurar_impressora(path: String) -> Result<(), String> {
    PRINTER.configurar(path)
}

#[tauri::command]
pub fn caminho_impressora() -> Option<String> {
    PRINTER.device_path.lock().unwrap().clone()
}

#[tauri::command]
pub fn imprimir_recibo(data: String) -> Result<usize, String> {
    let r: ReciboData =
        serde_json::from_str(&data).map_err(|e| format!("Erro ao analisar dados: {}", e))?;
    let bytes = r.to_escpos();
    log::info!("Imprimindo recibo: {} bytes ESC/POS", bytes.len());
    PRINTER.imprimir_bytes(&bytes)
}

#[tauri::command]
pub fn teste_impressora() -> Result<usize, String> {
    let mut test = Vec::new();
    test.extend(escpos::INIT);
    test.extend(escpos::blank_lines(1));
    test.extend(escpos::title("TESTE DE IMPRESSORA"));
    test.extend(escpos::line_center("O Frangao"));
    test.extend(escpos::line_center(&chrono::Local::now().format("%d/%m/%Y %H:%M").to_string()));
    test.extend(escpos::blank_lines(2));
    test.extend(escpos::FEED_CUT);
    test.extend(escpos::CUT);
    log::info!("Teste impressora: {} bytes", test.len());
    PRINTER.imprimir_bytes(&test)
}

#[tauri::command]
pub fn ativar_modo_teste_impressao() {
    PRINTER.set_test_mode(true);
}

/// Called by Tauri on plugin init (no-op for this plugin)
pub fn init() {
    log::info!("Printer plugin ready. Use detectar_impressora() ou configure manualmente.");
}
