from fabric import Connection
import subprocess
from enum import Enum
import inspect
import yaml
from lib.echo import echo
from pathlib import Path
import traceback
from lib.obter_configuracoes import obter_configuracoes
import re
from lib.criptografia_senha import descriptografar
import time
import os  
import shutil

class TipoConexao(Enum):
    SSH = 1
    WSL = 2
    LINUX = 3

class ClienteLinux():
    def __init__(self, tipo_conexao : TipoConexao, host = None, usuario = None, senha = None) -> None:
        self.tipo_conexao = tipo_conexao

        self.host = host
        self.usuario = usuario
        self.senha = senha

    
    def _obter_conexao_ssh(self):
        return Connection(
            host = self.host,
            user = self.usuario,
            port = 22,
            connect_timeout=5,
            connect_kwargs={"password": self.senha})
    
    def _conexao_ok_ssh(self):

        if not self.host or not self.usuario or not self.senha:
            return False

        try:

            with self._obter_conexao_ssh() as conn:
                result = conn.run("hostname", hide=True)

                return result.ok
        except:
            return False
        
    def _conexao_ok_wsl(self):

        def verificar():
            try:
                result = subprocess.run(["wsl", "hostname"], capture_output=True)
                return result.returncode == 0
            except:
                return False
            
        if (verificar()):
            return True
        else:
            tempo = 3
            echo(f"Falha na verificacao se WSL esta ativo. Tentando novamente em {tempo} segundos...")
            time.sleep(tempo)

            return verificar()

    def _conexao_ok_linux(self):
        try:
            return os.name == "posix" and os.uname().sysname.lower() == "linux"
        except:
            return False
    
    def conexao_ok(self):
        if self.tipo_conexao == TipoConexao.SSH:
            return self._conexao_ok_ssh()
        elif self.tipo_conexao == TipoConexao.WSL:
            return self._conexao_ok_wsl()
        elif self.tipo_conexao == TipoConexao.LINUX:
            return self._conexao_ok_linux()

        return False

    def _traduzir_mensagem_erro(self, mensagem):
        mensagem_texto = str(mensagem)
        mensagem_minuscula = mensagem_texto.lower()

        if "no space left on device" in mensagem_minuscula:
            return (
                "Servidor Linux sem espaco em disco. "
                "Libere espaco no servidor e tente novamente. "
                f"Detalhe original: {mensagem_texto}"
            )

        if "disk quota exceeded" in mensagem_minuscula:
            return (
                "Limite de espaco em disco excedido no servidor Linux. "
                "Libere espaco ou ajuste a cota e tente novamente. "
                f"Detalhe original: {mensagem_texto}"
            )

        if "size mismatch in put!" in mensagem_minuscula and "0 !=" in mensagem_minuscula:
            return (
                "Falha ao copiar arquivo para o servidor Linux. "
                "O arquivo remoto ficou com 0 bytes, o que normalmente indica falta de espaco, "
                "cota excedida, falta de inodes ou outra falha de escrita no servidor. "
                "Verifique o armazenamento do Linux e tente novamente. "
                f"Detalhe original: {mensagem_texto}"
            )

        if "size mismatch in put!" in mensagem_minuscula:
            return (
                "Falha ao copiar arquivo para o servidor Linux devido a divergencia no tamanho "
                "do arquivo gravado remotamente. Verifique espaco, cota e permissao de escrita "
                f"no servidor. Detalhe original: {mensagem_texto}"
            )

        return mensagem_texto
    
    def _executar_comando_wsl(self,comando:str, disparar_excecao_se_retorno_nao_zero = True):

        def executar(ultima_tentativa = False):
            try:

                time.sleep(1)

                result = subprocess.run(
                    ["wsl", "--", "sh", "-lc", comando],
                    capture_output=True
                )

                if disparar_excecao_se_retorno_nao_zero and result.returncode != 0:
                    
                    raise ErroExecucaoComando(
                        comando,
                        self._traduzir_mensagem_erro(
                            "codigo de retorno: {codigo_de_retorno}, saida: {saida}, erro: {erro}".format(
                                codigo_de_retorno = result.returncode,
                                saida = result.stdout.decode('utf-8'),
                                erro = result.stderr.decode('utf-8')
                            )
                        )
                    )
                
                
                
                return ResultadoComando(result.returncode, result.stdout.decode('utf-8'))
                
            except Exception as e:
                if ultima_tentativa:
                    echo(traceback.format_exc() , "error")

                raise ErroExecucaoComando(comando, self._traduzir_mensagem_erro(str(e)))
            
        
        try:
            return executar(ultima_tentativa=False)
        except:
            tempo = 3
            echo(f"Houve erro na primeira execucao do comando, tentando novamente em {tempo} segundos")
            time.sleep(tempo)

            return executar(ultima_tentativa=True)


    def _executar_comando_ssh(self,comando, disparar_excecao_se_retorno_nao_zero = True):

        try:
            with self._obter_conexao_ssh() as conn:
                result = conn.run(comando, warn = not disparar_excecao_se_retorno_nao_zero)

            if disparar_excecao_se_retorno_nao_zero and result.return_code != 0:
                raise ErroExecucaoComando(comando, self._traduzir_mensagem_erro(result.stderr))
            
            return ResultadoComando(result.return_code, result.stdout)
            
        except Exception as e:
            echo(traceback.format_exc() , "error")
            raise ErroExecucaoComando(comando, self._traduzir_mensagem_erro(str(e)))

    def _executar_comando_linux(self,comando:str, disparar_excecao_se_retorno_nao_zero = True):

        try:
            result = subprocess.run(
                ["sh", "-lc", comando],
                capture_output=True,
                text=True
            )

            if disparar_excecao_se_retorno_nao_zero and result.returncode != 0:
                raise ErroExecucaoComando(
                    comando,
                    self._traduzir_mensagem_erro(
                        "codigo de retorno: {codigo_de_retorno}, saida: {saida}, erro: {erro}".format(
                            codigo_de_retorno = result.returncode,
                            saida = result.stdout,
                            erro = result.stderr
                        )
                    )
                )

            return ResultadoComando(result.returncode, result.stdout)

        except Exception as e:
            echo(traceback.format_exc() , "error")
            raise ErroExecucaoComando(comando, self._traduzir_mensagem_erro(str(e)))
    
    def executar_comando(self,comando, disparar_excecao_se_retorno_nao_zero = True , echoar = True, descricao = ""):

        if not self.conexao_ok():
            raise ErroConexao(f"Não foi possível conectar ao servidor Linux. Dados de conexao: {self.obter_dados_cliente_linux()}")
        
        if echoar:
            echo(comando)

        if descricao:
            echo(descricao)
        
        if self.tipo_conexao == TipoConexao.WSL:
            return self._executar_comando_wsl(comando, disparar_excecao_se_retorno_nao_zero)
        elif self.tipo_conexao == TipoConexao.SSH:
            return self._executar_comando_ssh(comando,disparar_excecao_se_retorno_nao_zero)
        elif self.tipo_conexao == TipoConexao.LINUX:
            return self._executar_comando_linux(comando,disparar_excecao_se_retorno_nao_zero)

    def _copiar_arquivo_ssh(self,arquivo_origem, arquivo_destino):
        try:

            with self._obter_conexao_ssh() as conn:
                conn.put(arquivo_origem, remote=arquivo_destino)
            
        except Exception as e:
            echo(traceback.format_exc() , "error")
            raise ErroExecucaoComando(
                f"cp {arquivo_origem} {arquivo_destino}",
                self._traduzir_mensagem_erro(str(e))
            )

    def _copiar_arquivo_wsl(self,arquivo_origem, arquivo_destino):
        try:

            if self.executar_comando(f"test -f {arquivo_destino}", disparar_excecao_se_retorno_nao_zero=False).codigo_retorno == 0:
                self.executar_comando(f"rm -f {arquivo_destino}")

            arquivo_origem_fs_linux = re.compile(r'c:\\', re.IGNORECASE).sub('/mnt/c/', arquivo_origem).replace("\\", "/")

            result = subprocess.run(["wsl", "cp", "-auv", arquivo_origem_fs_linux, arquivo_destino],capture_output=True)

            if result.returncode != 0:
                raise ErroExecucaoComando(
                    f"cp {arquivo_origem_fs_linux} {arquivo_destino}",
                    self._traduzir_mensagem_erro(result.stderr.decode('utf-8'))
                )
            
        except Exception as e:
            echo(traceback.format_exc() , "error")
            raise ErroExecucaoComando(
                f"cp {arquivo_origem} {arquivo_destino}",
                self._traduzir_mensagem_erro(str(e))
            )

    def _normalizar_caminho_origem_linux(self, caminho):
        return str(caminho).replace("\\", "/")

    def _copiar_arquivo_linux(self,arquivo_origem, arquivo_destino):
        try:
            Path(arquivo_destino).parent.mkdir(parents=True, exist_ok=True)
            shutil.copy2(arquivo_origem, arquivo_destino)

        except Exception as e:
            echo(traceback.format_exc() , "error")
            raise ErroExecucaoComando(
                f"cp {arquivo_origem} {arquivo_destino}",
                self._traduzir_mensagem_erro(str(e))
            )

    def copiar_arquivo(self,arquivo_origem, arquivo_destino):
        if not self.conexao_ok():
            raise ErroConexao(f"Não foi possível conectar ao servidor Linux. Dados de conexao: {self.obter_dados_cliente_linux()}")

        arquivo_origem_copia = self._normalizar_caminho_origem_linux(arquivo_origem) if self.tipo_conexao == TipoConexao.LINUX else arquivo_origem
        
        if not Path(arquivo_origem_copia).exists():
            raise ArquivoInexistente(arquivo_origem)
        
        echo(f"copiando {arquivo_origem_copia} => {arquivo_destino}")

        if self.tipo_conexao == TipoConexao.WSL:
            self._copiar_arquivo_wsl(arquivo_origem_copia, arquivo_destino)
        elif self.tipo_conexao == TipoConexao.SSH:          
            self._copiar_arquivo_ssh(arquivo_origem_copia, arquivo_destino)
        elif self.tipo_conexao == TipoConexao.LINUX:
            self._copiar_arquivo_linux(arquivo_origem_copia, arquivo_destino)

    def copiar_pasta(self, pasta_origem: str, pasta_destino: str, extensoes=None, recursivo: bool = True):
        """
        Copia arquivos de uma pasta local para o servidor Linux (SSH/WSL),
        filtrando por extensão e preservando a estrutura de subpastas.

        :param pasta_origem: Caminho da pasta local (Windows).
        :param pasta_destino: Caminho base no Linux/WSL.
        :param extensoes: String (".crt") ou lista [".crt", ".key"]. Se None, copia todos.
        :param recursivo: Se True, percorre subpastas.
        """

        if not self.conexao_ok():
            raise ErroConexao(f"Não foi possível conectar ao servidor Linux. Dados: {self.obter_dados_cliente_linux()}")

        pasta_origem_copia = self._normalizar_caminho_origem_linux(pasta_origem) if self.tipo_conexao == TipoConexao.LINUX else pasta_origem
        pasta_origem_path = Path(pasta_origem_copia)

        if not pasta_origem_path.exists() or not pasta_origem_path.is_dir():
            raise ArquivoInexistente(pasta_origem)

        # Normalizar extensões
        extensoes_normalizadas = None
        if extensoes:
            if isinstance(extensoes, str):
                extensoes = [extensoes]

            extensoes_normalizadas = {
                (("." + ext.lower().lstrip(".")) if not ext.startswith(".") else ext.lower())
                for ext in extensoes
            }

        echo(f"Iniciando cópia da pasta {pasta_origem_copia} para {pasta_destino}")

        arquivos_iter = pasta_origem_path.rglob("*") if recursivo else pasta_origem_path.glob("*")

        for arquivo in arquivos_iter:
            if not arquivo.is_file():
                continue

            if extensoes_normalizadas is not None:
                if arquivo.suffix.lower() not in extensoes_normalizadas:
                    continue

            rel_path = arquivo.relative_to(pasta_origem_path)

            # Pasta remota
            if rel_path.parent == Path("."):
                pasta_remota = pasta_destino.rstrip("/")
            else:
                subpasta = str(rel_path.parent).replace(os.sep, "/")
                pasta_remota = f"{pasta_destino.rstrip('/')}/{subpasta}"

            # Criar pasta remota
            self.executar_comando(f"mkdir -p {pasta_remota}", echoar=False)

            # Destino final
            arquivo_destino = f"{pasta_remota}/{arquivo.name}"

            self.copiar_arquivo(str(arquivo), arquivo_destino)


    def obter_dados_cliente_linux(self):

        if self.tipo_conexao == TipoConexao.SSH:
            s = f"""
            tipo de conexao: SSH
            host: {self.host}
            usuario: {self.usuario}
            """
        elif self.tipo_conexao == TipoConexao.WSL:
            s = f"""
            tipo de conexao: WSL
            """
        elif self.tipo_conexao == TipoConexao.LINUX:
            s = f"""
            tipo de conexao: LINUX
            usuario: {self.usuario}
            """
        else:
            s = f"""
            tipo de conexao: NAO_DEFINIDO
            """

        return inspect.cleandoc(s)
    
def obter_cliente_linux(arquivo_configuracao = None, configuracoes = None):

    configuracoes_ = configuracoes if configuracoes is not None else obter_configuracoes(arquivo_configuracao)

    if not configuracoes_:
        echo(f"Falha ao carregar configuracoes do arquivo {arquivo_configuracao}" , "error")
        return None

    tipo_conexao = str(configuracoes_['acessos']['servidorLinux']['tipoConexao']).upper()
    host_ssh = None if tipo_conexao != "SSH" else configuracoes_['acessos']['servidorLinux']['host']
    usuario_ssh = None if tipo_conexao != "SSH" else configuracoes_['acessos']['servidorLinux']['usuario']
    senha_ssh = None if tipo_conexao != "SSH" else descriptografar(configuracoes_['acessos']['servidorLinux']['senha'] )
    usuario_linux = None if tipo_conexao != "LINUX" else configuracoes_['acessos']['servidorLinux']['usuario']

    clienteLinux = ClienteLinux(
        tipo_conexao =  TipoConexao.WSL if tipo_conexao == "WSL" else \
                        TipoConexao.SSH if tipo_conexao == "SSH" else \
                        TipoConexao.LINUX if tipo_conexao == "LINUX" else None,
        usuario=usuario_ssh if tipo_conexao == "SSH" else usuario_linux,
        senha=senha_ssh,
        host=host_ssh
        
    )

    return clienteLinux

class ErroExecucaoComando(Exception):
    def __init__(self, comando, mensagem):
        self.comando = comando
        self.mensagem = mensagem

    def __str__(self):
        return f"Erro ao executar comando {self.comando} => {self.mensagem}"
    
class ErroConexao(Exception):
    def __init__(self, mensagem):
        self.mensagem = mensagem

    def __str__(self):
        return f"Erro ao conectar ao servidor Linux => {self.mensagem}"
    
class ArquivoInexistente(Exception):
    def __init__(self, arquivo):
        self.arquivo = arquivo

    def __str__(self):
        return f"Arquivo {self.arquivo} inexistente"    
    
class ResultadoComando():
    def __init__(self, codigo_retorno, saida):
        self.codigo_retorno = codigo_retorno
        self.saida = saida

    def __str__(self):
        return f"Retorno: {self.codigo_retorno} => {self.saida}"
