# Archivo: DocsManagement.py
# Ruta: src/App/Utilities_module/DocsManagement.py
# Descripción: Módulo REUTILIZABLE para gestión de documentos y subida FTP con ClamAV
# Autor: Equipo de Desarrollo IGSA
# Fecha: 2025

import os
import tempfile
import ftplib
import subprocess
import logging
from pathlib import Path
from werkzeug.utils import secure_filename
from werkzeug.datastructures import FileStorage
from dotenv import load_dotenv
from typing import Optional
from datetime import datetime

# Importar solo las funciones que usamos
from Consultas_SQL.Utilities.DocsManagementSQL import (
    crear_docs_head,
    crear_docs_detail,
    obtener_siguiente_doc_line
)

load_dotenv()

# Configurar logging
logger = logging.getLogger('docs_management')

# ============================================
# CONFIGURACIÓN GLOBAL
# ============================================

# Tamaño máximo de archivo (2MB en bytes)
TAMAÑO_MAXIMO = 2 * 1024 * 1024  # 2MB

# Extensiones permitidas
EXTENSIONES_PERMITIDAS = {'.pdf', '.doc', '.docx', '.jpg', '.jpeg', '.png', '.xlsx', '.xls'}

# Base URL para acceso web
BASE_URL = "https://file.sycelephant.com"

# Configuración FTP
FTP_CONFIG = {
    'host': os.getenv('FTP_HOST'),
    'user': os.getenv('FTP_USER'),
    'password': os.getenv('FTP_PASS'),
    'base_path': '/public_html/file/'
}

# ============================================
# FUNCIÓN PRINCIPAL REUTILIZABLE
# ============================================

def upload_document(file_obj: FileStorage, origen: str, user_id: int, created_by: str, 
                   title: str, description: str, ruta_completa: str) -> dict:
    """
    Función REUTILIZABLE para subir UN documento con validación completa
    Puede usarse para: Formularios, Cotizaciones, Oportunidades, etc.
    
    Args:
        file_obj (FileStorage): Objeto archivo de Flask
        origen (str): Módulo origen (ej: "Q_SpQ_FormsHead", "Q_SpQ_Quotations")
        user_id (int): ID del usuario
        created_by (str): Nombre completo del usuario
        title (str): Título del archivo
        description (str): Descripción del archivo
        ruta_completa (str): Ruta completa (ej: "Ventas/Formularios/1001-2/FormPEmx")
    
    Returns:
        dict: Resultado de la operación
    """
    
    temp_path = None
    
    try:
        logger.info(f"Iniciando upload de documento: {title} para {created_by}")
        
        # 1. VALIDACIONES BÁSICAS
        validacion_result = validar_archivo_basico(file_obj)
        if not validacion_result['valid']:
            return {
                'success': False,
                'message': validacion_result['message'],
                'code': 'VALIDATION_ERROR'
            }
        
        # 2. CREAR ARCHIVO TEMPORAL
        temp_path = crear_archivo_temporal(file_obj)
        if not temp_path:
            return {
                'success': False,
                'message': 'Error al crear archivo temporal',
                'code': 'TEMP_FILE_ERROR'
            }
        
        # 3. VALIDACIÓN DE SEGURIDAD (ClamAV)
        seguridad_result = validar_seguridad_archivo(temp_path)
        if not seguridad_result['valid']:
            return {
                'success': False,
                'message': seguridad_result['message'],
                'code': 'SECURITY_ERROR'
            }
        
        # 4. SUBIR ARCHIVO A FTP
        upload_result = subir_archivo_ftp(temp_path, title, ruta_completa)
        if not upload_result['success']:
            return upload_result
        
        # 5. CREAR/OBTENER DOCS_HEAD
        docs_head_result = gestionar_docs_head(origen, user_id, created_by)
        if not docs_head_result['success']:
            return docs_head_result
        
        docs_id = docs_head_result['docs_id']
        
        # 6. CREAR DOCS_DETAIL
        docs_detail_result = crear_registro_documento(
            docs_id, title, description, upload_result['remote_path'], 
            validacion_result['extension']
        )
        if not docs_detail_result['success']:
            return docs_detail_result
        
        # 7. CONSTRUIR RESPUESTA EXITOSA
        resultado_final = {
            'success': True,
            'message': f'Documento "{title}" subido exitosamente',
            'data': {
                'docs_id': docs_id,
                'doc_line_id': docs_detail_result['doc_line_id'],
                'download_url': upload_result['download_url'],
                'remote_path': upload_result['remote_path'],
                'file_size': validacion_result['file_size'],
                'file_type': validacion_result['extension'],
                'original_name': file_obj.filename
            }
        }
        
        logger.info(f"Documento subido exitosamente: {docs_detail_result['doc_line_id']}")
        return resultado_final
        
    except Exception as e:
        error_msg = f"Error inesperado en upload_document: {str(e)}"
        logger.error(error_msg, exc_info=True)
        return {
            'success': False,
            'message': 'Error interno al procesar el documento',
            'code': 'INTERNAL_ERROR',
            'technical_error': error_msg
        }
        
    finally:
        # Limpiar archivo temporal
        if temp_path and os.path.exists(temp_path):
            try:
                os.remove(temp_path)
                logger.debug(f"Archivo temporal eliminado: {temp_path}")
            except Exception as e:
                logger.warning(f"No se pudo eliminar archivo temporal {temp_path}: {str(e)}")

# ============================================
# FUNCIONES DE VALIDACIÓN REUTILIZABLES
# ============================================

def validar_archivo_basico(file_obj: FileStorage) -> dict:
    """
    Validaciones básicas del archivo (tamaño, extensión, etc.)
    REUTILIZABLE para cualquier módulo
    """
    try:
        # Verificar que existe archivo
        if not file_obj or not file_obj.filename:
            return {
                'valid': False,
                'message': 'No se proporcionó ningún archivo válido'
            }
        
        # Obtener extensión
        filename = secure_filename(file_obj.filename)
        extension = Path(filename).suffix.lower()
        
        # Validar extensión
        if extension not in EXTENSIONES_PERMITIDAS:
            return {
                'valid': False,
                'message': f'Tipo de archivo no permitido. Extensiones válidas: {", ".join(EXTENSIONES_PERMITIDAS)}'
            }
        
        # Validar tamaño
        file_obj.seek(0, os.SEEK_END)
        file_size = file_obj.tell()
        file_obj.seek(0)  # Regresar al inicio
        
        if file_size > TAMAÑO_MAXIMO:
            tamaño_mb = TAMAÑO_MAXIMO / (1024 * 1024)
            return {
                'valid': False,
                'message': f'El archivo excede el tamaño máximo permitido de {tamaño_mb:.1f}MB'
            }
        
        if file_size == 0:
            return {
                'valid': False,
                'message': 'El archivo está vacío'
            }
        
        return {
            'valid': True,
            'filename': filename,
            'extension': extension,
            'file_size': file_size
        }
        
    except Exception as e:
        logger.error(f"Error en validación básica: {str(e)}")
        return {
            'valid': False,
            'message': 'Error al validar el archivo'
        }

def validar_seguridad_archivo(temp_path: str) -> dict:
    """
    Validación de seguridad usando ClamAV
    REUTILIZABLE para cualquier módulo
    """
    try:
        # 1. Validación básica de headers (detección rápida)
        header_result = validar_headers_archivo(temp_path)
        if not header_result['valid']:
            return header_result
        
        # 2. Escaneo con ClamAV
        clamav_result = escanear_con_clamav(temp_path)
        if not clamav_result['clean']:
            return {
                'valid': False,
                'message': f'Archivo infectado o sospechoso: {clamav_result["threat"]}'
            }
        
        return {
            'valid': True,
            'message': 'Archivo seguro'
        }
        
    except Exception as e:
        logger.error(f"Error en validación de seguridad: {str(e)}")
        return {
            'valid': False,
            'message': 'Error al validar la seguridad del archivo'
        }

def validar_headers_archivo(file_path: str) -> dict:
    """
    Validación básica de headers de archivo para detección rápida de amenazas
    """
    try:
        with open(file_path, 'rb') as f:
            header = f.read(1024)  # Leer primeros 1024 bytes
        
        # Patrones sospechosos básicos
        patrones_sospechosos = [
            b'X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR',  # EICAR test
            b'<script',  # Scripts embebidos
            b'javascript:',  # JavaScript
            b'vbscript:',  # VBScript
        ]
        
        for patron in patrones_sospechosos:
            if patron in header.lower():
                return {
                    'valid': False,
                    'message': 'Archivo contiene patrones sospechosos'
                }
        
        return {
            'valid': True,
            'message': 'Headers válidos'
        }
        
    except Exception as e:
        logger.error(f"Error al validar headers: {str(e)}")
        return {
            'valid': False,
            'message': 'Error al validar estructura del archivo'
        }

def escanear_con_clamav(file_path: str) -> dict:
    """
    Escanea archivo con ClamAV
    """
    try:
        # Ejecutar ClamAV
        result = subprocess.run(
            ['clamscan', '--no-summary', file_path], 
            capture_output=True, 
            text=True,
            timeout=100  # Timeout de 30 segundos
        )
        
        if result.returncode == 0:
            logger.debug(f"ClamAV: Archivo limpio - {file_path}")
            return {
                'clean': True,
                'threat': None
            }
        else:
            threat_info = result.stdout.strip() if result.stdout else 'Amenaza desconocida'
            logger.warning(f"ClamAV: Amenaza detectada - {threat_info}")
            return {
                'clean': False,
                'threat': threat_info
            }
            
    except subprocess.TimeoutExpired:
        logger.error(f"ClamAV timeout para archivo: {file_path}")
        return {
            'clean': False,
            'threat': 'Timeout en escaneo de virus'
        }
    except FileNotFoundError:
        logger.error("ClamAV no está instalado o no se encuentra en PATH")
        return {
            'clean': False,
            'threat': 'Sistema antivirus no disponible'
        }
    except Exception as e:
        logger.error(f"Error ejecutando ClamAV: {str(e)}")
        return {
            'clean': False,
            'threat': f'Error en escaneo: {str(e)}'
        }

# ============================================
# FUNCIONES DE GESTIÓN DE ARCHIVOS REUTILIZABLES
# ============================================

def crear_archivo_temporal(file_obj: FileStorage) -> Optional[str]:
    """
    Crea un archivo temporal desde FileStorage
    """
    try:
        # Crear archivo temporal con extensión apropiada
        filename = secure_filename(file_obj.filename)
        extension = Path(filename).suffix
        
        temp_fd, temp_path = tempfile.mkstemp(suffix=extension)
        
        with os.fdopen(temp_fd, 'wb') as temp_file:
            file_obj.seek(0)  # Asegurar que estamos al inicio
            temp_file.write(file_obj.read())
        
        logger.debug(f"Archivo temporal creado: {temp_path}")
        return temp_path
        
    except Exception as e:
        logger.error(f"Error creando archivo temporal: {str(e)}")
        return None

def subir_archivo_ftp(temp_path: str, title: str, ruta_completa: str) -> dict:
    """
    Sube archivo al servidor FTP
    🆕 CORREGIDA: URL de descarga correcta para navegador
    """
    conn_ftp = None
    
    try:
        # Generar nombre seguro del archivo
        nombre_archivo = secure_filename(title)
        if not nombre_archivo:
            nombre_archivo = f"archivo_{int(datetime.now().timestamp())}"
        
        # Construir rutas
        ftp_directory = FTP_CONFIG['base_path'] + ruta_completa.replace('\\', '/')
        remote_file_path = f"{ftp_directory}/{nombre_archivo}"
        
        # 🆕 CORREGIR: Guardar URL pública accesible desde navegador
        # En lugar de la ruta física del FTP, guardamos la URL pública
        public_url = f"{BASE_URL}/{ruta_completa}/{nombre_archivo}"
        
        # Conectar a FTP
        conn_ftp = ftplib.FTP(FTP_CONFIG['host'])
        conn_ftp.login(FTP_CONFIG['user'], FTP_CONFIG['password'])
        
        # Crear directorios si no existen
        crear_directorios_ftp(conn_ftp, ftp_directory)
        
        # Subir archivo
        with open(temp_path, 'rb') as archivo:
            conn_ftp.storbinary(f"STOR {remote_file_path}", archivo)
        
        logger.info(f"Archivo subido exitosamente a FTP: {remote_file_path}")
        logger.info(f"URL pública accesible: {public_url}")
        
        return {
            'success': True,
            'remote_path': public_url,  # 🆕 CAMBIO: Devolver URL pública en lugar de ruta FTP
            'download_url': public_url,  # Esta ya estaba correcta
            'filename': nombre_archivo,
            'ftp_path': remote_file_path  # 🆕 OPCIONAL: Mantener ruta FTP para logs/debug
        }
        
    except ftplib.error_perm as e:
        error_msg = f"Error de permisos FTP: {str(e)}"
        logger.error(error_msg)
        return {
            'success': False,
            'message': 'Error de permisos al subir archivo',
            'code': 'FTP_PERMISSION_ERROR',
            'technical_error': error_msg
        }
        
    except Exception as e:
        error_msg = f"Error subiendo archivo a FTP: {str(e)}"
        logger.error(error_msg)
        return {
            'success': False,
            'message': 'Error al subir archivo al servidor',
            'code': 'FTP_ERROR',
            'technical_error': error_msg
        }
        
    finally:
        if conn_ftp:
            try:
                conn_ftp.quit()
            except:
                pass

def crear_directorios_ftp(ftp_conn: ftplib.FTP, directory_path: str) -> None:
    """
    Crea directorios en FTP de forma recursiva
    """
    try:
        # Intentar cambiar al directorio
        try:
            ftp_conn.cwd(directory_path)
            logger.debug(f"Directorio FTP existe: {directory_path}")
            return
        except ftplib.error_perm:
            # El directorio no existe, necesitamos crearlo
            pass
        
        # Crear directorios de forma recursiva
        path_parts = directory_path.strip('/').split('/')
        current_path = ''
        
        for part in path_parts:
            if not part:
                continue
                
            current_path += '/' + part
            
            try:
                ftp_conn.cwd(current_path)
            except ftplib.error_perm:
                # Directorio no existe, crearlo
                try:
                    ftp_conn.mkd(current_path)
                    logger.debug(f"Directorio FTP creado: {current_path}")
                except ftplib.error_perm as e:
                    if 'exists' not in str(e).lower():
                        raise e
        
        logger.info(f"Estructura de directorios FTP verificada: {directory_path}")
        
    except Exception as e:
        logger.error(f"Error creando directorios FTP: {str(e)}")
        raise e

# ============================================
# FUNCIONES DE BASE DE DATOS REUTILIZABLES
# ============================================

def gestionar_docs_head(origen: str, user_id: int, created_by: str) -> dict:
    """
    Crea un nuevo DocsHead (cada upload individual genera su propio grupo)
    REUTILIZABLE para cualquier módulo
    """
    try:
        return crear_docs_head(user_id, origen, created_by)
        
    except Exception as e:
        logger.error(f"Error gestionando DocsHead: {str(e)}")
        return {
            'success': False,
            'message': 'Error interno al gestionar registro de documentos',
            'code': 'DOCS_HEAD_ERROR'
        }

def crear_registro_documento(docs_id: int, title: str, description: str, 
                        ruta: str, file_type: str) -> dict:
    """
    Crea registro en DocsDetail
    REUTILIZABLE para cualquier módulo
    """
    try:
        # Obtener siguiente número de línea
        doc_line = obtener_siguiente_doc_line(docs_id)
        
        # Crear registro
        return crear_docs_detail(docs_id, doc_line, title, description, ruta, file_type)
        
    except Exception as e:
        logger.error(f"Error creando registro de documento: {str(e)}")
        return {
            'success': False,
            'message': 'Error interno al registrar documento',
            'code': 'DOCS_DETAIL_ERROR'
        }

# ============================================
# FUNCIÓN DE DIAGNÓSTICO REUTILIZABLE
# ============================================

def verificar_configuracion() -> dict:
    """
    Verifica que la configuración del módulo sea correcta
    REUTILIZABLE para cualquier módulo
    """
    errores = []
    
    # Verificar configuración FTP
    if not FTP_CONFIG['host']:
        errores.append('FTP_HOST no configurado')
    if not FTP_CONFIG['user']:
        errores.append('FTP_USER no configurado')
    if not FTP_CONFIG['password']:
        errores.append('FTP_PASS no configurado')
    
    # Verificar ClamAV
    try:
        result = subprocess.run(['clamscan', '--version'], capture_output=True, timeout=5)
        if result.returncode != 0:
            errores.append('ClamAV no responde correctamente')
    except (FileNotFoundError, subprocess.TimeoutExpired):
        errores.append('ClamAV no está instalado o no accesible')
    
    # Verificar permisos de directorio temporal
    try:
        temp_test = tempfile.mktemp()
        with open(temp_test, 'w') as f:
            f.write('test')
        os.remove(temp_test)
    except Exception:
        errores.append('No se pueden crear archivos temporales')
    
    return {
        'configuracion_valida': len(errores) == 0,
        'errores': errores,
        'tamaño_maximo_mb': TAMAÑO_MAXIMO / (1024 * 1024),
        'extensiones_permitidas': list(EXTENSIONES_PERMITIDAS),
        'base_url': BASE_URL
    }
    
def upload_document_to_existing_docs(file_obj: FileStorage, docs_id: int, title: str, 
                                    description: str, ruta_completa: str) -> dict:
    """
    Sube un documento a un DocsHead existente (no crea nuevo DocsHead)
    
    Args:
        file_obj (FileStorage): Objeto archivo de Flask
        docs_id (int): DocsID existente donde agregar el archivo
        title (str): Título del archivo
        description (str): Descripción del archivo
        ruta_completa (str): Ruta completa (ej: "Ventas/Formularios/1001-2/FormPEmx")
    
    Returns:
        dict: Resultado de la operación
    """
    
    temp_path = None
    
    try:
        logger.info(f"Subiendo documento a DocsID existente {docs_id}: {title}")
        
        # 1. VALIDACIONES BÁSICAS
        validacion_result = validar_archivo_basico(file_obj)
        if not validacion_result['valid']:
            return {
                'success': False,
                'message': validacion_result['message'],
                'code': 'VALIDATION_ERROR'
            }
        
        # 2. CREAR ARCHIVO TEMPORAL
        temp_path = crear_archivo_temporal(file_obj)
        if not temp_path:
            return {
                'success': False,
                'message': 'Error al crear archivo temporal',
                'code': 'TEMP_FILE_ERROR'
            }
        
        # 3. VALIDACIÓN DE SEGURIDAD (ClamAV)
        seguridad_result = validar_seguridad_archivo(temp_path)
        if not seguridad_result['valid']:
            return {
                'success': False,
                'message': seguridad_result['message'],
                'code': 'SECURITY_ERROR'
            }
        
        # 4. SUBIR ARCHIVO A FTP
        upload_result = subir_archivo_ftp(temp_path, title, ruta_completa)
        if not upload_result['success']:
            return upload_result
        
        # 5. 🆕 CREAR SOLO DOCS_DETAIL (DocsHead ya existe)
        docs_detail_result = crear_registro_documento(
            docs_id, title, description, upload_result['remote_path'], 
            validacion_result['extension']
        )
        if not docs_detail_result['success']:
            return docs_detail_result
        
        # 6. CONSTRUIR RESPUESTA EXITOSA
        resultado_final = {
            'success': True,
            'message': f'Documento "{title}" subido exitosamente',
            'data': {
                'docs_id': docs_id,
                'doc_line_id': docs_detail_result['doc_line_id'],
                'download_url': upload_result['download_url'],
                'remote_path': upload_result['remote_path'],
                'file_size': validacion_result['file_size'],
                'file_type': validacion_result['extension'],
                'original_name': file_obj.filename
            }
        }
        
        logger.info(f"Documento subido a DocsID {docs_id}: {docs_detail_result['doc_line_id']}")
        return resultado_final
        
    except Exception as e:
        error_msg = f"Error inesperado en upload_document_to_existing_docs: {str(e)}"
        logger.error(error_msg, exc_info=True)
        return {
            'success': False,
            'message': 'Error interno al procesar el documento',
            'code': 'INTERNAL_ERROR',
            'technical_error': error_msg
        }
        
    finally:
        # Limpiar archivo temporal
        if temp_path and os.path.exists(temp_path):
            try:
                os.remove(temp_path)
                logger.debug(f"Archivo temporal eliminado: {temp_path}")
            except Exception as e:
                logger.warning(f"No se pudo eliminar archivo temporal {temp_path}: {str(e)}")
                
