#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
===============================================================================
 Archivo:        Main service manager.py
 Autor:           (TU_EMPRESA)
 Fecha creación: AAAA-MM-DD
 Versión:        VERSIÓN_INICIAL_BLUEPRINT_LEGACY_MIGRATION
===============================================================================
 Descripción:
    Breve descripción del propósito del archivo y su funcionalidad

===============================================================================
 Copyright:
    © AÑO TU_EMPRESA. Todos los derechos reservados.
    Este archivo forma parte de NOMBRE_PROYECTO y se distribuye bajo
    los términos de la licencia TIPO_LICENCIA.

===============================================================================
 Interface Change List:
    Versión     Fecha        Autor                Descripción del cambio
    ---------   ----------   ------------------   ---------------------------
    vX.X.X    AAAA-MM-DD TU_NOMBRE   Descripción breve
    vX.X.X    AAAA-MM-DD TU_NOMBRE   Descripción breve
===============================================================================
"""

__all__ = ["ExampleClass", "main"]

# =============================================================================
# Importación de librerías
# =============================================================================

# === Importes estándar ===
import sys
import time
import copy
import os
import re
import json
from datetime import datetime, timedelta, timezone
from pprint import pprint 
from collections import defaultdict
from centros_autodetectados import (
    encolar_dato_recibido, campos_reservados, sin_pendientes,
    bloqueo_automaticos, completar_alta, configurar as configurar_autodeteccion,
)
from shared_db_utils import log_centros
from blueprints.etiquetas import etiquetas_bp


# === Importes de terceros ===
import requests
from flask_cors import CORS
from dateutil import parser
from flask import Flask, g, jsonify, request, send_from_directory, send_file, after_this_request, make_response, url_for, Blueprint
from flask_jwt_extended import (JWTManager, create_access_token, create_refresh_token,
                                get_jwt_identity, jwt_required, set_access_cookies, set_refresh_cookies, decode_token)
# from itsdangerous import URLSafeTimedSerializer, SignatureExpired, BadSignature
from werkzeug.security import check_password_hash, generate_password_hash
from pymongo import MongoClient, UpdateOne, InsertOne, errors, ReplaceOne
from pymongo.errors import BulkWriteError
# from bson import ObjectId
from bson.objectid import ObjectId
from bson import json_util
# import numpy as np


# REGISTRO FINAL ("capa extra" de seguridad)
#SECRET_API_PREFIX = '/itg_7xR9L2m_secure'
SECRET_API_PREFIX = '/'
# === Importes internos ===
IS_LOCAL = False

# Imports para  pruebas locales
if  IS_LOCAL:
    from shared_db_utils import get_db, SERVICES_DB_MAP, insert_log_in_user_history, ensure_analytic_collection, ensure_analytic_indexes

    from blueprints.notifications import notifications_bp
    from blueprints.agent_data import agent_data_bp
    from blueprints.routes_migration import check_legacy_expiration

    from modules.email_sender import send_automatic_email
    from modules.email_notifications import update_emails, delete_contact_list, OP_SET, fetch_emails_by_location
    from modules.consolidado_estados import consolidado_empresa

    from nanoox.nanoox_timeseries import (
                                            tipo_de_datos, 
                                            process_sensor_data,
                                            get_historial_control,
                                            get_generator, 
                                            get_control_by_centro_id,
                                            get_control_version_by_centro_id
                                            )
    
    from nanoox.get_data import get_latest_data_ts
    from nanoox.comap_data import get_latest_comap_timestamp
    from nanoox.generate_report import gen_data_report

    
    from nanoox.reports_management_ts_service import _gen_data_report

    from verificacion_status_servicios_centros import verificacion_status_servicios_por_centro

    from Analytic.Scapp_analytics import (compute_daily_analytic_data, 
                                            compute_multiple_analytic_days, 
                                            compute_analytic_area_data, 
                                            compute_analytic_area_versus_data, 
                                            compute_analytics_by_region
                                        )
    from Analytic.centro_ids import build_centro_id, normalize_area_id, normalize_centro_id
    from Analytic.Scapp_feeder_score import (
                                            FEEDER_SCORE_COLLECTION_NAME,
                                            compute_feeder_score_by_area,
                                            compute_feeder_score_by_region,
                                            fetch_feeder_score_data,
                                            get_last_feeder_score_received_at,
                                            save_feeder_score_payload,
                                            validate_and_normalize_feeder_score_payload
                                        )

# Imports en servidor o produccion
else:
    from shared_db_utils import get_db, SERVICES_DB_MAP, insert_log_in_user_history, ensure_analytic_collection, ensure_analytic_indexes
    
    from blueprints.notifications import notifications_bp
    from blueprints.agent_data import agent_data_bp
    from blueprints.routes_migration import check_legacy_expiration

    from modules.email_sender import send_automatic_email
    from modules.email_notifications import update_emails, delete_contact_list, OP_SET, fetch_emails_by_location
    from modules.consolidado_estados import consolidado_empresa

    
    from nanoox.nanoox_timeseries import (
                                            tipo_de_datos, 
                                            process_sensor_data,
                                            get_historial_control,
                                            get_generator, 
                                            get_control_by_centro_id,
                                            get_control_version_by_centro_id
                                            )
    
    from nanoox.nanoox_bolt_ts import (
                                            save_flux_batch,
                                            save_flux_single,
                                            get_flux_historical_data,
                                            get_flux_heartbeat
                                            )

    from nanoox.get_data import get_latest_data_ts
    from nanoox.comap_data import get_latest_comap_timestamp
    from nanoox.generate_report import gen_data_report

    
    from nanoox.reports_management_ts_service import _gen_data_report

    from verificacion_status_servicios_centros import verificacion_status_servicios_por_centro

    from Analytic.Scapp_analytics import (compute_daily_analytic_data, 
                                            compute_multiple_analytic_days, 
                                            compute_analytic_area_data, 
                                            compute_analytic_area_versus_data, 
                                            compute_analytics_by_region
                                        )
    from Analytic.centro_ids import build_centro_id, normalize_area_id, normalize_centro_id
    from Analytic.Scapp_feeder_score import (
                                            FEEDER_SCORE_COLLECTION_NAME,
                                            compute_feeder_score_by_area,
                                            compute_feeder_score_by_region,
                                            fetch_feeder_score_data,
                                            get_last_feeder_score_received_at,
                                            save_feeder_score_payload,
                                            validate_and_normalize_feeder_score_payload
                                        )

    from nanoox.generate_flux_report import process_flux_day

    import openpyxl

from Analytic.Scapp_notes import (
    MAX_NOTES_REQUEST_BYTES,
    NOTES_COLLECTION_NAME,
    fetch_notes_for_range,
    save_notes_payload,
    validate_and_normalize_notes_payload,
)
# =============================================================================
# Constantes y configuraciones globales
# =============================================================================

SERVICIOS_SOPORTADOS = ["enviro", "nanoox", "scapp", "pss", "flux"]
# Configuración de Mapeo de Servicios (Nombre API -> Nombre DB Mongo)
# SERVICES_DB_MAP = {
#     "enviro": "ENVIRO",
#     "nanoox": "NANOOX__TS",
#     "flux":   "FLUX__TS",
#     "scapp":  "ScappDB",
#     "pss":    "PssDB"
# }

# Definicion de nombres de bases de datos globales de un solo cliente
USER_DATABASE_NAME = "users"
ENVIRO_DATABASE_NAME = "ENVIRO"
NANOOX_DATABASE_NAME =  "NANOOX__TS" 
SCAPP_DATABASE_NAME = "ScappDB" 
FLUX_DATABASE_NAME = "FLUX__TS"
EMAIL_COLLECTION_NAME = "emails"
# analytic_coll = client[SCAPP_DATABASE_NAME]['data_analytic']

HISTORIAL_CONTROL_ARRAY_LEN_TO_SEND = 50
region_analytic_access = ["Gerencia"] #Permisos para acceso a Analítica de Región. Agregar cargo a lista para dar permiso

# =============================================================================
# Definiciones locales
# =============================================================================

class SimpleLogger:
    def info(self, msg):
        print(f"[INFO] {msg}")
        # print(msg)
    def warning(self, msg):
        print(f"[WARNING] {msg}")
        #print(msg)
    def error(self, msg):
        print(f"[ERROR] {msg}")
    
    def debug(self, msg):
        # print(f"[DEBUG] {msg}")
        return


logger = SimpleLogger()


app = Flask(__name__, static_folder='build')      

allowed_origins = [
    #'*',
    "http://localhost:3000",
    "https://localhost:3000",
    "http://localhost:3001",
    "https://localhost:3001",
    "http://localhost:8100",
    "https://localhost:8100",
    "https://localhost:5173",
    "http://localhost:5173",
    "https://localhost",       # Android
    "capacitor://localhost",   # Común en iOS Capacitor
    "ionic://localhost",       # Común en Ionic antiguo
    "https://test.itgchile.com", # Tu dominio real
]

# Configura CORS web + movil (iOS + Android)
CORS(app, 
    origins=allowed_origins, 
    supports_credentials=True, 
    methods=['GET','POST','OPTIONS','DELETE','PUT']
)

# 2. CONFIGURACIÓN JWT (La clave del problema)
app.config['JWT_SECRET_KEY'] = 'ITG-Secret-jwt-key'
app.config['JWT_RESET_SECRET_KEY'] = 'ITG-Secret-reset-key'           # Clave separada para tokens de restablecimiento
app.config['JWT_ACCESS_TOKEN_EXPIRES']  = timedelta(minutes=15)   #(minutes=15)  days=1
app.config['JWT_REFRESH_TOKEN_EXPIRES'] = timedelta(days=7) #(days=7)  100
#app.config["JWT_TOKEN_LOCATION"] = ["cookies"]
# Permite que Flask busque el token en Cookies (Web) Y en Headers (Móvil)
app.config["JWT_TOKEN_LOCATION"] = ["cookies", "headers"]

# --- FIX CRÍTICO: Forzar Path Raíz ---
# Si no pones esto, a veces la cookie se asigna a /login y no sirve para /refresh
app.config["JWT_ACCESS_COOKIE_PATH"] = "/"
app.config["JWT_REFRESH_COOKIE_PATH"] = "/"

# --- FIX SEGURIDAD IOS ---
# Si estás en HTTPS (Producción):
app.config["JWT_COOKIE_SECURE"] = True    
app.config["JWT_COOKIE_SAMESITE"] = "None"

app.config["JWT_COOKIE_CSRF_PROTECT"] = False


# inicializar el JWTManager sobre app 
jwt = JWTManager(app)
# --- DEFINICIÓN DEL BLUEPRINT (NUEVO) ---
# Este objeto contendrá TODA tu lógica de negocio (API)
api_bp = Blueprint('api_v1', __name__)

ensure_analytic_indexes()

# ---------------------------------------------------
#   CALLBACK para revocacion de tokens basados en JTI
# ---------------------------------------------------
@jwt.token_in_blocklist_loader
def check_if_token_revoked(jwt_header, jwt_payload):
    """
    Se ejecuta para cualquier token (access o refresh).
    Solo revocaremos aqui los refresh tokens cuyo jti no coincida
    con el ultimo almacenado en la BD. Los access tokens
    los dejamos expirar normalmente.
    """
    client = get_db()
    users_collection = client[USER_DATABASE_NAME].users

    jti        = jwt_payload["jti"]
    token_type = jwt_payload.get("type", None)  # viene como "access" o "refresh"
    username   = jwt_payload["sub"]

    # Solo bloqueamos los refresh tokens
    if token_type == "refresh":
        user = users_collection.find_one(
            {"username": username}, {"refresh_jti": 1}
        )
        # Si no hay usuario o el jti no coincide → revocado
        return not user or user.get("refresh_jti") != jti

    # Para access tokens, nunca los bloqueamos aqui
    return False

# 1. LOG DE ENTRADA (Mantenemos tu lógica original)
# @api_bp.before_request
@api_bp.before_app_request
def log_request_info():
    """
    Se ejecuta ANTES de procesar la ruta.
    Loguea qué está entrando y si trae cookies.
    """
    print(f"\n[DEBUG-ENTRADA] Ruta: {request.path} | Metodo: {request.method}", file=sys.stderr)
    print(f"[DEBUG-ENTRADA] Origin: {request.headers.get('Origin')}", file=sys.stderr)
    print(f"[DEBUG-ENTRADA] User-Agent: {request.headers.get('User-Agent')}", file=sys.stderr)
    
    cookies = request.cookies
    if cookies:
        print(f"[DEBUG-ENTRADA] Cookies recibidas: {cookies}", file=sys.stderr)
    else:
        print(f"[DEBUG-ENTRADA] PELIGRO: No llegaron cookies en este request.", file=sys.stderr)


# 2. SEGURIDAD Y LOG DE SALIDA (FUSIÓN OPTIMIZADA)
@api_bp.after_request
def global_security_and_logging(response):
    """
    Se ejecuta DESPUÉS de cualquier petición (API o Web).
    1. Inyecta seguridad CORS para iOS.
    2. Loguea la respuesta y las cookies salientes.
    """
    
    # --- A. INYECCIÓN DE SEGURIDAD CORS ---
    origin = request.headers.get('Origin')
    
    # Solo permitimos orígenes en la lista blanca
    if origin in allowed_origins:
        response.headers['Access-Control-Allow-Origin'] = origin
        response.headers['Access-Control-Allow-Credentials'] = 'true'
        response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, X-Requested-With, x-client-type'
        response.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS, DELETE, PUT'
    
    # Fix para caché agresivo de Safari/iOS
    response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"

    # --- B. LOGGING DE SALIDA (DEBUG) ---
    print(f"[DEBUG-SALIDA] Status: {response.status}", file=sys.stderr)
    
    # Opcional: Ver headers CORS en log
    # print(f"[DEBUG-SALIDA] CORS Origin enviado: {response.headers.get('Access-Control-Allow-Origin')}", file=sys.stderr)

    # Análisis forense de cookies salientes (Set-Cookie)
    cookie_headers = response.headers.getlist('Set-Cookie')
    
    if cookie_headers:
        print(f"[DEBUG-SALIDA] --> FLASK ESTÁ INTENTANDO ESCRIBIR COOKIES:", file=sys.stderr)
        for cookie in cookie_headers:
            print(f"    [RAW COOKIE]: {cookie}", file=sys.stderr)
            
            # Validaciones de seguridad
            if "SameSite=None" not in cookie:
                print("    [ALERTA] Falta SameSite=None", file=sys.stderr)
            if "Secure" not in cookie and "SameSite=None" in cookie:
                print("    [ERROR FATAL] SameSite=None requiere Secure", file=sys.stderr)
            if "Path=/" not in cookie:
                 print("    [ALERTA] Falta Path=/", file=sys.stderr)
    else:
        # Solo imprimimos esto si NO es un archivo estático para no ensuciar el log
        if not request.path.endswith(('.js', '.css', '.png', '.ico')):
            print("[DEBUG-SALIDA] NO se detectó header Set-Cookie.", file=sys.stderr)

    print("-" * 30, file=sys.stderr)
    return response

# --- FIN BLOQUE SOLUCIÓN ---

# def get_db():
#     """Obtiene una conexion de MongoDB por request."""
#     if 'db_client' not in g:
#         g.db_client = MongoClient("mongodb://localhost:27017/")
#     return g.db_client

def create_directory(directory_path):
    try:
        # Crear el directorio, ignorando el error si ya existe
        os.makedirs(directory_path, exist_ok=True)
        print(f"Directorio '{directory_path}' creado con exito o ya existia.")
    except Exception as e:
        # Manejar cualquier otro error que pueda ocurrir
        print(f"Error al crear el directorio '{directory_path}': {e}")

directorio_actual = 'var/www/itgmanager/itgmanager' # os.getcwd()
# directorio_actual = os.getcwd()

print(f"Directorio actual: {directorio_actual}")
REPORTS_DIRECTORY = os.path.join(directorio_actual, "reportes")#".reportes"
# REPORTS_DIRECTORY = "reportes"
create_directory(REPORTS_DIRECTORY)


# ------Definicion de funciones Auxiliares------ #

# ! -- Servir los archivos estaticos de React -- #
# Funcion para detectar si el User-Agent corresponde a un dispositivo movil
def is_mobile(user_agent_str):
    mobile_regex = r"(android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini)"
    return re.search(mobile_regex, user_agent_str, re.IGNORECASE) is not None


# ! -- Funciones Auxiliares Login -- #

# Funciones de acceso y permisos de visualizacion
def get_accesos_usuario(users_collection,  username):
    """
   # Obtiene los id de los centros a los cuales tiene permiso el usuario
   # especificado, en la coleccion indicada y los entrega como un diccionario.
   #
   #Args:
   #users_collection: coleccion en la cual se encuentran los permisos de los usuarios (DB users) .
   #username: string con nombre de usuario.
   #
   #Returns:
   #Dict {region: [{area : [id_centro]}]} donde:
   #- region es un string que representa la region a la que pertenece el centro.
   #- area es un string que representa la comuna a la que pertenece el centro.
   #- id_centro es un string que representa el identificador unico de cada centro.
    """
    # Busca el documento del usuario por su nombre de usuario
    usuario = users_collection.find_one({"username": username})

    if usuario:
        # Accede a la key  "accesos" en el documento del usuario
        permisos = usuario.get("permisos", {})
        
        # Imprime la lista de accesos
        #print(f"Accesos del usuario:\n{permisos}")        
        return permisos
    else:
        print(f"Usuario '{username}' no encontrado.")

        return {}

#*----- Metodo para validar que el usuario poseen permisos en los id_centros indicados---- #
def validacion_permisos_usuario(permisos):
    #Si existen los permisos
    if permisos:
        return True
    else:
        return False

#*----- Metodo para validar que el usuario exista y que el token de acceso sea el mismo---- #
def check_token(client, username, access_token):
    users_collection = client[USER_DATABASE_NAME].users
    # Busca el documento del usuario por su nombre de usuario
    usuario = users_collection.find_one({"username": username})
    # Si existe el usario
    if usuario:
        # Obtener token de refresco de base de datos
        token = usuario.get("access_token", {})
        #Comparar token de base de datos con el entregado por el Front End
        if token == access_token:
            print ("Los tokens SI coinciden")
            return True
        else:
            print ("Los tokens NO coinciden")
            return False
    #Si no existe el usuario retorna None
    else:
        print(f"Usuario '{username}' no encontrado.")
        return None

def servicios_activos_scapp(ciclos_collection):
    """
   # Obtiene los id de los centros en los cuales esta operativo el servicio
   # Scapp.
   #
   #Args:
   #ciclos_collection: coleccion en la cual se encuentran el estado de los
   # servicios por centro.
   #
   #Returns:
   #Array1D (scapp_activos) donde:
   #- scapp_activos son todos los id de centros que contrataron Scapp.
    """
    
    query = ciclos_collection.find({"scapp.status": True})
    scapp_activos = []
    for res in query:
        scapp_activos.append(res["centro_id"])
    # scapp_activos = np.array(scapp_activos)
    scapp_activos = scapp_activos
    return (scapp_activos)

#*----- Metodo para obtener los id_centros con servicios de enviro activos---- #
def servicios_activos_enviro(ciclos_collection):
    """
    # Obtiene los id de los centros en los cuales esta operativo el servicio
    # Enviro.
    #
    #Args:
    #ciclos_collection: coleccion en la cual se encuentran el estado de los
    # servicios por centro.
    #
    #Returns:
    #Array1D (enviro_activos) donde:
    #- enviro_activos son todos los id de centros que contrataron Enviro.
     """
    query = ciclos_collection.find({"enviro.status": True})
    enviro_activos = []
    for res in query:
        enviro_activos.append(res["centro_id"])
    # enviro_activos = np.array(enviro_activos)
    print(f"{enviro_activos=}")
    enviro_activos = enviro_activos
    return (enviro_activos)

#*----- Metodo para obtener los id_centros con servicios de nanoox activos---- #
def servicios_activos_nanoox(ciclos_collection):
    """
    # Obtiene los id de los centros en los cuales esta operativo el servicio
    # Nanoox.
    #
    #Args:
    #ciclos_collection: coleccion en la cual se encuentran el estado de los
    # servicios por centro.
    #
    #Returns:
    #Array1D (nanoox_activos) donde:
    #- nanoox_activos son todos los id de centros que contrataron Nanoox.
     """    
    query = ciclos_collection.find({"nanoox.status": True})
    nanoox_activos = []
    for res in query:
        nanoox_activos.append(res["centro_id"])
    # nanoox_activos = np.array(nanoox_activos)
    nanoox_activos = nanoox_activos
    return (nanoox_activos)

#*----- Metodo para obtener los id_centros con servicios de pss activos---- #
def servicios_activos_pss(ciclos_collection):
    """
    # Obtiene los id de los centros en los cuales esta operativo el servicio
    # PSS.
    #
    #Args:
    #ciclos_collection: coleccion en la cual se encuentran el estado de los
    # servicios por centro.
    #
    #Returns:
    #Array1D (pss_activos) donde:
    #- pss_activos son todos los id de centros que contrataron PSS.
     """        
    query = ciclos_collection.find({"pss.status": True})
    pss_activos = []
    for res in query:
        pss_activos.append(res["centro_id"])
    # pss_activos = np.array(pss_activos)
    pss_activos = pss_activos
    return (pss_activos)
#NEW_FEATURE
def servicios_activos_flux(ciclos_collection):
    """
    Retorna una lista de IDs de centros que tienen el servicio FLUX activo.
    Soporta status booleanos (True) y strings ('A', 'B', 'C').
    """
    query = {
        "$or": [
            {"flux.status": True},
            {"flux.status": {"$in": ["A", "B", "C"]}}
        ]
    }
    
    # Proyección para traer solo el centro_id y ahorrar ancho de banda
    cursor = ciclos_collection.find(query, {"centro_id": 1, "_id": 0})
    
    # Convertimos el cursor a una lista plana de IDs
    lista_centros_activos = [doc['centro_id'] for doc in cursor]
    
    return lista_centros_activos

#*----- Metodo para obtener los centros formtadeados para Front End a los que tiene permiso el usuario con servicios activos---- #
def interseccion_permisos_con_servicios(diccionario_id_centros, servicios_activos):
    """
    # Realiza la operacion logica "And (&)" o la interseccion entre
    # los id de centros a los que tiene permiso el usuario y los
    # servicios activos de cada centro, retornando los centros del servicio
    # en el formato de entrada para el Front End. 
    #
    #
    #Args:
    #diccionario_id_centros: diccionario con los id de los centros a los que tiene acceso
    # el usuario.
    #servicios_activos: arreglo con los id de los centros que poseen el servicio
    # especificado activo
    #
    #Returns:
    #Dict {region: [{area : [nombre_centro]}]} donde:
    #- region es un string que representa la region a la que pertenece el centro.
    #- area es un string que representa la comuna a la que pertenece el centro.
    #- nombre_centro es un string que representa el nombre de cada centro.
     """      
    # Diccionario con permisos
    diccionario = diccionario_id_centros
    # Arreglo con servicios activos
    arreglo = servicios_activos
    
    # Obtener los valores del tercer nivel como una lista plana
    valores_tercer_nivel = [valor for subdiccionario in diccionario.values() for lista in subdiccionario.values() for valor in lista]
    
    # Calcular la interseccion
    interseccion = list(set(valores_tercer_nivel) & set(arreglo))
    
    # Verificar si la interseccion es un conjunto vacio
    if not interseccion:
        return {}  # Devolver un diccionario vacio si la interseccion es vacia
    
    # Crear un diccionario temporal para almacenar los valores actualizados
    diccionario_actualizado = {}
    
    # Actualizar el diccionario manteniendo solo los valores presentes en la interseccion
    for region, subregiones in diccionario.items():
        for subregion, destinos in subregiones.items():
            destinos_actualizados = [destino.split('-')[0] for destino in destinos if destino in interseccion]
            if destinos_actualizados:
                # Solo agregar si la lista de destinos actualizados no esta vacia
                diccionario_actualizado.setdefault(region, {})[subregion] = destinos_actualizados
    
    return diccionario_actualizado

def validacion_permisos_usuario(permisos):
    if permisos:
        return True
    else:
        return False

#!---Funciones Auxiliares Manejo de Usuarios---¡#
def crear_usuario(usuarios_collection, data_user, usuario_actual):
    username = data_user.get('username')
    password = data_user.get('password')
    cargo = data_user.get('cargo')
    correo = data_user.get('correo')
    permisos_solicitados = data_user.get('permisos')

    if not username or not password or not cargo or not correo or not permisos_solicitados:
        return jsonify({"mensaje": "Faltan campos necesarios para el registro."}), 400

    if usuarios_collection.find_one({"username": username}):
        return jsonify({"mensaje": f"El nombre de usuario {username} ya existe."}), 409

    if len(password) < 3:
        return jsonify({"mensaje": "La contraseña es demasiado corta."}), 400

    if not _check_email_format(correo):
        return jsonify({"mensaje": "Formato de correo electrónico inválido."}), 400

    hashed_password = generate_password_hash(password)
    data_user['password'] = hashed_password

    try:
        usuario_actual_info = usuarios_collection.find_one({'username': usuario_actual})
        cargo_actual = usuario_actual_info['cargo']
        permisos_actual = sin_pendientes(usuario_actual_info['permisos'], usuarios_collection.database.ciclos, username)

        if cargo_actual == 'Operario':
            return jsonify({"mensaje": "No tienes permiso para crear usuarios."}), 400

        # --- Nueva detección de formato simplificado ---
        permisos_procesados = permisos_solicitados
        if cargo in ["Jefe Area", "Jefe Region"]:
            if cargo == "Jefe Area" and es_formato_simplificado_jefe_area(permisos_solicitados):
                permisos_procesados = transformar_permisos_simplificados(
                    cargo, permisos_solicitados, permisos_actual
                )
            elif cargo == "Jefe Region" and es_formato_simplificado_jefe_region(permisos_solicitados):
                permisos_procesados = transformar_permisos_simplificados(
                    cargo, permisos_solicitados, permisos_actual
                )
            elif es_formato_area_sin_centros(permisos_solicitados):
                # NUEVO: Expande las áreas sin centros usando los del creador
                permisos_procesados = {}
                for region, areas in permisos_solicitados.items():
                    if region not in permisos_actual:
                        continue
                    permisos_procesados.setdefault(region, {})
                    for area, servicios in areas.items():
                        if area not in permisos_actual[region]:
                            continue
                        permisos_procesados[region][area] = {}
                        for centro in permisos_actual[region][area]:
                            permisos_procesados[region][area][centro] = servicios.copy()
            else:
                # Se asume formato detallado (centros anidados)
                permisos_procesados = permisos_solicitados

        permisos_procesados = sin_pendientes(permisos_procesados, usuarios_collection.database.ciclos, username)
        # Validar permisos resultantes
        permisos_validados = validacion_permisos_a_otorgar(
            cargo_actual, permisos_actual, permisos_procesados)
        if permisos_validados is None:
            return jsonify({"mensaje": "Permisos inválidos para este usuario."}), 401

        data_user['permisos'] = permisos_validados
        data_user.update(construir_campos_auxiliares_usuario(permisos_validados))

        if verificar_permisos(cargo_actual, cargo):
            usuarios_collection.insert_one(data_user)
            return jsonify({"mensaje": f"Creación de usuario {username} exitosa con permisos asignados."}), 201
        else:
            return jsonify({"mensaje": "No se puede continuar por falta de rango."}), 400

    except Exception as e:
        print(f"Error al crear usuario {username}: {e} {e.__traceback__.tb_lineno}")
        return jsonify({"mensaje": f"Error al crear usuario {username}: {str(e)}"}), 500

def es_formato_area_sin_centros(permisos):
    """
    Detecta estructuras como region -> area -> servicios (sin centros).
    Ejemplo:
    {
      'loslagos': {
        'chiloe': {
          'enviro': {'status': False},
          'nanoox': {'status': True}
        }
      }
    }
    """
    if not isinstance(permisos, dict):
        return False
    for region, areas in permisos.items():
        if not isinstance(areas, dict):
            return False
        for area, servicios in areas.items():
            if not isinstance(servicios, dict):
                return False
            # Si los valores dentro son servicios, detecta este formato
            if all(isinstance(v, dict) and "status" in v for v in servicios.values()):
                return True
    return False


def original_crear_usuario(usuarios_collection, data_user, usuario_actual):
    username = data_user.get('username')
    password = data_user.get('password')
    cargo = data_user.get('cargo')
    correo = data_user.get('correo')
    permisos_solicitados = data_user.get('permisos')
    # permisos = data_user.get('permisos')
    
    data_user['permisos'] = permisos_solicitados
    
    if not username or not password or not cargo or not correo or not permisos_solicitados:
        return jsonify({"mensaje": "Faltan campos necesarios para el registro."}), 400

    if usuarios_collection.find_one({"username": username}):
        return jsonify({"mensaje": f"El nombre de usuario {username} ya existe."}), 409

    if len(password) < 3:
        return jsonify({"mensaje": "La contrasena es demasiado corta."}), 400

    if not _check_email_format(correo):
        return jsonify({"mensaje": "Formato de correo electronico invalido."}), 400

    hashed_password = generate_password_hash(password)
    data_user['password'] = hashed_password
    
    try:
        usuario_actual_info = usuarios_collection.find_one({'username': usuario_actual})
        cargo_actual = usuario_actual_info['cargo']
        permisos_actual = usuario_actual_info['permisos']

        if cargo_actual == 'Operario':
            return jsonify({"mensaje": "No tienes permiso para crear usuarios."}), 400

        if verificar_permisos(cargo_actual, cargo):
            if cargo in ["Jefe Region", "Jefe Area"]:
                permisos_transformados = transformar_permisos_simplificados(cargo, permisos_solicitados, permisos_actual)
                permisos_validados = validacion_permisos_a_otorgar(cargo_actual, permisos_actual, permisos_transformados)
                if permisos_validados is not None:
                    data_user['permisos'] = permisos_validados
                else:
                    return jsonify({"mensaje": "Permisos invalidos para este usuario."}), 401
            else:
                permisos_validados = validacion_permisos_a_otorgar(cargo_actual, permisos_actual, permisos_solicitados)
                if permisos_validados is not None:
                    data_user['permisos'] = permisos_validados
                else:
                    return jsonify({"mensaje": "Permisos invalidos para este usuario."}), 401

            usuarios_collection.insert_one(data_user)
            return jsonify({"mensaje": f"Creacion de usuario {username} exitosa con permisos {data_user['permisos']}."}), 201

        else:
            return jsonify({"mensaje": "No se puede continuar por falta de rango."}), 400
    except Exception as e:
        print(f"Error al crear usuario {username}: {e} {e.__traceback__.tb_lineno}")
        return jsonify({"mensaje": f"Error al crear usuario {username}: {str(e)}"}), 500

def verificar_permisos(cargo_usuario_existente, cargo_usuario_nuevo):
    #Comprueba si el usuario actual puede crear el cargo solicitado
    cargos_jerarquicos = ['Superusuario', 'Gerencia', 'Jefe Region', 'Jefe Area', 'Jefe Centro', 'Operario']
    if cargo_usuario_existente not in cargos_jerarquicos:
        raise ValueError("El cargo del usuario existente no es valido.")
    
    if cargo_usuario_nuevo not in cargos_jerarquicos:
        raise ValueError("El cargo del usuario nuevo no es valido.")
    
    indice_usuario_existente = cargos_jerarquicos.index(cargo_usuario_existente)
    indice_usuario_nuevo = cargos_jerarquicos.index(cargo_usuario_nuevo)
    
    if indice_usuario_nuevo > indice_usuario_existente:
    # if indice_usuario_existente  > indice_usuario_nuevo:
        print("El usuario tiene permiso para continuar.")
        return True
    else:
        print("El usuario no tiene permiso para continuar 1.")
        return False

def transformar_permisos_simplificados(cargo, permisos_simplificados, permisos_usuario_actual):
    nuevos_permisos = {}
    try:
        print(f"{permisos_simplificados.items()=}")
        if cargo == "Jefe Region":
            for region, servicios in permisos_simplificados.items():
                if region in permisos_usuario_actual:
                    nuevos_permisos[region] = {}
                    for area, centros in permisos_usuario_actual[region].items():
                        nuevos_permisos[region][area] = {}
                        for centro in centros:
                            nuevos_permisos[region][area][centro] = servicios.copy()
    
        elif cargo == "Jefe Area":
            for region_area, servicios in permisos_simplificados.items():
                region, area = region_area.split("-")
                if region in permisos_usuario_actual and area in permisos_usuario_actual[region]:
                    nuevos_permisos.setdefault(region, {})[area] = {}
                    for centro in permisos_usuario_actual[region][area]:
                        nuevos_permisos[region][area][centro] = servicios.copy()
    except Exception as e:
        print(f"Error al transformar_permisos_simplificados: {e} {e.__traceback__.tb_lineno}")
    return nuevos_permisos
  
def verificar_permisos_de_edicion(cargo_usuario_existente, cargo_usuario_a_editar):
    #Comprueba si el usuario actual puede crear el cargo solicitado
    cargos_jerarquicos = ['Superusuario', 'Gerencia', 'Jefe Region', 'Jefe Area', 'Jefe Centro', 'Operario']
    if cargo_usuario_existente not in cargos_jerarquicos:
        raise ValueError("El cargo del usuario existente no es valido.")
    
    if cargo_usuario_a_editar not in cargos_jerarquicos:
        raise ValueError("El cargo del usuario nuevo no es valido.")
    
    indice_usuario_existente = cargos_jerarquicos.index(cargo_usuario_existente)
    indice_usuario_nuevo = cargos_jerarquicos.index(cargo_usuario_a_editar)
    # print(f"{indice_usuario_existente=}")
    # print(f"{indice_usuario_nuevo=}")
    
    if indice_usuario_nuevo > indice_usuario_existente :
        print("El usuario tiene permiso para continuar.")
        return True
    else:
        print("El usuario no tiene permiso para continuar.")
        return False


def permisos_comunes_region(permisos_actual, permisos_x_crear):
    permisos_comunes = {
        region: {
            area: {
                key: {
                    subkey: permisos_x_crear[region][area][key].get(subkey, permisos_actual[region][area][key].get(subkey))
                    for subkey in permisos_actual[region][area][key]
                }
                for key in permisos_actual[region][area]
                if key in permisos_x_crear[region][area]
            }
            for area in permisos_x_crear[region]
            if area in permisos_actual[region]
        }
        for region in permisos_x_crear
        if region in permisos_actual
    }
    return permisos_comunes

def convertir_diccionario(diccionario):
    if not isinstance(diccionario, dict):
        return diccionario
    
    resultado = {}
    for clave, valor in diccionario.items():
        if isinstance(valor, bool):
            resultado[clave] = {'status': valor}
        else:
            resultado[clave] = convertir_diccionario(valor)
    return resultado

def centros_comunes(permisos_actual, permisos_x_crear):
    valores_comunes = {
        region: {
            area: [centro for centro in permisos_actual[region][area] if centro in permisos_x_crear[region][area]]
            for area in permisos_x_crear[region]
            if area in permisos_actual[region] and area in permisos_x_crear[region]
        }
        for region in permisos_x_crear
        if region in permisos_actual and region in permisos_x_crear
    }
    return valores_comunes


def validacion_permisos_a_otorgar(cargo_usuario_actual, permisos_actual, permisos_x_crear):
    #Valida los centros que puede otorgarse permiso
    return verificar_nivel_vacio(permisos_comunes_region(permisos_actual,permisos_x_crear))


    
def verificar_nivel_vacio(diccionario):
    def verificar(diccionario, nivel):
        # Si hemos alcanzado el cuarto nivel, no profundizamos mas
        if nivel > 3:
            return diccionario

        for clave, valor in diccionario.items():
            if isinstance(valor, dict):
                if nivel < 3:  # Solo profundizamos si estamos en el nivel 1 o 2
                    resultado = verificar(valor, nivel + 1)  # Recursivamente verificar cada nivel
                    if resultado is None:  # Si encuentra un nivel vacio, devolver None
                        return None
                    elif resultado == {}:
                        return None
                elif nivel == 3 and not valor:  # Si estamos en el tercer nivel y el diccionario esta vacio
                    return None
            elif not valor:  # Si el valor es una lista vacia o similar, devolver None
                return None
        
        return diccionario  # Si no se encontro ningun nivel vacio, devolver el diccionario de entrada

    return verificar(diccionario, 1)

def _check_email_format(email):
    # Simple chequeo de formato de correo electronico, podria ser mas complejo
    return re.match(r"[^@]+@[^@]+\.[^@]+", email)

def eliminar_usuario(users_collection, username):
    """Funcion para eliminar un usuario de la base de datos.
    
    Parametros:
    - users_collection: La coleccion de MongoDB donde se almacenan los usuarios.
    - username (str): El nombre de usuario para identificar el documento a eliminar.
    
    Retorna:
    - Un codigo de estado HTTP.
    """
    # Define el filtro para encontrar al usuario especifico
    filtro = {"username": username}

    # Elimina el primer documento que cumple con el filtro
    resultado = users_collection.delete_one(filtro)

    # Verifica si se realizo la eliminacion correctamente
    if resultado.deleted_count > 0:
        print(f"Usuario '{username}' eliminado correctamente.")
        return 200
    else:
        print(f"Usuario '{username}' no encontrado o no se elimino ningun usuario.")
        return 400
        
#OLD FUNCTION
# def editar_usuarios(collection, username, json_data):
#     filtro_usuario = {"username": username}
#     nuevos_valores = filtrar_campos(json_data)
#     print(f"{json_data=}")
#     print(f"{filtro_usuario=}")
#     print(f"{nuevos_valores=}")
#     try:
#         if collection.find_one(filtro_usuario):
#                 resultado = collection.update_one(filtro_usuario, {"$set": nuevos_valores})
        
#                 if resultado.modified_count > 0:
#                     print(f"Usuario {username} actualizado exitosamente.")
#                     # return jsonify({"mensaje": f"Usuario {username} actualizado exitosamente."}), 201
#         else:
#             print(f'No se encontro ningun usuario: {username}.')
#             return jsonify({"mensaje": f"No se encontro ningun usuario: {username}."}), 500
#     except Exception as e:
#             print(f"Entramos a excepcion: {e.__traceback__.tb_lineno}: \n{e}")
#             return jsonify({"mensaje": f"Error en script: {e.__traceback__.tb_lineno}: \n{e}."}), 409

# NEW
def editar_usuarios(users_col, username, updates, editor_info=None):
    if editor_info is None:
        raise Exception("No se ha proporcionado info del editor")
    permisos_editor = sin_pendientes(editor_info['permisos'], users_col.database.ciclos, username)
    cargo_editor = editor_info['cargo']

    user_target = users_col.find_one({'username': username})
    if not user_target:
        raise Exception("Usuario no encontrado.")

    # Obtener el cargo actualizado, o el anterior si no se envia
    cargo_nuevo = updates.get('cargo', user_target['cargo'])
    print(f"{cargo_nuevo=}")
    permisos_nuevos = updates.get('permisos')
    print(f"in {permisos_nuevos=}")

    if permisos_nuevos is not None:
        # Detecta y expande solo si el formato es SIMPLIFICADO para Jefe Area/Region
        if cargo_nuevo == "Jefe Area" and es_formato_simplificado_jefe_area(permisos_nuevos):
            permisos_nuevos = transformar_permisos_simplificados(
                cargo_nuevo, permisos_nuevos, permisos_editor
            )
        elif cargo_nuevo == "Jefe Region" and es_formato_simplificado_jefe_region(permisos_nuevos) and \
                all('-' not in k for k in permisos_nuevos.keys()):
            permisos_nuevos = transformar_permisos_simplificados(
                cargo_nuevo, permisos_nuevos, permisos_editor
            )
        # La edicion de permisos debe tratar el payload recibido como fuente de verdad.
        # Si el frontend deja de enviar una region/area/centro, eso implica eliminarla
        # del usuario objetivo, no conservarla desde la DB.
        print(f"out {permisos_nuevos=}")
        nuevos_permisos_final = sin_pendientes(permisos_nuevos, users_col.database.ciclos, username)
        permisos_validados = validacion_permisos_a_otorgar(
            cargo_editor, permisos_editor, nuevos_permisos_final)
        if permisos_validados is None:
            raise Exception("Permisos invalidos o superiores a los del editor.")
        updates['permisos'] = permisos_validados
        updates.update(construir_campos_auxiliares_usuario(permisos_validados))
    
    if "password" in updates:
        updates["password"] = generate_password_hash(updates["password"])

    users_col.update_one({'username': username}, {'$set': updates})

def es_servicio_dict(d):
    # Un dict de servicio tiene como claves solo servicios (enviro, nanoox...) y sus valores dict con "status"
    if not isinstance(d, dict):
        return False
    for v in d.values():
        if not isinstance(v, dict) or "status" not in v:
            return False
    return True

def es_formato_simplificado_jefe_region(permisos_nuevos):
    # Todas las claves regiones, y sus valores SOLO dict de servicios (no dicts anidados)
    for region, servicios in permisos_nuevos.items():
        if not es_servicio_dict(servicios):
            return False
    return True

def es_formato_simplificado_jefe_area(permisos_nuevos):
    # Todas las claves region-area, y sus valores SOLO dict de servicios (no dicts anidados)
    for k, servicios in permisos_nuevos.items():
        if '-' not in k or not es_servicio_dict(servicios):
            return False
    return True

def merge_permisos(permisos_actuales, nuevos_permisos):
    """
    Merge profundo: actualiza solo los valores presentes en nuevos_permisos.
    Mantiene los demas valores del arbol de permisos original.
    """
    if not isinstance(nuevos_permisos, dict):
        return nuevos_permisos

    resultado = permisos_actuales.copy() if isinstance(permisos_actuales, dict) else {}

    for clave, nuevo_valor in nuevos_permisos.items():
        if clave in resultado:
            resultado[clave] = merge_permisos(resultado[clave], nuevo_valor)
        else:
            resultado[clave] = nuevo_valor
    return resultado

def filtrar_campos(json_data):
    campos_filtrados = {}
    for key, value in json_data.items():
#        if key != "username" and value is not None and isinstance(value, str) and value.strip() != "":
        if value is not None and (not isinstance(value, str) or value.strip() != ""):
            campos_filtrados[key] = value
    return campos_filtrados

def transformar_permisos(data):
    resultado = {}
    
    for permiso in data['permisos']:
        region = permiso['REGION'][0].lower()  # Se asume que cada lista contiene un solo elemento
        areas = permiso['AREAS']
        
        # Asegurarse de que la region ya existe en el diccionario
        if region not in resultado:
            resultado[region] = {}
        
        for area_obj in areas:
            area = area_obj['AREA'].lower()
            centros = area_obj['CENTRO']
            
            # Asegurarse de que el area ya existe en el diccionario bajo la region
            if area not in resultado[region]:
                resultado[region][area] = {}
            
            # Construir el diccionario de centros para cada area
            for centro, sistemas in centros.items():
                centro_formato = sistemas
                
                resultado[region][area][f"{centro.lower()}-{area}-{region}"] = centro_formato
    
    return resultado

def extraer_nombre_centro_desde_id(centro_id, area, region):
    sufijo = f"-{area}-{region}"
    if isinstance(centro_id, str) and centro_id.endswith(sufijo):
        return centro_id[:-len(sufijo)]
    return centro_id

def construir_campos_auxiliares_usuario(permisos):
    regiones = []
    areas = []
    centros = []
    servicios = set()
    servicios_por_centro = {}

    if not isinstance(permisos, dict):
        return {
            "regiones": regiones,
            "areas": areas,
            "centros": centros,
            "servicios": [],
            "serviciosPorCentro": servicios_por_centro
        }

    for region, areas_dict in permisos.items():
        regiones.append(region)

        if not isinstance(areas_dict, dict):
            continue

        for area, centros_dict in areas_dict.items():
            areas.append(f"{region}:{area}")

            if not isinstance(centros_dict, dict):
                continue

            for centro_id, servicios_dict in centros_dict.items():
                nombre_centro = extraer_nombre_centro_desde_id(centro_id, area, region)
                centro_front = f"{region}:{area}:{nombre_centro}"
                centros.append(centro_front)

                servicios_activos = []
                if isinstance(servicios_dict, dict):
                    for servicio, config in servicios_dict.items():
                        if isinstance(config, dict) and config.get("status") is True:
                            servicios_activos.append(servicio)
                            servicios.add(servicio)

                servicios_por_centro[centro_front] = servicios_activos

    return {
        "regiones": sorted(set(regiones)),
        "areas": sorted(set(areas)),
        "centros": sorted(set(centros)),
        "servicios": sorted(servicios),
        "serviciosPorCentro": servicios_por_centro
    }

def verificar_permisos_edicion(cargo_usuario_existente, cargo_usuario_db):
    #Comprueba si el usuario actual puede crear el cargo solicitado
    cargos_jerarquicos = ['Superusuario', 'Gerencia', 'Jefe Region', 'Jefe Area', 'Jefe Centro', 'Operario']
    
    indice_usuario_existente = cargos_jerarquicos.index(cargo_usuario_existente)
    indice_usuario_db = cargos_jerarquicos.index(cargo_usuario_db)
    
    if indice_usuario_db > indice_usuario_existente:
        print("El usuario tiene permiso para continuar.")
        return True
    else:
        print("El usuario no tiene permiso para continuar 2.")
        return False

# def insert_log_in_user_history(collection, username, edit_object, log_type):
#     timestamp = datetime.now()
#     if log_type=='renew_password':
#         action = f"reestablecio su contraseña"
#     elif log_type=='add_user':
#         action = f"creo al usuario {edit_object}"
#     elif log_type=='edit_user':
#         action = f"edito al usuario {edit_object}"
#     elif log_type=='delete_user':
#         action = f"elimino al usuario {edit_object}"
#     elif log_type=='add_center':
#         action = f"creo el centro {edit_object}"
#     elif log_type=='edit_center':
#         action = f"edito el centro {edit_object}"
#     elif log_type=='delete_center':
#         action = f"elimino el centro {edit_object}"
#     elif log_type=='add_scapp_threshold':
#         action = f"edito los umbrales de Region Scapp del centro {edit_object}"
#     elif log_type=='add_center_permission':
#         action = f"agregó automáticamente permiso del centro {edit_object[1]} al ususario {edit_object[0]}"
#     elif log_type == 'read_notification':
#         # edit_object esperamos que sea una tupla/dict: (service_name, centro_id, notif_title)
#         svc = edit_object.get('service', '?')
#         centro = edit_object.get('centro', '?')
#         alerta_msg = edit_object.get('message', 'Sin detalle')
#         action = f"confirmó lectura de alerta '{alerta_msg}' ({svc}) en centro {centro}"
    
#     log_entry = {
#         'date': timestamp,
#         'username': username,
#         'type': log_type,
#         'msg': f'El usuario {username} {action} a las {timestamp.isoformat()}'
#     }
#     collection.insert_one(log_entry)
#     # print(log_entry['msg'])
#     return

## Funciones auxiliares para reestablecer contraseña con token reset de un solo uso usando Flask-JWT-Extended
def create_reset_token(username, expires_delta):
    # Cambia temporalmente la clave para firmar
    original_key = app.config['JWT_SECRET_KEY']
    app.config['JWT_SECRET_KEY'] = app.config['JWT_RESET_SECRET_KEY']
    token = create_access_token(identity=username, expires_delta=expires_delta, additional_claims={'reset': True})
    app.config['JWT_SECRET_KEY'] = original_key
    return token


def decode_reset_token(token):
    # Cambia temporalmente la clave para verificar firma
    original_key = app.config['JWT_SECRET_KEY']
    app.config['JWT_SECRET_KEY'] = app.config['JWT_RESET_SECRET_KEY']
    decoded = decode_token(token)
    app.config['JWT_SECRET_KEY'] = original_key
    return decoded

def ciclos_insert(client, username, centros):
    operaciones = []
    user_db = client[USER_DATABASE_NAME]
    enviro_db = client[ENVIRO_DATABASE_NAME]
    ciclos_collection = user_db.ciclos
    users_historial_collection = user_db.historial_users
    estado_centros_collection = user_db.estados

    centros_insertados = []
    #centros_existentes = []
    print(f'centros_recibidos: {centros}')
    for info_centro in centros:
        print(f"{info_centro=}")
        comprobacion = ciclos_collection.find_one({"centro_id": info_centro['centro_id']})
        try:
            dt = datetime.now()
            # info_centro['gps_coordinates']['manual']['timestamp'] = dt
            
            # info_centro['gps_coordinates'].setdefault('auto', {})
            # info_centro['gps_coordinates']['auto']['lat'] = None
            # info_centro['gps_coordinates']['auto']['lon'] = None

            # --- CAMBIOS MÍNIMOS AQUÍ ---
            # Asegurar estructura gps_coordinates y subdicts
            info_centro.setdefault('gps_coordinates', {})
            info_centro['gps_coordinates'].setdefault('manual', {})
            info_centro['gps_coordinates'].setdefault('auto', {})

            active_mode = (info_centro['gps_coordinates'].get('active') or '').strip().lower()

            if active_mode == 'auto':
                # Si es auto: manual debe existir pero con valores None
                info_centro['gps_coordinates']['manual']['lat'] = None
                info_centro['gps_coordinates']['manual']['lon'] = None
                info_centro['gps_coordinates']['manual']['timestamp'] = None
            else:
                # Si no es auto: se mantiene el comportamiento original (poner timestamp)
                info_centro['gps_coordinates']['manual']['timestamp'] = dt
                # (opcional defensivo) aseguramos llaves, sin cambiar la lógica original
                info_centro['gps_coordinates']['manual'].setdefault('lat', None)
                info_centro['gps_coordinates']['manual'].setdefault('lon', None)

            # En ambos casos, auto.lat/lon deben existir (None por defecto)
            info_centro['gps_coordinates']['auto'].setdefault('lat', None)
            info_centro['gps_coordinates']['auto'].setdefault('lon', None)
            # --- FIN CAMBIOS MÍNIMOS ---   

        except Exception as e:
            print(f"No se ha convertido timestamp log de centro por error en linea {e.__traceback__.tb_lineno}: \n{e}")
            return jsonify({"mensaje": "No se logro realizar la operacion"}), 500
        if comprobacion == None:
            # Lista de operaciones bulk_write
            operaciones.append(InsertOne(info_centro))
            centros_insertados.append(info_centro["centro_id"])

        elif comprobacion:
            # centros_existentes.append(centro['centro_id'])
            print(f"El centro {comprobacion['centro_id']} ya esta registrado \n")
    # Insertar documentos solo si no existen valores para la llave centro_id

    try:
        resultado = ciclos_collection.bulk_write(operaciones, ordered=False, bypass_document_validation=True)
        print(f'Insertados: {resultado.inserted_count} documentos')
        if len(centros_insertados)>0:
            print(f"Se cumple {len(centros_insertados)}>0")
            for centro_creado in centros_insertados:
                crear_estado_centro_default(estado_centros_collection, centro_creado)
                insert_log_in_user_history(users_historial_collection, username, centro_creado, 'add_center')
        # verificacion_status_servicios_por_centro
        return jsonify({"mensaje": f" Exito en crear centros! "}), 200
    except Exception as e:
        print(f'Warning: {e} ̣̣̣ {e.__traceback__.tb_lineno}')
        return jsonify({"mensaje": "No se logro realizar la operacion"}), 500
    

def centros_en_db(coleccion_ciclos):
    centros = []
    for dato in coleccion_ciclos.find():
        centros.append(dato["centro_id"])
    return centros

def guardar_permiso(users_collection, region, area, centro):
    """
    Actualiza los permisos de un usuario en MongoDB sin sobrescribir datos existentes.
    
    Parameters:
    - region: Nombre de la region.
    - area: Nombre del area.
    - centro: Nombre del centro.
    """
    
    permiso_key = f'{centro}-{area}-{region}'
    
    permiso = {
        permiso_key: {
            "enviro": {
                "status": True
            },
            "nanoox": {
                "status": True
            },
            "scapp": {
                "status": True
            },
            "pss": {
                "status": True
            },
            "flux": {
                "status": True
            }
        }
    }
    
    # Crear una pipeline de actualizaciones
    updates = [
        # Primero, verificamos si el usuario ya tiene permisos en esa region y area
        UpdateOne(
            {'username': "itg", f'permisos.{region}.{area}.{permiso_key}': {'$exists': False}},
            {'$set': {f'permisos.{region}.{area}.{permiso_key}': permiso[permiso_key]}},
            upsert=False  # Insertar si no existe el campo
        ),
        UpdateOne(
            {'username': "itg7", f'permisos.{region}.{area}.{permiso_key}': {'$exists': False}},
            {'$set': {f'permisos.{region}.{area}.{permiso_key}': permiso[permiso_key]}},
            upsert=False  # Insertar si no existe el campo
        ),
    ]

    # Ejecutar las actualizaciones
    result = users_collection.bulk_write(updates)
    
    if result.modified_count > 0 or result.upserted_count > 0:
        return True
    else:
        return False

def permisos_superusuario(user_db):
    ciclos_collection = user_db.ciclos
    users_collection = user_db.users 
    for centro in centros_en_db(ciclos_collection):
        centro, area, region = centro.split('-')
        guardar_permiso(users_collection, region, area, centro)
    return print('Todos los permisos de superusuario actualizados')

def original_actualizar_permisos_jefes(data_centros, user_db):
    """
    Procesa data_centros (lista) y agrega automáticamente permisos a jefaturas.
    - Jefe Area: requiere permisos.<region>.<area>
      Copia template desde el primer centro en esa area.
    - Jefe Region: requiere permisos.<region>
      Copia template primero desde esa area; si no hay, desde cualquier otra area de la region.
    Si no hay template disponible, usa fallback desde el input (enviro/nanoox/scapp/pss con init/fin).
    """

    user_collection = user_db.users
    users_historial_collection = user_db.historial_users

    if not data_centros or not isinstance(data_centros, list):
        print("Advertencia: data_centros está vacío o no es una lista.")
        return

    count_total_updates = 0

    for item in data_centros:
        try:
            centro_id_raw = item.get("centro_id")
            if not centro_id_raw or not isinstance(centro_id_raw, str):
                continue

            centro_id = centro_id_raw.strip().lower()

            # Parse desde la derecha: CENTRO-AREA-REGION (CENTRO puede tener guiones)
            partes = centro_id.split("-")
            if len(partes) < 3:
                print(f"Error formato ID insuficiente: {centro_id}")
                continue

            region = partes[-1]
            area = partes[-2]

            # Validación básica para paths Mongo
            if any("." in x or "$" in x for x in (centro_id, region, area)):
                print(f"Error: centro_id/area/region contienen caracteres inválidos ('.' o '$'): {centro_id}")
                continue

            # Fallback desde el input actual (init/fin)
            fallback_servicios = {}
            for k in ("enviro", "nanoox", "scapp", "pss"):
                v = item.get(k)
                if isinstance(v, dict):
                    fallback_servicios[k] = {"init": v.get("init"), "fin": v.get("fin")}
                else:
                    fallback_servicios[k] = {"init": None, "fin": None}

            mongo_path = f"permisos.{region}.{area}.{centro_id}"

            # Query correcta: distingue Jefe Area y Jefe Region
            query = {
                "$or": [
                    {"cargo": "Jefe Area",  f"permisos.{region}.{area}": {"$exists": True}},
                    {"cargo": "Jefe Region", f"permisos.{region}": {"$exists": True}},
                ]
            }

            # Proyección mínima (rápida y sin “slicing raro”)
            projection = {"username": 1,"cargo": 1, f"permisos.{region}": 1}

            users_cursor = user_collection.find(query, projection)

            bulk_operations = []
            logs_queue = [] # Cola temporal para guardar a quién debemos loguear

            for user in users_cursor:
                cargo = user.get("cargo")
                username = user.get("username", "Desconocido")
                permisos = user.get("permisos") or {}
                permisos_region = permisos.get(region)

                if not isinstance(permisos_region, dict):
                    continue

                # Evitar trabajo si ya existe el centro (si existe, saltar)
                permisos_area_actual = permisos_region.get(area)
                if isinstance(permisos_area_actual, dict) and centro_id in permisos_area_actual:
                    continue

                # Elegir template según cargo
                template = None

                if cargo == "Jefe Area":
                    # Debe existir esa área (la query ya lo filtró)
                    if isinstance(permisos_area_actual, dict) and len(permisos_area_actual) > 0:
                        # Primer centro existente como template
                        primer_centro = next(iter(permisos_area_actual))
                        if isinstance(permisos_area_actual.get(primer_centro), dict):
                            template = permisos_area_actual[primer_centro]

                elif cargo == "Jefe Region":
                    # 1) intentar template en misma área
                    if isinstance(permisos_area_actual, dict) and len(permisos_area_actual) > 0:
                        primer_centro = next(iter(permisos_area_actual))
                        if isinstance(permisos_area_actual.get(primer_centro), dict):
                            template = permisos_area_actual[primer_centro]
                    else:
                        # 2) buscar template en cualquier otra área de la región
                        for _, area_dict in permisos_region.items():
                            if not isinstance(area_dict, dict) or len(area_dict) == 0:
                                continue
                            primer_centro = next(iter(area_dict))
                            if isinstance(area_dict.get(primer_centro), dict):
                                template = area_dict[primer_centro]
                                break

                # Si no hay template, usar fallback del input
                if template is None:
                    template = fallback_servicios

                # Filtro anti-race / idempotente real (solo setear si no existe aún)
                filtro_update = {
                    "_id": user["_id"],
                    mongo_path: {"$exists": False},
                }

                bulk_operations.append(
                    UpdateOne(
                        filtro_update,
                        {"$set": {mongo_path: template}}
                    )
                )

                # Preparar Log: Guardamos info para generar el log si el update funciona
                logs_queue.append({
                    "target_user": username,
                    "centro": centro_id
                })

            if bulk_operations:
                result = user_collection.bulk_write(bulk_operations, ordered=False)
                updates_local = result.modified_count

                if updates_local > 0:
                    for entry in logs_queue:
                        insert_log_in_user_history(
                            collection=users_historial_collection,
                            username="$System", # El admin que ejecuta
                            edit_object=[entry['target_user'], entry['centro']],
                            log_type="add_center_permission"
                        )

            else:
                updates_local = 0

            count_total_updates += updates_local
            print(f"Centro '{centro_id}': Se actualizaron {updates_local} usuarios (Bulk).")

        except Exception as e:
            lineno = getattr(getattr(e, "__traceback__", None), "tb_lineno", "?")
            print(f"Error procesando centro {item.get('centro_id', 'desc')}: en línea {lineno}:\n{e}")

    print(f"=== Proceso Finalizado. Total usuarios modificados: {count_total_updates} ===")

#NEW_FEATURE
def actualizar_permisos_jefes(data_centros, user_db):
    """
    VERSIÓN CORREGIDA PARA SOPORTAR FLUX
    """
    user_collection = user_db.users
    users_historial_collection = user_db.historial_users

    if not data_centros or not isinstance(data_centros, list):
        print("Advertencia: data_centros está vacío o no es una lista.")
        return

    count_total_updates = 0

    # 1. Definimos los servicios esperados dinámicamente o agregamos flux explícitamente
    SERVICIOS_SOPORTADOS = ["enviro", "nanoox", "scapp", "pss", "flux"]

    for item in data_centros:
        try:
            centro_id_raw = item.get("centro_id")
            if not centro_id_raw or not isinstance(centro_id_raw, str):
                continue

            centro_id = centro_id_raw.strip().lower()

            partes = centro_id.split("-")
            if len(partes) < 3:
                continue

            region = partes[-1]
            area = partes[-2]

            if any("." in x or "$" in x for x in (centro_id, region, area)):
                continue

            # --- CORRECCIÓN 1: Generar Fallback incluyendo FLUX ---
            fallback_servicios = {}
            for k in SERVICIOS_SOPORTADOS:
                v = item.get(k)
                # Si el servicio viene en el input, tomamos sus fechas, si no, nulos
                if isinstance(v, dict):
                    fallback_servicios[k] = {"init": v.get("init"), "fin": v.get("fin"), "status": False}
                else:
                    # Opcional: Si quieres que flux siempre exista aunque no venga en el input:
                    fallback_servicios[k] = {"init": None, "fin": None, "status": False}

            mongo_path = f"permisos.{region}.{area}.{centro_id}"

            query = {
                "$or": [
                    {"cargo": "Jefe Area",  f"permisos.{region}.{area}": {"$exists": True}},
                    {"cargo": "Jefe Region", f"permisos.{region}": {"$exists": True}},
                ]
            }

            projection = {"username": 1,"cargo": 1, f"permisos.{region}": 1}

            users_cursor = user_collection.find(query, projection)

            bulk_operations = []
            logs_queue = [] 

            for user in users_cursor:
                cargo = user.get("cargo")
                username = user.get("username", "Desconocido")
                permisos = user.get("permisos") or {}
                if not isinstance(permisos, dict):
                    log_centros.warning(
                        "permisos.jefe_omitido usuario=%s centro=%s motivo='permisos' no es un objeto (%s)",
                        username, centro_id, type(permisos).__name__)
                    continue
                permisos_region = permisos.get(region)

                if not isinstance(permisos_region, dict):
                    log_centros.warning(
                        "permisos.jefe_omitido usuario=%s centro=%s motivo='permisos.%s' no es un objeto (%s)",
                        username, centro_id, region, type(permisos_region).__name__)
                    continue

                permisos_area_actual = permisos_region.get(area)
                if isinstance(permisos_area_actual, dict) and centro_id in permisos_area_actual:
                    continue

                # --- Estrategia de Template ---
                template = None

                # Lógica de búsqueda de template (Igual que antes)
                if cargo == "Jefe Area":
                    if isinstance(permisos_area_actual, dict) and len(permisos_area_actual) > 0:
                        primer_centro = next(iter(permisos_area_actual))
                        if isinstance(permisos_area_actual.get(primer_centro), dict):
                            template = permisos_area_actual[primer_centro].copy() # Importante: .copy()

                elif cargo == "Jefe Region":
                    if isinstance(permisos_area_actual, dict) and len(permisos_area_actual) > 0:
                        primer_centro = next(iter(permisos_area_actual))
                        if isinstance(permisos_area_actual.get(primer_centro), dict):
                            template = permisos_area_actual[primer_centro].copy()
                    else:
                        for _, area_dict in permisos_region.items():
                            if not isinstance(area_dict, dict) or len(area_dict) == 0:
                                continue
                            primer_centro = next(iter(area_dict))
                            if isinstance(area_dict.get(primer_centro), dict):
                                template = area_dict[primer_centro].copy()
                                break

                # --- CORRECCIÓN 2: "Merge" Inteligente ---
                # Si encontramos un template (de un centro viejo), es probable que no tenga 'flux'.
                # Debemos inyectarle 'flux' desde el fallback si falta.
                
                if template is not None:
                    # Revisamos si al template le faltan servicios nuevos (como flux)
                    for servicio_nuevo in SERVICIOS_SOPORTADOS:
                        if servicio_nuevo not in template:
                            # Inyectamos el servicio nuevo usando los datos del input actual (fallback)
                            template[servicio_nuevo] = fallback_servicios[servicio_nuevo]
                else:
                    # Si no había template, usamos el fallback completo que ya tiene flux
                    template = fallback_servicios

                filtro_update = {
                    "_id": user["_id"],
                    mongo_path: {"$exists": False},
                }

                bulk_operations.append(
                    UpdateOne(
                        filtro_update,
                        {"$set": {mongo_path: template}}
                    )
                )

                logs_queue.append({
                    "target_user": username,
                    "centro": centro_id
                })

            updates_local = 0
            if bulk_operations:
                result = user_collection.bulk_write(bulk_operations, ordered=False)
                updates_local = result.modified_count
                log_centros.info("permisos.jefes_concedido centro=%s usuarios=%s",
                                 centro_id, ",".join(q["target_user"] for q in logs_queue))
            else:
                log_centros.info("permisos.jefes_sin_cambios centro=%s (sin jefes pendientes de recibirlo)", centro_id)

            count_total_updates += updates_local

        # Se mantiene el comportamiento (no interrumpe el alta), pero queda constancia.
        except BulkWriteError as e:
            # ordered=False: los demás usuarios sí recibieron el permiso.
            fallidos = [logs_queue[err["index"]]["target_user"] for err in e.details.get("writeErrors", [])
                        if isinstance(err.get("index"), int) and err["index"] < len(logs_queue)]
            concedidos = [q["target_user"] for q in logs_queue if q["target_user"] not in fallidos]
            if concedidos:
                log_centros.info("permisos.jefes_concedido centro=%s usuarios=%s",
                                 item.get("centro_id"), ",".join(concedidos))
            log_centros.warning(
                "permisos.jefes_fallido centro=%s usuarios=%s motivo=nodo de permisos inválido o error de escritura",
                item.get("centro_id"), ",".join(fallidos) or "no_identificados")
        except Exception as e:
            log_centros.warning(
                "permisos.jefes_propagacion_incompleta centro=%s error=%s",
                item.get("centro_id"), e, exc_info=True)

def filtrar_valores(datos):
    valores_actualizados = {}
    for key, subdict in datos.items():
        for subkey, value in subdict.items():
            if value not in ('', None):
                if key not in valores_actualizados:
                    valores_actualizados[key] = {}
                valores_actualizados[key][subkey] = value
    return valores_actualizados

def actualizar_documento(ciclos_collection, filtro, datos):
    # Filtrar los valores del diccionario
    valores_actualizados = filtrar_valores(datos)
    print(f"{valores_actualizados=}")
     # añadimos timestamp UTC si vienen lat y lon bajo gps_coordinates, 
    gps = valores_actualizados.get('gps_coordinates')
    if gps:
        gps = gps["manual"]
    print(f"{gps=}")
    if gps is not None and 'lat' in gps and 'lon' in gps:
        # Genera el timestamp con zona UTC
        gps['timestamp'] = datetime.now() #timezone.utc
        
    if not valores_actualizados:
        print('No hay valores para actualizar.')
        return jsonify({"mensaje": "El centro no existe"}), 500

    # Construir la consulta de actualizacion
    update_query = {f'{key}.{subkey}': value
                    for key, subdict in valores_actualizados.items()
                    for subkey, value in subdict.items()}

    # Actualizar el documento en MongoDB
    result = ciclos_collection.update_one(
        filtro,
        {'$set': update_query}
    )
    if result.modified_count > 0:
        print('Documento actualizado exitosamente.')
        # verificacion_status_servicios_por_centro()
    else:
        print('No se encontro el documento o no se realizaron cambios.')
    # return

def obtener_lista_centros(centros_usuario):
    lista_centros=[]
    for region in centros_usuario:
        for area in centros_usuario[region]:
            for centro in centros_usuario[region][area]:
                lista_centros.append(centro)
    return lista_centros

def eliminar_centro_de_permisos(users_collection, centro_id):
    centro_split = centro_id.split('-')
    query = users_collection.find({}, {'_id': 0, 'username': 1})
    usuarios = [doc['username'] for doc in query if 'username' in doc]
    for usuario in usuarios:
        permiso_centro = users_collection.find_one({'username':usuario})['permisos']
        # print(f"{permiso_centro=}")
        try:
            if permiso_centro:
                permiso_centro = users_collection.find_one({'username':usuario})['permisos'][centro_split[2]][centro_split[1]][centro_id]
                print(f"{usuario}, {permiso_centro=}")
                update_result = users_collection.update_one(
                    {'username':usuario},
                    {"$unset": {f"permisos.{centro_split[2]}.{centro_split[1]}.{centro_id}": ""}}
                )
                if update_result.modified_count > 0:
                    print(f"Se ha eliminado el centro {centro_id} para el usuario {usuario}.")
                else:
                    print(f"No se encontro el centro {centro_id} para el usuario {usuario} o ya fue eliminado.")
        except:
            print(f"El usuario {usuario} no posee permisos para el centro {centro_id}")
            continue
    return 

def eliminar_estados_centro(estado_centros_collection, centro_id):
    try:
        query = estado_centros_collection.find_one({"centro_id":centro_id})
        if query:
            delete = estado_centros_collection.delete_one({"centro_id":centro_id})
            if delete.deleted_count > 0:
                print(f"Se han eliminado los estados del centro {centro_id}")
    except Exception as e:
        print(f"No se han podido eliminar los estado del centro {centro_id} por error en linea {e.__traceback__.tb_lineno}: \n{e}")
    return

def crear_umbral_default(enviro_db, centro_id, var):
    col_enviro_umbrales = enviro_db.umbrales
    bulk_operations=[]
    operation = InsertOne(
    {
      "fecha": datetime.now().strftime("%Y-%m-%d"),
      "centro_id": centro_id,
      "variable": var,
      "status": False,
      "tipo": "simple",
      "a": None,
      "b": None,
      "c": None,
      "d": None,
      "enviado": None
    })
    bulk_operations.append(operation)
    try:
        if bulk_operations:
            col_enviro_umbrales.bulk_write(bulk_operations)
            print(f'Umbral de {var} creado')
        else:
            print(f'No hay datos de umbral de {var} para guardar')
    except Exception as e:
        print(f'Error creando datos de umbrales de {var} para {centro_id} en linea {e.__traceback__.tb_lineno}: \n{e}')

def umbrales_default(enviro_db, centro_id):
    variables = ['Oxigeno', 'Doxigeno', 'Salinidad', 'Temperatura']
    for var in variables:
        crear_umbral_default(enviro_db, centro_id, var)
    print(f'Se han creado los umbrales por defecto del centro {centro_id}')

def crear_estado_centro_default(estado_centros_collection, centro_id):
    print("CREANDO ESTADO CENTRO DEFAULT")
    try:
        bulk_operations = []
        operation = InsertOne(
        {
            "centro_id": centro_id,
            "enviro": "B",
            "nanoox": "B",
            "flux": "B"
        })
        bulk_operations.append(operation)
    
        if bulk_operations:
            estado_centros_collection.bulk_write(bulk_operations)
            print(f'Estado de Servicios por defecto para centro {centro_id} creado')
        else:
            print(f'No hay datos para crear Estado de Servicios para centro {centro_id}')
    except Exception as e:
        print(f'Error creando Estado de Servicios por defecto para {centro_id} en linea {e.__traceback__.tb_lineno}: \n{e}')
    return

def get_last_measure_sensor_date(modulos):
    if modulos:
        last_msg_date=[]
        for id in modulos:
            # print(id)
            for j in range(len(modulos[id]["cans"])):
                for bomba in modulos[id]["cans"][j]['bombas']:
                    last_msg_date.append(bomba['hora'])
        # print(lista_de_horas)
        last_msg_date.sort(key=lambda last_msg_date: datetime.strptime(last_msg_date, '%H:%M:%S'), reverse=True)
        # print(lista_de_horas)
        final_date = str(datetime.now().strftime("%Y-%m-%d") + ' ' + last_msg_date[0])
        # print(f"{final_date=}")
    else:
        final_date = ''
    return final_date

# --- NUEVA FUNCIÓN AUXILIAR PARA EMAILS ---
def procesar_configuracion_emails(client, data_centros):
    """
    Guarda la configuración de emails. 
    Usa OP_SET para reemplazar la lista completa (Snapshot) en DB 
    con lo que viene del Front.
    """
    print("Iniciando procesamiento de emails (Snapshot Mode)...")
    
    if not data_centros or not isinstance(data_centros, list):
        return

    for item in data_centros:
        try:
            # 1. Parsing ID
            centro_id = item.get("centro_id", "").strip()
            partes = centro_id.split("-")
            if len(partes) < 3: continue

            # Lógica de split para obtener Region, Area, Nombre
            try:
                # Intento 1: Formato Nombre-Area-Region
                c_region = partes[-1]
                c_area = partes[-2]
                c_nom = "-".join(partes[:-2])
            except:
                continue

            # 2. Obtener config de emails
            emails_config = item.get("emails")
            if not isinstance(emails_config, dict):
                continue

            # 3. Iterar y Guardar (REEMPLAZANDO la lista vieja)
            for servicio_key, lista_emails in emails_config.items():
                servicio_normalizado = servicio_key.lower().strip()
                
                # Validamos servicio y que sea una lista (aunque sea vacía [])
                if servicio_normalizado in SERVICES_DB_MAP and isinstance(lista_emails, list):
                    
                    db_name = SERVICES_DB_MAP[servicio_normalizado]
                    collection = client[db_name][EMAIL_COLLECTION_NAME]
                    
                    # [CAMBIO CRITICO] Usamos OP_SET
                    # Esto hace que la DB sea idéntica a lo que envía el Frontend
                    update_emails(
                        collection, 
                        c_region, 
                        c_area, 
                        c_nom, 
                        lista_emails, 
                        operation=OP_SET 
                    )
                    
        except Exception as e:
            print(f"Error procesando emails item: {e}")
            continue


#!---FUNCIONES AMBIENTAL---!
#!----FUNCIONES NANOOX----!

#------Declaracion de rutas HTTP basado en Flask-------#

#!---FRONT-END---!

# Ruta para servir los archivos estaticos de React
@app.route('/', defaults={'path': ''})

@app.route('/<path:path>')
def serve(path):
    user_agent_str = request.headers.get('User-Agent', '')
    static_folder = 'build'  # carpeta por defecto para PCs

    # Cambiar carpeta estatica si es un dispositivo movil
    if is_mobile(user_agent_str):
        static_folder = 'build'# 'build_FE_mobile'

    # Comprobar si el archivo solicitado existe
    if path != "" and os.path.exists(os.path.join(static_folder, path)):
        return send_from_directory(static_folder, path)
    else:
        return send_from_directory(static_folder, 'index.html')
    
#!---BACK-END---!
    
# ? Test de comunicacion FE
@api_bp.route('/check_server_status', methods=['HEAD'])
def check_server_status():
    return ('', 200)

# ? Server ping - usado por cliente backend
@app.route( '/ping', methods=['GET'] )
def ping():

    print("pong")
    return jsonify({"message": "pong"})

#Ingreso y refresco de token
@api_bp.route('/login', methods=['POST'])
def login():
    """ 
    # login: inicio de sesion hibrido WEB/MOBILE, recibe unicamente la informacion a la que tiene acceso
    # recibe  json en el formato:
    #   {
    #       "username": "usuario",
    #       password": "password"
    #   }   
    """
    client = get_db()
    users_collection = client[USER_DATABASE_NAME].users

    client_type = request.headers.get("X-Client-Type","web")
    print(f"[LOGIN] Cliente detectado: {client_type}")
    
    # Validar que la solicitud contenga un JSON
    if not request.is_json:
        return jsonify({"mensaje": "Falta JSON en la solicitud"}), 400
    
    username = request.json.get("username", None)  #obtiene username del json enviado en la peticion http
    password = request.json.get("password", None)  #obtiene username del json enviado en la peticion http
    remember = request.json.get("remember", False)  # booleano
    print(f"json: {request.json}")
    # print(f"remember: {remember}")
    # Validar que el username y password esten presentes en base a las dos lineas anteriores
    if not username or not password:
        return jsonify({"mensaje": "Falta username o password"}), 400
    

    # Busca el documento del usuario por su nombre de usuario
    user = users_collection.find_one({"username": username})
    print(user)          
    
    # Verificar que el usuario existe y que la contrasena es correcta
    if not user or not check_password_hash(user.get('password'), password):
        return jsonify({"mensaje": "Credenciales invalidas"}), 401
    
    # --- GENERACIÓN DE TOKENS ---

    # Crear los tokens de acceso y actualizacion
    access_token = create_access_token(identity=username)
    # refresh corto o largo segun “remember me” de usuario en front
    rt_expires = timedelta(days=7) if remember else timedelta(days=2)
    refresh_token = create_refresh_token(identity=username,  expires_delta=rt_expires)

    # Extraer JTI del refresh token recien creado
    decoded_rt = decode_token(refresh_token)
    jti_refresh = decoded_rt["jti"]
    
   # Guardar el access, refresh token y JTI en la base de datos
    users_collection.update_many({"username": username}, {'$set': {'access_token': access_token, 'refresh_token': refresh_token, 'refresh_jti': jti_refresh}})   

    print(f'usuario {username} logeado correctamente')

    response_data = {
            "username":      username,
            "cargo":         user.get("cargo"),
            # "access_token":  access_token,
            # "refresh_token": refresh_token
        }

    # SI ES MÓVIL -> Bearer Token
    if client_type == "mobile":
        response_data["access_token"] = access_token
        response_data["refresh_token"] = refresh_token

    resp = jsonify(response_data)

    # Inyectar tokens en cookies segun tu configuracion de Flask-JWT-Extended
    set_access_cookies(resp, access_token)
    set_refresh_cookies(resp, refresh_token, max_age=rt_expires.total_seconds())
    print(f"login {resp =}")

    return resp, 200


## MANEJO Y REFRESCO DE TOKENS ##
@api_bp.route('/protected', methods=['GET'])
@jwt_required()
def protected():
    """
    Ruta protegida que requiere un token de acceso (access token) valido en el encabezado Authorization.

    Esta funcion es util para verificar que un usuario ha iniciado sesion y tiene autorizacion para acceder a recursos protegidos.
    El token debe incluirse en el header HTTP como: Authorization: Bearer <access_token>

    Retorna:
        JSON con el nombre de usuario (identity) que esta actualmente autenticado.
    """
    current_user = get_jwt_identity()
    return jsonify(logged_in_as=current_user), 200

@api_bp.route('/refresh', methods=['POST'])
@jwt_required(refresh=True)
def refresh():
    """
    Ruta para renovar el token de acceso (access token) híbrido (web/mobile) utilizando un token de actualizacion (refresh token).

    Esta funcion permite al usuario mantener la sesion activa sin volver a iniciar sesion, 
    siempre que el refresh token aun sea valido.
    
    El refresh token debe incluirse en el encabezado HTTP como: Authorization: Bearer <refresh_token>

    Retorna:
        Un nuevo token de acceso (access_token) en formato JSON.
    """
    current_user = get_jwt_identity()
    new_access_token = create_access_token(identity=current_user)
    client_type = request.headers.get("X-Client-Type","web")
    print(f"[REFRESH] Cliente detectado: {client_type}")

    if client_type == "mobile":
        # Al móvil le devolvemos el token nuevo en JSON para que actualice su storage
        resp = jsonify({
            "msg": "Token renovado",
            "access_token": new_access_token
        })
    else:
        # A la web solo le decimos "OK", el token va en la cookie
        resp = jsonify({"msg": "Token renovado"})
    
    # Siempre intentamos actualizar la cookie (para Web)
    set_access_cookies(resp, new_access_token)

    return resp, 200

# ---------------------------------------------------
#   LOGOUT → revoca el refresh token y limpia cookies
# ---------------------------------------------------
@api_bp.route('/logout', methods=['POST'])
@jwt_required(refresh=True)
def logout():
    username = get_jwt_identity()
    client = get_db()
    users_collection = client[USER_DATABASE_NAME].users
    # Elimina el JTI de la BD → revoca cualquier sesion
    users_collection.update_one(
        {"username": username},
        {"$unset": {"refresh_jti": ""}}
    )
    # Borra cookies en el cliente
    resp = jsonify(msg="Logout exitoso")
    resp.delete_cookie(app.config["JWT_ACCESS_COOKIE_NAME"])
    resp.delete_cookie(app.config["JWT_REFRESH_COOKIE_NAME"])
    print(f"Cerrando sesion {resp=}")
    return resp, 200

# ! -- Recepcion de datos desde clientes ( Centros con estaciones ) -- #

@api_bp.route('/<SERVICE>/receive_client_data/<REGION>/<AREA>/<CENTRO>', methods=['POST'])
def receive_client_data( SERVICE, REGION, AREA, CENTRO ):
    """# all_sensors_data['data']: [
            {
                'Nombre' 
                "fecha"
                "hora"
                "dif_last"
                "device_id"
                "dev_eui"
                "f_port"
                "snr"
                "rssi"
                "channel_rssi"
                "spreading_factor"
                "f_cnt"
                "uplink_message"
                "timeStamp"
                "batteryPercent"
                "Port 2"
                "formatted_time"
                "Port 3"
                "batteryVoltageMv"
                "flags"
                prof_i sensores data for i={0,1,2,3}
            } , 
            {...} , ...
        ]
         """
    # SERVICE = "enviro"
    # CENTRO  = "Chalihue"
    # REGION  = "Chiloe"
    # AREA    = "Loslagos"
    if SERVICE == "ENVIRO":
        try:
                client = get_db()
                enviro_data_collection = client[ENVIRO_DATABASE_NAME].data

                CENTRO  = CENTRO.replace(' ','').replace('-','').capitalize()
                REGION  = REGION.replace(' ','').replace('-','').capitalize()
                AREA    = AREA.replace(' ','').replace('-','').capitalize()

                #!_________________________________________________________________________________

                # Obtener la coleccion de MongoDB correspondiente a los datos de sensores del cliente
                collection = enviro_data_collection
            
                # Obtener los datos del sensor del cuerpo de la solicitud POST
                all_sensors_data = request.json
                #pprint(f'full json recibido: {all_sensors_data}')
        
                # Verificar si se recibieron datos
                if not all_sensors_data:
                    print("Datos no recibidos")
                    return jsonify({"mensaje": "Datos no recibidos"}), 400
        
                #print(f'all_sensors_data: {all_sensors_data}')
        
                if not all_sensors_data:
                    print("Datos no recibidos")
                    return jsonify({"mensaje": "Datos no recibidos"}), 400
        
                #print(f'all_sensors_data: \ttipo: {type(all_sensors_data)} \tlen: {len(all_sensors_data)} ')

                # Definir el ID del centro:
                centroId = f'{CENTRO}-{AREA}-{REGION}'

                if isinstance(all_sensors_data, dict):
                    print("Entramos al loop de <dict>")
                    all_sensors_data = all_sensors_data.get('data')
                    #print(f'all_sensors_data_dict: {all_sensors_data}')
                
                elif isinstance(all_sensors_data, list):
                    print("Entramos al loop de <list>")
                    #print(f'all_sensors_data_list: {all_sensors_data}')
                else:
                    print("no se esta cumpliendo con la condicion de clase")
            
                # Guardar elementos recibidos en BD
                _save_client_data( collection, all_sensors_data, centroId ) 

                # Revisar si metadata esta actualizada para este centro
                check_metadata(REGION, AREA, CENTRO)
            
                print(f'Guardado datos desde centro {centroId}')
                print("Datos recibidos exitosamente")
                return jsonify({"mensaje": "Datos recibidos exitosamente"}), 200

    
        except Exception as e:
            print(f'Error en receive_client_data linea {e.__traceback__.tb_lineno}: \n{e} ')
            return jsonify({"mensaje": f"Error al procesar los datos: {str(e)}"}), 500
    elif SERVICE == "nanoox":
        print(f'Datos Nanoox se envian de otra forma')
        return jsonify({"mensaje": "Datos Nanoox se envian de otra forma"}), 500
    elif SERVICE == "scapp":
        print(f'Datos Scapp se envian de otra forma')
        return jsonify({"mensaje": "Datos Scapp se envian de otra forma"}), 500
    elif SERVICE == "pss":
        print(f'Datos PSS se envian de otra forma')
        return jsonify({"mensaje": "Datos PSS se envian de otra forma"}), 500

#*----- Metodo para  obtener los permisos de los centros para un usuario---- #
@api_bp.route('/<SERVICE>/get_centers', methods=['POST'])
@jwt_required()
def get_permisos_usuarios(SERVICE):
    usuario = get_jwt_identity()
    # print(f"get_centers {usuario=}")
    # print(f"data_session_user {request.json}")
    client = get_db()
    users_collection = client[USER_DATABASE_NAME].users
    ciclos_collection = client[USER_DATABASE_NAME].ciclos

    permisos = get_accesos_usuario(users_collection,  usuario)
    validacion = validacion_permisos_usuario(permisos)
    if validacion:
        if SERVICE == "enviro":
            respuesta = interseccion_permisos_con_servicios(permisos,servicios_activos_enviro(ciclos_collection))
        elif SERVICE == "nanoox":
            respuesta = interseccion_permisos_con_servicios(permisos,servicios_activos_nanoox(ciclos_collection))
        elif SERVICE == "scapp":
            respuesta = interseccion_permisos_con_servicios(permisos,servicios_activos_scapp(ciclos_collection))
        elif SERVICE == "pss":
            respuesta = interseccion_permisos_con_servicios(permisos,servicios_activos_pss(ciclos_collection))
        elif SERVICE == "flux":
            respuesta = interseccion_permisos_con_servicios(permisos, servicios_activos_flux(ciclos_collection))
        print(f'get_centers: {respuesta=}')
        return jsonify({"IDs": respuesta}), 201
    else:
        return jsonify({"mensaje": "El usuario no posee permisos de visualizacion"}), 401


@api_bp.route('/<SERVICE>/get_data/<REGION>/<AREA>/<CENTRO>', methods=['GET'])
def get_data(SERVICE, REGION, AREA, CENTRO):
    logger.debug("EN GET DATA")
    if SERVICE == "enviro":
        """Devuelve los datos mas recientes de todas las estaciones."""
        try:
            client = get_db()
            enviro_data_collection = client[ENVIRO_DATABASE_NAME].data

            REGION  = REGION.replace(' ','').replace('-','').capitalize()
            AREA    = AREA.replace(' ','').replace('-','').capitalize()
            CENTRO  = CENTRO.replace(' ','').replace('-','').capitalize()
            
            # Obtener la coleccion de MongoDB correspondiente a los datos de sensores del cliente
            collection = enviro_data_collection
    
            # Obtener la fecha actual en el formato adecuado
            today = datetime.now().strftime("%Y-%m-%d")
    
            # Construir la ID del centro
            centro_id = f"{CENTRO}-{AREA}-{REGION}"
    
            # Buscar el documento correspondiente al centro y la fecha de hoy
            document = collection.find_one({'centroId': centro_id, 'fecha': today})
    
            if not document:
                print(f'No hay datos para el centro {CENTRO} en el dia solicitado')
                return jsonify({"mensaje": f"Error: no hay datos del centro"}), 500
            
            # Obtener los datos de todas las estaciones del dia de hoy para el centro solicitado
            latest_data = _get_all_station_data(document, all_day_data=False)
    
            # Verificar que hayan datos:
            if latest_data == {}: 
                return jsonify({"mensaje": f"Datos solicitados no encontrados en el servidor"}), 500
                
            sorted_array = [latest_data[k] for k in sorted(latest_data)]
            
            dict_to_send = {
                'batch_data' : False,
                'sensor_data' : sorted_array
            }
            return jsonify(dict_to_send), 200
        except Exception as e:
            print(f'Error en get_data linea {e.__traceback__.tb_lineno}: \n{e}')
            return jsonify({"mensaje": f"Error al obtener los datos: {str(e)}"}), 500
    
    elif SERVICE.lower() == "nanoox":

        logger.info(f"{f' Solicitando datos para {SERVICE } de  {REGION}/{AREA}/{CENTRO} ':=^60}\n")

        try:
            client = get_db()
            nanoox_db = client[NANOOX_DATABASE_NAME]

            REGION  = REGION.replace(' ','').replace('-','').capitalize()
            AREA    = AREA.replace(' ','').replace('-','').capitalize()
            CENTRO  = CENTRO.replace(' ','').replace('-','').capitalize()

            centro_id = f"{CENTRO}-{AREA}-{REGION}"
            centro_id_nanoox = f"{REGION}-{AREA}-{CENTRO}"

            tipo_control = get_control_by_centro_id(nanoox_db, centro_id_nanoox)

            control_version = get_control_version_by_centro_id(
                nanoox_db,
                metadata_col_name=f"{CENTRO}_metadata",
                centro_id=centro_id_nanoox
            )
            print(f'{CENTRO=} {control_version=}')

            center_latest_data = get_latest_data_ts(nanoox_db, centro_id_nanoox)

            # ✅ FIX 1: asegurar que center_latest_data sea dict (para que **center_latest_data no reviente)
            if not isinstance(center_latest_data, dict):
                logger.warning(
                    f"get_latest_data_ts devolvió {type(center_latest_data)} -> se envuelve como 'raw'"
                )
                center_latest_data = {"raw": center_latest_data}

            # ✅ FIX 2: modulos siempre lista
            modulos = center_latest_data.get("modulos") or []
            if not isinstance(modulos, list):
                modulos = []

            # ✅ FIX 3: hora segura, manteniendo ' --' para que el front siga parseando igual
            hora = " --"
            try:
                ts = modulos[0]["cans"][0]["timestamp"]
                if hasattr(ts, "strftime"):
                    hora = ts.strftime("%Y-%m-%d %H:%M:%S")
            except (IndexError, KeyError, TypeError) as e:
                logger.warning(f"Advertencia obteniendo hora: {e}")

            # Un centro COMAP puro no tiene modulos ni bombas: sin este respaldo
            # el front recibe " --" y el reloj del AppBar queda en Invalid Date.
            if hora == " --":
                hora_comap = get_latest_comap_timestamp(
                    center_latest_data.get("generators_comap")
                )
                if hora_comap:
                    hora = hora_comap

            if not modulos:
                print(f"No existen datos de bombas para el centro {REGION}/{AREA}/{CENTRO}")

            # Si es versión antigua, intenta generadores (sin romper si no hay)
            generator = None
            try:
                if not control_version:
                    print(f"{CENTRO} debe consultar generadores antiguos")
                    generator = get_generator(nanoox_db, centro_id_nanoox)
                    print(f"{CENTRO} {generator=}")
                    center_latest_data["generadores"] = generator
            except Exception as e:
                logger.warning(f"Error obteniendo generadores antiguos: {e}")

            umbrales = get_umbrales_nanoox(nanoox_db, centro_id_nanoox, new_version=control_version)

            logger.info(f'Nanoox: Enviados correctamente datos de {REGION}/{AREA}/{CENTRO} ')
            return jsonify({
                **center_latest_data,
                "hora": hora,
                "umbrales": umbrales
            }), 200

        except Exception as e:
            logger.error(f'Error en get_data linea {e.__traceback__.tb_lineno}: \n{e} ')
            return jsonify({
                "error": f"Error interno del servidor en linea {e.__traceback__.tb_lineno}: \n{e}"
            }), 500

    elif SERVICE.lower() == "flux":
        # logger.debug(f"{f' Solicitando datos para {SERVICE} de {REGION}/{AREA}/{CENTRO} ':=^60}")
        print(f"{f' Solicitando datos para {SERVICE} de {REGION}/{AREA}/{CENTRO} ':=^60}")
        
        try:
            client = get_db()
            flux_db = client[FLUX_DATABASE_NAME]

            # 1. Normalización
            centro_clean = CENTRO.replace(' ','').replace('-','').lower()
            print(f"{centro_clean=}")
            collection_name = f"{centro_clean}_flux_ts"
            print(f"{collection_name=}")
            
            # 2. Obtener Data Series (Histórico)
            historical_data = get_flux_historical_data(
                db=flux_db, 
                collection_name=collection_name, 
                sensor_key=centro_clean,
                N=360, 
                step=1
            )
            
            # 3. Obtener Hora (Heartbeat) usando el nuevo Helper
            hora = get_flux_heartbeat(flux_db, collection_name)

            # 4. Respuesta
            response_payload = {
                "sensor_data": historical_data,
                "hora": hora,
            }
            print(f"flux_to_fe <_{centro_clean}_> : {response_payload}")
            return jsonify(response_payload), 200

        except Exception as e:
            print(f'Error en get_data FLUX linea {e.__traceback__.tb_lineno}: \n{e} ')
            return jsonify({
                "error": f"Error interno FLUX: {str(e)}"
            }), 500

    else:
        return jsonify({"mensaje": f"Servicio {SERVICE} no soportado"}), 404

def get_last_n_measurements(db, bomba_id, N = 360 ):
    """
    Obtiene las ultimas N mediciones de O2 y SO2 del documento correspondiente al dia de hoy.

    :param db: Instancia de la base de datos de MongoDB.
    :param bomba_id: ID de la bomba para filtrar los documentos.
    :param N: Numero de mediciones a obtener.
    :return: Diccionario con listas de valores de O2 y SO2.
    """
    # Obtener la fecha de hoy en formato AA-MM-dd
    today_date = datetime.today().strftime("%Y-%m-%d")

    try:

        nanoox_db = db        
        # Buscar el documento mas reciente con la bomba y fecha de hoy
        document = nanoox_db.sensores.find_one(
            {"bomba_id": bomba_id, "fecha": today_date},
            sort=[("_id", -1)]  # Ordenamos por _id descendente para obtener el mas reciente
        )
        

        # print(f"{bomba_id=}")
        
        if not document or "sensores" not in document:
            return {"O2": [], "SO2": []}
        


        # Extraer las mediciones O2 y SO2 de los sensores, filtrando valores None
        O2_values = [round(s["O2"],2) for s in document["sensores"] if s.get("O2") is not None]
        SO2_values = [round(s["SO2"],2) for s in document["sensores"] if s.get("SO2") is not None]
        TS = [s["last_measure_sensor"] for s in document["sensores"] if s.get("last_measure_sensor") != "Sin datos"]
        

        # Tomar solo los ultimos N valores
        return {
            "O2": O2_values[-N:][::10],
            "SO2": SO2_values[-N:][::10],
            "TS": TS[-N:][::10]
        }

    except Exception as e:
        print(f"Error: {e} {e.__traceback__.tb_lineno}")

def latest_measurements_conditioner(db, REGION,AREA,CENTRO, center_requested_data):
    # print("ESTAMOS EN LAST_MEASUREMENTS_CONDITIONER")
    data = copy.deepcopy(center_requested_data)

    try:

        for idx_mod, modulo in enumerate(center_requested_data):              #["modulos"]
            
            for idx_can,can in enumerate(center_requested_data[modulo]["cans"]):        
            
                for idx_bomba, bomba in enumerate(can.get("bombas", [])):        
                    aviot = bomba.get("ip_aviot")
                    bomba_name = bomba.get("nombre")
                    bomba_id = f"{REGION}-{AREA}-{CENTRO}-{aviot}-{modulo}-{bomba_name}"  
                    result = get_last_n_measurements(db, bomba_id= bomba_id)   
                    # print(f"{bomba_name} {result}")

                    # data["modulos"][idx_mod]["cans"][idx_can]["bombas"][idx_bomba].update({"latest_measurements": result})
                    data[modulo]["cans"][idx_can]["bombas"][idx_bomba].update({"latest_measurements": result})
    except Exception as e:
        print(f"Error: {e} {e.__traceback__.tb_lineno}")
    return data

#?-------INICIO DE RUTAS----------
@api_bp.route('/<SERVICE>/get_latest_data/<REGION>/<AREA>/<CENTRO>', methods=['GET'])
def get_latest_data(SERVICE, REGION, AREA, CENTRO):
    # print(f'$$$$$$$$$$$$$ Solicitando datos de {REGION}/{AREA}/{CENTRO} $$$$$$$$')
    if SERVICE == "enviro":   
        """Devuelve los datos mas recientes de todas las estaciones."""
        try:
            client = get_db()
            enviro_data_collection = client[ENVIRO_DATABASE_NAME].data

            REGION  = REGION.replace(' ','').replace('-','').capitalize()
            AREA    = AREA.replace(' ','').replace('-','').capitalize()
            CENTRO  = CENTRO.replace(' ','').replace('-','').capitalize()
            
            # Obtener la coleccion de MongoDB correspondiente a los datos de sensores del cliente
            collection = enviro_data_collection
    
            # Obtener la fecha actual en el formato adecuado
            today = datetime.now().strftime("%Y-%m-%d")
    
            # Construir la ID del centro
            centro_id = f"{CENTRO}-{AREA}-{REGION}"
    
            # Buscar el documento correspondiente al centro y la fecha de hoy
            document = collection.find_one({'centroId': centro_id, 'fecha': today})
    
            if not document:
                print(f'No hay datos para el centro {CENTRO} en el dia solicitado')
                return jsonify({"mensaje": f"Error: no hay datos del centro"}), 500
            
            # Obtener los datos de todas las estaciones del dia de hoy para el centro solicitado
            latest_data = _get_all_station_data(document, all_day_data=False)
    
            # Verificar que hayan datos:
            if latest_data == {}: 
                return jsonify({"mensaje": f"Datos solicitados no encontrados en el servidor"}), 500
                
            sorted_array = [latest_data[k] for k in sorted(latest_data)]
            
            dict_to_send = {
                'batch_data' : False,
                'sensor_data' : sorted_array
            }
            return jsonify(dict_to_send), 200
        except Exception as e:
            print(f'Error en get_latest_data linea {e.__traceback__.tb_lineno}: \n{e}')
            return jsonify({"mensaje": f"Error al obtener los datos: {str(e)}"}), 500
    
    elif SERVICE == "nanoox":
#        client_NAME = 'cliente'
        try:
            client = get_db()
            nanoox_db = client[NANOOX_DATABASE_NAME]
            # Estandarizar inputs
            REGION  = REGION.replace(' ','').replace('-','').capitalize()
            AREA    = AREA.replace(' ','').replace('-','').capitalize()
            CENTRO  = CENTRO.replace(' ','').replace('-','').capitalize()
            
            # Obtener todos los IDs de CAN y Bombas e historial_control del centro
            can_ids, bomb_ids, historial_control_ids = get_center_ids( nanoox_db, REGION, AREA, CENTRO )
    
            # Diccionario de datos para enviar al front
            #Se elimino el argumento client_NAME
            modulos = _get_last_cand_and_bomb_data( nanoox_db, can_ids, bomb_ids )
            
            # Verificar si se han obtenido datos para el centro
            if not modulos: print(f"No existen datos de bombas para el centro {REGION}/{AREA}/{CENTRO}")
            
            # Obtener historial de control de las Aviot's del centro
            historial_control = {}
            
            for historial_control_id in historial_control_ids:
                aviot_id = historial_control_id.split('-')[-1] # Obtener la IP de la Aviot
                historial_control[ aviot_id ] = []
                #Se elimino el argumento salmonera
                historial_aviot = get_historial_control(nanoox_db, historial_control_id )
                
                if historial_aviot and len(historial_aviot) > 0:
                    historial_control[ aviot_id ].append(historial_aviot)
            
            formatted_modulos = []
            for modulos_data in modulos.values():
                formatted_modulos.append( modulos_data )
                
            print(f'Enviados correctamente datos de {REGION}/{AREA}/{CENTRO} ')
            print(f"{formatted_modulos=}")
            return jsonify({"modulos": formatted_modulos , "historial_control": historial_control }), 200
        
        except Exception as e:
            print(f'Error en get_latest_data linea {e.__traceback__.tb_lineno}: \n{e} ')
            return jsonify({ "error": f"Error interno del servidor en linea {e.__traceback__.tb_lineno}: \n{e}"  }), 500

# Obtener datos de todo un dia para un Centro
@api_bp.route('/<SERVICE>/get_all_day_data/<REGION>/<AREA>/<CENTRO>', methods=['GET'])
def all_day_data(SERVICE, REGION, AREA, CENTRO):
    if SERVICE == "enviro":
        """Entrega datos del ultimo dia de todas las estaciones."""
        try:
            client = get_db()
            enviro_data_collection = client[ENVIRO_DATABASE_NAME].data

            REGION  = REGION.replace(' ','').replace('-','').capitalize()
            AREA    = AREA.replace(' ','').replace('-','').capitalize()
            CENTRO  = CENTRO.replace(' ','').replace('-','').capitalize()
            # Obtener la coleccion de MongoDB correspondiente a los datos de sensores del cliente
#           collection = server_db[f"{client}_sensor_data"]
            collection = enviro_data_collection
    
            # Obtener la fecha actual en el formato adecuado
            today = datetime.now().strftime("%Y-%m-%d")
    
            # Construir la ID del centro
            centro_id = f"{CENTRO}-{AREA}-{REGION}"
    
            # Buscar el documento correspondiente al centro y la fecha de hoy
            document = collection.find_one({'centroId': centro_id, 'fecha': today})
    
            if not document:
                print(f'No hay datos para el centro {CENTRO} en el dia solicitado')
                return jsonify({"mensaje": f"Error: no hay datos del centro"}), 500
            
            # Obtener los datos de todas las estaciones del dia de hoy para el centro solicitado
            all_day_data = _get_all_station_data(document, all_day_data=True)
            
            # Verificar que hayan datos:
            if all_day_data == {}: 
                return jsonify({"mensaje": f"Datos solicitados no encontrados en el servidor"}), 500
            
            sorted_array = [all_day_data[k] for k in sorted(all_day_data)]
            dict_to_send = {
                'batch_data' : True,
                'sensor_data' : sorted_array
            }
            
            return jsonify(dict_to_send), 200
        except Exception as e:
            print(f'Error en all_day_data linea {e.__traceback__.tb_lineno}: \n{e}')
            return jsonify({"mensaje": f"Error al obtener los datos: {str(e)}"}), 500

    elif SERVICE == "nanoox":
        
#        client_NAME = 'cliente'
        try:
            client = get_db()
            nanoox_db = client[NANOOX_DATABASE_NAME]
            # Estandarizar inputs
            REGION  = REGION.replace(' ','').replace('-','').capitalize()
            AREA    = AREA.replace(' ','').replace('-','').capitalize()
            CENTRO  = CENTRO.replace(' ','').replace('-','').capitalize()
            # Obtener todos los IDs de CAN y Bombas e historial_control del centro
            can_ids, bomb_ids, historial_control_ids = get_center_ids( nanoox_db, REGION, AREA, CENTRO )
    
            # Diccionario de datos para enviar al front
            #Se elimino el argumento client_NAME
            modulos = _get_last_cand_and_bomb_data( nanoox_db, can_ids, bomb_ids )
            
            # Verificar si se han obtenido datos para el centro
            if not modulos: print(f"No existen datos de bombas para el centro {REGION}/{AREA}/{CENTRO}")
            
            # Obtener historial de control de las Aviot's del centro
            historial_control = {}
            
            for historial_control_id in historial_control_ids:
                aviot_id = historial_control_id.split('-')[-1] # Obtener la IP de la Aviot
                historial_control[ aviot_id ] = []
                #Se elimino el argumento salmonera
                historial_aviot = get_historial_control(nanoox_db, historial_control_id )
                
                if historial_aviot and len(historial_aviot) > 0:
                    historial_control[ aviot_id ].append(historial_aviot)
            
            formatted_modulos = []
            for modulos_data in modulos.values():
                formatted_modulos.append( modulos_data )
                
            print(f'Enviados correctamente datos de {REGION}/{AREA}/{CENTRO} ')
            
            return jsonify({"modulos": formatted_modulos , "historial_control": historial_control }), 200
        
        except Exception as e:
            print(f'Error en get_all_day_data linea {e.__traceback__.tb_lineno}: \n{e} ')
            return jsonify({ "error": f"Error interno del servidor en linea {e.__traceback__.tb_lineno}: \n{e}"  }), 500

# ? Datos de multiples dias
# Obtener datos de todas las estaciones de multiples dias
@api_bp.route('/<SERVICE>/get_multiple_days_data/<REGION>/<AREA>/<CENTRO>', methods=['GET', 'POST'])
def multiple_days_data(SERVICE, REGION, AREA, CENTRO):
    if SERVICE == "enviro":
        """Entrega datos de multiples dias de todas las estaciones de un centro especifico."""
        try:
            client = get_db()
            enviro_data_collection = client[ENVIRO_DATABASE_NAME].data

            REGION  = REGION.replace(' ','').replace('-','').capitalize()
            AREA    = AREA.replace(' ','').replace('-','').capitalize()
            CENTRO  = CENTRO.replace(' ','').replace('-','').capitalize()
            
            # Construir el identificador del centro
            centro_id = f"{CENTRO}-{AREA}-{REGION}"
    
            # Obtener la coleccion de MongoDB correspondiente a los datos del sensor
            collection = enviro_data_collection
    #        collection = server_db[f"{client}_sensor_data"]
    
            # Obtener lista de fechas solicitadas en formato adecuado
            day_list = []
    
            # Obtener el rango de fechas
            if request.method == 'POST':
                date_range = request.json
                start_date = datetime.strptime(date_range['startDate'], '%d/%m/%Y')
                end_date = datetime.strptime(date_range['endDate'], '%d/%m/%Y')
            else:
                end_date = datetime.now()
                start_date = end_date - timedelta(days=6)
    
            # Consulta a la base de datos para obtener todos los documentos en el rango de fechas
            query = {
                'centroId': centro_id,
                'fecha': {
                    '$gte': start_date.strftime("%Y-%m-%d"),
                    '$lte': end_date.strftime("%Y-%m-%d")
                }
            }
            documents = collection.find(query)
    
            print(f'Solicitado {len(day_list)} dias para el centro {centro_id}')
    
            # Inicializar un diccionario para almacenar los datos
            all_stations_data  = {}
    
            # Procesar los documentos recuperados
            for document in documents:
                if document:
                    # Buscar el documento correspondiente al centro y la fecha
                    day_data = _get_all_station_data(document, all_day_data=True)
                    
                    for station_name, data in day_data.items():
                        if station_name not in all_stations_data:
                            all_stations_data[station_name] = []
                        all_stations_data[station_name].extend(data)
    
            dict_to_send = {
                'batch_data' : True,
                'sensor_data' : [all_stations_data[k] for k in sorted(all_stations_data)]
            }
            print(f'enviando {len(dict_to_send["sensor_data"][0])} datos')
            
            return jsonify(dict_to_send), 200 
        
        except Exception as e:
            print(f'Error en multiple_days_data linea {e.__traceback__.tb_lineno}: \n{e}')
            return jsonify({"mensaje": f"Error al procesar los datos: {str(e)}"}), 500

    elif SERVICE == "nanoox":
        print(f'Datos Nanoox se obtiene de otra forma')
        return jsonify({"mensaje": "Datos Nanoox se obtiene de otra forma"}), 500
    elif SERVICE == "scapp":
        print(f'Datos Scapp se obtiene de otra forma')
        return jsonify({"mensaje": "Datos Scapp se obtiene de otra forma"}), 500
    elif SERVICE == "pss":
        print(f'Datos PSS se obtiene de otra forma')
        return jsonify({"mensaje": "Datos PSS se obtiene de otra forma"}), 500


# ? ----- Recepcion y guardado de datos con reenvio automatico----- ? # 


@api_bp.route('/<SERVICE>/new_data/<REGION>/<AREA>/<CENTRO>', methods=['POST'])
def new_data(SERVICE, REGION, AREA, CENTRO):
    raw_centro = (REGION, AREA, CENTRO)
    recibido = datetime.now().astimezone()

    print("++++++++++++ENTRANDO A RX DE DATOS PREVIO A SERVICE+++++++++++")
    if SERVICE == "nanoox":
        try:
            print("++++++++++++ENTRANDO A RX DE DATOS NANOOX+++++++++++")

            # Estandarizar inputs usando list comprehension
            REGION, AREA, CENTRO = [
                x.replace(' ', '').replace('-', '').capitalize() 
                for x in (REGION, AREA, CENTRO)
            ]
            centro_id = f"{CENTRO}-{AREA}-{REGION}"


            centro_id_nanoox = f"{REGION}-{AREA}-{CENTRO}"
            client = get_db()
            nanoox_db = client[NANOOX_DATABASE_NAME]
            # print(nanoox_db)
            # print(nanoox_db.list_collection_names)
            
        #     # Obtener el JSON de la solicitud
            received_json = request.json
            print(f"{centro_id} Json recibido:")                    
            print(received_json.keys())
            # print(f"{' Json recibido ':*^40}")
            # pprint(received_json)

            
                
    


            if not received_json:
                return jsonify({"mensaje": "Datos no recibidos"}), 400
            
            # Determinar el formato de los datos
            data_type = tipo_de_datos(received_json)

            
            flag_new_version = "control_version" in received_json

            if flag_new_version:
                print(f"{centro_id_nanoox}: Datos recibidos version  ITG ANEC")
            
            else:
                print(f"{centro_id_nanoox}: Datos recibidos version  ITG ALLWARE")


            # print(f"El dato recibido es del tipo {data_type}")
            if data_type == "dict":
                print(f"Nanoox {centro_id_nanoox}: Procesando datos en formato simple (dict)")
                process_sensor_data(nanoox_db, 
                                    received_json.get("data", {}), 
                                    centro_id_nanoox, 
                                    new_version = flag_new_version
                                    )

            elif data_type == "nested":
                print(f"Nanoox {centro_id_nanoox}: Procesando datos en formato anidado (list)")
                for item in received_json.get("data", []):                    
                    process_sensor_data(nanoox_db,item, 
                                        centro_id_nanoox,
                                        new_version = flag_new_version)
            else:
                print(f"Nanoox {centro_id_nanoox}: Formato de datos no reconocido")
                return jsonify({"mensaje": "Datos recibidos erroneamente por servidor"}), 422
            
        #     print(f"Recibido dato de cliente {REGION}/{AREA}/{CENTRO}")
        #     return jsonify({"mensaje": "Datos recibidos exitosamente por servidor"}), 200
            encolar_dato_recibido("nanoox", raw_centro, centro_id_nanoox, recibido)
            return jsonify({"mensaje": "Datos recibidos exitosamente por servidor"}), 200

        
        except Exception as e:
            # linea_error = e.__traceback__.tb_lineno if e.__traceback__ else "desconocida"
        #     print(f"Error en new_data linea {linea_error}: {e}")
            return jsonify({"mensaje": "Error interno del servidor"}), 500
    
    elif SERVICE.lower() == 'flux':
        print("++++++++++++ENTRANDO A RX DE DATOS FLUXXX +++++++++++")
        try:
            data_input = request.json
            client = get_db()
            # Detectamos la estructura de entrada para decidir la estrategia
            if isinstance(data_input, list):
                # --- MODO ASÍNCRONO (BATCH) ---
                ids_procesados = save_flux_batch(client, data_input, CENTRO)
                encolar_dato_recibido("flux", raw_centro, CENTRO, recibido)
                return jsonify({"id": ids_procesados}), 200
            
            elif isinstance(data_input, dict):
                # --- MODO SÍNCRONO (SINGLE) ---
                respuesta = save_flux_single(client, data_input, CENTRO)
                encolar_dato_recibido("flux", raw_centro, CENTRO, recibido)
                
                if respuesta is False:
                    # Fallo: Código de error
                    return jsonify({"error": "No se pudo guardar el dato"}), 500
                else:
                    # Éxito: 200 con None (null en json)
                    return jsonify(respuesta), 200
            
            else:
                return jsonify({"mensaje": "Formato inválido. Se espera JSON Object o Array."}), 400

        except ValueError as ve:
            print(f"Error Validación Flux: {ve}")
            return jsonify({"mensaje": f"Datos inválidos: {str(ve)}"}), 400
            
        except errors.PyMongoError as pe:
            print(f"Error Mongo Flux: {pe}")
            return jsonify({"mensaje": "Error de persistencia en base de datos"}), 500

    # Rutas para otros servicios
    elif SERVICE in ("enviro", "scapp", "pss"):
        print(f"Datos {SERVICE} se guardan de otra forma")
        return jsonify({"mensaje": f"Datos {SERVICE} se guardan de otra forma"}), 500

    else:
        return jsonify({"mensaje": f"Servicio {SERVICE} no reconocido"}), 404


@api_bp.route( '/<SERVICE>/<REGION>/<AREA>/<CENTRO>', methods=['POST'] )
def post_ping(SERVICE ,REGION, AREA, CENTRO):

    print("post pong")
    # pprint( request.json)

    print(REGION, AREA,CENTRO)
    
    
    try:
        client = get_db()
        CENTRO  = CENTRO.replace(' ','').replace('-','').replace("<", "").replace(">", "").capitalize()
        REGION  = REGION.replace(' ','').replace('-','').replace("<", "").replace(">", "").capitalize()
        AREA    = AREA.replace(' ','').replace('-','').replace("<", "").replace(">", "").capitalize()
        
        if SERVICE == "ENVIRO":
                
            # Obtener la coleccion de MongoDB correspondiente a los datos de sensores del cliente
#           collection = server_db[f"{client}_sensor_data"]
            # collection = enviro_data_collection
            collection = client[ENVIRO_DATABASE_NAME].data


    
            # Obtener los datos del sensor del cuerpo de la solicitud POST
            all_sensors_data = request.json
    
            # Verificar si se recibieron datos
            if not all_sensors_data:
                return jsonify({"mensaje": "Datos no recibidos"}), 400
    
            all_sensors_data = all_sensors_data.get('data')
    
            if not all_sensors_data:
                return jsonify({"mensaje": "Datos no recibidos"}), 400
    
            # print(f'all_sensors_data: \ttipo: {type(all_sensors_data)} \tlen: {len(all_sensors_data)} ')
    
            # Definir el ID del centro:
            centroId = f'{CENTRO}-{AREA}-{REGION}'
            
    
            print("LLEGAMOS A GUARDAR DATOS")
    
            # Guardar elementos recibidos en BD
            _save_client_data( collection, all_sensors_data, centroId )
            
            
            # Revisar si metadata esta actualizada para este centro
            check_metadata( REGION, AREA, CENTRO)
            
            print(f'Guardado datos desde centro {centroId}')
            return jsonify({"mensaje": "Datos recibidos exitosamente"}), 200

        elif SERVICE == "nanoox":
            print(f'Datos Nanoox se guardan de otra forma')
            return jsonify({"mensaje": "Datos Nanoox se guardan de otra forma"}), 500
        elif SERVICE == "scapp":
            print(f'Datos Scapp se guardan de otra forma')
            return jsonify({"mensaje": "Datos Scapp se guardan de otra forma"}), 500
        elif SERVICE == "pss":
            print(f'Datos PSS se guardan de otra forma')
            return jsonify({"mensaje": "Datos PSS se guardan de otra forma"}), 500
   
    except Exception as e:
        print(f'Error en post_ping linea {e.__traceback__.tb_lineno}: \n{e} ')
        return jsonify({"mensaje": f"Error al procesar los datos: {str(e)}"}), 500

    return 'server pong'


# !--- Generacion de Reporteria ---!


# ? Generar reportes
#Enviro
@api_bp.route('/<SERVICE>/generate_data_report/<REGION>/<AREA>/<CENTRO>', methods=['POST'])
def generate_server_data_report(SERVICE, REGION, AREA, CENTRO):
    if SERVICE == "enviro":
        print("GENERANDO REPORTE ENVIRO")
        client = get_db()
        enviro_data_collection = client[ENVIRO_DATABASE_NAME].data
        REGION  = REGION.replace(' ','').replace('-','').capitalize()
        AREA    = AREA.replace(' ','').replace('-','').capitalize()
        CENTRO  = CENTRO.replace(' ','').replace('-','').capitalize()
        
        # Construir el identificador del centro
        centro_id = f"{CENTRO}-{AREA}-{REGION}"
    
        # Obtener la coleccion de MongoDB correspondiente a los datos del sensor
        collection = enviro_data_collection
    #    collection = server_db[f"{client}_sensor_data"]
        
        # Obtener rango de fechas
        date_range = request.json
        # Asumir que las fechas vienen en formato "DiA/MES/AÑO"
        start_date = datetime.strptime(date_range['startDate'], '%d/%m/%Y')
        end_date = datetime.strptime(date_range['endDate'], '%d/%m/%Y')
        
        # Crear array de fechas solicitadas
        date_array = []
        delta = end_date - start_date
        for i in range(delta.days + 1):
            day = start_date + timedelta(days=i)
            date_array.append(day.strftime("%Y-%m-%d"))
        
        # Asignar nombre de reporte
        if start_date == end_date:
            date = date_range['startDate'].replace('/','-')
            excel_save_path = f"{REPORTS_DIRECTORY}/{centro_id}_{date}.xlsx"
            excel_send_path = "./reportes/asignacion_selectoras.xlsx"
        else:
            i_date = date_range['startDate'].replace('/','-')
            f_date = date_range['endDate'].replace('/','-')
            excel_save_path = f"{REPORTS_DIRECTORY}/{centro_id}_{i_date}_{f_date}.xlsx"
            excel_send_path = f"./reportes/{centro_id}_{i_date}_{f_date}.xlsx"

        try:
            if generate_report_from_server( date_array, collection, centro_id, excel_save_path ):
                

                # Marcar el archivo para ser eliminado despues de la solicitud
                @after_this_request
                def remove_file(response):
                    try:
                        os.remove(excel_save_path)
                    except Exception as error:
                        app.logger.error("Error al eliminar archivo", error)
                    return response
        
                return send_file(excel_send_path, as_attachment=True)
            
            else:
                return jsonify({"mensaje": f"Error generar reporte"}), 500
        except Exception as e:
            print(e, e.__traceback__.tb_lineno)
            return jsonify({"mensaje": f"Error generar reporte"}), 500
        
    elif SERVICE == "nanoox":
        print(f'Nanoox NO posee reporteria')
        return jsonify({"mensaje": "Nanoox NO posee reporteria"}), 500
    elif SERVICE == "scapp":
        print(f'Datos Scapp NO posee reporteria')
        return jsonify({"mensaje": "Scapp NO posee reporteria"}), 500
    elif SERVICE == "pss":
        print(f'Datos PSS NO posee reporteria')
        return jsonify({"mensaje": "PSS NO posee reporteria"}), 500

#Nanoox
@api_bp.route('/<SERVICE>/generate_nanoox_report/<REGION>/<AREA>/<CENTRO>/<CONTROL>', methods=['POST']) #/<CONTROL>
def generate_nanoox_report(SERVICE, REGION, AREA, CENTRO, CONTROL): #, CONTROL
    print(SERVICE)
        
    SERVICE = SERVICE.upper()
    if SERVICE =="NANOOX":
        print("*********** °ENTRANDO A CREAR REPORTE NANOOX! ********")
        client = get_db()
        nanoox_db = client[NANOOX_DATABASE_NAME]

        data = request.json
        file_flag = None
        control = str(CONTROL)
        """ print(f"{control=}")


        print(f"{data=}")
        print(f"{CENTRO=}")
        print(f"{AREA=}")
        print(f"{REGION=}") """
        # REGION = 'loslagos'
        # print(f"HARD {REGION=}")
        centro_id = f"{CENTRO}-{AREA}-{REGION}"

        # print(f"{centro_id=}")
        print(f"{REPORTS_DIRECTORY=}")     

        logger.debug(f"{REPORTS_DIRECTORY=}")   
    
        # file_name = f"{centro_id}-reporte_sensores.xlsx"
        # file_remove = str(excel_save_path+file_name)
        capitalized_center_name = CENTRO.capitalize()
        print(f'{request.json=}')
        ret_status, dest_path= gen_data_report(rango_fechas=data, 
                     control_type=CONTROL, 
                    excel_save_path= REPORTS_DIRECTORY,                                        

                     db_name= NANOOX_DATABASE_NAME,                                                
                     centro_name= CENTRO)
        
        logger.debug( f"{ret_status=}")
        logger.debug(f"{ dest_path=}")
        
        
        
        # ts_request =time.time_ns()
        # # ret_status, dest_path = _gen_data_report(db= nanoox_db,  
        # #                                rango_fechas= data,                                         
        # #                                excel_save_path= REPORTS_DIRECTORY,                                        
        # #                                centro_id = capitalizar_centro_id(f"{REGION}-{AREA}-{CENTRO}"), 
        # #                                centro_name = capitalized_center_name,  
        # #                                sensores_coll_name = f"{capitalized_center_name}_sensores",
        # #                                metadata_coll_name = f"{capitalized_center_name}_metadata",                                    
        # #                                control_type= CONTROL,
        # #                                ts_request = ts_request)


        
        if ret_status is True:
            print("respuesta 200")
            file_flag = True
        # if generate_report_from_server( date_array, collection, centro_id, excel_save_path ):
            # Marcar el archivo para ser eliminado despues de la solicitud
            try:
                @after_this_request
                def remove_file(response):
                    try:
                        os.remove(dest_path) #excel_save_path
                        ...
                    except Exception as error:
                        app.logger.error("Error al eliminar archivo", error)
                    return response

                # return send_file(file_remove, as_attachment=True)
                if file_flag is True:
                    print("entramos a mandar reporte")
                    try:
                        relative_file_path = dest_path.split("reportes")[-1]
                    except Exception as e:
                        print(e)

                    print(f"{relative_file_path=}")
                    # return send_file(f"./reportes{relative_file_path}", as_attachment=True, download_name= relative_file_path.replace(f"_{ts_request}", ""))
                    return send_file(f"./reportes{relative_file_path}", as_attachment=True, download_name= relative_file_path)
                
                    # return send_file(dest_path, as_attachment=True)
                
            except Exception as e:
                return jsonify({"mensaje": f"No hay datos para generar reportes, con error {e}"}), 204
        
        
    
        
        elif ret_status is False:
            print("respuesta 204")
            return jsonify({"mensaje": f"No hay datos para generar reportes"}), 204
        elif ret_status is None:
            return jsonify({"mensaje": f"Error generar reporte"}), 500
    else:
        return jsonify({"mensaje": f"No existe reporte para el servicio {SERVICE} al menos en esta ruta"}), 500
    return jsonify({"mensaje": f"No existe reporte para el servicio {SERVICE} al menos en esta ruta"}), 200

#Original
#New_feature
@api_bp.route('/flux/generate_report/<CENTRO>/<FECHA_INICIO>/<FECHA_FIN>', methods=['GET'])
def download_flux_report_range(CENTRO, FECHA_INICIO, FECHA_FIN):
    """
    Endpoint actualizado con motor generate_flux_report.
    - Retorna 200 (xlsx) si hay datos.
    - Retorna 404 (JSON) si no hay datos en el rango.
    - Retorna 500 (JSON) si hay error.
    """
    try:
        print(f"{FECHA_INICIO=}")
        print(f"{FECHA_FIN=}")
        print()
        # 1. Configuración de Directorios
        base_dir = os.path.dirname(os.path.abspath(__file__)) 
        reports_dir = os.path.join(base_dir, "reportes")
        os.makedirs(reports_dir, exist_ok=True)
        
        # Sanitización de nombre
        centro_clean = CENTRO.replace(" ", "").replace("/", "-")
        filename = f"Reporte_Flujometros_{centro_clean}_{FECHA_INICIO}_{FECHA_FIN}.xlsx"
        filepath = os.path.join(reports_dir, filename)

        # 2. Preparar conexión DB (Reutilizando la de Flask)
        client = get_db()
        db = client[FLUX_DATABASE_NAME]

        # 3. Preparar Fechas (De string YYYY-MM-DD a objetos date)
        try:
            start_date = datetime.strptime(FECHA_INICIO, "%Y-%m-%d").date()
            end_date = datetime.strptime(FECHA_FIN, "%Y-%m-%d").date()
        except ValueError:
            return jsonify({"mensaje": "Formato de fecha inválido. Use YYYY-MM-DD."}), 400

        # Calcular lista de días
        days_between = (end_date - start_date).days + 1
        dates_list = [start_date + timedelta(days=i) for i in range(days_between)]

        # 4. Generación del Reporte (In-Memory Workbook)
        wb = openpyxl.Workbook()
        if "Sheet" in wb.sheetnames:
            wb.remove(wb["Sheet"]) # Limpiar hoja por defecto

        data_found = False # Bandera para control 404

        # --- NUCLEO: Iterar días usando el nuevo script ---
        for date_obj in dates_list:
            # process_flux_day retorna True si encontró y escribió datos para ese día
            # Pasamos [CENTRO] como lista porque la función espera una lista de IDs
            has_data_day = process_flux_day(db, wb, date_obj, [CENTRO])
            
            if has_data_day:
                data_found = True

        # 5. Validación final (Caso 404)
        if not data_found:
            msg = f"No se encontraron datos para el centro '{CENTRO}' entre {FECHA_INICIO} y {FECHA_FIN}."
            return jsonify({"mensaje": msg, "error_code": "DATA_NOT_FOUND"}), 404

        # 6. Guardar Archivo
        wb.save(filepath)
        wb.close() # Buena práctica cerrar el WB explícitamente

        # Verificar integridad
        if not os.path.exists(filepath):
            return jsonify({"mensaje": "Error interno: El archivo no se pudo generar."}), 500

        # 7. Limpieza Post-Envío
        @after_this_request
        def remove_file(response):
            try:
                if os.path.exists(filepath):
                    os.remove(filepath)
            except Exception as error:
                print(f"Error eliminando archivo temporal: {error}")
            return response

        # 8. Envío
        return send_file(
            filepath,
            as_attachment=True,
            download_name=filename,
            mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
        )

    except Exception as e:
        print(f"Error crítico en descarga Flujometro: {e}")
        # Importante: traceback para depuración en logs del servidor
        import traceback
        traceback.print_exc()
        return jsonify({"mensaje": f"Error interno del servidor: {str(e)}"}), 500

@api_bp.route('/<SERVICE>/generate_historical_report/<REGION>/<AREA>/<CENTRO>', methods=['POST'])
def generate_historical_report(SERVICE, REGION, AREA, CENTRO):
    # return jsonify({"mensaje": "Aqui se debe generar el reporte historico de control de nanoox"}), 200
    client = get_db()
    nanoox_db = client[NANOOX_DATABASE_NAME]

    SERVICE = SERVICE.upper()
    data = request.json

    # REGION = 'loslagos'
    # print(f"HARD {REGION=}")
    centro_id = f"{CENTRO}-{AREA}-{REGION}"

    # centro_id = f"{CENTRO}-{AREA}-{REGION}"

    # excel_save_path = f"{REPORTS_DIRECTORY}/{centro_id}-reporte_historial.xlsx"
    # print(f"{REPORTS_DIRECTORY=}")    
    excel_save_path = f"{REPORTS_DIRECTORY}/"
    file_name = f"{centro_id}-reporte_historial.xlsx"
    file_remove = str(excel_save_path+file_name)

    print(f"{file_name=}")
    print(f"{file_remove=}")
    print(f"{excel_save_path=}")

    reporte_status= gen_reporte_historial(nanoox_db, data,excel_save_path, file_name )
    print(f"{reporte_status=}")
    if reporte_status is True:
        print("Hay datos y se genera excel")
    # if generate_report_from_server( date_array, collection, centro_id, excel_save_path ):
        # Marcar el archivo para ser eliminado despues de la solicitud
        try:
            @after_this_request
            def remove_file(response):
                try:
                    os.remove(file_remove)
                except Exception as error:
                    app.logger.error("Error al eliminar archivo", error)
                return response
            if reporte_status:
                return send_file(f"./reportes/{centro_id}-reporte_historial.xlsx", as_attachment=True)
            else:
                return jsonify({"mensaje": f"No hay datos para generar reportes, con error {e}"}), 204
        except Exception as e:
            return jsonify({"mensaje": f"No hay datos para generar reportes, con error {e}"}), 204
    elif reporte_status is False:
        print("NO Hay datos y NO se genera excel")
        return jsonify({"mensaje": f"No hay datos para generar reportes, con error"}), 204

    elif reporte_status is None:
        print("Error servidor")
        return jsonify({"mensaje": f"Error generar reporte"}), 500


@api_bp.route('/<SERVICE>/generate_generator_report/<REGION>/<AREA>/<CENTRO>', methods=['POST'])
def generate_generator_report(SERVICE, REGION, AREA, CENTRO):
    # return jsonify({"mensaje": "Aqui se debe generar el reporte historico de control de nanoox"}), 200
    client = get_db()
    nanoox_db = client[NANOOX_DATABASE_NAME]

    SERVICE = SERVICE.upper()
    data = request.json
    print("###########ENTRANDO A GENERAR REPORTE GENERADOR ####################")
    centro_id = f"{CENTRO}-{AREA}-{REGION}"
    centro_id = convertir_mal_menor(centro_id)

    excel_save_path = f"{REPORTS_DIRECTORY}/"
    file_name = f"{centro_id}-reporte_generador.xlsx"
    file_remove = str(excel_save_path+file_name)

    print(f"{file_name=}")
    print(f"{file_remove=}")
    print(f"{excel_save_path=}")

    reporte_status= gen_reporte_generadores(nanoox_db, data,excel_save_path, file_name )

    if reporte_status is True:
        print("Habian datos")
    # if generate_report_from_server( date_array, collection, centro_id, excel_save_path ):
        # Marcar el archivo para ser eliminado despues de la solicitud
        try:
            @after_this_request
            def remove_file(response):
                try:
                    os.remove(file_remove)
                except Exception as error:
                    app.logger.error("Error al eliminar archivo", error)
                return response

            return send_file(f"./reportes/{centro_id}-reporte_generador.xlsx", as_attachment=True)

        except Exception as e:
            return jsonify({"mensaje": f"No hay datos para generar reportes, con error {e}"}), 204
    elif reporte_status is False:
        print("No habian datos en la fecha, respondemos 204")
        return jsonify({"mensaje": f"No hay datos para generar reportes, con error {e}"}), 204
    elif reporte_status is None:
        print("Error servidor")
        return jsonify({"mensaje": f"Error generar reporte"}), 500

# !--- CRUD de Usuarios ---!
#Crear usuario
@api_bp.route('/crear_usuario', methods=['POST'])
@jwt_required()
def api_crear_usuario():
    # print("ENTRANDO A LA NUEVA RUTA DE CREAR USUARIOS")
    client = get_db()
    users_collection = client[USER_DATABASE_NAME].users
    users_historial_collection = client[USER_DATABASE_NAME].historial_users

    usuario_actual = get_jwt_identity()  # obtenido desde el token
    data = request.json

    # Asegurar que sea lista
    if isinstance(data, dict):
        data = [data]

    resultados = []

    for data_user in data:
        try:
            print(f'Intentando crear: {data_user=}')
            resultado, codigo_estado = crear_usuario(users_collection, data_user, usuario_actual)

            # Log si fue exitoso
            if codigo_estado == 201:
                insert_log_in_user_history(
                    users_historial_collection, usuario_actual, data_user.get('username'), 'add_user'
                )

            resultados.append({
                "usuario": data_user.get('username'),
                "estado": codigo_estado,
                "resultado": resultado.get_json() if hasattr(resultado, 'get_json') else resultado
            })
        except Exception as e:
            print(f"Error al crear usuario {data_user.get('username')}: {e}")
            resultados.append({
                "usuario": data_user.get('username'),
                "estado": 500,
                "resultado": f"Error inesperado: {str(e)}"
            })

    return jsonify({"resultados": resultados}), 207  # 207: Multi-Status


#Eliminar usuario
@api_bp.route('/delete_user/<user>/<delete_user>', methods=['DELETE'])
def delete_user(user, delete_user):
    """
    Endpoint para eliminar un usuario existente en la base de datos.
    
    Parametros:
    - username (str): Nombre de usuario del usuario a eliminar.

    Retorna:
    - Un objeto JSON con el resultado de la operacion y un codigo de estado HTTP.
    
    Ejemplo de uso:
    DELETE /delete_user/nombre_de_usuario
    """
    client = get_db()
    users_collection = client[USER_DATABASE_NAME].users
    users_historial_collection = client[USER_DATABASE_NAME].historial_users

    usuario_actual = user
    usuario_delete = delete_user
    print(f'usuario_actual: {usuario_actual} \n')
    print(f'usuario_delete :{usuario_delete} \n')
    usuario_actual_info = users_collection.find_one({'username': usuario_actual})
    usuario_delete_info = users_collection.find_one({'username': usuario_delete})
    #print(f'usuario_actual_info: {usuario_actual_info} \n')
    cargo_actual = usuario_actual_info['cargo']
    cargo_edit = usuario_delete_info['cargo']

    # if cargo_actual == 'Operario':
    #     # return "No posee permisos para editar/eliminar"
    #     return jsonify({"mensaje": "No posee permisos para editar/eliminar."}), 400

    if verificar_permisos_de_edicion(cargo_actual, cargo_edit):
        try:
            # Llamada a la funcion que elimina el usuario
            status_code = eliminar_usuario(users_collection, usuario_delete)
            
            # Respuesta en caso de exito
            if status_code == 200:
                insert_log_in_user_history(users_historial_collection, usuario_actual, usuario_delete, 'delete_user')
                return jsonify({"mensaje": f"Usuario '{usuario_delete}' eliminado correctamente."}), 200
            # Respuesta en caso de que no se encuentre el usuario o no se elimine
            else:
                return jsonify({"mensaje": f"Usuario '{usuario_delete}' no encontrado o no se elimino ningun usuario."}), 400
        except Exception as e:
            # Respuesta en caso de un error inesperado
            return jsonify({"mensaje": f"Error inesperado: {str(e)}"}), 500
    else:
        return jsonify({"mensaje": "No se puede continuar por falta de rango."}), 400



@api_bp.route('/obtener_usuarios', methods=['GET'])
def api_obtener_usuarios():
    try:
        client = get_db()
        users_collection = client[USER_DATABASE_NAME].users
        # Aqui asumimos que 'usuarios_collection' es una variable que ya tienes definida
        # y que representa la coleccion de usuarios en tu base de datos de MongoDB.

        # Seleccionamos solo los campos que queremos retornar, excluyendo el campo password por seguridad y MongoDB ID .
        usuarios = users_collection.find({}, {'_id':0 , 'password': 0})

        # Convertimos los usuarios a una lista para poder enviarlos como JSON
        lista_usuarios = list(usuarios)

        # Retornamos la lista de usuarios como un JSON
        return jsonify(lista_usuarios), 200
    except Exception as e:
        # En caso de error, retornamos un mensaje indicando el fallo
        return jsonify({"mensaje": "Error al obtener la lista de usuarios", "error": str(e)}), 500

# NEW
@api_bp.route('/editar_usuarios', methods=['POST'])
@jwt_required()
def api_editar_usuarios_multiples():
    """
    Permite editar multiples usuarios, incluyendo transformacion de permisos simplificados para Jefe Area y Jefe Region.
    {
    "edits": [
        {
        "username": "usuarioA",
        "updates": {
            "correo": "nuevoA@ejemplo.com",
            "cargo": "Jefe Area",
            "permisos": {
            "loslagos": {
                "chiloe": {
                "chalihue-chiloe-loslagos": {
                    "enviro": { "status": true },
                    "nanoox": { "status": false },
                    "pss":    { "status": true },
                    "scapp":  { "status": true }
                }
                }
            }
            }
        }
        }
        // …otros objetos de edicion…
    ]
    }
    """
    editor = get_jwt_identity()
    

    client    = get_db()
    users_col   = client[USER_DATABASE_NAME].users
    history_col = client[USER_DATABASE_NAME].historial_users

    # body  = request.get_json(force=True) or {}
    body = request.json
    # print(f"{body=}")
    edits = body.get('edits')
    if not isinstance(edits, list) or not edits:
        return jsonify({'error': "Se requiere un array 'edits' no vacio."}), 400

    editor_info = users_col.find_one({'username': editor})
    if not editor_info:
        return jsonify({'error': 'Editor no encontrado.'}), 404
    cargo_editor = editor_info['cargo']
    if cargo_editor == 'Operario':
        return jsonify({'error': 'No posee permisos para editar.'}), 403

    results = []
    for edit in edits:
        username = edit.get('username')
        updates  = edit.get('updates')
        item = {'username': username}

        if not username or not isinstance(updates, dict):
            item.update({'status': 'error', 'msg': 'Payload invalido.'})
            results.append(item)
            continue

        target_info = users_col.find_one({'username': username})
        if not target_info:
            item.update({'status': 'error', 'msg': 'Usuario no encontrado.'})
            results.append(item)
            continue

        # Jerarquia de cargos
        if not verificar_permisos(cargo_editor, target_info['cargo']):
            item.update({'status': 'denied', 'msg': 'Rango insuficiente.'})
            results.append(item)
            continue

        try:
            editar_usuarios(users_col, username, updates, editor_info=editor_info)

            # INTEGRACIÓN PUSH: Suscripción automática a nuevos centros
            fcm_token = target_info.get('fcm_token') # Token guardado en DB
            permisos_nuevos = updates.get('permisos')

            if fcm_token and permisos_nuevos:
                topicos_a_suscribir = []
                
                # Recorremos la estructura anidada de permisos
                # Estructura: Region -> Area -> Centro -> Servicio
                for region, areas in permisos_nuevos.items():
                    for area, centros in areas.items():
                        for centro_raw, servicios in centros.items():
                            
                            # Verificamos si AL MENOS UN servicio se activó (status: true)
                            se_activo = False
                            for srv, data in servicios.items():
                                if isinstance(data, dict) and data.get('status') is True:
                                    se_activo = True
                                    break
                            
                            if se_activo:
                                # Transformamos el ID crudo al formato Topic
                                # Entrada: "itg-puertomontt-loslagos"
                                # Salida esperada: "Loslagos-Puertomontt-Itg"
                                try:
                                    parts = centro_raw.split('-')
                                    if len(parts) >= 3:
                                        c_name = parts[0].capitalize()
                                        a_name = parts[1].capitalize()
                                        r_name = parts[2].capitalize()
                                        
                                        topic_formateado = f"{r_name}-{a_name}-{c_name}"
                                        topicos_a_suscribir.append(topic_formateado)
                                except Exception as e_topic:
                                    print(f" Error formateando tópico {centro_raw}: {e_topic}")

                # Llamamos a la función mágica si hay algo que suscribir
                if topicos_a_suscribir:
                    print(f" [AUTO-SUB] Suscribiendo {username} a: {topicos_a_suscribir}")
                    suscribir_token_a_topicos(fcm_token, topicos_a_suscribir)

            insert_log_in_user_history(
                history_col,
                editor,
                username,
                'edit_user',
            )
            item.update({'status': 'ok'})

        except Exception as e:
            print(f"Error editando usuario {username}: {e}")
            item.update({'status': 'error', 'msg': str(e)})

        results.append(item)

    return jsonify({'results': results}), 207

@api_bp.route('/ediciones_usuario', methods=['GET'])
@jwt_required()
def usuarios_a_editar():
    username = get_jwt_identity()

    client = get_db()
    users_collection = client[USER_DATABASE_NAME].users
    
    lista_editables = []
    proyeccion = {"_id": 0, "password": 0, "refresh_token": 0, "access_token": 0}
    #username = username
    usuario_actual_info = users_collection.find_one({'username': username})
    cargo_actual = usuario_actual_info['cargo']
    # print(f"{usuario_actual_info=}")
    # print(f"{cargo_actual=}")    
    # permisos_actual = usuario_actual_info['permisos']
    for usuario in users_collection.find():
        if verificar_permisos(cargo_actual, usuario['cargo']):
            resultados = users_collection.find({'username': usuario['username']}, proyeccion)
            lista_editables.append(resultados[0])
    if lista_editables is not None:
        print(f"{lista_editables=}")
        return lista_editables
    else:
        #return "El ususario no tiene usuarios editables"
        return jsonify({"mensaje": f"El usuario {username} no tiene usuarios editables"}), 401


#!--Reestablecer contraseña via e-mail--¡#
@api_bp.route('/get_link', methods=['POST'])
def reset_password():
    """
    A partir del nombre de usuario o correo, obtiene su email desde la DB y 
    envía un e-mail con un token para restablecer la contraseña en el Frontend.
    """
    print("Entrando a get_link!")
    
    # 1. Validaciones
    data = request.json
    print(f"{data=}")

    claves = ['username', 'correo']
    seleccion = next((k for k in claves if data.get(k)), None)
    
    if not seleccion:
        return jsonify({'Error': 'Ni username ni correo tienen valor.'}), 400

    valor = data[seleccion]

    # 2. Conexion a DB
    client = get_db()
    users_collection = client['users']['users']

    # 3. Query a DB para validar existencia
    if seleccion == 'username':
        user = users_collection.find_one({'username': valor})
    else:
        user = users_collection.find_one({'correo': valor})

    if not user:
        return jsonify({'Error': f'El usuario/correo {valor} no se encuentra registrado.'}), 404

    correo = user.get('correo')
    username = user.get('username')

    if not correo:
        return jsonify({'Error': 'El usuario no posee un correo asociado.'}), 404

    # 4. Generar JWT de restablecimiento
    expires = timedelta(hours=1)
    token = create_reset_token(username, expires)
    print("Decodificando TOKEN")

    # 5. Extraer JTI y almacenar en DB
    decoded = decode_reset_token(token)
    jti = decoded.get('jti')
    expires_at = datetime.now() + expires
    
    try:
        print("Insertando JTI en db")
        users_collection.update_one(
            {'username': username},
            {'$set': {
                'reset_jti': jti,
                'reset_expires_at': expires_at
            }}
        )
        print("><> JTI en DB")
    except Exception as e:
        print(f"Error al insertar JTI: {e} {e.__traceback__.tb_lineno}")
        return jsonify({'Error': 'Server Internal Error.'}), 500

    # 6. ESTÁNDAR DE LA INDUSTRIA: Construir la URL hacia el Frontend
    # Lo ideal a futuro es que el dominio base venga de una variable de entorno (os.environ.get('FRONTEND_URL'))
    frontend_domain = "https://ventisqueros.itgchile.com"
    
    # Armamos la ruta tal cual la espera React Router
    link = f"{frontend_domain}/ResetPassword/{token}"
    print(f"Enlace generado para el frontend: {link}")

    # 7. Enviar correo a usuario
    print("Entrando a enviar mail")
    send_automatic_email(username, correo, link)
    print(f"Enlace enviado a usuario/correo {username}/{correo}")
    
    # Devolvemos un JSON limpio
    return jsonify({'Message': 'Enlace enviado para restablecer la contraseña.'}), 200


@api_bp.route('/update_password', methods=['POST'])
def update_password():
    data = request.json
    token = data.get('token')
    new_password = data.get('new_password')

    if not token or not new_password:
        return jsonify({'Error': 'Faltan datos (token o nueva contraseña).'}), 400

    try:
        # 1. Decodificar el token para asegurar que es válido y no expiró
        decoded = decode_reset_token(token)
        username = decoded.get('sub') # Asumiendo que guardaste el user en el 'sub' (subject)
        jti = decoded.get('jti')

        client = get_db()
        users_collection = client['users']['users']
        user = users_collection.find_one({'username': username})

        if not user:
            return jsonify({'Error': 'Usuario no encontrado.'}), 404

        # 2. (OPCIONAL PERO RECOMENDADO) Validar que el JTI coincida con el de la DB 
        # Esto previene que un token se use dos veces.
        if user.get('reset_jti') != jti:
             return jsonify({'Error': 'Este enlace ya fue utilizado o es inválido.'}), 401

        # 3. Hashear la nueva contraseña (¡NUNCA GUARDAR EN TEXTO PLANO!)
        # hashed_password = bcrypt.hashpw(new_password.encode('utf-8'), bcrypt.gensalt())
        
        # 4. Actualizar la DB y "quemar" el JTI para que no se re-use
        users_collection.update_one(
            {'username': username},
            {
                '$set': {'password': new_password}, # Usa la clave hasheada aquí
                '$unset': {'reset_jti': "", 'reset_expires_at': ""} # Limpiamos el token de la db
            }
        )

        return jsonify({'Message': 'Contraseña actualizada exitosamente.'}), 200

    except Exception as e:
        print(f"Error al actualizar password: {e}")
        return jsonify({'Error': 'Token inválido o expirado.'}), 401

@api_bp.route('/ResetPassword/<token>', methods=['POST'])
def reset_token(token):
    """
    Ruta dinamica para ingresar nueva contraseña solo si el token de acceso es valido (nombre, clave_secreta y duracion).
    De modificar exitosamente la clave, genera un log en historial de usuarios.
    """
    # POST: procesar formulario o JSON
    #1. Verificar y extraer username del token
    try:
        decoded = decode_reset_token(token)
        print(f"reset {decoded=}")
    except Exception:
        return jsonify({'Error': 'Token invalido o expirado.'}), 403
    
    #2. Validar claim 'reset'
    if not decoded.get('reset'):
        return jsonify({'Error': 'Token no valido para restablecimiento.'}), 403

    #3. Obtener ID unico deL Token
    jti = decoded.get('jti')

    #4. Conexion a DB
    client = get_db()
    users_db = client['users']
    users_collection = users_db.users
    users_historial_collection = users_db.historial_users

    #5. Buscar usuario que tenga este JTI activo
    user = users_collection.find_one({
        'reset_jti': jti,
        'reset_expires_at': {'$gt': datetime.now()}
    })
    print(f"{user=}")
    if not user or user==None:
        print('Token ya usado o expirado.')
        return jsonify({'Error': 'Token ya usado o expirado.'}), 403
    
    username = user['username']
    if not username:
        return jsonify({'Error': 'No hay datos del usuario.'}), 404

    #6. Obtener nueva contraseña
    data = request.get_json(silent=True) or request.form
    new_password = data.get('password')
    if not new_password:
        return jsonify({'Error': 'Se requiere la nueva contraseña.'}), 406

    #7. Actualizar contraseña en DB y limpiar JTI    
    result = users_collection.update_one(
        {'username': username},
        {'$set': {'password': generate_password_hash(new_password)},
         '$unset': {'reset_jti': '', 'reset_expires_at': ''}}
    )
    if result.modified_count == 0:
        return jsonify({'Error': 'Error al actualizar contraseña.'}), 500
    print("Se ha modificado exitosamente la contraseña en la DB")
    #8. Registro de log de restablecimiento exitoso tras actualizar password
    # timestamp = datetime.now()
    # log_entry = {
    #     'date': timestamp,
    #     'usuario': username,
    #     'msg': f'El usuario {username} modifico su contraseña a las {timestamp.isoformat()}'
    # }
    # users_historial_collection.insert_one(log_entry)
    insert_log_in_user_history(users_historial_collection, username, None, 'renew_password')
    # print(f"Log insertado en Histortial de Usuarios\n")
    print(f'Contraseña actualizada correctamente para el usuario {username}.')
    return jsonify({'Message': f'Contraseña actualizada correctamente para el usuario {username}.'}), 200

@api_bp.route('/get_users_historical_logs', methods=['GET'])
def get_logs():
    """
    Endpoint to fetch user action logs by specific date, date range, and type.

    Query parameters:
    - type: one of ['edit_user', 'add_user', 'delete_user', 'renew_password'] or 'all' (default 'all')
    - date: single day in 'YYYY-MM-DD' format (optional)
    - start_date: beginning of range in 'YYYY-MM-DD' format (optional)
    - end_date: end of range in 'YYYY-MM-DD' format (optional)

    Examples:
    /logs?date=2025-06-19&type=edit_user
    /logs?start_date=2025-06-01&end_date=2025-06-10&type=all
    """
    client = get_db()
    users_db = client['users']
    users_historial_collection = users_db.historial_users

    # Retrieve query parameters
    type_param = request.args.get('type', 'all')
    date_str = request.args.get('date')
    start_str = request.args.get('start_date')
    end_str = request.args.get('end_date')

    query = {}

    # Filter by type if specified and not 'all'
    if type_param and type_param.lower() != 'all':
        query['type'] = type_param

    # Single-day filter
    if date_str:
        try:
            date_obj = datetime.strptime(date_str, '%Y-%m-%d')
        except ValueError:
            return jsonify({'error': 'Invalid date format, use YYYY-MM-DD'}), 400
        next_day = date_obj + timedelta(days=1)
        query['date'] = {'$gte': date_obj, '$lt': next_day}

    # Range filter (only if date not provided)
    elif start_str or end_str:
        date_filter = {}
        if start_str:
            try:
                start_obj = datetime.strptime(start_str, '%Y-%m-%d')
            except ValueError:
                return jsonify({'error': 'Invalid start_date format, use YYYY-MM-DD'}), 400
            date_filter['$gte'] = start_obj
        if end_str:
            try:
                end_obj = datetime.strptime(end_str, '%Y-%m-%d')
            except ValueError:
                return jsonify({'error': 'Invalid end_date format, use YYYY-MM-DD'}), 400
            # Include the end date fully by adding one day
            date_filter['$lt'] = end_obj + timedelta(days=1)
        if date_filter:
            query['date'] = date_filter
    print(f"{list(query)=}")
    # Execute the query and sort by date ascending
    # cursor = users_historial_collection.find(query).sort('date', 1)
    cursor = list(users_historial_collection.find(query, {'_id': 0}).sort('date', 1))
    print(f"{cursor=}")
    # Prepare results for JSON serialization
    results = []
    for doc in cursor:
        print(f"{list(doc)=}")
        # Optionally format date as string
        if 'date' in doc and isinstance(doc['date'], datetime):
            doc['date'] = doc['date'].strftime('%Y-%m-%dT%H:%M:%S')
        results.append(doc)

    return jsonify(results), 200

@api_bp.route('/report_gps/<REGION>/<AREA>/<CENTRO>', methods=['POST'])
def report_position(REGION, AREA, CENTRO):
    try:
        data = request.get_json(force=True)
        # data = request.json
        print(f"{data=}")
        
        CENTRO = CENTRO.lower()
        AREA = AREA.lower()
        REGION = REGION.lower()

        centro_id = f"{CENTRO}-{AREA}-{REGION}"
        print(f"{centro_id=}")

    except Exception as e:
        return jsonify({'Error': f'JSON invalido {e}'}), 400
    
    lat = data.get('lat')
    lon = data.get('lon')

    if not centro_id or lat is None or lon is None:
        return jsonify({'Error': 'Faltan parametros: centro_id, lat y lon son obligatorios'}), 400

    # Convertir a float si vienen como strings
    try:
        lat = float(lat)
        lon = float(lon)
    except ValueError:
        return jsonify({'Error': 'lat y lon deben ser valores numericos'}), 400

    # Subdocumento georef con valores planos y timestamp
    coordinates = {
        'lat': lat,
        'lon': lon,
        'timestamp': datetime.now() # seg. desde epoch como float
    }

    #Conexion a DB y coleccion
    client = get_db()
    users_db = client[USER_DATABASE_NAME]
    ciclos_collection = users_db['ciclos']
    
    result = ciclos_collection.update_one(
        {'centro_id': centro_id},
        {'$set': {'gps_coordinates.auto': coordinates}}
        # upsert=True
    )

    if result.matched_count == 0:
        return jsonify({'Error': f'No existe documento con ID {centro_id}'}), 404
        
    print(f"Datos de GPS actualizados para centro {centro_id}")
    return jsonify({'Message': f'Datos de GPS actualizados para centro {centro_id}'}), 200

@api_bp.route('/centros/detectados', methods=['GET'])
@jwt_required()
def centros_detectados():
    db = get_db().users
    username = get_jwt_identity()
    usuario = db.users.find_one({'username': username}, {'cargo': 1}) or {}
    if usuario.get('cargo') != 'Superusuario':
        log_centros.warning("centros.detectados_rechazado motivo=sin_permiso usuario=%s", username)
        return jsonify(codigo='sin_permiso'), 403
    documentos = list(db.ciclos.find({'origen': 'auto', '$or': [
        {'estado_alta': 'pendiente'}, {'autodeteccion.publicacion_completa': {'$ne': True}},
    ]}, {'_id': 0}))
    return jsonify(documentos)


@api_bp.route('/centros/detectados/<centro_id>/confirmar', methods=['POST'])
@jwt_required()
def confirmar_centro_detectado(centro_id):
    db = get_db().users
    username = get_jwt_identity()
    usuario = db.users.find_one({'username': username}, {'cargo': 1}) or {}
    if usuario.get('cargo') != 'Superusuario':
        log_centros.warning("centros.confirmar_rechazado motivo=sin_permiso centro=%s usuario=%s", centro_id, username)
        return jsonify(codigo='sin_permiso'), 403
    try:
        with bloqueo_automaticos():
            doc = db.ciclos.find_one({'centro_id': centro_id, 'origen': 'auto'})
            if not doc:
                log_centros.warning("centros.confirmar_rechazado motivo=no_encontrado centro=%s usuario=%s", centro_id, username)
                return jsonify(codigo='centro_no_encontrado'), 404
            if doc.get('autodeteccion', {}).get('publicacion_completa'):
                log_centros.info("centros.confirmar_sin_cambios motivo=ya_publicado centro=%s usuario=%s", centro_id, username)
                return jsonify(ok=True)
            completar_alta(db, doc, guardar_permiso)
            _, area, region = centro_id.split('-')
            path = f'permisos.{region}.{area}.{centro_id}'
            # $type object también coincide con arrays de objetos; excluirlos expresamente.
            elegibles = [u['_id'] for u in db.users.find({
                'permisos': {'$type': 'object', '$not': {'$type': 'array'}},
                f'permisos.{region}': {'$type': 'object', '$not': {'$type': 'array'}}, '$or': [
                {'cargo': 'Jefe Area', f'permisos.{region}.{area}': {'$type': 'object', '$not': {'$type': 'array'}}},
                {'cargo': 'Jefe Region', '$or': [
                    {f'permisos.{region}.{area}': {'$exists': False}},
                    {f'permisos.{region}.{area}': {'$type': 'object', '$not': {'$type': 'array'}}},
                ]},
            ]}, {'_id': 1})]
            if doc.get('estado_alta') != 'confirmado':
                db.ciclos.update_one({'_id': doc['_id']}, {'$set': {'estado_alta': 'confirmado'}})
                log_centros.info("centros.confirmado centro=%s usuario=%s jefes_elegibles=%d (publicación iniciada)",
                                 centro_id, username, len(elegibles))
            else:
                log_centros.info("centros.publicacion_reintento centro=%s usuario=%s jefes_elegibles=%d",
                                 centro_id, username, len(elegibles))
            actualizar_permisos_jefes([doc], db)
            faltantes = [u.get('username') for u in db.users.find(
                {'_id': {'$in': elegibles}, path: {'$exists': False}}, {'username': 1})]
            if faltantes:
                log_centros.warning("centros.publicacion_incompleta centro=%s usuario=%s faltan=%s (reintento manual)",
                                    centro_id, username, ",".join(map(str, faltantes)))
                return jsonify(codigo='publicacion_incompleta'), 503
            db.ciclos.update_one({'_id': doc['_id']}, {'$set': {
                'autodeteccion.publicacion_completa': True,
                'confirmado_por': username, 'confirmado_en': datetime.now(),
            }})
            log_centros.info("centros.publicado centro=%s usuario=%s jefes=%d", centro_id, username, len(elegibles))
        try:
            insert_log_in_user_history(db.historial_users, username, centro_id, 'confirm_detected_center')
        except Exception as error:
            log_centros.warning("centros.auditoria_fallida accion=confirmar centro=%s usuario=%s error=%s",
                                centro_id, username, error)
        return jsonify(ok=True)
    except BlockingIOError:
        log_centros.warning("centros.confirmar_pospuesto motivo=exclusion_ocupada centro=%s usuario=%s", centro_id, username)
        return jsonify(codigo='centro_reintentar'), 503
    except (OSError, RuntimeError) as error:
        log_centros.warning("centros.confirmar_fallido centro=%s usuario=%s error=%s", centro_id, username, error)
        return jsonify(codigo='centro_reintentar'), 503
    except Exception:
        log_centros.exception("centros.confirmar_fallido centro=%s usuario=%s", centro_id, username)
        return jsonify(codigo='centro_reintentar'), 503


def eliminar_automatico(client, documento, username):
    db = client.users
    centro_id = documento['centro_id']
    usuario = db.users.find_one({'username': username}, {'cargo': 1}) or {}
    if usuario.get('cargo') != 'Superusuario':
        log_centros.warning("centros.borrado_rechazado motivo=sin_permiso centro=%s usuario=%s", centro_id, username)
        return jsonify(codigo='sin_permiso'), 403
    try:
        with bloqueo_automaticos():
            doc = db.ciclos.find_one({'_id': documento['_id'], 'origen': 'auto'})
            if not doc:
                log_centros.warning("centros.borrado_rechazado motivo=no_encontrado centro=%s usuario=%s", centro_id, username)
                return jsonify(codigo='centro_no_encontrado'), 404
            centro, area, region = centro_id.split('-')
            path = f'permisos.{region}.{area}.{centro_id}'
            filtro = {path: {'$exists': True}}
            retirados = db.users.update_many(filtro, {'$unset': {path: ''}}).modified_count
            estados = db.estados.delete_many({'centro_id': centro_id}).deleted_count
            if db.users.count_documents(filtro) or db.estados.count_documents({'centro_id': centro_id}):
                log_centros.warning("centros.borrado_incompleto centro=%s usuario=%s motivo=permisos_o_estados_persisten "
                                    "(el ciclo se conserva para reintentar)", centro_id, username)
                return jsonify(codigo='centro_reintentar'), 503
            for database in SERVICES_DB_MAP.values():
                try:
                    delete_contact_list(client[database][EMAIL_COLLECTION_NAME], region, area, centro)
                except Exception as error:
                    log_centros.warning("centros.borrado_emails_fallido centro=%s base=%s error=%s (mejor esfuerzo)",
                                        centro_id, database, error)
            db.ciclos.delete_one({'_id': doc['_id']})
            log_centros.info("centros.borrado_automatico centro=%s usuario=%s permisos_retirados=%d estados=%d",
                             centro_id, username, retirados, estados)
        try:
            insert_log_in_user_history(db.historial_users, username, centro_id, 'delete_center')
        except Exception as error:
            log_centros.warning("centros.auditoria_fallida accion=borrar centro=%s usuario=%s error=%s",
                                centro_id, username, error)
        return jsonify(mensaje=f"El centro '{centro_id}' ha sido eliminado exitosamente.")
    except BlockingIOError:
        log_centros.warning("centros.borrado_pospuesto motivo=exclusion_ocupada centro=%s usuario=%s", centro_id, username)
        return jsonify(codigo='centro_reintentar'), 503
    except Exception:
        log_centros.exception("centros.borrado_fallido centro=%s usuario=%s", centro_id, username)
        return jsonify(codigo='centro_reintentar'), 503


#!---CRUD de Centros desde Interfaz Grafica---¡
@api_bp.route('/crear_centro', methods=['POST'])
@jwt_required()
def api_crear_centro():
    print("ENTRANDO A CREAR CENTRO!")
    username = get_jwt_identity()
    client = get_db()
    # nanoox_db = client[NANOOX_DATABASE_NAME]
    user_db = client[USER_DATABASE_NAME]
    data_centros = request.json
    if isinstance(data_centros, list) and any(campos_reservados(x) for x in data_centros):
        log_centros.warning("centros.alta_manual_rechazada motivo=campos_reservados usuario=%s", username)
        return jsonify(codigo="campos_reservados"), 400
    # raw = request.json
    
    print(f'data_centros: {data_centros}')
    try:
        # Crear el centro en la colección de ciclos/usuarios
        ciclos_insert(client, username, data_centros)
        automaticos = {d["centro_id"] for d in user_db.ciclos.find(
            {"origen": "auto", "centro_id": {"$in": [d["centro_id"] for d in data_centros]}}, {"centro_id": 1})}
        data_centros = [d for d in data_centros if d["centro_id"] not in automaticos]
        if automaticos:
            # Un automático no se publica por el alta manual: se confirma en Detectados.
            log_centros.warning("centros.alta_manual_excluida motivo=centro_automatico centros=%s usuario=%s",
                                ",".join(sorted(automaticos)), username)
        if not data_centros:
            return jsonify(codigo="centro_automatico"), 409
        #A ctualizar permisos básicos
        permisos_superusuario(user_db)
        # Actualizar permisos de jefes Region y Area
        actualizar_permisos_jefes(data_centros, user_db)
        # Validar status de fechas
        verificacion_status_servicios_por_centro()
        #[NUEVA FUNCIONALIDAD] Procesar listas de emails por servicio
        procesar_configuracion_emails(client, data_centros)


        return jsonify({"mensaje": "Operacion exitosa"}), 200
    except Exception as e:
        print(f'Error: {e} {e.__traceback__.tb_lineno}')
        return jsonify({"mensaje": "No se logro realizar la operacion"}), 500
    
#!---Edicion de Centros desde Interfaz Grafica---¡#
@api_bp.route('/obtener_centros', methods=['GET'])
@jwt_required()
def centros_editables():
    client = get_db()
    user_db = client[USER_DATABASE_NAME]
    users_collection = user_db.users
    ciclos_collection = user_db.ciclos

    print(f"ENTRANDO A CENTROS EDITABLES")
    username = get_jwt_identity()
    # print(f'usuario_obtener_centros: {username}')
    usuario_actual_info = users_collection.find_one({'username': username})
    # print(f'usuario_actual: {usuario_actual_info}')
    cargo_actual = usuario_actual_info['cargo']
    # print(f'cargo: {cargo_actual}')
    proyeccion = {"_id": 0, "password": 0, "rol": 0, "refresh_token": 0, "access_token": 0}
    if cargo_actual not in ['Superusuario', 'Gerencia', 'Jefe Region', 'Jefe Area']:
        return jsonify({"mensaje": f"El usuario {username} no posee permisos de edicion"}), 500
    else:
        centros = obtener_lista_centros(users_collection.find_one({'username': username}, proyeccion)['permisos'])
        ciclos_centro = []
        # print(f'centros devueltos: {centros}')
        for centro in ciclos_collection.find({'centro_id': {'$in': centros}}, {"_id":0}):
            ciclos_centro.append(centro)
        # print(f'ciclos_centro devueltos: {ciclos_centro}')
        return jsonify(ciclos_centro), 200
    
@api_bp.route('/editar_centro/<centro_id>', methods=['POST'])
@jwt_required()
def editar_centro(centro_id):
    print("ENTRANDO A EDITAR CENTRO")
    #Obtiene el nombre del usuario que esta accediendo a traves del token
    username = get_jwt_identity()
    #Conexion a DB y colecciones
    client = get_db()
    user_db = client[USER_DATABASE_NAME]
    users_historial_collection = user_db.historial_users
    ciclos_collection = user_db.ciclos
    #Obtencion del JSON enviado por el FE
    datos_centro = request.json
    if campos_reservados(datos_centro):
        log_centros.warning("centros.edicion_rechazada motivo=campos_reservados centro=%s usuario=%s", centro_id, username)
        return jsonify(codigo="campos_reservados"), 400
    print(f"{datos_centro=}")
    try:
        #Inserta informacion de centro en coleccion Ciclos
        actualizar_documento(ciclos_collection, {'centro_id':centro_id}, datos_centro)
        #Inserta el log de la edicion del centro en la coleccion users_historial
        insert_log_in_user_history(users_historial_collection, username, centro_id, 'edit_center')
        verificacion_status_servicios_por_centro()
        # [NUEVO] Actualizar Emails
        # IMPORTANTE: procesar_configuracion_emails espera una LISTA de centros.
        # Como editar es de a uno, envolvemos datos_centro en una lista [datos_centro]
        if "emails" in datos_centro:
            # Aseguramos que el ID venga en el payload, si no, lo inyectamos del URL param
            if "centro_id" not in datos_centro:
                datos_centro["centro_id"] = centro_id
            
            procesar_configuracion_emails(client, [datos_centro])

        return jsonify({"mensaje": f"El centro {centro_id} se ha editado correctamente"}), 200
    except Exception as e:
        print(f'Error editar_centro: {e} {e.__traceback__.tb_lineno}')
        return jsonify({"mensaje": f"El centro {centro_id} no se ha logrado editar en este momento, intente nuevamente"}), 401

@api_bp.route('/eliminar_centro/<centro_id>', methods=['POST'])
@jwt_required()
def eliminar_centro(centro_id):
    username = get_jwt_identity()
    client = get_db()
    user_db = client[USER_DATABASE_NAME]
    users_historial_collection = user_db.historial_users
    ciclos_collection = user_db.ciclos
    users_collection = user_db.users
    estado_centros_collection = user_db.estados
    
    documento = ciclos_collection.find_one({"centro_id": centro_id})
    if documento and documento.get("origen") == "auto":
        return eliminar_automatico(client, documento, username)
    # Validar existencia
    if documento is None:
        return jsonify({"mensaje": f"El centro {centro_id} no existe"}), 500

    try:
        # 1. Eliminar de Ciclos (Principal)
        resultado = ciclos_collection.delete_one({'centro_id': centro_id})
        
        if resultado.deleted_count > 0:
            # 2. Limpieza de datos asociados
            eliminar_centro_de_permisos(users_collection, centro_id)
            eliminar_estados_centro(estado_centros_collection, centro_id)
            
            # 3. [NUEVO] Eliminar listas de emails en bases de datos de servicios
            # Necesitamos desglosar el ID para llamar a delete_contact_list
            try:
                # Intenta separar ID. Ajusta el split según tu lógica real (ej: si nombre tiene guiones)
                # Asumimos formato inverso según lógica previa: Nombre-Area-Region
                partes = centro_id.split('-')
                if len(partes) >= 3:
                    c_region = partes[-1]
                    c_area = partes[-2]
                    c_nombre = "-".join(partes[:-2])
                    
                    # Iterar por todos los servicios posibles para borrar rastro
                    for nombre_servicio, db_name in SERVICES_DB_MAP.items():
                        try:
                            service_collection = client[db_name][EMAIL_COLLECTION_NAME]
                            delete_contact_list(service_collection, c_region, c_area, c_nombre)
                        except Exception as ex_serv:
                            print(f"Warn: No se pudo borrar email list de {nombre_servicio}: {ex_serv}")
            except Exception as ex_parse:
                print(f"Error parseando ID para eliminar emails: {ex_parse}")

            # 4. Log
            insert_log_in_user_history(users_historial_collection, username, centro_id, 'delete_center')
            return jsonify({"mensaje": f"El centro '{centro_id}' ha sido eliminado exitosamente."}), 200
        else:
            return jsonify({"mensaje": "Error al eliminar el documento principal"}), 500

    except Exception as e:
        print(f"Error critico eliminando centro: {e}")
        return jsonify({"mensaje": "Error interno del servidor"}), 500


# !--- Manejo de Alertas---!
#-------------Funciones-------------------------!
# def guardar_alertas_activas(alertas):
#     #alertas es el json enviado desde el local
#     col_enviro_alertas.insert_many(alertas)
#     print('Alertas guardadas en ITGSM')
#     return

# def guardar_alertas_activas(alertas):
#     print(f'alertas_desde_local: {alertas}')
#     try:
#         col_enviro_alertas.insert_many(alertas)
#         print("Documentos insertados exitosamente.")
#     except errors.BulkWriteError as bwe:
#         print("Error en la insercion en bloque, manejando duplicados...")
#         # Filtrar documentos duplicados
#         error_details = bwe.details
#         write_errors = error_details.get('writeErrors', [])
#         duplicated_ids = [error['op']['_id'] for error in write_errors if error['code'] == 11000]

#         if duplicated_ids:
#             print(f"IDs duplicados encontrados: {duplicated_ids}")
#             # Remover documentos duplicados del conjunto original
#             documents = [doc for doc in alertas if doc['_id'] not in duplicated_ids]
#             if documents:  # Verificar si aun hay documentos por insertar
#                 try:
#                     col_enviro_alertas.insert_many(documents)
#                     print("Documentos restantes insertados exitosamente.")
#                 except errors.BulkWriteError as bwe_inner:
#                     print("Error en la insercion en bloque (segunda vez):", bwe_inner.details)
#             else:
#                 print("No hay documentos restantes por insertar despues de filtrar duplicados.")
def guardar_alertas_activas(client, service, alertas):
    if service == "ENVIRO":
        # coleccion = col_enviro_alertas
        coleccion = client[ENVIRO_DATABASE_NAME].alertas

    elif service == "NANOOX":
        # coleccion = col_nanoox_alertas
        coleccion = client[NANOOX_DATABASE_NAME].alertas

    print(f'alertas_desde_local: {alertas}')
    try:
        coleccion.insert_many(alertas)
        print("Documentos insertados exitosamente.")
    except errors.BulkWriteError as bwe:
        print("Error en la insercion en bloque, manejando duplicados...")
        # Filtrar documentos duplicados
        error_details = bwe.details
        write_errors = error_details.get('writeErrors', [])
        duplicated_ids = [error['op']['_id'] for error in write_errors if error['code'] == 11000]

        if duplicated_ids:
            print(f"IDs duplicados encontrados: {duplicated_ids}")
            # Remover documentos duplicados del conjunto original
            documents = [doc for doc in alertas if doc['_id'] not in duplicated_ids]
            if documents:  # Verificar si aun hay documentos por insertar
                try:
                    coleccion.insert_many(documents)
                    print("Documentos restantes insertados exitosamente.")
                except errors.BulkWriteError as bwe_inner:
                    print("Error en la insercion en bloque (segunda vez):", bwe_inner.details)
            else:
                print("No hay documentos restantes por insertar despues de filtrar duplicados.")

def guardar_fallas_activas(client, service, fallas):
    if service == "ENVIRO":
        # coleccion = col_enviro_fallas
        coleccion = client[ENVIRO_DATABASE_NAME].alertas_fallas


    elif service == "NANOOX":
        # coleccion = col_nanoox_fallas
        coleccion = client[NANOOX_DATABASE_NAME].alertas_fallas

    print(f'fallas_desde_local: {fallas}')
    try:
        coleccion.insert_many(fallas)
        print("Documentos insertados exitosamente.")
    except errors.BulkWriteError as bwe:
        print("Error en la insercion en bloque, manejando duplicados...")
        # Filtrar documentos duplicados
        error_details = bwe.details
        write_errors = error_details.get('writeErrors', [])
        duplicated_ids = [error['op']['_id'] for error in write_errors if error['code'] == 11000]

        if duplicated_ids:
            print(f"IDs duplicados encontrados: {duplicated_ids}")
            # Remover documentos duplicados del conjunto original
            documents = [doc for doc in fallas if doc['_id'] not in duplicated_ids]
            if documents:  # Verificar si aun hay documentos por insertar
                try:
                    coleccion.insert_many(documents)
                    print("Documentos restantes insertados exitosamente.")
                except errors.BulkWriteError as bwe_inner:
                    print("Error en la insercion en bloque (segunda vez):", bwe_inner.details)
            else:
                print("No hay documentos restantes por insertar despues de filtrar duplicados.")

# def guardar_notas(service, centro_id, notas):
#     if service == "ENVIRO":
#         coleccion = col_enviro_notas

#     elif service == "NANOOX":
#         coleccion = col_nanoox_notas

#     print(f'notas_desde_local: {notas}')
#     print(f'centro: {centro_id}')
#     try:
#         coleccion.insert_many(notas)
#         print("Documentos insertados exitosamente.")
#     except errors.BulkWriteError as bwe:
#         print("Error en la insercion en bloque, manejando duplicados...")
#         # Filtrar documentos duplicados
#         error_details = bwe.details
#         write_errors = error_details.get('writeErrors', [])
#         duplicated_ids = [error['op']['_id'] for error in write_errors if error['code'] == 11000]

#         if duplicated_ids:
#             print(f"IDs duplicados encontrados: {duplicated_ids}")
#             # Remover documentos duplicados del conjunto original
#             documents = [doc for doc in notas if doc['_id'] not in duplicated_ids]
#             if documents:  # Verificar si aun hay documentos por insertar
#                 try:
#                     coleccion.insert_many(documents)
#                     print("Documentos restantes insertados exitosamente.")
#                 except errors.BulkWriteError as bwe_inner:
#                     print("Error en la insercion en bloque (segunda vez):", bwe_inner.details)
#             else:
#                 print("No hay documentos restantes por insertar despues de filtrar duplicados.")
def guardar_notas(client, service, centro_id, notas):
    if service == "ENVIRO":
        # coleccion = col_enviro_notas
        coleccion = client[ENVIRO_DATABASE_NAME].notas


    elif service == "NANOOX":
        # coleccion = col_nanoox_notas
        coleccion = client[NANOOX_DATABASE_NAME].notas


    print(f'notas_desde_local: {notas}')
    print(f'centro: {centro_id}')
    if notas:
        fecha = notas['fecha']
        hora = notas['hora']
        bomba_id = notas['id']
        comentario = notas['nota']['value']

        operation = [
        UpdateOne(
            {'fecha': fecha,"centro_id": centro_id, 'bomba_id': bomba_id},
            {
                '$push': {
                    'notas': {
                        'hora': hora,
                        'value': comentario
                    }
                }
            },
            upsert = True
        )
    ]
    try:
        result = coleccion.bulk_write(operation)
        print("Documentos insertados exitosamente.")
    except errors.BulkWriteError as bwe:
        print("Error en la insercion en bloque, manejando duplicados...")
        # Filtrar documentos duplicados
        error_details = bwe.details
        write_errors = error_details.get('writeErrors', [])
        duplicated_ids = [error['op']['_id'] for error in write_errors if error['code'] == 11000]

        if duplicated_ids:
            print(f"IDs duplicados encontrados: {duplicated_ids}")
            # Remover documentos duplicados del conjunto original
            documents = [doc for doc in notas if doc['_id'] not in duplicated_ids]
            if documents:  # Verificar si aun hay documentos por insertar
                try:
                    coleccion.insert_many(documents)
                    print("Documentos restantes insertados exitosamente.")
                except errors.BulkWriteError as bwe_inner:
                    print("Error en la insercion en bloque (segunda vez):", bwe_inner.details)
            else:
                print("No hay documentos restantes por insertar despues de filtrar duplicados.")

def capitalizar_centro_id(cadena):
    palabras = cadena.split('-')
    palabras_capitalizadas = [palabra.capitalize() for palabra in palabras]
    return '-'.join(palabras_capitalizadas)

# def obtener_alertas_activas(service, centro_id):
#     if service == "ENVIRO":
#         coleccion = col_enviro_alertas

#     elif service == "NANOOX":
#         coleccion = col_nanoox_alertas
    
#     centro = capitalizar_centro_id(centro_id)
#     # Definir el pipeline de agregacion
#     pipeline = [
#         {
#             '$match': {
#                 'centro_id' : centro,
#                 'fin': None,
#                 'usuario': None,
#                 'fin': {'$exists': True},
#                 'usuario': {'$exists': True}
#             }
#         },
#         {
#             '$project': {
#                 '_id': {'$toString': '$_id'},  # Excluir el campo _id, si no lo quieres en los resultados
#                 # '_id': 0,
#                 'fecha': 1,
#                 'centro_id': 1,
#                 'variable': 1,
#                 'valor': 1,
#                 'alerta': 1,            
#                 'usuario': 1,
#                 'fin': 1
#             }
#         }
#     ]
    
#     # Ejecutar la agregacion
#     resultados = list(coleccion.aggregate(pipeline))
#     return resultados
def convertir_mal_menor(centro_original):
    division = centro_original.split("-")
    division.reverse()
    centro_formateado = "-".join(division)
    return centro_formateado

def obtener_alertas_activas(client, service, centro_id):
    centro = capitalizar_centro_id(centro_id)
    date = datetime.now().strftime("%Y-%m-%d")
    # print(f"alertas activas id {centro_id=}")
    # centro = convertir_mal_menor(centro_id)
    # print(f"alertas activas {centro=}")
    if service == "ENVIRO":
        # coleccion = col_enviro_alertas
        coleccion = client[ENVIRO_DATABASE_NAME].alertas

        pipeline = [
        {
            '$match': {
                'centro_id' : centro,
                'fin': None,
                'usuario': None,
                'fin': {'$exists': True},
                'usuario': {'$exists': True}
            }
        },
        {
            '$project': {
                '_id': {'$toString': '$_id'},  # Excluir el campo _id, si no lo quieres en los resultados
                # '_id': 0,
                'fecha': 1,
                'centro_id': 1,
                'variable': 1,
                'valor': 1,
                'alerta': 1,            
                'usuario': 1,
                'fin': 1, 
                'fin_estado': 1
            }
        }
    ]

    elif service == "NANOOX":
        # print('ENTRAMOS A ALERTAS ACTIVAS NANOOX')
        # coleccion = col_nanoox_alertas
        coleccion = client[NANOOX_DATABASE_NAME].alertas
        # centro = convertir_mal_menor(centro)
        centro = capitalizar_centro_id(centro_id)
        pipeline = [
        {
            '$match': {
                'centro_id' : centro,
                'fecha': {"$regex": f"^{date}"},
                'fin_estado': { '$exists': True, '$eq': None },
                'usuario': { '$exists': True, '$eq': None },
                # 'fin_estado': None,
                # 'usuario': None,
                # 'fin_estado': {'$exists': True},
                # 'usuario': {'$exists': True}
            }
        },
        {
            '$project': {
                '_id': {'$toString': '$_id'},  # Excluir el campo _id, si no lo quieres en los resultados
                # '_id': 0,
                'fecha': 1,
                'centro_id': 1,
                'variable': 1,
                'valor': 1,
                'alerta': 1,
                'rango': 1,           
                'usuario': 1,
                'fin': 1,
                'fin_estado': 1
            }
        }
    ]
    
    # Ejecutar la agregacion
    resultados = list(coleccion.aggregate(pipeline))
    return resultados

def obtener_fallas_activas(client, service, centro_id):
    centro = capitalizar_centro_id(centro_id)
    date = datetime.now().strftime("%Y-%m-%d")
    if service == "ENVIRO":
        # coleccion = col_enviro_fallas
        coleccion = client[ENVIRO_DATABASE_NAME].alertas_fallas

    elif service == "NANOOX":
        # coleccion = col_nanoox_fallas
        coleccion = client[NANOOX_DATABASE_NAME].alertas_fallas
        centro = capitalizar_centro_id(centro_id)
        # centro = convertir_mal_menor(centro)
    
    # Definir el pipeline de agregacion
    pipeline = [
        {
            '$match': {
                'centro_id' : centro,
                'fecha': {"$regex": f"^{date}"},
                'fin_estado': { '$exists': True, '$eq': None },
                'usuario': { '$exists': True, '$eq': None },
            }
        },
        {
            '$project': {
                '_id': {'$toString': '$_id'},  # Excluir el campo _id, si no lo quieres en los resultados
                # '_id': 0,
                'fecha': 1,
                'centro_id': 1,
                'variable': 1,
                'valor': 1,
                'alerta':1,
                'falla': 1,            
                'usuario': 1,
                'fin': 1,
                'fin_estado': 1
            }
        }
    ]
    
    # Ejecutar la agregacion
    resultados = list(coleccion.aggregate(pipeline))
    return resultados
# def estado_enviro(centro_id):
#     estado_enviro = []
#     print(f'obtener_alertas_activas: {obtener_alertas_activas(centro_id)}')
#     for activa in obtener_alertas_activas(centro_id):
#         print(f'activa: {activa["alerta"]}')
#         estado_enviro.append(activa['alerta'])
#     critico = any(estado == 'C' for estado in estado_enviro)
#     print(f'critico: {critico}')
#     atencion = any(estado == 'A' for estado in estado_enviro)
#     print(f'atencion: {atencion}')
#     if critico:
#         return 'C'
#     elif atencion:
#         return 'A'
#     else:
#         return 'B'
    
def estado_enviro(client, centro_id):
    estado_enviro = []
    # print(f'obtener_alertas_activas: {obtener_alertas_activas(client, "ENVIRO",centro_id)}')
    for activa in obtener_alertas_activas(client, "ENVIRO", centro_id):
        # print(f'activa: {activa["alerta"]}')
        estado_enviro.append(activa['alerta'])
    critico = any(estado == 'C' for estado in estado_enviro)
    # print(f'critico: {critico}')
    atencion = any(estado == 'A' for estado in estado_enviro)
    # print(f'atencion: {atencion}')
    if critico:
        return 'C'
    elif atencion:
        return 'A'
    else:
        return 'B'

def estado_centro(estado_enviro, estado_nanoox):
    estado_centro = [estado_enviro, estado_nanoox]
    critico = any(estado == 'C' for estado in estado_centro)
    atencion = any(estado == 'A' for estado in estado_centro)
    if critico:
        return 'C'
    elif atencion:
        return 'A'
    else:
        return 'B'

# def actualizar_revision_alerta(centro_id, idx, usuario_revision, fecha):
#     bulk_operations = []
#     operation = UpdateOne(
#     {
#         # '_id': ObjectId(idx),
#         '_id': idx,
#         'centro_id' : centro_id
#     },
#         {'$set':
#          {
#              'fin': fecha,
#              'usuario': usuario_revision
#          }
#         },
#         upsert = False
#     )
#     bulk_operations.append(operation)
#     if bulk_operations:
#         col_enviro_alertas.bulk_write(bulk_operations)
#         print(f'Alerta {idx} revisada por usuario {usuario_revision}')

def actualizar_revision_alerta(client, service, centro_id, idx, usuario_revision, fecha):
    bulk_operations = []
    if service == "ENVIRO":
        # coleccion = col_enviro_alertas
        coleccion = client[ENVIRO_DATABASE_NAME].alertas

    elif service == "NANOOX":
        # coleccion = col_nanoox_alertas
        coleccion = client[NANOOX_DATABASE_NAME].alertas
    operation = UpdateOne(
    {
        # '_id': ObjectId(idx),
        '_id': idx,
        'centro_id' : centro_id
    },
        {'$set':
         {
             'fin': fecha,
             'usuario': usuario_revision
         }
        },
        upsert = False
    )
    bulk_operations.append(operation)
    if bulk_operations:
        coleccion.bulk_write(bulk_operations)
        print(f'Alerta {idx} revisada por usuario {usuario_revision}')

def actualizar_revision_falla(client, service, centro_id, idx, usuario_revision, fecha):
    bulk_operations = []
    if service == "ENVIRO":
        # coleccion = col_enviro_fallas
        coleccion = client[ENVIRO_DATABASE_NAME].alertas_fallas


    elif service == "NANOOX":
        # coleccion = col_nanoox_fallas
        coleccion = client[NANOOX_DATABASE_NAME].alertas_fallas
    
    fin_estado_falla = coleccion.find_one({"_id":idx, "centro_id":centro_id})#['fin_estado']
    print(f"{fin_estado_falla=}")
    if not fin_estado_falla:
        print(f"No se encontro el documento con _id={idx} y centro_id={centro_id}; Probablemente ya fue consolidado.")
        return 

    # if fin_estado_falla:
    operation = UpdateOne(
    {
        # '_id': ObjectId(idx),
        '_id': idx,
        # 'variable': variable,
        'centro_id' : centro_id
    },
        {'$set':
        {
            'fin_estado': fecha,
            'usuario': usuario_revision
        }
        },
        upsert = False
    )          
    bulk_operations = [operation]
    if bulk_operations:
        coleccion.bulk_write(bulk_operations)
        print(f'Falla {idx} en centro {centro_id} revisada por usuario {usuario_revision} el {fecha}')

#Funcion en ITGSM para recepcion y actualizacion de fin estado alerta
def actualizar_fin_estado_alerta(client, service, centro_id, idx, fecha):
    bulk_operations = []
    if service == "ENVIRO":
        # coleccion = col_enviro_alertas
        coleccion = client[ENVIRO_DATABASE_NAME].alertas


    elif service == "NANOOX":
        # coleccion = col_nanoox_alertas
        coleccion = client[NANOOX_DATABASE_NAME].alertas
    operation = UpdateOne(
    {
        # '_id': ObjectId(idx),
        '_id': idx,
        'centro_id' : centro_id
    },
        {'$set':
         {
             'fin_estado': fecha,
         }
        },
        upsert = False
    )
    bulk_operations.append(operation)
    if bulk_operations:
        coleccion.bulk_write(bulk_operations)
        print(f'Alerta {idx} actualizada con fin estado a las {fecha} desde Local')

#----------Funciones para obtencion de ESTADOS-------------------#
def obtener_centros_areas_regiones():
    car = list(enviro_metadata_collection.find())[0]
    return car

def obtener_ids(data):
    # Inicializar la lista para almacenar los valores de "_id"
    ids = []
    
    # Recorrer el diccionario y agregar los valores de "_id" a la lista
    for region, region_values in data.items():
        for area, area_values in region_values.items():
            for centro, centro_values in area_values.items():
                if '_id' in centro_values:
                    ids.append(centro_values['_id'])
    
    # Imprimir la lista de valores de "_id"
    # print(f"{ids=}")
    return ids

# def obtener_valores_actuales_centros(data):
#     nuevos_valores = {}
#     #Aqui se debe reemplazar el estado nanoox cuando se tenga
#     # estado_nanoox= None
#     for centro in obtener_ids(data):
#         nuevos_valores[centro] = estado_centro(estado_enviro(centro), None)
#     return (nuevos_valores)

def obtener_valores_actuales_centros(data):
    nuevos_valores = {}
    #Aqui se debe reemplazar el estado nanoox cuando se tenga
    # estado_nanoox= None
    # print(f'obtener_ids(data): {obtener_ids(data)}')
    for centro in obtener_ids(data):
        # print(f'{centro=}')
        # print(f'estado_enviro: {obtener_estado_servicio_centro(centro)["enviro"]}')
        # print(f'estado_nanoox:{obtener_estado_servicio_centro(centro)["nanoox"]}')
        # nuevos_valores[centro] = estado_centro(estado_enviro(centro), None)
        nuevos_valores[centro] = estado_centro(obtener_estado_servicio_centro(centro.lower())['enviro'], obtener_estado_servicio_centro(centro.lower())['nanoox'])
    # print(f'{nuevos_valores=}')
    return (nuevos_valores)

def agrupar_por_region_area(data):
    result = defaultdict(dict)
    
    # Iterar sobre las regiones, areas y centros
    for region, areas in data.items():
        for area, centros in areas.items():
            key = f"{region}-{area}"
            for centro, info in centros.items():
                result[key][info['_id']] = None
    
    # Convertir defaultdict a dict normal
    result = dict(result)
    
    # Imprimir el diccionario resultante
    # print(result)
    return result

def agregar_estado_centros_a_consolidado(agrupacion_por_region_area, dict_estados_centro):
    result = agrupacion_por_region_area
    nuevos_valores = dict_estados_centro
    # Iterar sobre el diccionario resultante y actualizar los valores
    for region_area, centros in result.items():
        for centro_id in centros:
            if centro_id in nuevos_valores:
                result[region_area][centro_id] = nuevos_valores[centro_id]
    
    # Imprimir el diccionario actualizado
    # print(result)
    return result

# Funcion para aplicar las condiciones de actualizacion de valores
def actualizar_valor(valor):
    if 'C' in valor:
        return 'C'
    elif 'A' in valor:
        return 'A'
    else:
        return 'B'
    
def estado_resultante_area_region(consolidado_estados_por_centro):
    # Generar el nuevo diccionario
    nuevo_diccionario = {}
    result = consolidado_estados_por_centro
    
    # Iterar sobre el diccionario resultante
    for region_area, centros in result.items():
        # Concatenar los valores de 'CENTRO-AREA-REGION', reemplazando None por ""
        concatenated_values = ''.join(value if value is not None else 'B' for value in centros.values())
        # Guardar en el nuevo diccionario
        nuevo_diccionario[region_area] = concatenated_values
    
    # Imprimir el nuevo diccionario
    # print(f'estados_colapsados_area_region: {nuevo_diccionario}')
    
    # Aplicar la funcion de actualizacion a los valores del diccionario
    for region_area in nuevo_diccionario:
        nuevo_diccionario[region_area] = actualizar_valor(nuevo_diccionario[region_area])
    
    # Imprimir el diccionario actualizado
    # print(f'estado resultante: {nuevo_diccionario}')
    return (nuevo_diccionario)

def estado_resultante_region(estados_resultante_area_region):
    nuevo_diccionario = estados_resultante_area_region
    # Diccionario resultante agrupado por REGION
    diccionario_agrupado = defaultdict(str)
    
    # Iterar sobre el diccionario 'nuevo_diccionario'
    for region_area, valores_concatenados in nuevo_diccionario.items():
        # Separar la REGION y el AREA
        region, area = region_area.split('-')
        # Concatenar los valores en el diccionario agrupado
        diccionario_agrupado[region] += valores_concatenados
    
    # Convertir defaultdict a dict normal
    diccionario_agrupado = dict(diccionario_agrupado)
    
    # Imprimir el diccionario agrupado
    # print(f'estados_colapsados_region: {diccionario_agrupado}')
    # print(diccionario_agrupado)
    
    # Aplicar la funcion de actualizacion a los valores del diccionario
    for region in diccionario_agrupado:
        diccionario_agrupado[region] = actualizar_valor(diccionario_agrupado[region])
    
    # Imprimir el diccionario actualizado
    return (diccionario_agrupado)

# def obtener_todos_los_estados_empresa():
#     data = obtener_centros_areas_regiones()
#     nuevos_valores = obtener_valores_actuales_centros(data)
#     estados_centros = agregar_estado_centros_a_consolidado(agrupar_por_region_area(data), nuevos_valores)
#     estados_areas = estado_resultante_area_region(estados_centros)
#     estados_regiones = estado_resultante_region(estados_areas)
#     # print(f'estados_centros: {estados_centros}')
#     # print(f'estados_areas: {estados_areas}')
#     # print(f'estados_regiones: {estados_regiones}')
#     consolidado_estados = {"centros": estados_centros, "areas": estados_areas, "regiones": estados_regiones }
#     return consolidado_estados

# def obtener_todos_los_estados_empresa():
#     data = obtener_centros_areas_regiones()
#     nuevos_valores = obtener_valores_actuales_centros(data)
#     estados_centros = agregar_estado_centros_a_consolidado(agrupar_por_region_area(data), nuevos_valores)
#     estados_areas = estado_resultante_area_region(estados_centros)
#     estados_regiones = estado_resultante_region(estados_areas)
#     # print(f'estados_centros: {estados_centros}')
#     # print(f'estados_areas: {estados_areas}')
#     # print(f'estados_regiones: {estados_regiones}')
#     consolidado_estados = {"centros": estados_centros, "areas": estados_areas, "regiones": estados_regiones }
#     return consolidado_estados

def transformar_diccionario(data):
    resultado = {}
    
    for region, areas in data.items():
        resultado[region] = {}
        
        for area, centros in areas.items():
            resultado[region][area] = {}
            
            for centro in centros:
                centro_key = centro.split('-')[0]
                resultado[region][area][centro_key] = {'_id': centro}
                
    return resultado

# def consolidado_empresa_por_usuario_activo(username):
#     print("ENTRANDO A CONSOLIDADO")
#     permisos_usuario = get_accesos_usuario(users_collection,  username)
#     permisos_centros_activos = interseccion_permisos_con_servicios(permisos_usuario,servicios_activos_enviro(ciclos_collection))
#     data = transformar_diccionario(permisos_centros_activos)
#     nuevos_valores = obtener_valores_actuales_centros(data)
#     estados_centros = agregar_estado_centros_a_consolidado(agrupar_por_region_area(data), nuevos_valores)
#     estados_areas = estado_resultante_area_region(estados_centros)
#     estados_regiones = estado_resultante_region(estados_areas)
#     print(f'estados_centros: {estados_centros}')
#     print(f'estados_areas: {estados_areas}')
#     print(f'estados_regiones: {estados_regiones}')
#     consolidado_estados = {"centros": estados_centros, "areas": estados_areas, "regiones": estados_regiones }
#     return consolidado_estados

# def consolidado_empresa_por_usuario_activo(username):
#     print('ENTRANDO A CONSOLIDADOS')
#     permisos_usuario = get_accesos_usuario(users_collection,  username)
#     data = transformar_diccionario(permisos_usuario)
#     print(f'data: {data}')
#     nuevos_valores = obtener_valores_actuales_centros(data)
#     print(f'{nuevos_valores=}')
#     print(f'agrupar por region area: {agrupar_por_region_area(data)}')
#     estados_centros = agregar_estado_centros_a_consolidado(agrupar_por_region_area(data), nuevos_valores)
#     print(f'{estados_centros=}')
#     estados_areas = estado_resultante_area_region(estados_centros)
#     estados_regiones = estado_resultante_region(estados_areas)
#     # print(f'estados_centros: {estados_centros}')
#     # print(f'estados_areas: {estados_areas}')
#     # print(f'estados_regiones: {estados_regiones}')
#     consolidado_estados = {"centros": estados_centros, "areas": estados_areas, "regiones": estados_regiones }
#     return consolidado_estados

def obtener_valores_actuales_centros_por_servicio(page,data):
    nuevos_valores = {}
    print(f'ENTRANDO A ESTADO ACTUAL CENTRO EN PAGINA {page}')
    #Aqui se debe reemplazar el estado nanoox cuando se tenga
    # estado_nanoox= None
    # print(f'obtener_ids(data): {obtener_ids(data)}')
    if page == 'HOME':
        for centro in obtener_ids(data):
            # print(f'{centro=}')
            # print(f'estado_enviro: {obtener_estado_servicio_centro(centro)["enviro"]}')
            # print(f'estado_nanoox:{obtener_estado_servicio_centro(centro)["nanoox"]}')
            # nuevos_valores[centro] = estado_centro(estado_enviro(centro), None)
            nuevos_valores[centro] = estado_centro(obtener_estado_servicio_centro(centro.lower())['enviro'], obtener_estado_servicio_centro(centro.lower())['nanoox'])
            # print(f'{nuevos_valores=} \n')
    elif page == 'ENVIRO':
        for centro in obtener_ids(data):
            nuevos_valores[centro] = estado_centro(obtener_estado_servicio_centro(centro.lower())['enviro'], None)
    elif page == 'NANOOX':
        for centro in obtener_ids(data):
            nuevos_valores[centro] = estado_centro(None, obtener_estado_servicio_centro(centro.lower())['nanoox'])
    # print(f'{nuevos_valores=}')
    return (nuevos_valores)

def obtener_valores_actuales_centros_por_servicio_2(client, page,data, username):
    nuevos_valores = {}
    if page == 'HOME':
        for centro in obtener_ids(data):
            # print(f'{centro=}')
            # print(f'estado_enviro: {obtener_estado_servicio_centro(centro)["enviro"]}')
            # print(f'estado_nanoox:{obtener_estado_servicio_centro(centro)["nanoox"]}')
            # nuevos_valores[centro] = estado_centro(estado_enviro(centro), None)
            nuevos_valores[centro] = estado_centro(obtener_estado_servicio_centro(client, centro.lower())['enviro'], obtener_estado_servicio_centro(client, centro.lower())['nanoox'])
    elif page == 'ENVIRO':
        ciclos_collection = client[USER_DATABASE_NAME].ciclos
        for centro in obtener_ids(data):
            # print(f"{centro=}")
            status_servicio_centro=  ciclos_collection.find_one({"centro_id": centro},{"_id":0})['enviro']['status'] & status_servicio_permiso_usuario(client, username, centro, 'enviro')
            if status_servicio_centro:
                nuevos_valores[centro] = estado_centro(obtener_estado_servicio_centro(client, centro.lower())['enviro'], None)
    elif page == 'NANOOX':
        ciclos_collection = client[USER_DATABASE_NAME].ciclos
        for centro in obtener_ids(data):
            status_servicio_centro=  ciclos_collection.find_one({"centro_id": centro},{"_id":0})['nanoox']['status'] & status_servicio_permiso_usuario(client, username, centro, 'nanoox')
            if status_servicio_centro:
                nuevos_valores[centro] = estado_centro(None, obtener_estado_servicio_centro(client, centro.lower())['nanoox'])
    # print(f'{nuevos_valores=}')
    return (nuevos_valores)

def clean_none(data):
    if isinstance(data, dict):
        # Recorrer cada clave y valor en el diccionario
        keys_to_delete = []
        for key, value in data.items():
            # Llamada recursiva para limpiar los sub-diccionarios o listas
            cleaned_value = clean_none(value)
            if cleaned_value is None:
                # Marcar clave para eliminacion si el valor es None
                keys_to_delete.append(key)
            else:
                # Actualizar el valor limpiado
                data[key] = cleaned_value
        # Eliminar claves con valor None
        for key in keys_to_delete:
            del data[key]
        # Devolver None si el diccionario esta vacio despues de la limpieza
        if not data:
            return None
    elif isinstance(data, list):
        # Limpiar cada elemento en la lista
        data = [clean_none(item) for item in data if clean_none(item) is not None]
        # Devolver None si la lista esta vacia despues de la limpieza
        if not data:
            return None
    return data

def status_servicio_permiso_usuario(client, username, centro_id, servicio):
    users_collection = client[USER_DATABASE_NAME].users
    pipeline = [
        {"$match": {"username": username}},
        {"$project": {
            "permisos": {
                "$objectToArray": "$permisos"
            }
        }},
        {"$unwind": "$permisos"},
        {"$project": {
            "region": "$permisos.k",
            "ciudades": {
                "$objectToArray": "$permisos.v"
            }
        }},
        {"$unwind": "$ciudades"},
        {"$project": {
            "ciudad": "$ciudades.k",
            "centros": {
                "$objectToArray": "$ciudades.v"
            }
        }},
        {"$unwind": "$centros"},
        {"$project": {
            "centro_id": "$centros.k",
            "servicios": {
                "$objectToArray": "$centros.v"
            }
        }},
        {"$unwind": "$servicios"},
        {"$match": {
            "centro_id": centro_id,
            "servicios.k": servicio
        }},
        {"$project": {
            "_id": 0,
            "status": "$servicios.v.status"
        }}
    ]   
    result = list(users_collection.aggregate(pipeline))
    return result[0]['status']

def consolidado_empresa_por_usuario_activo_y_servicio(username, page):
    print('ENTRANDO A CONSOLIDADOS ACTUALIZADO')
    permisos_usuario = get_accesos_usuario(users_collection,  username) 
    # print(f"permisos_usuario_orig = {permisos_usuario}")
    # if page == 'ENVIRO':
        # permisos_usuario = interseccion_permisos_con_servicios(permisos_usuario,servicios_activos_enviro(ciclos_collection))
    # elif page == 'NANOOX':
        # permisos_usuario = interseccion_permisos_con_servicios(permisos_usuario,servicios_activos_nanoox(ciclos_collection))
    # print(f"{permisos_usuario=}")
    # interseccion_permisos_con_servicios(permisos_usuario,servicios_activos_enviro(ciclos_collection))
    data = transformar_diccionario(permisos_usuario)
    print(f'data: {data}')
    nuevos_valores = obtener_valores_actuales_centros_por_servicio(page, data)
    # print(f'{nuevos_valores=}')
    # print(f'agrupar por region area: {agrupar_por_region_area(data)}')
    estados_centros = agregar_estado_centros_a_consolidado(agrupar_por_region_area(data), nuevos_valores)
    # print(f'{estados_centros=}')
    estados_areas = estado_resultante_area_region(estados_centros)
    estados_regiones = estado_resultante_region(estados_areas)
    # print(f'estados_centros: {estados_centros}')
    # print(f'estados_areas: {estados_areas}')
    # print(f'estados_regiones: {estados_regiones}')
    consolidado_estados = {"centros": estados_centros, "areas": estados_areas, "regiones": estados_regiones }
    return consolidado_estados


def consolidado_empresa_por_usuario_activo_y_servicio_2(client, username, page):
    print('ENTRANDO A CONSOLIDADOS')
    users_collection = client[USER_DATABASE_NAME].users
    if page == 'HOME':
        permisos_usuario = get_accesos_usuario(users_collection,  username) #Obtiene Permisos de Usuario
    else:
        # permisos_usuario = verificar_permiso_vacio(procesar_permisos(list(users_collection.find({'username':username}))[0]))
        permisos_usuario = procesar_permisos(client, list(users_collection.find({'username':username}))[0])
    # print(f"{permisos_usuario = }")
    data = transformar_diccionario(permisos_usuario) #Retorna diccionario anidado {REGION:{AREA:{CENTRO}}}
    # print(f'data: {data}')
    # nuevos_valores = obtener_valores_actuales_centros(data)
    # antiguos_valores = obtener_valores_actuales_centros_por_servicio(page, data)
    # print(f'{antiguos_valores=}')
    nuevos_valores = obtener_valores_actuales_centros_por_servicio_2(client, page, data, username)
    # print(f'{nuevos_valores=}')
    # print(f'agrupar por region area: {agrupar_por_region_area(data)}')
    # estados_centros = agregar_estado_centros_a_consolidado(agrupar_por_region_area(data), nuevos_valores)
    estados_centros = clean_none(agregar_estado_centros_a_consolidado(agrupar_por_region_area(data), nuevos_valores))
    print(f'{estados_centros=}')
    estados_areas = estado_resultante_area_region(estados_centros)
    estados_regiones = estado_resultante_region(estados_areas)
    # print(f'estados_centros: {estados_centros}')
    # print(f'estados_areas: {estados_areas}')
    # print(f'estados_regiones: {estados_regiones}')
    consolidado_estados = {"centros": estados_centros, "areas": estados_areas, "regiones": estados_regiones }
    return consolidado_estados


def obtener_diferencia(fecha_inicio_str, fecha_fin_str):
    # Definir el formato de fecha
    formato = "%Y-%m-%d-%H:%M:%S"
    
    # Convertir las cadenas de fecha a objetos datetime
    fecha_inicio = datetime.strptime(fecha_inicio_str, formato)
    fecha_fin = datetime.strptime(fecha_fin_str, formato)
    
    # Calcular la diferencia
    diferencia = fecha_fin - fecha_inicio
        # Convertir la diferencia en un string en el formato deseado
    dias, segundos = diferencia.days, diferencia.seconds
    horas = segundos // 3600
    minutos = (segundos % 3600) // 60
    segundos = segundos % 60
    
    return f"{dias}d-{horas}h-{minutos}m-{segundos}s"

def obtener_status_servicio(client, centro_id, servicio):
    # Buscar el documento correspondiente por 'centro_id'
    ciclos_collection = client[USER_DATABASE_NAME].ciclos
    documento = ciclos_collection.find_one({'centro_id': centro_id})
    
    if documento and servicio in documento:
        return documento[servicio].get('status', False)
    
    # Si el documento o el servicio no existe, devolver False
    return False

def ORIGINAL_obtener_estado_servicio_centro(client, centro_id):
    """
    Retorna el estado B, A o C de un centro en especifico desde la coleccion user.estados
    """
    estado_centros_collection = client[USER_DATABASE_NAME].estados
    # print(f"centro_id 2: {centro_id}")
    # print(f" obtener_estado_servicio_centro 2: {estado_centros_collection.find_one({'centro_id':centro_id},{'_id':0})}")
    estado_servicios = estado_centros_collection.find_one({'centro_id':centro_id},{'_id':0})
    # print(f'{estado_servicios=}')
    if estado_servicios:
        return estado_servicios
    else:
        return "B"
#NEW_FEATURE
def obtener_estado_servicio_centro(client, centro_id: str):
    """ Helper para evitar N+1 queries. Trae el doc completo. """
    try:
        # Ajusta 'USER_DATABASE_NAME' según tu variable global/config
        coll = client[USER_DATABASE_NAME].estados
        return coll.find_one({'centro_id': centro_id}, {'_id': 0})
    except Exception:
        return None

# def estado_enviro_servicios(centro_id):
#     estado_enviro = []
#     for activa in obtener_alertas_activas(centro_id):
#         estado_enviro.append(activa["alerta"])
#     # print(f'estado_enviro: {estado_enviro}')
#     critico = any(estado == 'C' for estado in estado_enviro)
#     atencion = any(estado == 'A' for estado in estado_enviro)
#     conteo_c = estado_enviro.count('C')
#     conteo_a = estado_enviro.count('A')
#     estado_servicio = obtener_status_servicio(centro_id.lower(), "enviro")
#     # print(f'count_a: {estado_enviro}')
#     if critico:
#         return {'status' : 'C', 'conteo_c': conteo_c, 'conteo_a': conteo_a, 'status_service': estado_servicio}
#     elif atencion:
#         return {'status' : 'A', 'conteo_c': conteo_c, 'conteo_a': conteo_a, 'status_service': estado_servicio}
#     else:
#         return {'status' : 'B', 'conteo_c': conteo_c, 'conteo_a': conteo_a, 'status_service': estado_servicio}
# def estado_nanoox_servicios(centro_id):
#     estado_servicio = obtener_status_servicio(centro_id.lower(), "nanoox")
#     return {'status' : 'B', 'conteo_c': 0, 'conteo_a': 0, 'status_service': estado_servicio}
#Estado servicio segun estado variables centro
# def estado_enviro_servicios(client, centro_id):

#     estado_enviro = []
#     # print(f'obtener_alertas_activas en enviro 2: {obtener_alertas_activas(client, "ENVIRO",centro_id)}')
#     for activa in obtener_alertas_activas(client, "ENVIRO",centro_id):
#         estado_enviro.append(activa["alerta"])
#     conteo_c = estado_enviro.count('C')
#     conteo_a = estado_enviro.count('A')
#     # print(f'obtener_estado_servicio_centro 2: {obtener_estado_servicio_centro(centro_id.lower())["enviro"]}')
#     estado_servicio = obtener_status_servicio(client, centro_id.lower(), "enviro")
#     estado = obtener_estado_servicio_centro(client, centro_id)['enviro']
#     return {'status' : estado, 'conteo_c': conteo_c, 'conteo_a': conteo_a, 'status_service': estado_servicio}
# def estado_nanoox_servicios(client, centro_id):
#     estado_nanoox = []
#     for activa in obtener_alertas_activas(client, "NANOOX",centro_id):
#         estado_nanoox.append(activa["alerta"])
#     conteo_c = estado_nanoox.count('C')
#     conteo_a = estado_nanoox.count('A')
#     estado_servicio = obtener_status_servicio(client, centro_id.lower(), "nanoox")
#     estado = obtener_estado_servicio_centro(client, centro_id)['nanoox']
#     return {'status' : estado, 'conteo_c': conteo_c, 'conteo_a': conteo_a, 'status_service': estado_servicio}
# def estado_pss_servicios(client, centro_id):
#     estado_servicio = obtener_status_servicio(client, centro_id.lower(), "pss")
#     return {'status' : 'B', 'conteo_c': 0, 'conteo_a': 0, 'status_service': estado_servicio}
# def estado_scapp_servicios(client, centro_id):
#     estado_servicio = obtener_status_servicio(client, centro_id.lower(), "scapp")
#     return {'status' : 'B', 'conteo_c': 0, 'conteo_a': 0, 'status_service': estado_servicio}

#NEW_FEATURE
def estado_enviro_servicios(client, centro_id, estado_doc):
    # Mantenemos lógica original pero usando el doc en caché 'estado_doc'
    try:
        alertas = obtener_alertas_activas(client, "ENVIRO", centro_id)
        lista = [a["alerta"] for a in alertas]
    except:
        lista = []
    
    estado_operativo = estado_doc.get('enviro', "B")
    
    try:
        status_srv = obtener_status_servicio(client, centro_id, "enviro")
    except:
        status_srv = True

    return {
        'status': estado_operativo,
        'conteo_c': lista.count('C'),
        'conteo_a': lista.count('A'),
        'status_service': status_srv
    }

def estado_nanoox_servicios(client, centro_id, estado_doc):
    try:
        alertas = obtener_alertas_activas(client, "NANOOX", centro_id)
        lista = [a["alerta"] for a in alertas]
    except:
        lista = []

    estado_operativo = estado_doc.get('nanoox', "B")
    
    try:
        status_srv = obtener_status_servicio(client, centro_id, "nanoox")
    except:
        status_srv = True

    return {
        'status': estado_operativo,
        'conteo_c': lista.count('C'),
        'conteo_a': lista.count('A'),
        'status_service': status_srv
    }

def estado_pss_servicios(client, centro_id, estado_doc):
    try:
        status_srv = obtener_status_servicio(client, centro_id, "pss")
    except:
        status_srv = True
    return {'status': 'B', 'conteo_c': 0, 'conteo_a': 0, 'status_service': status_srv}

def estado_scapp_servicios(client, centro_id, estado_doc):
    try:
        status_srv = obtener_status_servicio(client, centro_id, "scapp")
    except:
        status_srv = True
    return {'status': 'B', 'conteo_c': 0, 'conteo_a': 0, 'status_service': status_srv}

def original_estado_flux_servicios(client, centro_id, estado_doc):
    """
    Manejador para Flux.
    """
    # 1. Parsing del ID
    partes = centro_id.lower().split('-')
    region = partes[-1] if len(partes) >= 2 else ""
    area = partes[-2] if len(partes) >= 3 else partes[0]

    # 2. Lógica de Zona
    es_zona_flux = (region == 'flux') or (area in ['boat', 'boats', 'barcos'])

    # 3. Determinar Estado Operativo ('B')
    # Si la DB tiene un dato explicito (ej: 'A'), lo usamos.
    # Si la DB dice True o no dice nada pero es zona flux, retornamos 'B'.
    
    info_db = estado_doc.get('flux', {})
    status_db = info_db.get('status') # Puede ser True, False, o 'A'/'C'

    if isinstance(status_db, str):
        estado_final = status_db  # Mantenemos 'A' o 'C' si existen
    elif status_db is True:
        estado_final = 'B'        # Convertimos True -> 'B'
    elif es_zona_flux:
        estado_final = 'B'        # Por defecto en zona Flux -> 'B'
    else:
        estado_final = 'B'        # Default seguro para estructura (aunque status_service sea false)

    # Status Service (Permiso)
    # Lo dejamos en True aquí, pero recuerda que el 'normalizar_schema_legacy' tiene la última palabra.
    status_service_permiso = True

    return {
        'status': estado_final,   # Ahora retorna 'B', no true
        'conteo_c': 0,
        'conteo_a': 0,
        'status_service': status_service_permiso
    }

def estado_flux_servicios(client, centro_id, estado_doc):
    """
    Manejador para Flux (Versión Robusta / Anti-Crash).
    Soporta inconsistencias de DB: 
    - flux: {'status': 'B'} (Dict)
    - flux: 'B' (String)
    - flux: true (Bool)
    - flux: None (No existe)
    """
    # 1. Parsing del ID
    partes = centro_id.lower().split('-')
    region = partes[-1] if len(partes) >= 2 else ""
    # Manejo seguro para el área (evita error si el ID es muy corto)
    if len(partes) >= 3:
        area = partes[-2] 
    else:
        area = partes[0]

    # 2. Lógica de Zona (Regla de Negocio)
    es_zona_flux = (region == 'flux') or (area in ['boat', 'boats', 'barcos'])

    # 3. Obtención Defensiva del Estado desde DB
    # Obtenemos el valor crudo, puede ser Dict, Str, Bool o None
    raw_flux = estado_doc.get('flux') 
    
    status_db = None

    if isinstance(raw_flux, dict):
        # Caso ideal: Es un diccionario
        status_db = raw_flux.get('status')
    elif isinstance(raw_flux, str):
        # Caso Legacy String: "flux": "B"
        status_db = raw_flux
    elif isinstance(raw_flux, bool):
        # Caso Legacy Bool: "flux": true
        status_db = raw_flux
    
    # 4. Determinar Estado Final Normalizado ('A', 'B', 'C')
    estado_final = 'B' # Default seguro

    if isinstance(status_db, str):
        # Si la DB dice 'A', 'B' o 'C', respetamos eso
        estado_final = status_db
    elif status_db is True:
        # Si la DB dice True, lo traducimos a 'B' (Operativo)
        estado_final = 'B'
    elif es_zona_flux:
        # Si no hay dato en DB, pero es zona flux, asumimos 'B'
        estado_final = 'B'
    else:
        # Para el resto, 'B' (aunque visualmente se oculte por permisos)
        estado_final = 'B'

    # 5. Status Service (Permiso de Visualización)
    # Se deja en True para que 'update_status_service' y 'normalizar_schema_legacy' 
    # tomen la decisión final basada en permisos de usuario.
    status_service_permiso = True

    return {
        'status': estado_final,
        'conteo_c': 0,
        'conteo_a': 0,
        'status_service': status_service_permiso
    }

# def obtener_servicios_por_region_y_area(area, region):
#     result = enviro_metadata_collection.find(
#         {},
#         {
#             # f"Ambiental.{region}.{area}": 1,
#             f"{region}.{area}": 1,
#             "_id": 0
#         }
#     )

#     # Convertir el cursor a una lista de documentos
#     documentos = list(result)
#     # print(f'obtener_servicios_por_region_y_area: {documentos}')
#     return documentos

def capitalize_first_letter(field):
    return {
        '$concat': [
            {'$toUpper': {'$substrCP': [field, 0, 1]}},  # Convertir la primera letra en mayuscula
            {'$substrCP': [field, 1, {'$strLenCP': field}]},  # Mantener el resto en minuscula
        ]
    }

def obtener_servicios_por_region_y_area(client, username):
    collection = client[USER_DATABASE_NAME].users
    # Pipeline de agregacion modificado
    pipeline = [
        {'$match': {'username': username}},  # Filtrar por nombre de usuario
        {'$project': {
            'permisos': 1
        }},
        {'$replaceWith': {'$arrayToObject': {
            '$map': {
                'input': {'$objectToArray': '$permisos'},
                'as': 'region',
                'in': {
                    'k': capitalize_first_letter('$$region.k'),  # Capitalizar solo la primera letra de la region
                    'v': {'$arrayToObject': {
                        '$map': {
                            'input': {'$objectToArray': '$$region.v'},
                            'as': 'area',
                            'in': {
                                'k': capitalize_first_letter('$$area.k'),  # Capitalizar solo la primera letra del area
                                'v': {'$arrayToObject': {
                                    '$map': {
                                        'input': {'$objectToArray': '$$area.v'},
                                        'as': 'centro',
                                        'in': {
                                            'k': capitalize_first_letter({
                                                '$arrayElemAt': [
                                                    {'$split': ['$$centro.k', '-']}, 0
                                                ]
                                            }),  # Capitalizar la primera letra del nombre del centro
                                            'v': {'_id': {
                                                '$concat': [
                                                    capitalize_first_letter({
                                                        '$arrayElemAt': [
                                                            {'$split': ['$$centro.k', '-']}, 0
                                                        ]
                                                    }),  # Capitalizar la primera letra del centro
                                                    '-',
                                                    capitalize_first_letter('$$area.k'),  # Capitalizar la primera letra del area
                                                    '-',
                                                    capitalize_first_letter('$$region.k')  # Capitalizar la primera letra de la region
                                                ]
                                            }}
                                        }
                                    }
                                }}
                            }
                        }
                    }}
                }
            }
        }}}
    ]

    # Ejecutar la agregacion
    resultado = list(collection.aggregate(pipeline))

    # Devolver el diccionario estructurado
    if resultado:
        return resultado[0]
    return {}

# def estados_servicios_region_area(region, area):
#     print(f'ENTRANDO A ESTADO SERVICIOS REGION AREA')
#     id_centros = obtener_ids(obtener_servicios_por_region_y_area(area, region)[0])
#     print(f'{id_centros=}')
#     id_centros = [centro.lower() for centro in id_centros]
#     print(f'centros-lower{id_centros}')
#     dict_centros = {}
#     for centro in id_centros:
#         enviro_estado = estado_enviro_servicios(centro)
#         print(f'{enviro_estado=}')
#         nanoox_estado = estado_nanoox_servicios(centro)
#         print(f'{nanoox_estado=}')
#         scapp_estado = estado_scapp_servicios(centro)
#         pss_estado = estado_pss_servicios(centro)
#         dict_centros[centro] = {
#             'enviro': enviro_estado,
#             'nanoox': nanoox_estado,
#             'scapp': scapp_estado,
#             'pss': pss_estado
#         }
#         # dict = {centro: {'enviro': estado_enviro_servicios(centro), 'nanoox': estado_nanoox_servicios(centro), 'scapp': estado_scapp_servicios(centro), 'pss':estado_pss_servicios(centro)}}
#     return dict_centros 

#NEW_FEATURE
def estados_servicios_region_area_2(client, data_permisos):
    """
    Recorre todos los centros, obtiene su documento de estado una única vez (cache)
    y construye el diccionario de servicios (Enviro, Nanoox, Pss, Scapp y Flux).
    """
    id_centros = obtener_ids(data_permisos)
    dict_centros = {}

    for centro_id in id_centros:
        # Normalizamos ID para búsquedas
        centro_key = centro_id.lower()
        
        # 1. Obtener documento de base de datos (Optimización: 1 query por centro)
        estado_doc = obtener_estado_servicio_centro(client, centro_key)
        # Aseguramos un dict vacío si no hay datos para evitar errores de atributo
        doc_seguro = estado_doc if estado_doc else {}

        # 2. Construir respuesta para TODOS los servicios obligatorios
        servicios_centro = {
            'enviro': estado_enviro_servicios(client, centro_key, doc_seguro),
            'nanoox': estado_nanoox_servicios(client, centro_key, doc_seguro),
            'scapp': estado_scapp_servicios(client, centro_key, doc_seguro),
            'pss': estado_pss_servicios(client, centro_key, doc_seguro),
            # Flux se agrega AHORA A TODOS los centros
            'flux': estado_flux_servicios(client, centro_key, doc_seguro) 
        }

        # `centro_id` proviene de la misma jerarquía que usa el consolidado. Se conserva
        # exactamente para que nombres con mayúsculas internas (por ejemplo, S4S5) no
        # cambien al formatearse por segunda vez. `centro_key` se usa sólo para consultar BD.
        dict_centros[centro_id] = servicios_centro

    return dict_centros

# def actualizar_consolidado_alertas_revisadas(idx):
#     document = col_enviro_alertas.find_one({"_id": idx})
#     if document:
#         # Extraer la fecha y formatearla a "Y-M-D"
#         fecha_str = document['fecha'].split('-')[0:3]
#         fecha = "-".join(fecha_str)
        
#         # Construir el filtro para encontrar el documento
#         filtro = {
#             "fecha": fecha,
#             "centro_id": document['centro_id']
#             # "_id": {"$ne": document['_id']}
#         }
        
#         # Definir el documento que se va a insertar o actualizar
#         actualizacion = {
#             "$setOnInsert": {
#                 "fecha": fecha,
#                 "centro_id": document['centro_id'],
#                 "variables": {}
#             }
#         }
        
#         # Realizar la operacion de upsert
#         col_enviro_alertas.update_one(filtro, actualizacion, upsert = True)
    
#         # Ahora actualizamos la variable especifica
#         actualizacion_variable = {
#             "$push": {
#                 f"variables.{document['variable']}": {
#                     "inicio": document['fecha'],
#                     "valor": document['valor'],
#                     "alerta": document['alerta'],
#                     "usuario": document['usuario'],
#                     "fin": document['fin'],
#                     "duracion" : obtener_diferencia(document['fecha'], document['fin'])
#                 }
#             }
#         }    
#         # Actualizar la variable especifica en el documento
#         resultado = col_enviro_alertas.update_one(filtro, actualizacion_variable)
#         print(f' Consolidado de Alertas actualizado con alerta {idx}')
#         if resultado.matched_count > 0:
#             if resultado.modified_count > 0:
#                 print("El documento se actualizo correctamente.")
#                 actualizacion = True
#             else:
#                 print("El documento coincidio con el filtro, pero no se modifico.")
#                 actualizacion = False
#         else:
#             print("No se encontro ningun documento que coincidiera con el filtro.")
#             actualizacion = False

#         if actualizacion:
#              col_enviro_alertas.delete_one({"_id": idx})
#              print(f"La alerta {idx} se ha agregado al consolidado y removido de alertas")
#     else:
#         print(f' No se encontro alerta asociada al id {idx}')        
def check_alerta_incompleta(col_enviro_alertas, idx):
    # Crear la query
    query = {
        '$and': [
            {'_id': ObjectId(idx)},
            {'fin': {'$exists': True}},
            {'usuario': {'$exists': True}},
            {'fin_estado': {'$exists': True}},
            {
                '$or': [
                    {'fin': None},
                    {'usuario': None},
                    {'fin_estado': None}
                ]
            }
        ]
    }
    
    # Ejecutar la query
    resultados = list(col_enviro_alertas.find(query))
    if resultados:
        return False
    else:
        return True

# def actualizar_consolidado_alertas_revisadas(service, idx):
#     if service == "ENVIRO":
#         coleccion = col_enviro_alertas

#     elif service == "NANOOX":
#         coleccion = col_nanoox_alertas

#     document = coleccion.find_one({"_id": idx})
#     if document:
#         # Extraer la fecha y formatearla a "Y-M-D"
#         fecha_str = document['fecha'].split('-')[0:3]
#         fecha = "-".join(fecha_str)
        
#         # Construir el filtro para encontrar el documento
#         filtro = {
#             "fecha": fecha,
#             "centro_id": document['centro_id']
#             # "_id": {"$ne": document['_id']}
#         }
        
#         # Definir el documento que se va a insertar o actualizar
#         actualizacion = {
#             "$setOnInsert": {
#                 "fecha": fecha,
#                 "centro_id": document['centro_id'],
#                 "variables": {}
#             }
#         }
        
#         # Realizar la operacion de upsert
#         coleccion.update_one(filtro, actualizacion, upsert = True)
    
#         # Ahora actualizamos la variable especifica
#         actualizacion_variable = {
#             "$push": {
#                 f"variables.{document['variable']}": {
#                     "inicio": document['fecha'],
#                     "valor": document['valor'],
#                     "alerta": document['alerta'],
#                     "usuario": document['usuario'],
#                     "fin": document['fin'],
#                     "duracion" : obtener_diferencia(document['fecha'], document['fin'])
#                 }
#             }
#         }    
#         # Actualizar la variable especifica en el documento
#         resultado = coleccion.update_one(filtro, actualizacion_variable)
#         print(f' Consolidado de Alertas actualizado con alerta {idx}')
#         if resultado.matched_count > 0:
#             if resultado.modified_count > 0:
#                 print("El documento se actualizo correctamente.")
#                 actualizacion = True
#             else:
#                 print("El documento coincidio con el filtro, pero no se modifico.")
#                 actualizacion = False
#         else:
#             print("No se encontro ningun documento que coincidiera con el filtro.")
#             actualizacion = False

#         if actualizacion:
#              coleccion.delete_one({"_id": idx})
#              print(f"La alerta {idx} se ha agregado al consolidado y removido de alertas")
#     else:
#         print(f' No se encontro alerta asociada al id {idx}')  
def actualizar_consolidado_alertas_revisadas(client, service, idx):
    if service == "ENVIRO":
        # coleccion = col_enviro_alertas
        coleccion = client[ENVIRO_DATABASE_NAME].alertas


    elif service == "NANOOX":
        # coleccion = col_nanoox_alertas
        coleccion = client[NANOOX_DATABASE_NAME].alertas

    document = coleccion.find_one({"_id": idx})
    if document:
        # Extraer la fecha y formatearla a "Y-M-D"
        fecha_str = document['fecha'].split('-')[0:3]
        fecha = "-".join(fecha_str)
        
        # Construir el filtro para encontrar el documento
        filtro = {
            "fecha": fecha,
            "centro_id": document['centro_id']
            # "_id": {"$ne": document['_id']}
        }
        
        # Definir el documento que se va a insertar o actualizar
        actualizacion = {
            "$setOnInsert": {
                "fecha": fecha,
                "centro_id": document['centro_id'],
                "variables": {}
            }
        }
        
        # Realizar la operacion de upsert
        coleccion.update_one(filtro, actualizacion, upsert = True)
    
        # Ahora actualizamos la variable especifica
        actualizacion_variable = {
            "$push": {
                f"variables.{document['variable']}": {
                    "inicio": document['fecha'],
                    "valor": document['valor'],
                    "alerta": document['alerta'],
                    "usuario": document['usuario'],
                    "fin": document['fin'],
                    "fin_estado" : document['fin_estado'],
                    "duracion" : obtener_diferencia(document['fecha'], document['fin'])
                }
            }
        }    
        # Actualizar la variable especifica en el documento
        resultado = coleccion.update_one(filtro, actualizacion_variable)
        print(f' Consolidado de Alertas actualizado con alerta {idx}')
        if resultado.matched_count > 0:
            if resultado.modified_count > 0:
                print("El documento se actualizo correctamente.")
                actualizacion = True
            else:
                print("El documento coincidio con el filtro, pero no se modifico.")
                actualizacion = False
        else:
            print("No se encontro ningun documento que coincidiera con el filtro.")
            actualizacion = False

        if actualizacion:
             coleccion.delete_one({"_id": idx})
             print(f"La alerta {idx} se ha agregado al consolidado y removido de alertas")
    else:
        print(f' No se encontro alerta asociada al id {idx}')  

def actualizar_consolidado_fallas_revisadas(client, service,  idx): #centro_id,
    if service == "ENVIRO":
        # coleccion = col_enviro_fallas
        coleccion = client[ENVIRO_DATABASE_NAME].alertas_fallas


    elif service == "NANOOX":
        # coleccion = col_nanoox_fallas
        coleccion = client[NANOOX_DATABASE_NAME].alertas_fallas


    query = {"_id": idx} #"centro_id": centro_id,
    # document = coleccion.find_one({"_id": idx})
    document = coleccion.find_one(query)
    if document:
        # Extraer la fecha y formatearla a "Y-M-D"
        fecha_str = document['fecha'].split('-')[0:3]
        fecha = "-".join(fecha_str)
        
        # Construir el filtro para encontrar el documento
        filtro = {
            "fecha": fecha,
            "centro_id": document['centro_id']
            # "_id": {"$ne": document['_id']}
        }
        
        # Definir el documento que se va a insertar o actualizar
        actualizacion = {
            "$setOnInsert": {
                "fecha": fecha,
                "centro_id": document['centro_id'],
                "variables": {}
            }
        }
        
        # Realizar la operacion de upsert
        coleccion.update_one(filtro, actualizacion, upsert = True)
    
        # Ahora actualizamos la variable especifica
        actualizacion_variable = {
            "$push": {
                f"variables.{document['variable']}": {
                    "inicio": document['fecha'],
                    "valor": document['valor'],
                    "alerta": document['alerta'],
                    "usuario": document['usuario'],
                    "fin": document['fin'],
                    "fin_estado" : document['fin_estado'],
                    "duracion" : obtener_diferencia(document['fecha'], document['fin_estado'])
                }
            }
        }    
        # Actualizar la variable especifica en el documento
        resultado = coleccion.update_one(filtro, actualizacion_variable)
        print(f' Consolidado de Fallas actualizado con alerta {idx} ')
        if resultado.matched_count > 0:
            if resultado.modified_count > 0:
                print("El documento se actualizo correctamente.")
                actualizacion = True
            else:
                print("El documento coincidio con el filtro, pero no se modifico.")
                actualizacion = False
        else:
            print("No se encontro ningun documento que coincidiera con el filtro.")
            actualizacion = False

        if actualizacion:
             idx = document["_id"]
             coleccion.delete_one({"_id": idx})
             print(f"La falla {idx} se ha agregado al consolidado y removido de fallas")
    else:
        print(f' No se encontro falla asociada a la falla {idx}')

def capitalize_first_keys(d, depth=1, max_depth=3):
    if isinstance(d, dict):
        new_dict = {}
        for key, value in d.items():
            if depth <= max_depth:
                #new_key = key.title()
                new_key = key[:1].upper() + key[1:]
            else:
                new_key = key
            new_dict[new_key] = capitalize_first_keys(value, depth + 1, max_depth)
        return new_dict
    elif isinstance(d, list):
        return [capitalize_first_keys(item, depth, max_depth) for item in d]
    else:
        return d


def original_procesar_permisos(client, permisos_usuarios):
    """
    REaliza el cruce entre los permisos que posee el usuario con los permisos actuales almacenados en la coleccion ciclos,
    y construye un nuevo diccionario con el “permiso efectivo” (status) para cada servicio de cada centro, segun {REGION{AREA{CENTRO}}
    """
    ciclos_collection = client[USER_DATABASE_NAME].ciclos 
    # Inicializar el diccionario resultante
    result_dict = {}
    # print(f'input: {permisos_usuarios}')
    for region in permisos_usuarios['permisos']:
        result_dict[region] = {}
        for area in permisos_usuarios['permisos'][region]:
            result_dict[region][area] = {}
            for centro in permisos_usuarios['permisos'][region][area]:
                # print(f'centro find: {centro.lower()}')
                info_centro_ciclo = list(ciclos_collection.find({"centro_id": centro.lower()}))
                if info_centro_ciclo:
                    result_dict[region][area][centro] = {}
                    for servicio in permisos_usuarios['permisos'][region][area][centro]:
                        status = permisos_usuarios['permisos'][region][area][centro][servicio]['status']
                        if servicio == "enviro":
                            status_centro_ciclos = info_centro_ciclo[0]['enviro']['status']
                            comparacion_enviro = status and status_centro_ciclos
                            result_dict[region][area][centro][servicio] = {'status': comparacion_enviro}
                        elif servicio == "nanoox":
                            status_centro_ciclos = info_centro_ciclo[0]['nanoox']['status']
                            comparacion_nanoox = status and status_centro_ciclos
                            result_dict[region][area][centro][servicio] = {'status': comparacion_nanoox}
                        elif servicio == "scapp":
                            status_centro_ciclos = info_centro_ciclo[0]['scapp']['status']
                            comparacion_scapp = status and status_centro_ciclos
                            result_dict[region][area][centro][servicio] = {'status': comparacion_scapp}
                        elif servicio == "pss":
                            status_centro_ciclos = info_centro_ciclo[0]['pss']['status']
                            comparacion_pss = status and status_centro_ciclos
                            result_dict[region][area][centro][servicio] = {'status': comparacion_pss}
                else:
                    result_dict[region][area][centro] = []

    # Retornar el diccionario resultante
    return result_dict

#NEW_FEATURE
def procesar_permisos(client, permisos_usuarios):
    """
    Realiza el cruce entre los permisos que posee el usuario con los permisos actuales almacenados en la coleccion ciclos,
    y construye un nuevo diccionario con el “permiso efectivo” (status) para cada servicio de cada centro, segun {REGION{AREA{CENTRO}}
    """
    ciclos_collection = client[USER_DATABASE_NAME].ciclos 
    result_dict = {}

    for region in permisos_usuarios['permisos']:
        result_dict[region] = {}
        for area in permisos_usuarios['permisos'][region]:
            result_dict[region][area] = {}
            for centro in permisos_usuarios['permisos'][region][area]:
                
                # Buscamos el ciclo del centro
                info_centro_ciclo = list(ciclos_collection.find({"centro_id": centro.lower()}))
                
                if info_centro_ciclo:
                    datos_ciclo = info_centro_ciclo[0] # Alias para legibilidad
                    result_dict[region][area][centro] = {}
                    
                    for servicio in permisos_usuarios['permisos'][region][area][centro]:
                        # --- CAMBIO IMPORTANTE: Lógica Dinámica ---
                        # Verificamos si el servicio (ej. 'flux') existe en el documento del ciclo
                        if servicio in datos_ciclo and isinstance(datos_ciclo[servicio], dict):
                            
                            status_usuario = permisos_usuarios['permisos'][region][area][centro][servicio].get('status', False)
                            status_ciclo = datos_ciclo[servicio].get('status', False)
                            
                            # Logica AND: (Permiso Usuario) Y (Servicio Activo en Ciclo)
                            comparacion = status_usuario and status_ciclo
                            
                            result_dict[region][area][centro][servicio] = {'status': comparacion}
                        else:
                             # Si el servicio no existe en el ciclo (ej. centro antiguo sin flux), false por defecto
                            result_dict[region][area][centro][servicio] = {'status': False}
                else:
                    result_dict[region][area][centro] = {} # Corrección: Dict vacío en vez de lista vacía para consistencia

    return result_dict

def original_procesar_permisos_de_usuario(client, usuario):
    users_collection = client[USER_DATABASE_NAME].users
    # user = list(db.usars.find({'username':usuario}))
    user = list(users_collection.find({'username':usuario}))
    # print(f'usuario_para_permisos: {user}')
    if user:
        usuario_capitalizado = user[0]
        usuario_capitalizado['permisos'] =  capitalize_first_keys(usuario_capitalizado['permisos'])
        permisos_procesados = procesar_permisos(client, usuario_capitalizado)
        # print(f'final: {permisos_procesados}\n')
        return permisos_procesados
    else:
        return None

#NEW_FEATURE
def procesar_permisos_de_usuario(client, usuario):
    try:
        users_collection = client[USER_DATABASE_NAME].users
        user = list(users_collection.find({'username': usuario}))
        
        if user:
            usuario_capitalizado = user[0]
            # Capitalizamos las llaves como hacías antes
            usuario_capitalizado['permisos'] = capitalize_first_keys(usuario_capitalizado['permisos'])
            
            # Procesamos permisos (tu lógica existente de roles/accesos)
            permisos_procesados = procesar_permisos(client, usuario_capitalizado)
            
            # --- INTEGRACIÓN: APLICAMOS EL PARCHE LEGACY AQUÍ ---
            # Antes de retornar, estandarizamos la estructura.
            # Así, update_status_service recibe data con 'flux' garantizado.
            permisos_finales = normalizar_schema_legacy(permisos_procesados)
            
            return permisos_finales
        else:
            return None
    except Exception as e:
        print(f"Error procesando permisos usuario: {e}")
        return {}

#NEW_FEATURE
def normalizar_schema_legacy(permisos_usuario):
    """
    Normaliza permisos legacy.
    CORRECCIÓN: Si falta el permiso 'flux', verifica si el centro es zona Flux/Barcos.
    Si es así, otorga el permiso (status: True) automáticamente.
    """
    permisos_normalizados = copy.deepcopy(permisos_usuario)

    try:
        for region_key, areas in permisos_normalizados.items():
            if not isinstance(areas, dict): continue
            
            for area_key, centros in areas.items():
                if not isinstance(centros, dict): continue

                for centro_key, servicios in centros.items():
                    if isinstance(servicios, dict):
                        
                        # --- LÓGICA SMART LEGACY ---
                        # Si no existe la llave flux, la creamos inteligentemente
                        if 'flux' not in servicios or servicios['flux'] is None:
                            
                            # Analizamos el nombre del centro para ver si "merece" tener flux activo
                            partes = centro_key.lower().split('-')
                            region_id = partes[-1] if len(partes) >= 2 else ""
                            area_id = partes[-2] if len(partes) >= 3 else partes[0]
                            
                            # Si es zona flux/barcos -> status: True. Si no -> status: False
                            es_zona_flux = (region_id == 'flux') or (area_id in ['boat', 'boats', 'barcos'])
                            
                            servicios['flux'] = {'status': es_zona_flux}

    except Exception as e:
        print(f"Advertencia normalizando legacy: {e}")
        return permisos_usuario

    return permisos_normalizados


def ORIGINAL_update_status_service(original_dict1, dict2):
    new_dict = {}
    
    for key1, subdict1 in original_dict1.items():
        for key2, subdict2 in subdict1.items():
            # Ya no convertimos key2 a minusculas porque las llaves ya estan en el formato correcto
            for key_region, region_data in dict2.items():
                for key_area, area_data in region_data.items():
                    if key2 in area_data:
                        if key1 not in new_dict:
                            new_dict[key1] = {}
                        new_dict[key1][key2] = copy.deepcopy(subdict2)
                        for service, values in new_dict[key1][key2].items():
                            if service in area_data[key2]:
                                values['status_service'] = area_data[key2][service]['status']
    
    return new_dict

#NEW_FEATURE
def _permission_lookup_key(value):
    """
    Genera una llave exclusiva para comparar permisos.

    `casefold` permite comparar mayúsculas/minúsculas sin modificar el nombre que
    finalmente se envía al frontend. El `strip` evita rechazos por espacios accidentales.
    """
    return str(value).strip().casefold()


def update_status_service(original_dict1, dict2):
    """
    Cruza los estados calculados (`original_dict1`) con los permisos del usuario (`dict2`).

    Los identificadores de centro y servicio se comparan sin distinguir mayúsculas. Las
    llaves originales se conservan en la respuesta para no cambiar el contrato con el FE.
    """
    # Un usuario sin permisos nunca debe recibir los estados base completos.
    if not dict2:
        return {}

    try:
        # Aplanamos Region -> Area -> Centro en un índice por centro. El centro ya incluye
        # área y región en su ID, por ejemplo: Centro-Area-Region. Esto evita repetir el
        # recorrido jerárquico para cada centro calculado.
        permissions_by_center = {}
        for region_data in dict2.values():
            if not isinstance(region_data, dict):
                continue
            for area_data in region_data.values():
                if not isinstance(area_data, dict):
                    continue
                for center_name, center_permissions in area_data.items():
                    center_key = _permission_lookup_key(center_name)

                    # Dos llaves que sólo difieren en mayúsculas serían ambiguas. No se
                    # elige una arbitrariamente porque eso podría otorgar permisos erróneos.
                    if not center_key or center_key in permissions_by_center:
                        raise ValueError(f"Permiso de centro ambiguo: {center_name}")
                    permissions_by_center[center_key] = center_permissions

        result = {}
        for region_area, centers in original_dict1.items():
            for center_name, center_states in centers.items():
                # Esta comparación resuelve el caso Centro-Area-Region contra
                # Centro-area-region que dejó la respuesta de los servidores vacía.
                center_permissions = permissions_by_center.get(
                    _permission_lookup_key(center_name)
                )

                # Si el centro no está autorizado, no se incorpora a la respuesta.
                if not isinstance(center_permissions, dict):
                    continue

                # Un estado faltante cierra únicamente este centro. Así un dato inválido
                # no expone el centro ni deja sin respuesta a todos los demás autorizados.
                if not isinstance(center_states, dict):
                    continue

                # Los nombres de servicios también se normalizan para que NANOOX,
                # Nanoox y nanoox representen el mismo permiso.
                permissions_by_service = {}
                for service_name, service_permission in center_permissions.items():
                    service_key = _permission_lookup_key(service_name)
                    if not service_key or service_key in permissions_by_service:
                        raise ValueError(f"Permiso de servicio ambiguo: {service_name}")
                    permissions_by_service[service_key] = service_permission

                # Copiamos los estados para no mutar el diccionario original compartido.
                result.setdefault(region_area, {})[center_name] = copy.deepcopy(center_states)
                for service_name, service_state in result[region_area][center_name].items():
                    service_permission = permissions_by_service.get(
                        _permission_lookup_key(service_name), {'status': False}
                    )

                    # Se soportan tanto permisos actuales ({'status': bool}) como el
                    # formato booleano legacy. Un servicio ausente queda denegado.
                    if isinstance(service_permission, dict):
                        permission_status = service_permission.get('status', False)
                    else:
                        permission_status = bool(service_permission)
                    service_state['status_service'] = permission_status

        return result
    except Exception as e:
        print(f"Error crítico en update_status_service: {e}")
        # Ante datos inválidos o ambiguos se falla cerrado: no se exponen centros.
        return {}


# def actualizar_umbral(coleccion, centro_id, variable, nuevos_valores):
    
#     # Filtro para encontrar el documento
#     filtro = {
#         "centro_id": centro_id,
#         "variable": variable
#     }
    
#     # Actualizacion de los campos
#     actualizacion = {
#         "$set": nuevos_valores
#     }
    
#     # Actualizacion del documento
#     resultado = coleccion.update_one(filtro, actualizacion)

#     if resultado.modified_count > 0:
#         return True
#     else:
#         return False
def actualizar_umbral(collection, centro_id, variable, nuevos_valores):
    operaciones = [
        UpdateOne(
            {"centro_id": centro_id, "variable": variable},
            {"$set": nuevos_valores},
            upsert=True
        )
    ]

    try:
        resultado = collection.bulk_write(operaciones, ordered=False)
        if resultado.modified_count > 0:
            respuesta = True
        else:
            respuesta = False
        return respuesta
    except BulkWriteError as bwe:
        print(f"Error de escritura en bulk: {bwe.details}")
        respuesta = False
        return respuesta    

# def guardar_umbrales_activos(service, umbrales):
#     if service == "ENVIRO":
#         coleccion = col_enviro_umbrales

#     elif service == "NANOOX":
#         coleccion = col_nanoox_umbrales

#     print(f'umbrales_desde_local: {umbrales}')
#     try:
#         coleccion.insert_many(umbrales)
#         print("Documentos insertados exitosamente.")
#     except errors.BulkWriteError as bwe:
#         print("Error en la insercion en bloque, manejando duplicados...")
#         # Filtrar documentos duplicados
#         error_details = bwe.details
#         write_errors = error_details.get('writeErrors', [])
#         duplicated_ids = [error['op']['_id'] for error in write_errors if error['code'] == 11000]

#         if duplicated_ids:
#             print(f"IDs duplicados encontrados: {duplicated_ids}")
#             # Remover documentos duplicados del conjunto original
#             documents = [doc for doc in umbrales if doc['_id'] not in duplicated_ids]
#             if documents:  # Verificar si aun hay documentos por insertar
#                 try:
#                     coleccion.insert_many(documents)
#                     print("Documentos restantes insertados exitosamente.")
#                 except errors.BulkWriteError as bwe_inner:
#                     print("Error en la insercion en bloque (segunda vez):", bwe_inner.details)
#             else:
#                 print("No hay documentos restantes por insertar despues de filtrar duplicados.")


def guardar_umbrales_activos(client, service, umbrales):
    operations = []
    print(f"{service=}")
    print(f"{NANOOX_DATABASE_NAME=}")
    if service == "ENVIRO":
        # coleccion = col_enviro_umbrales
        coleccion = client[ENVIRO_DATABASE_NAME].umbrales


    elif service == "NANOOX":
        # coleccion = col_nanoox_umbrales
        coleccion = client[NANOOX_DATABASE_NAME].umbrales


    print(f'umbrales_desde_local: {umbrales}') 
    for umbral in umbrales:
        print(f"{umbral=}")
        print(f'{umbral["centro_id"]=}')
        print(f'{umbral["variable"]=}')
        filter = {"centro_id": umbral["centro_id"], "variable": umbral["variable"]}
        update = {"$set": umbral}
        operations.append(UpdateOne(filter, update, upsert=True))
    
    try:
        # Realizar la operacion de escritura en bloque
        result = coleccion.bulk_write(operations)
        print("Documentos restantes insertados exitosamente.")
    except errors.BulkWriteError as bwe:
        print(f"Error en la operacion de escritura en bloque: {bwe.details}")
    except errors.PyMongoError as e:
        print(f"Error en PyMongo: {e}")


# def guardar_umbrales_nanoox(client, umbrales):
    
def guardar_umbrales_nanoox(client, umbrales, collection_name="umbrales", new_version = False):
    """
    Guarda los umbrales en una colección de series de tiempo.
    Si la colección no existe, la crea como time series.
    """

    if new_version:
        collection_name="umbrales_v2"
        try:
            centro_id = umbrales.get("centro")
            nuevos_datos = umbrales.get("data")

            if not nuevos_datos or not isinstance(nuevos_datos, dict):
                print(f"Datos de umbrales inválidos: {nuevos_datos}")
                return

            
            # --- Verificar y crear colección como time series si no existe ---
            db = client[NANOOX_DATABASE_NAME]
            if collection_name not in db.list_collection_names():
                db.create_collection(
                    collection_name,
                    timeseries={
                        "timeField": "timestamp",   # campo obligatorio
                        "metaField": "centro_id",   # opcional: clave de agrupación
                        "granularity": "seconds"    # depende de la frecuencia de datos
                    }
                )
                print(f"Colección {collection_name} creada como time series")

            umbrales_col = db[collection_name]

            # Timestamp en UTC
            fecha_actual = datetime.utcnow()

            # Buscar el último dato guardado para ese centro
            existing_doc = umbrales_col.find_one(
                {"centro_id": centro_id},
                sort=[("timestamp", -1)]
            )

            if existing_doc:
                # Extraer solo los campos relevantes del último doc
                ultimo_dato = {
                    "setpoin_off": existing_doc.get("setpoin_off"),
                    "setpoin_on": existing_doc.get("setpoin_on")
                }

                print(ultimo_dato, nuevos_datos )
                # Comparar con los nuevos valores recibidos
                if ultimo_dato == nuevos_datos:
                    print(f"{centro_id}: Umbrales sin cambios, no se inserta nuevo documento.")
                    return


            # Insertar nuevo documento en la serie de tiempo
            doc = {
                "centro_id": centro_id,
                **nuevos_datos, 
                "timestamp": fecha_actual
            }

            umbrales_col.insert_one(doc)
            print(f"Nuevo umbral insertado en {collection_name} para centro {centro_id}")

        except Exception as e:
            print(f"Error al guardar umbrales en centro {centro_id if 'centro_id' in locals() else '?'}: {e}")

    else:
        collection_name="umbrales"
        centro = umbrales.get('centro')
        nuevos_datos = umbrales.get('data')
        fecha = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        col_nanoox_umbrales= client[NANOOX_DATABASE_NAME].umbrales
        # Buscar el documento existente por 'nombre'
        existing_doc = col_nanoox_umbrales.find_one({'centro_id': centro})
        print(f"Guardar umbrales Nanoox: {centro=} {nuevos_datos=}")

        if existing_doc:
            # Verificar si los datos son diferentes al existente
            if existing_doc.get('datos')[-1][:4] != nuevos_datos[:4]:
                nuevos_datos.append(fecha)
                # Preparar el array completo de datos para insertar
                # array_completo = [existing_doc['datos'][0]] + nuevos_datos

                # Preparar la operacion de actualizacion (upsert)
                update_operation = UpdateOne(
                    {'_id': existing_doc['_id']},
                    {'$push': {'datos': nuevos_datos}}
                )

                # Ejecutar la operacion de actualizacion
                result = col_nanoox_umbrales.bulk_write([update_operation])

                # Imprimir resultado
                print(f"Upsert realizado. Documentos modificados: {result.modified_count}, documentos insertados: {result.upserted_count}")
            else:
                print(f"{centro}: Datos de Umbrales sin cambios, no se actualiza en bd.")
        else:
            nuevos_datos.append(fecha)
            # Si no hay documento existente, insertar uno nuevo con todos los datos
            col_nanoox_umbrales.insert_one({'centro_id': centro, 'datos': [nuevos_datos]})
            print("Nuevo documento insertado.")


    

def get_umbrales_nanoox(db_nanoox, centro_id,  collection_name="umbrales", new_version = False):
    try:
        # Conexion a la base de datos MongoDB
        # collection = col_nanoox_umbrales
        
        # Buscar el último dato guardado para ese centro

        if new_version:
            collection_name = "umbrales_v2"

            collection = db_nanoox[collection_name]
            existing_doc = collection.find_one(
                {"centro_id": centro_id},
                sort=[("timestamp", -1)]
            )

            print(f"{existing_doc=}")

            if existing_doc:
                # Extraer solo los campos relevantes del último doc
                umbrales_data = {
                    "setpoin_off": existing_doc.get("setpoin_off"),
                    "setpoin_on": existing_doc.get("setpoin_on")
                }
                return umbrales_data

        else:
            collection = db_nanoox.umbrales

            # Obtener todos los documentos de la coleccion
            documents = collection.find_one({"centro_id":centro_id }) #,"date": date

            umbrales_data = None
            if documents:
                umbrales_data = documents['datos'][-1][:4]
                
            return umbrales_data
    
    except Exception as e:
        print(f"Error al obtener datos de generador para centro {centro_id}': {e} {e.__traceback__.tb_lineno}")
        raise

def get_umbrales_scapp(db_scapp, centro_id):
    """
    Obtiene el ultimo set de umbrales para un centro desde la coleccion 'umbrales'
    en la base de datos scapp. Cada documento tiene la forma:
      {
        "centro_id": "...",
        "datos": [
          {
            "SFR":     {"A": int,   "C": int},
            "AYUNO":   {"A": float, "C": float},
            "ALARMAS": {"A": int,   "C": int},
            "fecha":   "YYYY-MM-DD HH:MM:SS"
          },
          ...
        ]
      }
    Devuelve JSON con el ultimo entry (incluye SFR, AYUNO, ALARMAS y fecha).
    En caso de error o ausente, devuelve jsonify con el codigo apropiado.
    """
    try:
        collection = db_scapp.umbrales
        print(f"{centro_id=}")
        doc = collection.find_one({'centro_id': centro_id})
        print(f"{doc=}")
        if not doc:
            return jsonify({
                'error': f'No se encontraron umbrales para centro_id "{centro_id}"'
            }), 404

        datos = doc.get('datos')
        if not isinstance(datos, list) or len(datos) == 0:
            return jsonify({
                'error': f'Documento de umbrales mal formado o sin datos para centro_id "{centro_id}"'
            }), 404

        ultimo = datos[-1]
        print(f"{ultimo=}")
        # ultimo entry ya tiene: SFR, AYUNO, ALARMAS, fecha
        return ultimo

    except Exception as e:
        # Logging interno para debug
        print(f"[get_umbrales_scapp] Error al obtener umbrales para centro_id={centro_id}: {e}")
        return jsonify({
            'error': 'Error interno al obtener umbrales'
        }), 500

#Funciones Auxiliares Umbrales SCAPP
def guardar_umbrales_scapp(client, username, umbrales):
    """
    Guarda o actualiza en la coleccion 'umbrales' un documento con:
      - centro_id
      - datos: lista de entradas donde cada entrada tiene directamente las llaves
        "SFR", "AYUNO", "ALARMAS" y "fecha"
    Ejemplo de entrada JSON esperada (payload):
    {
      "centro": "centro_001",
      "datos": {
        "SFR":     {"A": 50, "C": 80},
        "AYUNO":   {"A": 0.5, "C": 2.0},
        "ALARMAS": {"A": 10, "C": 30}
      }
    }
    """
    print("ENTRANDOA GUARDAR UMBRALES SCAPP!!!")
    # 1) Extraer parametros
    centro = umbrales.get('centro')
    nuevos = umbrales.get('data')
    if centro is None or nuevos is None:
        return jsonify({'error': "Se requieren los parametros 'centro' y 'datos'"}), 404

    # 2) Validacion de llaves esperadas
    expected_keys = {"SFR", "AYUNO", "ALARMAS"}
    if not isinstance(nuevos, dict) or set(nuevos) != expected_keys:
        return jsonify({'error': f"'datos' debe ser un dict con llaves {expected_keys}"}), 404

    # 3) Validar SFR y ALARMAS como enteros
    for key in ("SFR", "ALARMAS"):
        sub = nuevos.get(key)
        if not isinstance(sub, dict) or not {"A", "C"}.issubset(sub):
            return jsonify({'error': f"'{key}' debe ser un dict con llaves 'A' y 'C'"}), 404
        if not (isinstance(sub["A"], int) and isinstance(sub["C"], int)):
            return jsonify({'error': f"Los valores de '{key}' deben ser enteros"}), 404

    # 4) Validar AYUNO como floats
    ayuno = nuevos.get("AYUNO")
    if not isinstance(ayuno, dict) or not {"A", "C"}.issubset(ayuno):
        return jsonify({'error': "'AYUNO' debe ser un dict con llaves 'A' y 'C'"}), 404
    try:
        nuevos["AYUNO"]["A"] = float(ayuno["A"])
        nuevos["AYUNO"]["C"] = float(ayuno["C"])
    except (TypeError, ValueError):
        return jsonify({'error': "Los valores de 'AYUNO' deben ser numericos (float)"}), 404

    # 5) Crear la entrada con fecha y los umbrales al mismo nivel
    fecha = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    entrada = {
        **nuevos,
        "fecha": fecha
    }

    # 6) Conectar a la coleccion
    col = client[SCAPP_DATABASE_NAME].umbrales
    user_db = client[USER_DATABASE_NAME]
    users_historial_collection = user_db.historial_users
    print("Conectados a la DB")
    try:
        # 7) Buscar documento existente
        existing = col.find_one({"centro_id": centro})
        
        # print(f"[guardar_umbrales_scapp] centro={centro}, nuevos={nuevos}")

        if existing:
            print("Entramos a existing umbrales")
            # ultimo registro
            last = existing.get("datos", [])[-1] if existing.get("datos") else {}
            # extraer solo los thresholds para comparar
            last_thresh = {k: v for k, v in last.items() if k in expected_keys}
            if last_thresh != nuevos:
                op = UpdateOne(
                    {"_id": existing["_id"]},
                    {"$push": {"datos": entrada}}
                )
                res = col.bulk_write([op])
                print(f"Actualizado: modified={res.modified_count}, upserted={res.upserted_count}")
            else:
                print(f"{centro}: sin cambios en umbrales, no se actualiza.")
        else:
            print("Entramos a nuevos umbrales")
            # insercion inicial
            col.insert_one({
                "centro_id": centro,
                "datos":     [entrada]
            })
            print(f"Nuevo documento de umbrales Scapp insertado en centro {centro}.")
            insert_log_in_user_history(users_historial_collection, username, centro, 'add_scapp_threshold')
    except Exception as e:
        print(f"Error guardando umbrales Scapp por error en linea {e.__traceback__.tb_lineno}: \n{e}")
        return jsonify({'error': "El Serrvidor encontro un error inesperado'"}), 500

    return


def enviar_umbrales_formateados_a_fe(client, service, centro_id):
    if service == "ENVIRO":
        # coleccion = col_enviro_umbrales
        coleccion = client[ENVIRO_DATABASE_NAME].umbrales

    elif service == "NANOOX":
        # coleccion = col_nanoox_umbrales
        coleccion = client[NANOOX_DATABASE_NAME].umbrales
    um= list(coleccion.find({"centro_id": centro_id},{"_id":0}))
    umbrales = []
    
    for documento in um:
        nombre = documento['variable']
        status = documento['status']
        tipo = documento['tipo']
        
        if tipo == 'simple':
            atencion = documento['c']
            critica = documento['d']
            umbrales.append({
                'nombre': nombre,
                'status': status,
                'tipo': tipo,
                'atencion': atencion if atencion is not None else None,
                'critica': critica if critica is not None else None,
            })
        elif tipo == 'compuesta':
            atencionInf = documento['a']
            criticaInf = documento['b']
            atencionSup = documento['c']
            criticaSup = documento['d']
            umbrales.append({
                'nombre': nombre,
                'status': status,
                'tipo': tipo,
                'atencionInf': atencionInf if atencionInf is not None else None,
                'criticaInf': criticaInf if criticaInf is not None else None,
                'atencionSup': atencionSup if atencionSup is not None else None,
                'criticaSup': criticaSup if criticaSup is not None else None
            })
    
    return(umbrales)

def actualizar_estado_servicio(client, centro_id, mensaje):
    # Selecciona la coleccion
    bulk_operations = []
    estado_centros_collection = client[USER_DATABASE_NAME].alertas_fallas
    operation = UpdateOne(
        {
            'centro_id' : centro_id
        },
            {'$set':mensaje},
        
        upsert = True
    )

    bulk_operations.append(operation)
    if bulk_operations:
        estado_centros_collection.bulk_write(bulk_operations)
        print(f'Estado de {centro_id} actualizado en ITGSM')
        return True
    else:
        return False
    
def actualizar_estado_variables(client, centro_id, mensaje):
    col_enviro_variables = client[ENVIRO_DATABASE_NAME].variables
    # Selecciona la coleccion
    bulk_operations = []
    operation = UpdateOne(
        {
            'centro_id' : centro_id
        },
            {'$set':mensaje},
        
        upsert = True
    )

    bulk_operations.append(operation)
    if bulk_operations:
        col_enviro_variables.bulk_write(bulk_operations)
        print(f'Estado variables de {centro_id} actualizado en ITGSM')
        return True
    else:
        return False
    

#---------------RUTAS------------------------!
#Para ITGSM Recibir alertas desde Local
@api_bp.route('/<service_name>/notifications_to_emails/<region>/<area>/<centro>', methods=['GET'])
def get_emergency_emails(service_name, region, area, centro):
    """
    Ruta API para obtener lista de correos por servicio y centro.
    Ejemplo limpio: GET /flux/notifications_to_emails/Loslagos/Puertomontt/Centro
    """
    try:
        # 1. Validación y Selección de la Base de Datos
        service_key = service_name.lower().strip()
        
        # La seguridad radica aquí: si no está en la lista blanca, rechaza.
        if service_key not in SERVICES_DB_MAP:
            return jsonify({
                "error": "Invalid Service", 
                "message": f"El servicio '{service_name}' no es válido."
            }), 400

        db_name = SERVICES_DB_MAP[service_key]
        
        # Conexión a la DB
        client = get_db() 
        collection = client[db_name][EMAIL_COLLECTION_NAME]

        # 2. Validación de Inputs de ubicación (Regex para evitar inyección)
        pattern = re.compile(r"^[a-zA-Z0-9\sñÑ-]+$")
        if not (pattern.match(region) and pattern.match(area) and pattern.match(centro)):
            return jsonify({"error": "Bad Request", "message": "Caracteres inválidos en URL"}), 400

        # 3. Llamada a la utilidad de lectura
        emails = fetch_emails_by_location(collection, region, area, centro)

        # 4. Respuesta
        if not emails:
            return jsonify({
                "status": "error", 
                "message": f"No hay emails configurados para {service_name.upper()} en esa ubicación"
            }), 404

        return jsonify({
            "status": "success",
            "service": service_name,
            "data": {
                "target_id": f"{region.title()}-{area.title()}-{centro.title()}",
                "emails": emails
            }
        }), 200

    except RuntimeError as re_err:
        print(f"Error DB en notifications_to_emails: {re_err}")
        return jsonify({"error": "Internal Error", "message": "Error leyendo base de datos"}), 500
    except Exception as e:
        print(f"Error inesperado: {e}")
        return jsonify({"error": "Unexpected Error", "message": str(e)}), 500

#Post alertas con nanoox
@api_bp.route( '/<SERVICE>/post_alertas', methods=['POST'])
def post_alertas(SERVICE):
    service = SERVICE
    alertas = request.json
    client = get_db()

    try:
        if alertas != []:
            # pprint(alertas)
            guardar_alertas_activas(client, service, alertas)
            return jsonify({"mensaje": f"Se han enviado correctamente las alertas {SERVICE} "}), 200
        else:
            print(f'No se han recibido datos de alertas')
            return jsonify({"mensaje": f"No se han recibido datos de alertas"}), 401
    except Exception as e:
        print(f"Error Enviando las alertas {SERVICE}: por error en linea {e.__traceback__.tb_lineno}: \n{e}")
        return jsonify({"mensaje": f" No se han enviado correctamente las alertas a ITGSM "}), 401

@api_bp.route( '/<SERVICE>/alertas_activas/<CENTRO>/<AREA>/<REGION>', methods=['GET'])
def alertas_to_fe(SERVICE, CENTRO, AREA, REGION):
    client = get_db()
    CENTRO = CENTRO.capitalize()
    AREA = AREA.capitalize()
    REGION = REGION.capitalize()    
    # centro_id = f'{CENTRO}-{AREA}-{REGION}'
    centro_id = f'{REGION}-{AREA}-{CENTRO}'
    print(f'SERVICE_ALERTA_ACTIVA: {SERVICE}')
    # if SERVICE == 'ENVIRO':
    # alertas_activas = obtener_alertas_activas(centro_id)
    try:
        alertas_activas = obtener_alertas_activas(client, SERVICE, centro_id)
        print( f"Se han enviado correctamente las alertas al FE {SERVICE}")
        return jsonify(alertas_activas), 200
    except Exception as e:
        print(f"ERROR Enviando las alertas {SERVICE} por error en linea {e.__traceback__.tb_lineno}: \n{e}")
        return jsonify({"mensaje": f" No se han enviado correctamente las alertas al FE "}), 401 

#revision con nanoox
@api_bp.route( '/<SERVICE>/revision_alerta', methods=['POST'])
def revisar_alerta_ITGSM(SERVICE):
    client = get_db()

    datos_alerta_revisada = request.json
    print(f'datos_alerta_revisada: {datos_alerta_revisada} \n')
    if datos_alerta_revisada:
        data_alertas_revisadas = datos_alerta_revisada['data']
        centro_id = datos_alerta_revisada['centro']
        
        if data_alertas_revisadas:
            # ids_revisados = []  # Lista para almacenar los _id de fallas que no pudieron revisarse
            ids_dict = {'no_revisados':[], 'revisados':[]}
            
            for falla in data_alertas_revisadas:
                idx = falla['_id']
                fecha_fin = falla['fin_estado']
                
                try:
                    actualizar_revision_alerta(client, SERVICE, centro_id, idx, 'local', fecha_fin)
                    actualizar_consolidado_alertas_revisadas(client, SERVICE, idx)
                    ids_dict["revisados"].append(idx)
                except Exception:
                    print("No se ha podido revisar la alerta con _id {idx}")
                    ids_dict["no_revisados"].append(idx)
                    # errores_ids.append(idx)  # Guardamos solo el _id en caso de error
            
            if ids_dict:
                print(f"alertas {ids_dict=}")
                return jsonify({
                    "mensaje": "Las alertas revisadas en SM son",
                    "data": ids_dict  # Devuelve un diccionario con los ids que se revisaron y los que no
                }), 200
            else:
                return jsonify({"mensaje": "No se han podido revisar las alertas enviadas"}), 401
        else:
            return jsonify({"mensaje": "Se enviaron revisiones de alertas vacias"}), 204
    else:
        return jsonify({"mensaje": "No se han enviado datos"}), 204
# def revisar_alerta_ITGSM(SERVICE): #,CENTRO_ID
#     print(f'datos_desde_RICHI: {request.json} \n')
#     datos_alerta_revisada = request.json
#     print(f'datos_alerta_revisada: {datos_alerta_revisada} \n')
#     if datos_alerta_revisada:
#         data_alertas_revisadas=datos_alerta_revisada['data']
#         if data_alertas_revisadas != []:
#             centro_id = datos_alerta_revisada['centro']
#             print(f"{centro_id=}")
#             for falla in datos_alerta_revisada['data']:
#                 idx = falla['_id']
#                 fecha_fin = falla['fin_estado']
#                 try:
#                     actualizar_revision_alerta(SERVICE, centro_id, idx, 'local', fecha_fin) #idx,
#                     actualizar_consolidado_alertas_revisadas(SERVICE, idx)
#                     # actualizar_consolidado_fallas_revisadas(SERVICE, centro_id, idx)
#                     return jsonify({"mensaje": f"Se han enviado correctamente las alertas "}), 200
#                 except Exception as e:
#                     print(f"No se han enviado correctamente las fallas por error en linea {e.__traceback__.tb_lineno}: \n{e}")
#                     return jsonify({"mensaje": f" No se han enviado correctamente las alertas "}), 401
#         else:
#             return jsonify({"mensaje": f"Se enviaron revisiones de alertas vacias"}), 204
#     else:
#         return jsonify({"mensaje": f"No se han enviado datos"}), 204

@api_bp.route( '/<SERVICE>/fin_estado_alerta_activa/<CENTRO_ID>', methods=['POST'])
def actualizar_fin_estado_alerta_ITGSM(SERVICE, CENTRO_ID):
    print(f'datos_desde_LOCAL: {request.json} \n')
    datos_alerta_no_revisada = request.json
    client = get_db()
    col_enviro_alertas = client[ENVIRO_DATABASE_NAME].alertas
    print(f'{datos_alerta_no_revisada=} \n')
    if datos_alerta_no_revisada:
        idx = datos_alerta_no_revisada['idx']
        fecha_fin = datos_alerta_no_revisada['fin_estado']
        try:
            actualizar_fin_estado_alerta(client, SERVICE, CENTRO_ID, idx, fecha_fin)
            if check_alerta_incompleta(col_enviro_alertas, idx):
                return jsonify({"mensaje": f"Se ha enviado correctamente el fin estado alerta pero no cunple para actualizar consolidado "}), 200
            else:
                actualizar_consolidado_alertas_revisadas(client,SERVICE, idx)
                return jsonify({"mensaje": f"Se han enviado correctamente el fin estado alerta e ingresado al consolidado"}), 200 
        
        except Exception as e:
            print(f"Error enviando las alertas {SERVICE} por error en linea {e.__traceback__.tb_lineno}: \n{e}")
            return jsonify({"mensaje": f" No se ha enviado correctamente el fin estado alerta "}), 401

# @api_bp.route( '/<SERVICE>/revision_alerta/<CENTRO_ID>', methods=['POST'])
# def revisar_alerta_ITGSM(SERVICE, CENTRO_ID):
#     print(f'datos_desde_SALVA: {request.json} \n')
#     if SERVICE == 'ENVIRO':
#         datos_alerta_revisada = request.json
#         print(f'datos_alerta_revisada: {datos_alerta_revisada} \n')
#         if datos_alerta_revisada:
#             idx = datos_alerta_revisada['idx']
#             usuario_revision = datos_alerta_revisada['usuario_revision']
#             fecha_fin = datos_alerta_revisada['fin']
#             try:
#                 actualizar_revision_alerta(CENTRO_ID, idx, usuario_revision, fecha_fin)
#                 actualizar_consolidado_alertas_revisadas(idx)
#                 return jsonify({"mensaje": f"Se han enviado correctamente las alertas "}), 200
#             except Exception as e:
#                 print(f"No se han enviado correctamente las alertas por error en linea {e.__traceback__.tb_lineno}: \n{e}")
#                 return jsonify({"mensaje": f" No se han enviado correctamente las alertas "}), 401
#     elif SERVICE == 'NANOOX':
#         return jsonify({"mensaje": f"Se han enviado correctamente las alertas "}), 200

@api_bp.route( '/<SERVICE>/post_fallas', methods=['POST'])
def post_fallas(SERVICE):
    client = get_db()
    service = SERVICE
    fallas = request.json
    try:
        if fallas != []:
            # pprint(fallas)
            guardar_fallas_activas(client, service, fallas)
            return jsonify({"mensaje": f"Se han recibido correctamente las fallas del servicio {service}"}), 200
        else:
            print(f'No se han recibido datos de fallas del servicio')
            return jsonify({"mensaje": f" No se han recibido datos de fallas del servicio {service}"}), 401
    except Exception as e:
        print(f"No se han enviado correctamente las fallas por error en linea {e.__traceback__.tb_lineno}: \n{e}")
        return jsonify({"mensaje": f" No se han enviado correctamente las fallas a ITGSM "}), 401
    
@api_bp.route( '/<SERVICE>/fallas_activas/<CENTRO>/<AREA>/<REGION>', methods=['GET'])
def fallas_to_fe(SERVICE, CENTRO, AREA, REGION):
    """ try:
        print(f"faia{REGION[0]=}")
        print(f"faia{REGION[1]=}")
    except:
        print("NO SE PUEDE SEPRAR REGION") """
    client = get_db()
    CENTRO = CENTRO.capitalize()
    AREA = AREA.capitalize()
    REGION = REGION.capitalize()
    # centro_id = f'{CENTRO}-{AREA}-{REGION}'
    centro_id = f'{REGION}-{AREA}-{CENTRO}'
    print(f'centro_id: {centro_id}')
    try:
        fallas_activas = obtener_fallas_activas(client, SERVICE, centro_id)
        print( f"Se han enviado correctamente las fallas al FE")
        return jsonify(fallas_activas), 200
    except Exception as e:
        print(f"No se han enviado correctamente las fallas por error en linea {e.__traceback__.tb_lineno}: \n{e}")
        return jsonify({"mensaje": f" No se han enviado correctamente las fallas al FE "}), 401
    
@api_bp.route( '/<SERVICE>/revision_falla', methods=['POST'])
def revisar_falla_ITGSM(SERVICE): #,CENTRO_ID
    # print(f'datos_desde_RICHI: {request.json} \n')
    client = get_db()
    print("REVISANDO FALLAS****-*-*-*-")
    fallas_revisada = request.json
    print(f'datos_falla_revisada: {fallas_revisada} \n')
    if fallas_revisada:
        data_fallas_revisadas = fallas_revisada['data']
        centro_id = fallas_revisada['centro']

        if data_fallas_revisadas:
            # centro_id = fallas_revisada['centro']
            ids_dict = {'no_revisados':[], 'revisados':[]}
            # ids_revisados =[]
            # ids_no_revisados =[]

            for falla in data_fallas_revisadas:
                idx = falla['_id']
                fecha_fin = falla['fin_estado']
                print("Variables en falla:")
                print(SERVICE, centro_id, idx, 'local', fecha_fin)
                try:
                    actualizar_revision_falla(client, SERVICE, centro_id, idx, 'local', fecha_fin)
                    actualizar_consolidado_fallas_revisadas(client, SERVICE,  idx)
                    ids_dict["revisados"].append(idx)
                except Exception:
                    print("No se ha podido revisar la alerta con _id {idx}")
                    ids_dict["no_revisados"].append(idx)

            if ids_dict:
                print(f"fallas {ids_dict=}")
                return jsonify({
                    "mensaje": "Las alertas revisadas en SM son",
                    "data": ids_dict  # Devuelve un diccionario con los ids que se revisaron y los que no
                }), 200
            else:
                return jsonify({"mensaje": "No se han podido revisar las alertas enviadas"}), 401
        else:
            return jsonify({"mensaje": "Se enviaron revisiones de alertas vacias"}), 204
    else:
        return jsonify({"mensaje": "No se han enviado datos"}), 204

    #             try:
    #                 actualizar_revision_falla(SERVICE, centro_id, idx, 'local', fecha_fin)
    #                 # actualizar_consolidado_fallas_revisadas(SERVICE, idx) #idx
    #                 actualizar_consolidado_fallas_revisadas(SERVICE,  idx) #centro_id,
    #                 return jsonify({"mensaje": f"Se han enviado correctamente las fallas "}), 200
    #             except Exception as e:
    #                 print(f"No se han enviado correctamente las fallas por error en linea {e.__traceback__.tb_lineno}: \n{e}")
    #                 return jsonify({"mensaje": f" No se han recibido correctamente las fallas "}), 401
    #     else:
    #         return jsonify({"mensaje": f"Se enviaron revisiones de fallas vacias"}), 204
    # else:
    #     return jsonify({"mensaje": f"No se han enviado datos"}), 204





def guardar_generadores_metadata(client,  payload, collection_name="metadata_generadores"):
    """
    Guarda la metadata de generadores en Mongo.
    Cada documento mantiene: generator_name, quadrants, centro_id.
    
    payload ejemplo:
    {
      "centro": "Fonchi-Richi-Salva",
      "data": {
        "generator_1": {"quadrants": ["quadrant_1", "quadrant_2"]},
        "generator_2": {"quadrants": ["quadrant_3"]}
      }
    }
    """

    centro_id = payload.get("centro")
    generators = payload.get("data", {})

    if not centro_id or not generators:
        print(" Datos inválidos, no se guardó nada.")
        return
    nanoox_db = client[NANOOX_DATABASE_NAME]
    center_name = centro_id.split("-")[-1]
    col = nanoox_db[f"{center_name}_{collection_name}"]
    operaciones = []

    for gen_name, gen_info in generators.items():
        quadrants = gen_info.get("quadrants", [])

        if gen_name == "0":
            print("bug conocido: Nombre de generador invalido ")
            continue
        
        # UpdateOne con upsert para actualizar si existe o crear nuevo
        operaciones.append(
            UpdateOne(
                {"generator_name": gen_name, "centro_id": centro_id},
                {"$set": {
                    "generator_name": gen_name,
                    "quadrants": quadrants,
                    "centro_id": centro_id
                }},
                upsert=True
            )
        )

    if operaciones:
        result = col.bulk_write(operaciones)
        print(f"✅ Generadores guardados: {result.modified_count} modificados, {result.upserted_count} insertados.")



@api_bp.route( '/<SERVICE>/post_generadores', methods=['POST'])
def post_generadores_desde_local(SERVICE):
    print("=============== ENTRANDO A RECIBIR GENERADORES ===============")

    generators_received_data = request.json
    print(f'{generators_received_data=}')

    centro_id = generators_received_data.get("centro")
    generators_data = generators_received_data.get("data")

    print(f"{generators_data=}")

    client = get_db()
    
    guardar_generadores_metadata(client, payload= generators_received_data)

    return "OK", 200
    

@api_bp.route( '/<SERVICE>/post_umbrales', methods=['POST'])
def post_umbrales_desde_local(SERVICE):
    # print("=============== ENTRANDO A RECIBIR UMBRALES ===============")
    service = SERVICE.upper()
    umbrales = request.json
    client = get_db()

    flag_new_version = "control_version" in umbrales

    if flag_new_version:
        print("ES VERSION ITG ANEC")


    if service =="ENVIRO":

        umbrales = request.json
        if umbrales != []:
            pprint(umbrales)
            guardar_umbrales_activos(client, service, umbrales)
            return jsonify({"mensaje": f"Se han enviado correctamente los umbrales "}), 200
        else:
            print(f'No se han recibido datos de umbrales')
            return jsonify({"mensaje": f" No se han enviado correctamente los umbrales a ITGSM"}), 401
        

        
    elif service =="NANOOX":
        print(f"Post_umbrales Nanoox {umbrales=}\n")
        if umbrales:
            guardar_umbrales_nanoox(client, umbrales, new_version = flag_new_version)
            return jsonify({"mensaje": f"Se han enviado correctamente los umbrales "}), 200
        else:
            print(f'No se han recibido datos de umbrales')
            return jsonify({"mensaje": f" No se han enviado correctamente los umbrales a ITGSM"}), 401
    # elif service =="SCAPP":
    #     print(f"Post_umbrales Scapp {umbrales=}\n")
    #     if umbrales:
    #         guardar_umbrales_scapp(client, umbrales)
    #         return jsonify({"mensaje": f"Se han enviado correctamente los umbrales "}), 200
    #     else:
    #         print(f'No se han recibido datos de umbrales Scapp')
    #         return jsonify({"mensaje": f" No se han enviado correctamente los umbrales a ITGSM"}), 401       

@api_bp.route( '/<SERVICE>/def_umbrales_region', methods=['POST'])
@jwt_required()
def post_umbrales_desde_scapp_local(SERVICE):
    username = get_jwt_identity()
    service = SERVICE.upper()
    umbrales = request.json
    

    if service =="SCAPP":
        print(f"Post_umbrales Scapp {umbrales=}\n")
        if umbrales:
            client = get_db()
            guardar_umbrales_scapp(client, username, umbrales)
            return jsonify({"mensaje": f"Se han enviado correctamente los umbrales "}), 200
        else:
            print(f'No se han recibido datos de umbrales Scapp')
            return jsonify({"mensaje": f" No se han enviado correctamente los umbrales a ITGSM"}), 401     


@api_bp.route('/<SERVICE>/obtener_umbrales_actuales/<CENTRO>/<AREA>/<REGION>', methods=['GET'])
def obtener_umbrales_fe(SERVICE, CENTRO, AREA, REGION):
    CENTRO = CENTRO.capitalize()
    AREA = AREA.capitalize()
    REGION = REGION.capitalize()
    centro_id = f'{CENTRO}-{AREA}-{REGION}'
    client = get_db()
    nanoox_db = client[NANOOX_DATABASE_NAME]
    scapp_db = client[SCAPP_DATABASE_NAME]

    # print(f'{centro_id=}')
    if SERVICE == "ENVIRO":
        umbrales = enviar_umbrales_formateados_a_fe(client, SERVICE, centro_id)
        return jsonify(umbrales), 200
    elif SERVICE == "NANOOX":
        centro_id = convertir_mal_menor(centro_id)
        umbrales = get_umbrales_nanoox(nanoox_db, centro_id, new_version = True)
        return jsonify(umbrales), 200
    elif SERVICE == "SCAPP":
        centro_id = convertir_mal_menor(centro_id)
        umbrales = get_umbrales_scapp(scapp_db, centro_id)
        return jsonify(umbrales), 200
#Post notas transparente a servicios
@api_bp.route( '/<SERVICE>/post_notas/<CENTRO_ID>', methods=['POST'])
def post_notas(SERVICE, CENTRO_ID):
    client = get_db()
    service = SERVICE
    notas = request.json
    centro_id = convertir_mal_menor(CENTRO_ID)
    try:
        if notas != []:
            guardar_notas(client, service,centro_id, notas)
            return jsonify({"mensaje": f"Se han recibido datos de las notas del servicio {service}"}), 200
        else:
            print(f'No se han recibido datos de notas')
            return jsonify({"mensaje": f" No se han recibido datos de notas del servicio {service}"}), 401
    except Exception as e:
        print(f"No se han enviado correctamente las notas por error en linea {e.__traceback__.tb_lineno}: \n{e}")
        return jsonify({"mensaje": f" No se han enviado correctamente las notas a ITGSM "}), 401

#------------ESTADOS SERVICIOS--------------------#
@api_bp.route('/<SERVICE>/post_estado_servicio_por_centro/<CENTRO>/<AREA>/<REGION>', methods=['POST'])
# @api_bp.route('/post_estado_servicio_por_centro/<CENTRO>/<AREA>/<REGION>', methods=['POST'])
def recepcion_estado_centro(SERVICE, CENTRO,AREA,REGION):
    estado_centro = request.json
    CENTRO = CENTRO.capitalize()
    AREA = AREA.capitalize()
    REGION = REGION.capitalize()
    centro_id = f'{CENTRO}-{AREA}-{REGION}'
    print(f'centro_id_post_estado_desde_local: {centro_id}')
    client = get_db()

    try:
        actualizar_estado_servicio(client, centro_id.lower(), estado_centro)
        print(f'Se ha recibido correctamente el estado de servicio del centro {centro_id} para el servicio {SERVICE}')
        return jsonify({"mensaje": f"Se ha recibido correctamente el estado de servicio del centro {centro_id} para el servicio {SERVICE} "}), 200
    except Exception as e:
        print(f"No Se ha recibido correctamente el estado de servicio del centro {centro_id} para el servicio {SERVICE} por error en linea {e.__traceback__.tb_lineno}: \n{e}")
        return jsonify({"mensaje": f" No Se ha recibido correctamente el estado de servicio del centro {centro_id} para el servicio {SERVICE} "}), 401 


#NEW_FEATURE
@api_bp.route('/estado_servicio_2/<AREA>/<REGION>', methods=['GET'])
@jwt_required()
def estado_servicio(AREA, REGION):
    """
    Endpoint principal. Orquesta la obtención de servicios, estados y permisos.
    """
    try:
        username = get_jwt_identity()
    except Exception as e:
        print(f"Error auth: {e}")
        # En producción, maneja esto según tu política de seguridad
        # username = "itg7" # Fallback solo para dev/demo si falla JWT

    client = get_db()
    
    try:
        # 1. Obtener Jerarquía de Permisos
        servicios_usuario = obtener_servicios_por_region_y_area(client, username)
        estructura_base = agrupar_por_region_area(servicios_usuario)
        
        # 2. Obtener Datos de Estado (Aquí ocurre la lógica de Flux para todos)
        estados_actuales = estados_servicios_region_area_2(client, servicios_usuario)
        
        # 3. Consolidar
        estados_consolidados = agregar_estado_centros_a_consolidado(estructura_base, estados_actuales)
        
        # 4. Aplicar Permisos de Usuario
        permisos_usuario = procesar_permisos_de_usuario(client, username)
        respuesta = update_status_service(estados_consolidados, permisos_usuario)
        print(f"{respuesta=}")
        print()
        return jsonify(respuesta), 200

    except Exception as e:
        import traceback
        print(f"Error en estado_servicio: {e}")
        traceback.print_exc()
        return jsonify({"mensaje": f"Error interno: {str(e)}"}), 500

@api_bp.route('/estado_servicio_ios', methods=['GET'])
@jwt_required()
def estado_servicio_ios():
    """
    Endpoint principal iOS. Orquesta la obtención de servicios, estados y permisos.
    """
    try:
        username = get_jwt_identity()
    except Exception as e:
        print(f"Error auth: {e}")
        # En producción, maneja esto según tu política de seguridad
        # username = "itg7" # Fallback solo para dev/demo si falla JWT

    client = get_db()
    
    try:
        # 1. Obtener Jerarquía de Permisos
        servicios_usuario = obtener_servicios_por_region_y_area(client, username)
        estructura_base = agrupar_por_region_area(servicios_usuario)
        
        # 2. Obtener Datos de Estado (Aquí ocurre la lógica de Flux para todos)
        estados_actuales = estados_servicios_region_area_2(client, servicios_usuario)
        
        # 3. Consolidar
        estados_consolidados = agregar_estado_centros_a_consolidado(estructura_base, estados_actuales)
        
        # 4. Aplicar Permisos de Usuario
        permisos_usuario = procesar_permisos_de_usuario(client, username)
        respuesta = update_status_service(estados_consolidados, permisos_usuario)
        print(f"{respuesta=}")
        print()
        return jsonify(respuesta), 200

    except Exception as e:
        import traceback
        print(f"Error en estado_servicio: {e}")
        traceback.print_exc()
        return jsonify({"mensaje": f"Error interno: {str(e)}"}), 500


@api_bp.route('/<SERVICE>/estado_centro/<CENTRO>/<AREA>/<REGION>', methods=['GET'])
def get_estado_centro(SERVICE, CENTRO, AREA, REGION):
    CENTRO = CENTRO.capitalize()
    AREA = AREA.capitalize()
    REGION = REGION.capitalize()
    centro_id = f'{CENTRO}-{AREA}-{REGION}'
    print(f'centro_id: {centro_id}')
    client = get_db()
    if SERVICE == 'ENVIRO':
        try:
            estado_servicio_enviro = estado_enviro(client, centro_id)
            estado_servicio_nanoox = None
            print(f'estado_enviro:{estado_servicio_enviro}')
            print(f'estado_centro: {estado_centro(estado_servicio_enviro, estado_servicio_nanoox)}')
            estado = estado_centro(estado_servicio_enviro, estado_servicio_nanoox)
            return jsonify(estado), 200
        except Exception as e:
            print(f"Error solicitando estado centro {centro_id} en linea {e.__traceback__.tb_lineno}: \n{e}")
            return jsonify({"mensaje": f"Error solicitando estado centro {centro_id}"}), 401


@api_bp.route('/<PAGE>/consolidado_estados', methods=['GET'])
@jwt_required()
def consolidado_estados_por_empresa(PAGE: str):
    """
    Envia JSON con estados consolidados de centros, areas y regiones.
    """
    print(f"---------PAGE RECIBIDA-------: {PAGE} \n")
    try:
        client = get_db()
        username = get_jwt_identity()
        consolidado = consolidado_empresa(client, username, PAGE)
        print(f'consolidado_estados new: {consolidado}')
        return jsonify(consolidado), 200
    except Exception as e:
        # app.logger.error(f"Error en consolidado_estados: {e}", exc_info=True)
        print(f"Error obteniendo consolidado estados en linea {e.__traceback__.tb_lineno}: \n{e}")
        return jsonify({"mensaje": "Error obteniendo consolidado estados"}), 500

#------------------------------------------

@api_bp.route('/<SERVICE>/post_estado_variables/<CENTRO>/<AREA>/<REGION>', methods=['POST'])
# @api_bp.route('/post_estado_servicio_por_centro/<CENTRO>/<AREA>/<REGION>', methods=['POST'])
def recepcion_estado_variables(SERVICE, CENTRO,AREA,REGION):
    json = request.json
    CENTRO = CENTRO.capitalize()
    AREA = AREA.capitalize()
    REGION = REGION.capitalize()
    centro_id = f'{CENTRO}-{AREA}-{REGION}'
    print(f'centro_id_post_estado_variables_desde_local: {centro_id}')
    client = get_db()
    try:
        actualizar_estado_variables(client, centro_id, json)
        print(f'Se ha recibido correctamente el estado de servicio del centro {centro_id} para el servicio {SERVICE}')
        return jsonify({"mensaje": f"Se ha recibido correctamente el estado de servicio del centro {centro_id} para el servicio {SERVICE} "}), 200
    except Exception as e:
        print(f"No Se ha recibido correctamente el estado de servicio del centro {centro_id} para el servicio {SERVICE} por error en linea {e.__traceback__.tb_lineno}: \n{e}")
        return jsonify({"mensaje": f" No Se ha recibido correctamente el estado de servicio del centro {centro_id} para el servicio {SERVICE} "}), 401 

@api_bp.route('/<SERVICE>/estados_variables_actuales/<CENTRO>/<AREA>/<REGION>', methods=['GET'])
def get_var_state(SERVICE,CENTRO,AREA,REGION):
    CENTRO = CENTRO.capitalize()
    AREA = AREA.capitalize()
    REGION = REGION.capitalize()
    centro_id = f'{CENTRO}-{AREA}-{REGION}'
    client = get_db()
    col_enviro_variables = client[ENVIRO_DATABASE_NAME].variables
    # Consulta con proyeccion para devolver solo los campos "atencion", "critica" y "emails"
    resultado = col_enviro_variables.find(
        {'centro_id': centro_id},
        {
            '_id': 0,  # Excluir el campo _id
            'atencion': 1,
            'critica': 1,
            'centro_id': 1,
            'fecha': 1
        }
    )
    # Convertir el cursor a una lista de documentos
    try:
        documentos = list(resultado)
        # print('{documentos=}')
        if documentos:
            documentos = documentos[0]
            print( f"Se han enviado correctamente las variables en mal estado hacia el FE")
            return jsonify(datos=documentos), 200
    except Exception as e:
        print(f"No se han enviado correctamente las variables en mal estado por error en linea {e.__traceback__.tb_lineno}: \n{e}")
        return jsonify({"mensaje": f" No se han enviado correctamente las variables en mal estado hacia el FE "}), 401

@api_bp.route('/<SERVICE>/tipo_control/<CENTRO>/<AREA>/<REGION>', methods=['GET'] )
def get_tipo_control(SERVICE, CENTRO,AREA,REGION):
    CENTRO = CENTRO.capitalize()
    AREA = AREA.capitalize()
    REGION = REGION.capitalize()
    centro_id = f'{CENTRO}-{AREA}-{REGION}'
    try:
        client = get_db()
        nanoox_db = client[NANOOX_DATABASE_NAME]
        tipo_control = get_control_by_centro_id(nanoox_db, centro_id)
        print(f"Se ha obtenido correctamente el tipo de control del centro {centro_id} ")
        return jsonify(datos=tipo_control), 200
    except Exception as e:
        print(f"No se ha obtenido correctamente el tipo de control del centro {centro_id} por error en linea {e.__traceback__.tb_lineno}: \n{e}")
        return jsonify({"mensaje": f" No se ha enviado correctamente el tipo de control del centro {centro_id} hacia el FE "}), 401

##RUTAS Y FUNCIONES PARA ANALiTICA

#Recepcion de datos desde remotos-Scapp
@api_bp.route('/scapp/mirror_analytics/<REGION>/<AREA>/<CENTRO>', methods=['POST'] )
def scapp_analytics_saving(CENTRO,AREA,REGION):
    """
    Recepcion de datos o coleccion de pendientes desde locales Scapp
    Input Format:
    {{"timestamp": {
        "$date": "2025-05-13T11:24:00.000Z"
    },
    "camera_id": "4-A",  # ID de la camara o sensor origen de los datos

    "data": {
        "name": "4",  # Nombre o ID interno de la camara

        "activity_data": [  # Lista cronologica de mediciones de actividad
        {
            "time": "2025-04-28T07:45:00.999000",  # Timestamp (ISO 8601)
            "value": 54.85                         # Valor de actividad (float)
        },
        ...
        ],

        "alarms_by_tasa": {  # Conteo de alarmas por nivel de tasa
        "baja": 0,
        "media": 0,
        "alta": 0
        },

        "alarms_by_tasa_levels": {  # Umbrales utilizados para clasificar las tasas
        "baja": [0, 8],
        "media": [12],
        "alta": [22.11]
        },

        "alarms_data": [],  # Lista de alarmas detalladas (actualmente vacia)

        "feeding_time_range": {  # Intervalo detectado de alimentacion
        "inicio": "2025-04-28T11:26:42.882000",
        "final": "2025-04-28T14:07:17.477000"
        },

        "tasa_data": [  # Serie temporal de valores de tasa
        {
            "time": "2025-04-28T07:30:00.999000+00:00",
            "value": 0
        },
        ...
        ],

        "tasa_values": [  # Conjunto de valores unicos de tasa observados
        0, 8, 22.11, 12
        ],

        "values_summary": {  # Resumen estadistico del proceso
        "alarms_by_ton": 0,        # Conteo de alarmas por tonelada
        "alarms_count": 0,         # Total de alarmas detectadas
        "alarms_threshold": 3,     # Umbral minimo para considerar una alarma
        "delivered_food": 3123.34, # Cantidad total de comida entregada (kg)
        "fasts_count": 0,          # Numero de ayunos detectados
        "perdidos_count": 0,        # Conteo de eventos perdidos
        "sfr_value": 57,           # Valor SFR (indicador especifico del sistema)
        "tasa_units": "kg/min",    # Unidades usadas para tasa
        "visitas_count": 1          # Numero de visitas detectadas
        }
    },

    "_id": {
        "$oid": ""  # ID unico del documento en MongoDB
    }}
    """
    t1 = datetime.now()
    print(f"ENTRANDO A MIRROR ANALYTICS \n")
    
    raw_centro = (REGION, AREA, CENTRO)
    recibido = datetime.now().astimezone()
    CENTRO = CENTRO.capitalize()
    AREA = AREA.capitalize()
    REGION = REGION.capitalize()
    centro_id = f'{REGION}-{AREA}-{CENTRO}'
    received_json = request.json
    # print(f"{received_json=} \n")
    # raw_json = json.dumps(received_json)
    # proccess_json = json_util.loads(json.dumps(received_json))
    received_json = request.json
    # proccess_json = received_json
    proccess_json = json.loads(received_json, object_hook=json_util.object_hook)
    if proccess_json:
        print(f"JSON recibido desde {centro_id}")
    
    
    # print(f"{proccess_json=} \n")
    
    try:
        client = get_db()
        scapp_db = client[SCAPP_DATABASE_NAME]

        bulk_scapp_analytics(scapp_db, proccess_json, centro_id)
        encolar_dato_recibido("scapp", raw_centro, centro_id, recibido)
        
        print(f"Se ha guardado exitosamente la analitica Scapp del centro {centro_id}")
        t_end = datetime.now()
        print(f"Datos de Analitica guardados en {(t_end-t1).total_seconds()*1e3:.4f}ms.")
        return jsonify({"mensaje": "Datos de analitica Scapp recibidos exitosamente por servidor"}), 200
    except Exception as e:
        print(f"No se ha guardado correctamente a analitica Scapp del centro {centro_id} por error en linea {e.__traceback__.tb_lineno}: \n{e}")
        t_end = datetime.now()
        print(f"Datos de Analitica procesados en {(t_end-t1).total_seconds()*1e3:.4f}ms.")
        return jsonify({"mensaje": "Error interno guardando datos de analitica"}), 500


def bulk_scapp_analytics(scapp_db, received_json, centro_id):
    """
    Acepta received_json como dict o lista de dicts.
    Para cada documento normaliza el timestamp al inicio del día y reemplaza
    el registro correspondiente a centro_id + camera_id + día.
    """
    coll = ensure_analytic_collection(scapp_db)

    # Normalizamos a lista
    print(f"{type(received_json)=}")
    if isinstance(received_json, dict):
        docs = [received_json]
    elif isinstance(received_json, list):
        docs = received_json
    else:
        raise ValueError("received_json debe ser dict o lista de dicts")

    ops = []

    #Timestamp para hora de actualizacion en FE
    updated_ts = datetime.now()

    for doc_in in docs:
        print(f"{type(doc_in)=} ")
        print(f"{doc_in.keys()=}")        
        ts = doc_in.get("timestamp")
        cam = doc_in.get("camera_id")

        # Validacion de timestamp
        if not isinstance(ts, datetime):
            try:
                ts = datetime.fromisoformat(ts)
            except Exception as e:
                raise ValueError(f"timestamp inválido: {ts!r}") from e

        if cam is None:
            raise ValueError("Falta 'camera_id' en alguno de los documentos")

        # Determinar rango de dia
        tz = ts.tzinfo or timezone.utc
        start_day = datetime(ts.year, ts.month, ts.day, tzinfo=tz)

        filtro = {
            "centro_id": centro_id,
            "camera_id": cam,
            "timestamp": start_day,
        }

        new_doc = {
            **{k: v for k, v in doc_in.items() if k != "_id"},
            "centro_id": centro_id,
            "timestamp": start_day,
            "updated": updated_ts 
        }
        ops.append(ReplaceOne(filtro, new_doc, upsert=True))

    if ops:
        result = coll.bulk_write(ops, ordered=True)
        print(
            f"centro: {centro_id} -> documentos recibidos: {len(ops)}, "
            f"modificados: {result.modified_count}, insertados: {result.upserted_count}"
        )


@api_bp.route('/scapp/mirror_notas/<REGION>/<AREA>/<CENTRO>', methods=['POST'])
def scapp_notes_saving(CENTRO, AREA, REGION):
    REGION = REGION.capitalize()
    AREA = AREA.capitalize()
    CENTRO = CENTRO.capitalize()
    centro_id = build_centro_id(REGION, AREA, CENTRO)

    if request.content_length is not None and request.content_length > MAX_NOTES_REQUEST_BYTES:
        return jsonify({"mensaje": "Payload de notas excede el tamaño permitido"}), 413

    try:
        received_json = request.json
        if isinstance(received_json, str):
            received_json = json.loads(received_json, object_hook=json_util.object_hook)
        docs = validate_and_normalize_notes_payload(received_json)
    except (ValueError, TypeError, json.JSONDecodeError) as e:
        print(f"Payload de notas invalido de {centro_id}: {e}")
        return jsonify({"mensaje": "Notas recibidas con sintaxis erronea", "detail": str(e)}), 400

    try:
        client = get_db()
        scapp_db = client[SCAPP_DATABASE_NAME]
        saved = save_notes_payload(scapp_db, docs, centro_id)
        print(f"Notas guardadas exitosamente para {centro_id}: {saved}.")
        return jsonify({"mensaje": "Notas Scapp recibidas exitosamente por servidor", "saved": saved}), 200
    except Exception as e:
        print(f"Error guardando notas de {centro_id} linea {e.__traceback__.tb_lineno}: {e}")
        return jsonify({"mensaje": "Notas recibidas erroneamente por servidor"}), 500


@api_bp.route('/scapp/mirror_feeder_score/<REGION>/<AREA>/<CENTRO>', methods=['POST'])
def scapp_feeder_score_saving(CENTRO, AREA, REGION):
    """
    Recepcion de datos FeederScore desde locales Scapp.
    El cliente actual envia JSON doble-serializado: request.json es un string con la lista.
    """
    t1 = datetime.now()
    REGION = REGION.capitalize()
    AREA = AREA.capitalize()
    CENTRO = CENTRO.capitalize()
    centro_id = build_centro_id(REGION, AREA, CENTRO)

    try:
        received_json = request.json
        if isinstance(received_json, str):
            received_json = json.loads(received_json, object_hook=json_util.object_hook)
        docs = validate_and_normalize_feeder_score_payload(received_json)
    except (ValueError, TypeError, json.JSONDecodeError) as e:
        print(f"FeederScore payload invalido de {centro_id}: {e}")
        return jsonify({"mensaje": "Datos recibidos con sintaxis erronea", "detail": str(e)}), 400

    try:
        client = get_db()
        scapp_db = client[SCAPP_DATABASE_NAME]
        save_feeder_score_payload(scapp_db, docs, centro_id)
        print(f"FeederScore guardado exitosamente para {centro_id} ({(datetime.now()-t1).total_seconds()*1e3:.2f}ms).")
        return jsonify({"mensaje": "Datos de feeder score Scapp recibidos exitosamente por servidor"}), 200
    except Exception as e:
        print(f"Error guardando FeederScore de {centro_id} linea {e.__traceback__.tb_lineno}: {e}")
        return jsonify({"mensaje": "Datos recibidos erroneamente por servidor"}), 401


# Ultima hora de dato recibido desde Centro SCAPP
@api_bp.route('/scapp/last_received_data', methods=['GET'])
def last_received_data():
    print("Entrando a SCAPP-LRD")
    centro_id= None
    try:
        # Leer centro_id desde la query string
        centro_id = request.args.get('centro_id')
        centro_id=capitalizar_centro_id(centro_id)
        print(f"{request.args=}")
        # print(f"LRD out: {centro_id=}")
    except Exception as e:
        print( f"{e.__traceback__.tb_lineno}: \n{e}")

    if not centro_id:
        print('error:' 'Falta el parametro centro_id')
        return jsonify({'error': 'Falta el parametro centro_id'}), 400

    # calculamos inicio de hoy
    start_today = get_start_of_today()
    try:
        client = get_db()
        analytic_coll = client[SCAPP_DATABASE_NAME]['data_analytic']
        # buscamos el ultimo timestamp de hoy para ese centro
        ts = fetch_last_timestamp_for_centro(analytic_coll, centro_id, start_today)
        print(f"LRD: {ts=}")
        # si no hay para hoy, fallback al global
        if ts is None:
            ts = fetch_last_timestamp_overall(analytic_coll, centro_id, None )
            if ts is None:
                return jsonify({
                    'fecha': "Sin registros",
                    'online': False,
                    'upcoming': None
                }), 206
                # return jsonify({'error': 'No se encontraron datos'}), 404
        
        # determinamos si el timestamp cae en la fecha de hoy
        online = (ts.date() == datetime.now().date())

        # si esta online, agregamos 'upcoming' = updated + 1h
        if online:
            upcoming_ts = ts + timedelta(hours=1)
            upcoming_ts = upcoming_ts.isoformat()
        else:
            upcoming_ts = None
        print(f"SCAPP-LRD Data generada")

        return jsonify({
            'fecha': ts.isoformat(),
            'online': online,
            'upcoming': upcoming_ts
        }), 200
    except Exception as e:
        print(f"No se ha obtenido el ultimo dato Scapp del centro {centro_id} por error en linea {e.__traceback__.tb_lineno}: \n{e}")
        return jsonify({"Error": "Datos no encontrados por servidor"}), 500


@api_bp.route('/scapp/last_received_feeder_score', methods=['GET'])
def last_received_feeder_score():
    centro_id_param = request.args.get('centro_id')
    if not centro_id_param:
        return jsonify({'error': 'Falta el parametro centro_id'}), 400

    try:
        centro_id = normalize_centro_id(centro_id_param)
    except ValueError as e:
        return jsonify({'error': str(e)}), 400

    try:
        client = get_db()
        scapp_db = client[SCAPP_DATABASE_NAME]
        received_at = get_last_feeder_score_received_at(scapp_db, centro_id)
        if received_at is None:
            return jsonify({
                'fecha': "Sin registros",
                'online': False,
                'upcoming': None
            }), 206

        online = received_at >= get_start_of_today()
        upcoming_ts = (received_at + timedelta(hours=1)).isoformat() if online else None
        return jsonify({
            'fecha': received_at.isoformat(),
            'online': online,
            'upcoming': upcoming_ts
        }), 200
    except Exception as e:
        print(f"No se ha obtenido el ultimo FeederScore del centro {centro_id_param} por error en linea {e.__traceback__.tb_lineno}: \n{e}")
        return jsonify({"Error": "Datos no encontrados por servidor"}), 500

# —–– Funcion auxiliar: inicio del dia sin pytz —––––––––––––––
def get_start_of_today() -> datetime:
    """
    Devuelve un datetime (naive) con la fecha de hoy a las 00:00
    segun el reloj del sistema.
    """
    now = datetime.now()
    return datetime(year=now.year, month=now.month, day=now.day)

# —–– Funcion auxiliar: maximo timestamp para un centro_id —––––
def fetch_last_timestamp_for_centro(collection ,centro_id: str, start_time: datetime):
    """
    Retorna el timestamp mas reciente para el centro_id desde start_time.
    """
    pipeline = [
        {'$match': {
            'centro_id': centro_id,
            'updated': {'$gte': start_time}
        }},
        {'$sort':  {'updated': -1}},
        {'$limit': 1},
        {'$project': {'_id': 0, 'updated': 1}}
    ]
    docs = list(collection.aggregate(pipeline))
    print(f"LRD: {docs=}")
    return docs[0]['updated'] if docs else None

# —–– Funcion auxiliar: fallback al mas reciente global —–––––––
def fetch_last_timestamp_overall(collection, centro_id: str, start_time: datetime = None):
    """
    Si start_time esta dado, filtra >= start_time; si no, toma el mas reciente en toda la coleccion.
    """
    query = {
        'centro_id': centro_id,
        'updated': {'$exists': True}
    }
    # Si piden desde un tiempo de corte, lo añadimos al match
    if start_time is not None:
        query['updated']['$gte'] = start_time

    cursor = collection.find(query).sort('updated', -1).limit(1)
    doc = next(cursor, None)
    return doc['updated'] if doc else None


def _fetch_scapp_notes(scapp_db, start_day, day_ranges, *, centro_id=None, scope_prefix=None):
    return fetch_notes_for_range(
        scapp_db[NOTES_COLLECTION_NAME],
        start_day,
        start_day + timedelta(days=day_ranges),
        centro_id=centro_id,
        scope_prefix=scope_prefix,
    )


def _analytics_notes_response(analytic_data, notes):
    return {
        "analytic_data": analytic_data or {},
        "notes": notes,
    }


@api_bp.route('/scapp/get_notas_center_data/<REGION>/<AREA>/<CENTRO>/<START_DAY>/<DAY_RANGES>', methods=['GET'])
def scapp_get_notes_center_data(CENTRO, AREA, REGION, START_DAY, DAY_RANGES):
    """Consulta independiente para integraciones; el frontend usa la respuesta analítica unificada."""
    start_day = validar_y_convertir_fecha(START_DAY)
    day_ranges = validar_y_convertir_dias_desde(DAY_RANGES)
    if not start_day:
        return jsonify({"Error": f"Parametros de dias en formato no valido-Se esperaba 'DD-MM-YYYY' y llego {START_DAY}"}), 400
    if not day_ranges:
        return jsonify({"Error": f"Parametros de rango de dias en formato no valido: {DAY_RANGES}"}), 400

    centro_id = build_centro_id(REGION, AREA, CENTRO)
    try:
        client = get_db()
        scapp_db = client[SCAPP_DATABASE_NAME]
        notes = _fetch_scapp_notes(scapp_db, start_day, day_ranges, centro_id=centro_id)
        return jsonify({"notes": notes}), 200
    except Exception as e:
        print(f"Error consultando notas de {centro_id} linea {e.__traceback__.tb_lineno}: {e}")
        return jsonify({"Error": "Notas no encontradas por servidor"}), 500


## ANALITICA POR CENTRO ##

@api_bp.route('/scapp/get_analytic_center_data/<REGION>/<AREA>/<CENTRO>/<START_DAY>/<DAY_RANGES>', methods=['GET'] )
def scapp_get_analytic_center_data(CENTRO,AREA,REGION, START_DAY, DAY_RANGES):
    """
    Ruta de comunicacion con FE para obtener datos de analitica por centro y por dia, a partir de una fecha inicial y un rango de dias desde esa fecha
    TODO: Insertar ejemplo formato de entrada y salida
    
    """
    CENTRO = CENTRO.capitalize()
    AREA = AREA.capitalize()
    REGION = REGION.capitalize()
    fecha_str = START_DAY
    ranges_str = DAY_RANGES
    print(f"LDRd {fecha_str=}")
    print(f"LDRd {ranges_str=}")
    start_day = validar_y_convertir_fecha(fecha_str)
    day_ranges = validar_y_convertir_dias_desde(ranges_str)
    # print(f"LDRd {start_day=}")
    # print(f"LDRd {day_ranges=}")
    if not start_day:
        return jsonify({"Error": f"Parametros de dias en formato no valido-Se esperaba 'DD-MM-YYYY' y llego {fecha_str}"}), 400
    elif not day_ranges:
        return jsonify({"Error": f"Parametros de rango de dias en formato no valido: {day_ranges}"}), 400
        
    centro_id = f'{REGION}-{AREA}-{CENTRO}'

    scapp_configs = {}
    parse_data = True
        
    try:
        client = get_db()
        scapp_db = client[SCAPP_DATABASE_NAME]

        multiple_analytic_days = compute_multiple_analytic_days( centro_id, start_day, day_ranges, scapp_configs, parse_data)
        try:
            notes = _fetch_scapp_notes(scapp_db, start_day, day_ranges, centro_id=centro_id)
        except Exception as notes_error:
            print(f"Error cargando notas para analitica de {centro_id}: {notes_error}")
            notes = []

        if multiple_analytic_days:
            print(f"Se ha generado exitosamente la analitica Scapp del centro {centro_id}")
        else:
            print(f"No hay informacion de analitica Scapp para el centro {centro_id}")
        return jsonify(_analytics_notes_response(
            multiple_analytic_days,
            notes,
        )), 200
    except Exception as e:
        print(f"No se ha generado correctamente a analitica Scapp del centro {centro_id} por error en linea {e.__traceback__.tb_lineno}: \n{e}")
        return jsonify({"Error": "Datos generados erroneamente por servidor"}), 500

@api_bp.route('/scapp_get_centers_access', methods=['GET'])
@jwt_required()
def get_analytic_centers(): #USERNAME
    """
    Valida y retorna permisos de usuario para analitca Scapp
    """
    try:
        # username = str(USERNAME)
        username = get_jwt_identity()
        client = get_db()
        users_collection = client[USER_DATABASE_NAME][USER_DATABASE_NAME]

        available_centers = scapp_analytics_centers(users_collection, username)
    except Exception as e:
        print(f"No se han obtenido correctamente los permisos de analitica Scapp del usuario {username} por error en linea {e.__traceback__.tb_lineno}: \n{e}")
        return jsonify({"Error": f"No se han obtenido correctamente los permisos del Usuario {username}"}), 401
    if available_centers:
        return jsonify({"centers": available_centers}), 200
    else:
        return jsonify({"Error": f"Usuario {username} no posee permisos visualizar datos de centros"}), 404

def validar_y_convertir_fecha(fecha_str):
    """
    Verifica que la fecha venga en el formato correcto y la convierte al formato datetime. De fallar retorna None
    """
    try:
        print(f"{fecha_str=}")
        # Intentar parsear la fecha con el formato esperado
        fecha_dt = datetime.strptime(fecha_str, "%d-%m-%Y")
        # Retornar una nueva instancia de datetime con los valores correspondientes
        return datetime(fecha_dt.year, fecha_dt.month, fecha_dt.day)
    except Exception as e:
        # Si falla el formato, retorna None o lanza un error
        print(f"Formato de fecha invalido. Se esperaba 'DD-MM-YYYY' y llego {fecha_str}: {e.__traceback__.tb_lineno}:\n{e}")
        return None

def validar_y_convertir_dias_desde(valor):
    """
    Verifica que el rango de dias venga en el formato correcto y la convierte a entero. De fallar retorna None
    """
    try:
        range_days = int(valor)
        if range_days > 0:
            return range_days
        else:
            print(f"Error: Rango= {valor} menor a 1 para rango de dias.")
            return None
    except Exception as e:
        print(f"Formato erroneo para rango de dias {type(valor)}: {e.__traceback__.tb_lineno}:\n{e}")
        return None


@api_bp.route('/scapp/compare_analytic_centers', methods=['GET'])
def compare_analytic_centers():
    """
    Ruta de comunicacion con FE para obtener datos de versus de analitica por centro y por dia, a partir de una fecha inicial y un rango de dias desde esa fecha
    
    _old: Recibe una lista de centro_id, fecha de inicio y rango y retorna los datos de analitica para multiples centros
    TODO: Insertar ejemplo formato de entrada y salida
    """
    # 1. Leer y validar parametros comunes
    centros_str = request.args.get('centros', '')
    fecha_str   = request.args.get('start_day')
    ranges_str  = request.args.get('day_ranges')
    # print(f"{centros_str=}")
    # print(f"{fecha_str=}")
    # print(f"{ranges_str=}")
    if not fecha_str or not ranges_str:
        return jsonify({"error": "Faltan parametros start_day o day_ranges"}), 400
    # 2. Parsear fecha en formato DD-MM-YYYY
    try:
        start_day  = validar_y_convertir_fecha(fecha_str)
        day_ranges = validar_y_convertir_dias_desde(ranges_str)
    except ValueError:
        return jsonify({
            "error": "El parametro start_day debe estar en formato DD-MM-YYYY, ej: 10-05-2025"
        }), 400
    if not start_day or not day_ranges:
        return jsonify({
            "error": "Los parametros start_day y day_ranges deben ser validos"
        }), 400

    # 3. Parsear lista de centros (entre 2 y 4)
    centros_list = [c.strip() for c in centros_str.split(',') if c.strip()]
    print(f"{centros_list=}")
    if len(centros_list) < 2 or len(centros_list) > 4:
        return jsonify({
            "error": "Debes especificar entre 2 y 4 centros, ej: ?centros=C1,C2,C3"
        }), 400

    # 4. Iterar y construir array de respuesta
    try:
        client         = get_db()
        scapp_db       = client[SCAPP_DATABASE_NAME]
        scapp_configs  = {}
        parse_data     = True

        response_list = []
        for centro_id in centros_list:
            # print(f"{centro_id=}")
            # print(f"{start_day=}")
            # print(f"{day_ranges=}")
            centro_consulta = centro_id
            try:
                centro_consulta = normalize_centro_id(centro_id)
            except ValueError:
                log_centros.warning("scapp.comparacion_sin_normalizar centro=%s motivo=formato_no_reconocido", centro_id)
            if centro_consulta != centro_id:
                log_centros.info("scapp.comparacion_normalizada centro=%s consulta=%s", centro_id, centro_consulta)
            datos = compute_multiple_analytic_days(
                centro_consulta, start_day, day_ranges,
                scapp_configs, parse_data
            )
            try:
                notes = _fetch_scapp_notes(
                    scapp_db,
                    start_day,
                    day_ranges,
                    centro_id=normalize_centro_id(centro_id),
                )
            except Exception as notes_error:
                print(f"Error cargando notas para comparacion de {centro_id}: {notes_error}")
                notes = []
            response_list.append({
                "name": centro_id,
                "analytic_data": datos,
                "notes": notes,
            })

        # 5. Devolver un array JSON
        return jsonify(response_list), 200

    except Exception as e:
        linea = e.__traceback__.tb_lineno
        msg   = f"Error en linea {linea}: {e}"
        print(msg)
        return jsonify({"mensaje": msg}), 500


def scapp_analytics_centers(users_collection, username):
    """
    A partir del nombre de usuario, obtiene y retorna los permisos para ese usuario
    TODO: Insertar ejemplo formato de entrada y salida
    """
    permisos_usuario =[]
    permisos_centro = get_accesos_usuario(users_collection, username)
    
    defectuosos = []
    for region in permisos_centro.keys():
        for area in permisos_centro[region]:
            for centro in permisos_centro[region][area]:
                bloque_scapp = permisos_centro[region][area][centro].get('scapp')
                if not isinstance(bloque_scapp, dict) or 'status' not in bloque_scapp:
                    defectuosos.append(centro)
                status_scapp = bloque_scapp.get('status') if isinstance(bloque_scapp, dict) else False
                if status_scapp:
                    permisos_usuario.append("-".join(reversed(centro.split("-"))).title())

    if defectuosos:
        # Antes abortaba todo el listado (KeyError -> 401); ahora se omiten y quedan registrados.
        log_centros.warning("scapp.permiso_sin_status usuario=%s centros=%s (omitidos del listado SCAPP)",
                            username, ",".join(defectuosos))
    return permisos_usuario


## ANALITICA POR AREA ##

@api_bp.route('/scapp/get_analytic_area_data/<REGION>/<AREA>/<START_DAY>/<DAY_RANGES>', methods=['GET'] )
def scapp_get_analytic_area_data(AREA, REGION, START_DAY, DAY_RANGES):
    """
    Ruta de comunicacion con FE para obtener datos de analitica por area y por dia, a partir de una fecha inicial y un rango de dias desde esa fecha
    _old: Recibe un area_id, fecha de inicio y rango de dias y retorna los datos de analitica del area solicitada por dia 

    TODO: Insertar ejemplo formato de entrada y salida
    """
    AREA = AREA.capitalize()
    REGION = REGION.capitalize()
    area_id = f'{REGION}-{AREA}'
    
    fecha_str = START_DAY
    ranges_str = DAY_RANGES
    
    start_day = validar_y_convertir_fecha(fecha_str)
    day_ranges = validar_y_convertir_dias_desde(ranges_str)
    
    # print(f"{area_id=}")
    if not start_day:
        return jsonify({"Error": f"Parametros de dias en formato no valido-Se esperaba 'DD-MM-YYYY' y llego {fecha_str}"}), 400
    elif not day_ranges:
        return jsonify({"Error": f"Parametros de rango de dias en formato no valido: {ranges_str}"}), 400
        
    
    scapp_configs = {}
    parse_data = True
        
    try:
        client = get_db()
        scapp_db = client[SCAPP_DATABASE_NAME]
        
        multiple_analytic_area_days = compute_analytic_area_data(area_id, start_day, day_ranges)
        try:
            notes = _fetch_scapp_notes(scapp_db, start_day, day_ranges, scope_prefix=area_id)
        except Exception as notes_error:
            print(f"Error cargando notas para analitica del area {area_id}: {notes_error}")
            notes = []
        if multiple_analytic_area_days:
            # print(f"{multiple_analytic_area_days=}")
            print(f"Se ha generado exitosamente la analitica Scapp del area {area_id}")
        else:
            print(f"No hay informacion de analitica Scapp para el area {area_id}")
        return jsonify(_analytics_notes_response(
            multiple_analytic_area_days,
            notes,
        )), 200
    except Exception as e:
        print(f"No se ha generado correctamente a analitica Scapp del area {area_id} por error en linea {e.__traceback__.tb_lineno}: \n{e}")
        return jsonify({"Error": "Datos generados erroneamente por servidor"}), 500

@api_bp.route('/scapp/compare_analytic_areas', methods=['GET'])
def compare_analytic_areas():
    """
    Ruta de comunicacion con FE para obtener datos de versus de analitica por por area y por dia, a partir de una fecha inicial y un rango de dias desde esa fecha
    _old: Recibe una lista de areas_id, una fecha de inicio y rango de dias. Retorna los datos de analitica para multiples areas en los dias solicitados

    TODO: Insertar ejemplo formato de entrada y salida
    """
    # 1. Leer y validar parametros comunes
    areas_str = request.args.get('areas', '')
    fecha_str   = request.args.get('start_day')
    ranges_str  = request.args.get('day_ranges')
    # print(f"{areas_str=}")
    # print(f"{fecha_str=}")
    # print(f"{ranges_str=}")
    start_day  = validar_y_convertir_fecha(fecha_str)
    day_ranges = validar_y_convertir_dias_desde(ranges_str)

    if not start_day:
        return jsonify({"Error": f"Parametros de dias en formato no valido-Se esperaba 'DD-MM-YYYY' y se recibio {fecha_str}"}), 400
    elif not day_ranges:
        return jsonify({"Error": f"Parametros de rango de dias en formato no valido: {ranges_str}"}), 400


    # 2. Parsear lista de centros (entre 2 y 4)
    areas_list = [c.strip() for c in areas_str.split(',') if c.strip()]
    # print(f"{areas_list=}")
    if len(areas_list) < 2 or len(areas_list) > 4:
        return jsonify({
            "Error": "Debes especificar entre 2 y 4 areas, ej: ?areas=A1,A2,A3"
        }), 400

    # 3. Iterar y construir array de respuesta
    try:
        client         = get_db()
        scapp_db       = client[SCAPP_DATABASE_NAME]
        scapp_configs  = {}
        parse_data     = True
        collection = scapp_db["data_analytic"]

        response_list = []
        for area_id in areas_list:
            print(f"{area_id=}")
            print(f"{start_day=}")
            print(f"{day_ranges=}")
            
            area_consulta = area_id
            try:
                area_consulta = normalize_area_id(area_id)
            except ValueError:
                log_centros.warning("scapp.comparacion_sin_normalizar area=%s motivo=formato_no_reconocido", area_id)
            if area_consulta != area_id:
                log_centros.info("scapp.comparacion_normalizada area=%s consulta=%s", area_id, area_consulta)
            datos= compute_analytic_area_versus_data(collection, area_consulta, start_day, day_ranges)
            # print(f"centro {area_id}, datos:  {datos}")
            if datos:
                print(f"Hay datos en {area_id}")
            else:
                print(f"Sin datos en {area_id}")
            try:
                notes = _fetch_scapp_notes(
                    scapp_db,
                    start_day,
                    day_ranges,
                    scope_prefix=normalize_area_id(area_id),
                )
            except Exception as notes_error:
                print(f"Error cargando notas para comparacion del area {area_id}: {notes_error}")
                notes = []
            response_list.append({
                "name": area_id,
                "analytic_data": datos,
                "notes": notes,
            })
        # print(response_list)
        # 4. Devolver un array JSON
        return jsonify(response_list), 200

    except Exception as e:
        linea = e.__traceback__.tb_lineno
        msg   = f"Error en linea {linea}: {e}"
        print(msg)
        return jsonify({"Error": msg}), 500

## ANALITICA POR REGION ##

@api_bp.route('/scapp/get_analytic_region_data/<REGION>/<START_DAY>/<DAY_RANGES>', methods=['GET'] )
# @jwt_required()
def scapp_get_analytic_region_data(REGION, START_DAY, DAY_RANGES):
    """
    Ruta de comunicacion con FE para obtener datos de analitica por region y por dia, a partir de una fecha inicial y un rango de dias desde esa fecha
    TODO: Insertar ejemplo formato de entrada y salida
    """
    # username = get_jwt_identity()
    client = get_db()

    # user_access = region_analytics_access(client, username,  )
    # if not user_access:
    #     return jsonify({"Error": f"EL usuario {username} no posee permiso para visualizar la Analítica de Región"}), 401


    REGION = REGION.capitalize()
    region_id = f'{REGION}'
    
    fecha_str = START_DAY
    ranges_str = DAY_RANGES
    
    start_day = validar_y_convertir_fecha(fecha_str)
    day_ranges = validar_y_convertir_dias_desde(ranges_str)
    

    if not start_day:
        return jsonify({"Error": f"Parametros de dias en formato no valido-Se esperaba 'DD-MM-YYYY' y se recibio {fecha_str}"}), 400
    elif not day_ranges:
        return jsonify({"Error": f"Parametros de rango de dias en formato no valido: {ranges_str}"}), 400
        
    
    scapp_configs = {}
    parse_data = True
    
    try:
        
        scapp_db = client[SCAPP_DATABASE_NAME]
        
        multiple_analytic_region_days = compute_analytics_by_region(scapp_db, region_id, start_day, day_ranges)
        try:
            notes = _fetch_scapp_notes(scapp_db, start_day, day_ranges, scope_prefix=region_id)
        except Exception as notes_error:
            print(f"Error cargando notas para analitica de la region {region_id}: {notes_error}")
            notes = []
        if multiple_analytic_region_days.get("analytic_data"):
            print(f"Se ha generado exitosamente la analitica Scapp de la Region {region_id}")
        else:
            print(f"No hay informacion de analitica Scapp de la Region {region_id}")
        return jsonify({
            **multiple_analytic_region_days,
            "notes": notes,
        }), 200
    except Exception as e:
        print(f"No se ha generado correctamente a analitica Scapp de la Region {region_id} por error en linea {e.__traceback__.tb_lineno}: \n{e}")
        return jsonify({"Error": "Datos generados erroneamente por servidor"}), 500


# AUTORIZACION FEEDERSCORE: el control de acceso ("jefe_area" o superior) se aplica SOLO en el
# frontend (visibilidad de menu + guard de vista), decision de producto 2026-07-06. Estos endpoints
# quedan intencionalmente sin auth server-side, consistente con los endpoints de analitica
# (get_analytic_*). Si a futuro se endurece, exigir aqui cargo/permiso via el mismo mecanismo.


@api_bp.route('/scapp/get_feeder_score_center_data/<REGION>/<AREA>/<CENTRO>/<START_DAY>/<DAY_RANGES>', methods=['GET'])
def scapp_get_feeder_score_center_data(CENTRO, AREA, REGION, START_DAY, DAY_RANGES):
    # Ver AUTORIZACION FEEDERSCORE arriba.
    REGION = REGION.capitalize()
    AREA = AREA.capitalize()
    CENTRO = CENTRO.capitalize()

    start_day = validar_y_convertir_fecha(START_DAY)
    day_ranges = validar_y_convertir_dias_desde(DAY_RANGES)

    if not start_day:
        return jsonify({"Error": f"Parametros de dias en formato no valido-Se esperaba 'DD-MM-YYYY' y llego {START_DAY}"}), 400
    elif not day_ranges:
        return jsonify({"Error": f"Parametros de rango de dias en formato no valido: {DAY_RANGES}"}), 400

    centro_id = build_centro_id(REGION, AREA, CENTRO)
    try:
        client = get_db()
        scapp_db = client[SCAPP_DATABASE_NAME]
        collection = scapp_db[FEEDER_SCORE_COLLECTION_NAME]
        feeder_score = fetch_feeder_score_data(
            collection,
            centro_id,
            start_day,
            start_day + timedelta(days=day_ranges),
        )
        return jsonify({"feeder_score_data": feeder_score}), 200
    except Exception as e:
        print(f"No se ha generado correctamente FeederScore del centro {centro_id} por error en linea {e.__traceback__.tb_lineno}: \n{e}")
        return jsonify({"Error": "Datos generados erroneamente por servidor"}), 500


@api_bp.route('/scapp/compare_feeder_score_centers', methods=['GET'])
def compare_feeder_score_centers():
    # Ver AUTORIZACION FEEDERSCORE arriba.
    centros_str = request.args.get('centros', '')
    fecha_str = request.args.get('start_day')
    ranges_str = request.args.get('day_ranges')

    if not fecha_str or not ranges_str:
        return jsonify({"error": "Faltan parametros start_day o day_ranges"}), 400

    start_day = validar_y_convertir_fecha(fecha_str)
    day_ranges = validar_y_convertir_dias_desde(ranges_str)
    if not start_day:
        return jsonify({"error": f"El parametro start_day debe estar en formato DD-MM-YYYY, ej: 10-05-2025 y llego {fecha_str}"}), 400
    elif not day_ranges:
        return jsonify({"error": f"Parametros de rango de dias en formato no valido: {ranges_str}"}), 400

    centros_list = [centro.strip() for centro in centros_str.split(',') if centro.strip()]
    if len(centros_list) < 2 or len(centros_list) > 4:
        return jsonify({
            "error": "Debes especificar entre 2 y 4 centros, ej: ?centros=Region-Area-C1,Region-Area-C2"
        }), 400

    try:
        normalized_centers = [normalize_centro_id(centro_id) for centro_id in centros_list]
    except ValueError as e:
        return jsonify({"error": str(e)}), 400

    try:
        client = get_db()
        scapp_db = client[SCAPP_DATABASE_NAME]
        collection = scapp_db[FEEDER_SCORE_COLLECTION_NAME]
        end_day = start_day + timedelta(days=day_ranges)
        response_list = []
        for centro_id in normalized_centers:
            response_list.append({
                "name": centro_id,
                "feeder_score_data": fetch_feeder_score_data(collection, centro_id, start_day, end_day),
            })
        return jsonify(response_list), 200
    except Exception as e:
        print(f"Error comparando FeederScore entre centros en linea {e.__traceback__.tb_lineno}: \n{e}")
        return jsonify({"mensaje": f"Error interno del servidor: {e}"}), 500


@api_bp.route('/scapp/get_feeder_score_area_data/<REGION>/<AREA>/<START_DAY>/<DAY_RANGES>', methods=['GET'])
def scapp_get_feeder_score_area_data(AREA, REGION, START_DAY, DAY_RANGES):
    # Ver AUTORIZACION FEEDERSCORE arriba.
    REGION = REGION.capitalize()
    AREA = AREA.capitalize()

    start_day = validar_y_convertir_fecha(START_DAY)
    day_ranges = validar_y_convertir_dias_desde(DAY_RANGES)

    if not start_day:
        return jsonify({"Error": f"Parametros de dias en formato no valido-Se esperaba 'DD-MM-YYYY' y llego {START_DAY}"}), 400
    elif not day_ranges:
        return jsonify({"Error": f"Parametros de rango de dias en formato no valido: {DAY_RANGES}"}), 400

    area_id = normalize_area_id(f"{REGION}-{AREA}")
    try:
        client = get_db()
        scapp_db = client[SCAPP_DATABASE_NAME]
        feeder_score = compute_feeder_score_by_area(scapp_db, area_id, start_day, day_ranges)
        return jsonify({"feeder_score_data": feeder_score}), 200
    except Exception as e:
        print(f"No se ha generado correctamente FeederScore del area {area_id} por error en linea {e.__traceback__.tb_lineno}: \n{e}")
        return jsonify({"Error": "Datos generados erroneamente por servidor"}), 500


@api_bp.route('/scapp/compare_feeder_score_areas', methods=['GET'])
def compare_feeder_score_areas():
    # Ver AUTORIZACION FEEDERSCORE arriba.
    areas_str = request.args.get('areas', '')
    fecha_str = request.args.get('start_day')
    ranges_str = request.args.get('day_ranges')

    start_day = validar_y_convertir_fecha(fecha_str)
    day_ranges = validar_y_convertir_dias_desde(ranges_str)

    if not start_day:
        return jsonify({"Error": f"Parametros de dias en formato no valido-Se esperaba 'DD-MM-YYYY' y se recibio {fecha_str}"}), 400
    elif not day_ranges:
        return jsonify({"Error": f"Parametros de rango de dias en formato no valido: {ranges_str}"}), 400

    areas_list = [area.strip() for area in areas_str.split(',') if area.strip()]
    if len(areas_list) < 2 or len(areas_list) > 4:
        return jsonify({
            "Error": "Debes especificar entre 2 y 4 areas, ej: ?areas=Region-A1,Region-A2"
        }), 400

    try:
        normalized_areas = [normalize_area_id(area_id) for area_id in areas_list]
    except ValueError as e:
        return jsonify({"Error": str(e)}), 400

    try:
        client = get_db()
        scapp_db = client[SCAPP_DATABASE_NAME]
        response_list = []
        for area_id in normalized_areas:
            response_list.append({
                "name": area_id,
                "feeder_score_data": compute_feeder_score_by_area(scapp_db, area_id, start_day, day_ranges),
            })
        return jsonify(response_list), 200
    except Exception as e:
        print(f"Error comparando FeederScore entre areas en linea {e.__traceback__.tb_lineno}: \n{e}")
        return jsonify({"Error": f"Error interno del servidor: {e}"}), 500


@api_bp.route('/scapp/get_feeder_score_region_data/<REGION>/<START_DAY>/<DAY_RANGES>', methods=['GET'])
def scapp_get_feeder_score_region_data(REGION, START_DAY, DAY_RANGES):
    # Ver AUTORIZACION FEEDERSCORE arriba.
    REGION = REGION.capitalize()
    region_id = f'{REGION}'

    start_day = validar_y_convertir_fecha(START_DAY)
    day_ranges = validar_y_convertir_dias_desde(DAY_RANGES)

    if not start_day:
        return jsonify({"Error": f"Parametros de dias en formato no valido-Se esperaba 'DD-MM-YYYY' y se recibio {START_DAY}"}), 400
    elif not day_ranges:
        return jsonify({"Error": f"Parametros de rango de dias en formato no valido: {DAY_RANGES}"}), 400

    try:
        client = get_db()
        scapp_db = client[SCAPP_DATABASE_NAME]
        feeder_score = compute_feeder_score_by_region(
            scapp_db,
            region_id,
            start_day,
            day_ranges,
            granularity=request.args.get("granularity"),
        )
        return jsonify({
            "name": region_id,
            "feeder_score_data": feeder_score,
        }), 200
    except Exception as e:
        print(f"No se ha generado correctamente FeederScore de la Region {region_id} por error en linea {e.__traceback__.tb_lineno}: \n{e}")
        return jsonify({"Error": "Datos generados erroneamente por servidor"}), 500


def _live_summary_gone():
    return jsonify({
        "ok": False,
        "error": "gone",
        "detail": "Live summary fue dado de baja el 2026-07-29.",
    }), 410


@api_bp.route('/scapp/mirror_live_summary/<REGION>/<AREA>/<CENTRO>', methods=['POST'])
def scapp_live_summary_saving(CENTRO, AREA, REGION):
    return _live_summary_gone()


@api_bp.route('/scapp/last_received_live_summary', methods=['GET'])
def last_received_live_summary():
    return _live_summary_gone()


@api_bp.route('/scapp/get_live_summary_center_data/<REGION>/<AREA>/<CENTRO>/<START_DAY>/<DAY_RANGES>', methods=['GET'])
def scapp_get_live_summary_center_data(CENTRO, AREA, REGION, START_DAY, DAY_RANGES):
    return _live_summary_gone()


@api_bp.route('/scapp/compare_live_summary_centers', methods=['GET'])
def compare_live_summary_centers():
    return _live_summary_gone()


@api_bp.route('/scapp/get_live_summary_area_data/<REGION>/<AREA>/<START_DAY>/<DAY_RANGES>', methods=['GET'])
def scapp_get_live_summary_area_data(AREA, REGION, START_DAY, DAY_RANGES):
    return _live_summary_gone()


@api_bp.route('/scapp/compare_live_summary_areas', methods=['GET'])
def compare_live_summary_areas():
    return _live_summary_gone()


@api_bp.route('/scapp/get_live_summary_region_data/<REGION>/<START_DAY>/<DAY_RANGES>', methods=['GET'])
def scapp_get_live_summary_region_data(REGION, START_DAY, DAY_RANGES):
    return _live_summary_gone()

def region_analytics_access(db, username, region_analytic_access):
    users_collection = db[USER_DATABASE_NAME].users

    #Construccion de Query para obtener cargo de usuario
    proyeccion = {"_id": 0, "password": 0, "refresh_token": 0, "access_token": 0}
    usuario_actual_info = users_collection.find_one({'username': username}, proyeccion)
    cargo_actual = usuario_actual_info['cargo']
    #Condicion: solo Gerencia puede acceder a analitica de region
    if cargo_actual and cargo_actual in region_analytic_access:
        return True
    else:
        return False

@app.teardown_request
def close_db_request(exc=None):
    """Se ejecuta tras cada request, incluso si hubo excepcion."""
    db_client = g.pop('db_client', None)
    if db_client:
        db_client.close()

@app.teardown_appcontext
def close_db_appcontext(exc=None):
    """Se ejecuta al destruir el application context."""
    db_client = g.pop('db_client', None)
    if db_client:
        db_client.close()

#@app.before_request
#def intercept_legacy_traffic():
    # Define la fecha en la que las apps viejas dejarán de funcionar
    # Formato: YYYY-MM-DD
#    return check_legacy_expiration(expiration_date_str="2026-03-15")

# REGISTRO DE BLUEPRINTS
print("--> [DEBUG] Registrando Blueprint en App Principal...")

# --- AGREGA ESTAS 3 LÍNEAS DE DIAGNÓSTICO ---
# deferred_functions es la lista interna de "cosas por hacer" del blueprint (rutas)
print(f"--> [DEBUG] ID del objeto Blueprint: {id(notifications_bp)}")
print(f"--> [DEBUG] Cantidad de rutas pendientes en el Blueprint: {len(notifications_bp.deferred_functions)}")
# --------------------------------------------
configurar_autodeteccion(guardar_permiso)
app.register_blueprint(etiquetas_bp)
app.register_blueprint(notifications_bp)
app.register_blueprint(agent_data_bp)
# MANTENER TU CÓDIGO ACTUAL
app.register_blueprint(api_bp, url_prefix=SECRET_API_PREFIX)

if __name__ == '__main__':
    
    debug = False
    # # host = '172.16.1.88'
    # host = '192.168.20.145'
    # host = '127.0.0.1'
    host = '0.0.0.0'
    port = 4024
           
    print("Version ITGServiceManager to BluePrints Migration S01 05-03-2026")

    # --- CODIGO DE DIAGNOSTICO ---
    print("\n--- MAPA DE RUTAS CARGADAS (SIN FILTRO) ---")
    rutas_encontradas = 0
    for rule in app.url_map.iter_rules():
        print(f"Ruta: {rule} | Endpoint: {rule.endpoint}")
        rutas_encontradas += 1
    print(f"Total rutas: {rutas_encontradas}")
    print("-------------------------------------------\n")
    # -----------------------------

    # app.run()
    # app.run(host=host, port=port, debug=debug)
    with app.app_context():
        app.run(host=host, port=port, debug=debug, use_reloader=True, threaded=True)
