from datetime import datetime
from enum import Enum
import json
import requests
from functools import wraps
import re
from flask import Flask
from functools import wraps

import os
import grpc

from flask_cors import CORS
import sys

app = Flask(__name__)
cors = CORS(app, resources={r"/*": {"origins": "*"}})

PATH_APP = os.path.dirname( __file__ )

path_app_main = os.path.join( PATH_APP, 'App/main' )
sys.path.append(path_app_main)
path_app_protos = os.path.join( PATH_APP, 'App/protos' )
sys.path.append(path_app_protos)

from csharp_dao import cloudServiceDAO_pb2, cloudServiceDAO_pb2_grpc
from python_carbonblackmanager import CBCManager_pb2, CBCManager_pb2_grpc
from werkzeug.security import check_password_hash

from flask import jsonify, request, make_response
import jwt
from google.protobuf.json_format import MessageToJson

#app = Flask(__name__)

app.config["TF_SECRET_KEY"] = "5adefc2f2a36b60de59f3412a4d8b0f7"
app.config[
    "SECRET_KEY"
] = "055d48d7699d412c5015ece728cfd847"  # **Almacenarlo en secret de cluster e investigar cómo recuperarlo desde python o definirlo vomo variable de sistema desde Dockerfile
dao_host = os.getenv("DAO_HOST", "dao.grpc")
dao_port = os.getenv("DAO_PORT", "443")
cbc_host = os.getenv("CBC_HOST", "cbc.grpc")
cbc_port = os.getenv("CBC_PORT", "443")
CBC_SA = os.environ["CBC_SA"]


class STATUS(Enum):
    REQUESTED = 1
    SUCCESS = 2
    SUPPLY_ERROR = 3
    ACTIVE = 4
    INACTIVE = 5
    SERVICE_ERROR = 6
    DELETED = 7


def token_required(f):
    @wraps(f)
    def decorator(*args, **kwargs):
        token = None
        from_vcd = False
        from_tf = False

        if "x-daas-token" in request.headers:
            token = request.headers[
                "x-daas-token"
            ]  # Token generado por autenticación básica (/session)
        elif "tf-daas-token" in request.headers:
            tf_token = request.headers["tf-daas-token"]  # Token generado desde portal
            from_tf = True
        elif request.headers["authorization"] and request.args["org_id"]:
            token = request.headers["authorization"]  # Bearer token recuperado de VCD
            from_vcd = True
        else:
            return jsonify({"message": "No se encuentra autenticado"})

        with open(PATH_APP + "/App/certs/csharp_dao/ca.pem", "rb") as fp:
            ca_cert = fp.read()
        creds = grpc.ssl_channel_credentials(root_certificates=ca_cert)

        core_channel = grpc.secure_channel(f"{dao_host}:{dao_port}", credentials=creds)
        stub = cloudServiceDAO_pb2_grpc.servicioDAOServiceStub(core_channel)

        if from_tf:
            try:
                data = jwt.decode(
                    token, app.config["TF_SECRET_KEY"], algorithms=["HS256"]
                )
                req = cloudServiceDAO_pb2.GetUserVCDRequest(public_id=data["public_id"])
                user_found = stub.getUserVCD(req)

                if not user_found:
                    return jsonify(
                        {
                            "message": "No se autenticó correctamente o el token es inválido"
                        }
                    )
                # validar la existencia de sólo 1 elemento en el arreglo y definirlo en una variable
            except:
                return jsonify({"message": "token inválido"})
        elif not from_vcd:
            try:
                data = jwt.decode(token, app.config["SECRET_KEY"], algorithms=["HS256"])
                req = cloudServiceDAO_pb2.GetUserVCDRequest(public_id=data["public_id"])
                user_found = stub.getUserVCD(req)

                if not user_found:
                    return jsonify(
                        {
                            "message": "No se autenticó correctamente o el token es inválido"
                        }
                    )
                # validar la existencia de sólo 1 elemento en el arreglo y definirlo en una variable
            except:
                return jsonify({"message": "token inválido"})
        else:
            req = cloudServiceDAO_pb2.GetOrgVCDRequest(
                org_id=int(request.args["org_id"])
            )
            org = stub.getOrgVCD(
                req
            )  # No encuentra información de organización por identificador

            if not org:
                return jsonify(
                    {"message": f"No se encuentra registrada la organización indicada"}
                )
            elif len(org.orgs) > 1:
                # No deben de existir organizaciones con un mismo nombre
                return jsonify(
                    {
                        "message": f"Se encontraron múltiples organizaciones con el identificador entregado al intentar validar usuario, ésto no debería de ser posible e impide reconocer el usuario al que desea hacer referencia. Reporte éste error"
                    }
                )
            org = org.orgs[0]
            api_version = org.api_version  # Extraemos valor de api_version
            api_versions = []

            if api_version is None or api_version.strip() == "":
                # En caso de no existir la versión de API desde la base de datos, se probará con una versión válida
                try:
                    res = requests.get(
                        f"{org.host}/cloudapi/1.0.0/orgs",
                        timeout=120,
                        headers={
                            "Authorization": token,
                        },
                    )  # Al no contar con el header accept en el formato requerido según la documentación de la API, nos arroja el listado de versiones de API válidas

                    if res.status_code != 406:
                        res.raise_for_status()
                    if (
                        "minorErrorCode" in res.json()
                        and res.json()["minorErrorCode"] == "NOT_ACCEPTABLE"
                    ):
                        r = re.search(r"(?<=\[)(.*)(?=\])", res.json()["message"])
                        api_versions = r.group(1).split(",")
                        api_versions = [
                            x.strip() for x in api_versions if "D" not in x
                        ]  # D es para versiones obsoletas (Deprecated). Éstas versiones son descartadas
                    else:
                        return jsonify(
                            {
                                "message": "La validación de token falló, reporte éste error con su proveedor del servicio"
                            }
                        )  # Es posible que ya no se entreguen las versiones de API desde una petición incompleta en un futuro (sin header Accept requerida), ésto requerirá ajustar petición o la validación
                except:
                    return jsonify(
                        {"message": "No fue posible validar versión de api VCD"}
                    )
            else:
                api_versions.append(api_version)

            for av in api_versions:
                try:
                    res = requests.get(
                        f"{org.host}/cloudapi/1.0.0/orgs",
                        timeout=120,
                        headers={
                            "Accept": f"application/*;version={av}",
                            "Authorization": token,
                        },
                    )

                    if res.status_code == 406:
                        continue

                    res.raise_for_status()

                    if not "resultTotal" in res.json() or res.json()["resultTotal"] < 1:
                        return jsonify(
                            {
                                "message": "No fue posible validar vigencia de token, reporte éste error con su proveedor del servicio"
                            }
                        )  # La respuesta a la petición cambió, hay que cambiar a otra petición simple de la api vcd (cualquiera que no requiera muchos parámetros)
                    else:
                        # No existe método update para tabla org_vcd
                        # Es posible actualizar versión de API en base de datos
                        # current_user = tenant
                        break
                except:
                    return jsonify({"message": "No se autenticó"})
        return f(token, *args, **kwargs)

    return decorator


@app.route("/session/terraform", methods=["POST"])
@token_required
def create_tfsession():
    with open("ca.pem", "rb") as fp:
        ca_cert = fp.read()

    creds = grpc.ssl_channel_credentials(root_certificates=ca_cert)
    core_channel = grpc.secure_channel(f"{dao_host}:{dao_port}", credentials=creds)
    stub = cloudServiceDAO_pb2_grpc.servicioDAOServiceStub(core_channel)

    req = cloudServiceDAO_pb2.GetUserVCDRequest(user_id=request.form["user_id"])
    res = stub.getUserVCD(req)

    if not res:
        return jsonify(
            {"status": "ERROR", "message": "No se encontró usuario registrado"}
        )
    # Posibles fechas de expiración:
    # - 1 Hora = 60
    # - 1 día = 1440
    # - 1 Semana = 10080
    # - 1 Mes = 43830
    # - Por siempre = 999999999 (1900 años)
    token = jwt.encode(
        {
            "public_id": res.user.public_id,
            "exp": datetime.datetime.utcnow()
            + datetime.timedelta(minutes=int(requests.form["expiration_time"])),
        },
        app.config["TF_SECRET_KEY"],
        "HS256",
    )

    return jsonify({"status": "SUCCESS", "token": token})


@app.route("/session", methods=["POST"])
def create_session():
    auth = request.authorization  # Autenticación simple: usuario@tenant + contraseña

    if not auth or not auth.username or not auth.password:
        return make_response(
            "could not verify",
            401,
            {"Authentication": 'Se requieren datos de autenticación"'},
        )

    full_username = re.match(
        "(.*)@(.*)", auth.username
    )  # Validación de formato 'usuario@tenant'

    if not full_username:
        return jsonify(
            {"message": "Se espera formato 'usuario@organización' para autenticación"}
        )

    with open("ca.pem", "rb") as fp:
        ca_cert = fp.read()
    creds = grpc.ssl_channel_credentials(root_certificates=ca_cert)
    core_channel = grpc.secure_channel(f"{dao_host}:{dao_port}", credentials=creds)
    stub = cloudServiceDAO_pb2_grpc.servicioDAOServiceStub(core_channel)
    req = cloudServiceDAO_pb2.GetOrgVCDRequest(name=full_username[2])
    org = stub.getOrgVCD(req)

    if not org:
        return jsonify(
            {
                "message": f"No se encuentra registrada la organización con nombre '{full_username[2]}'"
            }
        )
    elif len(org.orgs) > 1:
        # No deben de existir organizaciones con un mismo nombre
        return jsonify(
            {
                "message": f"Se encontraron múltiples organizaciones con nombre '{full_username[2]}' al intentar validar usuario, ésto no debería de ser posible e impide reconocer el usuario al que desea hacer referencia. Reporte éste error"
            }
        )

    req = cloudServiceDAO_pb2.GetUserVCDRequest(
        user_name=full_username[1]
    )  # Se requiere modificación para filtrado múltiple (Añadir org_id)
    user_found = stub.getUserVCD(req)

    if not user_found:
        return jsonify(
            {
                "message": f"No se encuentra registro de usuario con nombre '{full_username[1]}'"
            }
        )

    for uf in user_found.users:
        if uf.org_id == org.orgs[0].OrgVcdId:
            user_found = uf
            break

    if check_password_hash(user_found.password, auth.password):
        token = jwt.encode(
            {
                "public_id": user_found.public_id,
                "exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30),
            },
            app.config["SECRET_KEY"],
            "HS256",
        )

        return jsonify({"token": token})

    return make_response(
        "could not verify",
        401,
        {"Authentication": "No se autenticó correctamente"},
    )


@app.route("/category_services", methods=["GET"])
#@token_required
def get_product_type():
    if request.method == "GET":
        with open(PATH_APP + "/App/certs/csharp_dao/ca.pem", "rb") as fp:
            ca_cert = fp.read()
        creds = grpc.ssl_channel_credentials(root_certificates=ca_cert)
        core_channel = grpc.secure_channel(f"{dao_host}:{dao_port}", credentials=creds)
        stub = cloudServiceDAO_pb2_grpc.servicioDAOServiceStub(core_channel)
        req = cloudServiceDAO_pb2.GetProductTypeRequest()
        res = stub.getProductType(req)

        print(res)
        return res.response.response  # test jsonify   # falta formato


@app.route("/services")
@token_required
def get_services():
    if request.method == "GET":
        with open("ca.pem", "rb") as fp:
            ca_cert = fp.read()
        creds = grpc.ssl_channel_credentials(root_certificates=ca_cert)
        core_channel = grpc.secure_channel(f"{dao_host}:{dao_port}", credentials=creds)
        stub = cloudServiceDAO_pb2_grpc.servicioDAOServiceStub(core_channel)
        req = cloudServiceDAO_pb2.GetServicesByProductTypeRequest(
            IdProductType=int(request.args["service_type_id"])
        )
        res = stub.getServicesCatalog(req)

        return res  # mapeo pendiente # test jsonify


@app.route("/plans")
@token_required
def get_plans():
    if request.method == "GET":
        with open("ca.pem", "rb") as fp:
            ca_cert = fp.read()

        creds = grpc.ssl_channel_credentials(ca_cert)
        core_channel = grpc.secure_channel(f"{dao_host}:{dao_port}", credentials=creds)
        stub = cloudServiceDAO_pb2_grpc.servicioDAOServiceStub(core_channel)
        req = cloudServiceDAO_pb2.GetPlansServiceRequest(
            idService=request.args["service_id"]
        )
        res = stub.getPlansService(req)

        return res  # test jsonify


@app.route("/provisioning", methods=["POST"])
# @token_required
def provide():
    with open("ca.pem", "rb") as fp:
        ca_cert = fp.read()

    creds = grpc.ssl_channel_credentials(ca_cert)
    core_channel = grpc.secure_channel(f"{dao_host}:444", credentials=creds)
    stub = cloudServiceDAO_pb2_grpc.servicioDAOServiceStub(core_channel)
    req = cloudServiceDAO_pb2.SetContractedServicesRequest(
        plan_id=int(request.form["plan_id"]),
        user_id=int(request.form["user_id"]),
    )
    res = stub.setContractedServicesService(req)

    return jsonify(json.loads(res.response.response)[0])  # test jsonify


@app.route("/services_contracted")
@token_required
def get_contracted():
    if request.method == "GET":
        with open("ca.pem", "rb") as fp:
            ca_cert = fp.read()

        creds = grpc.ssl_channel_credentials(ca_cert)
        core_channel = grpc.secure_channel(f"{dao_host}:{dao_port}", credentials=creds)
        stub = cloudServiceDAO_pb2_grpc.servicioDAOServiceStub(core_channel)
        req = cloudServiceDAO_pb2.GetAllContractedServicesRequest(
            idUser=request.args["user_id"]
        )
        res = stub.getServicesAllContracted(req)

        return res  # Mapeo pendiente   # test jsonify


@app.route("/service/cbc/manage_subscription/<action>", methods=["POST"])
# @token_required
def manage_subscription(action):
    """
    NextGen Antivirus - Carbon Black Cloud. Contratación y cancelación de servicio para Tenant/Organización específica por toma-pedidos dirigido área de mesa de servicio.
    action: contracting, cancel
    métodos: POST
    retorno: json -> status, message
    """
    with open(PATH_APP + "/App/certs/python_carbonblackmanager/ca.pem", "rb") as fp:
        ca_cert = fp.read()
    creds = grpc.ssl_channel_credentials(ca_cert)
    # Canal de comunicación hacia microservicio de carbon black
    cbc_stub = CBCManager_pb2_grpc.CBCManagerServiceStub(
        grpc.secure_channel(f"{cbc_host}:{cbc_port}", credentials=creds)
    )
    # Canal de comunicación hacia microservicio de DAO
    with open(PATH_APP + "/App/certs/csharp_dao/ca.pem", "rb") as fp:
        ca_cert = fp.read()
    
    creds_dao = grpc.ssl_channel_credentials(ca_cert)

    dao_stub = cloudServiceDAO_pb2_grpc.servicioDAOServiceStub(
        grpc.secure_channel(f"{dao_host}:{dao_port}", credentials=creds_dao)
    )

    if action not in ["contracting", "cancel"]:
        return jsonify(
            {"status": "ERROR", "message": f"Acción solicitada '{action}' inválida"}
        )

    req = CBCManager_pb2.ServiceManagementRequest(
        service_form=CBCManager_pb2.ServiceForm(
            service="Next Gen Antivirus",
            movementType=CBCManager_pb2.MovementType.CONTRACTING
            if action == "contracting"
            else CBCManager_pb2.MovementType.CANCEL,
            plan=request.form["name_plan"],  # nombre de plan
            orgVdc=request.form["org_vdc"],  # nombre de orgVDC
            paymentMethod=request.form[
                "payment_model"
            ],  # Se espera que siempre sea el valor estático 'Cobro único/anual'
            licenses=int(request.form["licenses"]),  # número de licencias
            expireDate=request.form["expire_date"],  # vacío en contratación
            requestQueueId=int(request.form["request_queue_id"]),
        )
    )
    res = cbc_stub.ServiceManage(req)

    print(res)

    if not res.status == "SUCCESS":
        return (
            jsonify(
                {
                    "status": "ERROR",
                    "message": f"{res.message}: No fue posible realizar solicitud para {'contratación' if action == 'contracting' else 'cancelación'} de servicio. Intente nuevamente o reporte éste error",
                }
            ),
        )

    if action == "contracting":
        """Se inserta registro de servicio contratado. El aprovisionamiento debe de ser dummy debido a que no depende de nosotros en su totalidad, se generan registros para todas las licencias contratadas y se relacionan al servicio recién contratado."""
        req = cloudServiceDAO_pb2.SetContractedServicesRequest(
            plan_id=int(request.form["planid"]), user_id=int(request.form["user_id"])
        )
        res = dao_stub.setContractedServicesService(req)

        if not res.response.response or "id" not in res.response.response:
            return (
                jsonify(
                    {
                        "status": "ERROR",
                        "message": "No fue posible registrar contratación de servicio, solicite que se registre manualmente y reporte éste error",
                    }
                ),
            )

        csid = json.loads(res.response.response)[0]["id"]

        for x in range(int(request.form["licenses"])):
            req = cloudServiceDAO_pb2.SetLicensesRequest(
                name=f"{request.form['org_vdc']}{x}",  # meramente descriptivo
                contracted_services_id=str(csid),  # relación para contabilidad
                specs=str(
                    {
                        "api_host": "",  # se espera de mesa de servicio
                        "token": "",  # se espera de mesa de servicio
                        "license_count": f"{x+1}/{request.form['licenses']}",
                        "data": [
                            {
                                "version": 0,
                                "vm_name": "",
                                "vm_ownerName": "",
                                "vm_vdcName": "",
                                "vm_dateCreated": "",
                                "vm_href": "",
                                "sensor_name": "",
                                "sensor_loginUserName": "",
                                "sensor_registeredTime": "",
                                "sensor_deviceType": "",
                                "sensor_osVersion": "",
                                "sensor_sensorVersion": "",
                                "sensor_macAddress": "",
                                "sensor_deviceId": "",
                            }
                        ],
                    }
                ),
            )  # formato por defecto para licencias de carbon black cloud

            res = dao_stub.setLicensesService(req)

            if not res.response.response or "id" not in res.response.response:
                return (
                    jsonify(
                        {
                            "status": "ERROR",
                            "message": f"Falló registro de licencia #{x}. La petición para contratación se realizó exitosamente, sin embargo no se registraron {int(request.form['licenses']) - x} licencia(s) en nuestra base de datos. Reporte éste error y solicite que se registren las licencias faltantes",
                        }
                    ),
                )

    return jsonify(
        {"status": "SUCCESS", "message": action}
    )  # identificador de registros de licencias


@app.route("/service/cbc/manage_sensor/<action>", methods=["POST", "PUT"])
# @token_required
def manage_sensor(action):
    """
    NextGen Antivirus - Carbon Black Cloud. Instalación, desinstalación y actualización de estatus de sensores en máquinas virtuales de VCD por toma-pedidos dirigido área de mesa de servicio.
    action: install, uninstall, update_status
    métodos: POST, PUT
    retorno: json -> status, message
    """
    with open("ca.pem", "rb") as fp:
        ca_cert = fp.read()

    creds = grpc.ssl_channel_credentials(ca_cert)

    # Canal de comunicación hacia microservicio de carbon black
    cbc_stub = CBCManager_pb2_grpc.CBCManagerServiceStub(
        grpc.secure_channel(f"{cbc_host}:{cbc_port}", credentials=creds)
    )
    # Canal de comunicación hacia microservicio de DAO
    dao_stub = cloudServiceDAO_pb2_grpc.servicioDAOServiceStub(
        grpc.secure_channel(f"{dao_host}:444", credentials=creds)
    )

    if request.method == "PUT" and action == "update_status":
        # se recupera regitro(s) de licencia y se realiza petición para actualizar estatus
        cbc_api_host = (
            res.licenses.specs.api_host
        )  # se requiere host recuperado de tabla de licencias
        cbc_token = (
            res.licenses.specs.token
        )  # se requiere token reuperado de tabla de licencias

        try:
            res = requests.get(
                f"{cbc_api_host}/appservices/v5/orgs",
                timeout=120,
                headers={"X-AUTH-TOKEN": cbc_token},
            )
            res.raise_for_status()
        except:
            return jsonify(
                {
                    "status": "ERROR",
                    "message": "No fue posible recupear org key. Notifique éste error",
                }
            )

        org_key = res.organization.orgKey

        """
        Nota. Es posible desinstalar sensores y borrar registros visibles en el portal desde la API con los action_type UNINSTALL_SENSOR y DELETE_SENSOR respectivamente, sin embargo, son procesos asíncronos, es decir que no se espera una respuesta al efectuar la acción, por lo que podemos enviar una desinstalación y borrar registro inmediatamente, lo cuál no nos da la certeza de una desinstalación completada exitosamente.
        """
        try:
            res = requests.get(
                f"{cbc_api_host}/appservices/v6/orgs/{org_key}/device_actions",
                timeout=120,
                headers={"X-AUTH-TOKEN": cbc_token},
                data={
                    "action_type": request.form[
                        "action_type"
                    ],  # BACKGROUND_SCAN, QUARANTINE, BYPASS
                    "device_id": [request.form["device_id"]],
                    "options": {
                        "toggle": "ON" if eval(request.form["true"]) else "OFF"
                    },
                },
            )
            # CAMBIO: Validar estatus graduales o no modificables por nosotros para evitar habilitar el listado de cambios en el portal
            res.raise_for_status()
        except:
            return jsonify(
                {
                    "status": "ERROR",
                    "message": f"No fue posible actualizar estatus de sensor {request.form['action_type']}",
                }
            )

        return jsonify(
            {
                "status": "SUCCESS",
                "message": f"Establecido estatus {request.form['action_type']}",
            }
        )

    license_id = request.form["license_id"]
    license_id = int(license_id) if license_id == "" else 0

    request_queue_id = request.form["request_queue_id"]
    request_queue_id = int(request_queue_id) if request_queue_id == "" else 0

    licenses

    if not license_id and request_queue_id:
        req = cloudServiceDAO_pb2.GetRequestQueueRequest(
            request_queue_id=request_queue_id
        )
        res = dao_stub.getRequestQueueService(req)

        if not res.requestqueue:
            return (
                jsonify(
                    {
                        "status": "ERROR",
                        "message": f"{res.message}: No fue posible recuperar registro de servicio solicitado, verifique que el valor enviado en request_queue_id es correcto y existe en la base de datos.",
                    }
                ),
            )

        res = res.requestqueue[0]

    req = cloudServiceDAO_pb2.GetLicensesRequest(
        contracted_services_id=res.contracted_services_id if request_queue_id else 0,
        idlicenses=license_id,
    )
    res = dao_stub.GetLicenses(req)

    if not res.licenses:
        return (
            jsonify(
                {
                    "status": "ERROR",
                    "message": f"{res.message}: No fue posible recuperar registros de licencias con contracted_services_id = {res.contracted_services_id}, verifique que el valor enviado es correcto y existe en la base de datos.",
                }
            ),
        )

    licenses = res.licenses
    license_specs = {}
    license_version = {}

    for l in licenses:
        license_specs = l.specs
        license_specs = license_specs.replace("'", '"')
        license_specs = json.loads(license_specs)
        license_version = license_specs["data"][
            len(license_specs["data"]) - 1
        ]  # última versión de licencia

        if not license_id and request_queue_id and license_version["vm_name"] == "":
            licenses = l
            license_id = l.idlicenses
            break

    req = CBCManager_pb2.SensorManagementRequest(
        license_form=CBCManager_pb2.LicenseForm(
            orgVdc=request.form["org_vdc"],
            movementType=CBCManager_pb2.MovementType.INSTALL
            if action == "install"
            else CBCManager_pb2.MovementType.UNINSTALL,
            hrefVM=request.form["href"],  # desde la petición se espera un arreglo
            licenseId=license_id,  # elemento opcional siempre y cuando se envíe request_queue_id
            # requestQueueId=request_queue_id,  # elemento opcional siempre y cuando se envíe license_id
        )
    )
    res = cbc_stub.SensorManage(req)

    if not res.status == "SUCCESS":
        return (
            jsonify(
                {
                    "status": "ERROR",
                    "message": f"{res.message}: No fue posible solicitar {'instalación de sensores' if action == 'install' else 'desinstalación de sensores' if action == 'uninstall' else 'actualización de estatus de sensor'}. Intente nuevamente o reporte éste error",
                }
            ),
        )

    if request.method == "POST" and action == "install":
        # información de máquina virtual sobre la que se instalará sensor
        license_version["vm_name"] = request.form["name"]
        license_version["vm_ownerName"] = request.form["ownerName"]
        # lastest_version["vm_vdcName"] = request.form["vdcName"]
        license_version["vm_dateCreated"] = request.form["date_created"]
        license_version["vm_href"] = request.form["href"]

        req = cloudServiceDAO_pb2.SetUpdateLicensesRequest(
            idlicenses=licenses.idlicenses,
            name=licenses.nameLicenses,
            specs=str(license_specs),
            id_contracted_services=licenses.contracted_services_id,
        )

        res = dao_stub.setUpdateLicenses(req)

        if (
            not res.response.response
            or not "result" in res.response.response
            or json.loads(res.response.response)[0]["result"] != "exito"
        ):
            return (
                jsonify(
                    {
                        "status": "ERROR",
                        "message": "No fue posible actualizar registro de licencia con datos de Máquina Virtual objetivo. Reporte éste error.",
                    }  # No se recupera detalle de error debido a que la respuesta no da detalle de errores, sólo retorna el mensaje 'error' 02/08/2023
                ),
            )

        return (
            jsonify(
                {
                    "status": "SUCCESS",
                    "message": "Solicitud para instalación de sensor exitosa. Espere respuesta a su solicitud desde el portal de administración.",
                }
            ),
        )

    if request.method == "POST" and action == "uninstall":
        return (
            jsonify(
                {
                    "status": "SUCCESS",
                    "message": "Solicitud para desinstalación de sensor exitosa. Espere respuesta a su solicitud desde el portal de administración.",
                }
            ),
        )


@app.route(
    "/service/cbc/manage_user_accounts/<action>",
    methods=["POST", "GET", "PUT", "DELETE"],
)
# @token_required
def manage_user_accounts(action):
    """
    NextGen Antivirus - Carbon Black Cloud. Creación, recuperación, actualización y borrado de cuentas de usuario con rol fijo de Analista 3.
    action: add, get, change, delete
    métodos: POST, GET, PUT, DELETE
    retorno: json -> status, message
    """
    with open("ca.pem", "rb") as fp:
        ca_cert = fp.read()

    creds = grpc.ssl_channel_credentials(ca_cert)

    # Canal de comunicación hacia microservicio de carbon black
    cbc_stub = CBCManager_pb2_grpc.CBCManagerServiceStub(
        grpc.secure_channel(f"{cbc_host}:{cbc_port}", credentials=creds)
    )
    # Canal de comunicación hacia microservicio de DAO
    dao_stub = cloudServiceDAO_pb2_grpc.servicioDAOServiceStub(
        grpc.secure_channel(f"{dao_host}:{dao_port}", credentials=creds)
    )
    # Se recupera información de licencias necesaria para realizar peticiones a API de CBC
    req = cloudServiceDAO_pb2.GetLicensesRequest(
        org_id=request.form["org_id"], idService=request.form["service_id"]
    )
    res = dao_stub.GetLicenses(req)

    if not res:
        return jsonify(
            {
                "status": "SUCCESS",
                "message": "No se encontraron licencias relacionadas a usuario, revise si la información es correcta o reporte éste error",
            }
        )

    api_host = res.api_host  # sólo podemos recuperarlo de tabla de licencias
    token = res.token  # sólo podemos recuperarlo de tabla de licencias

    try:
        res = requests.get(
            f"{api_host}/appservices/v5/orgs",
            timeout=120,
            headers={"X-AUTH-TOKEN": token},
        )
        res.raise_for_status()
    except:
        return jsonify(
            {
                "status": "ERROR",
                "message": "No fue posible recuperar org key de ambiente de carbon black",
            }
        )

    org_key = res.organization.orgKey
    org_id = res.organization.id

    if request.method == "POST" and action == "add":
        req = CBCManager_pb2.CreateUserAccountRequest(
            auth_token=token,
            cbc_api_host=api_host,
            org_id=org_id,
            org_key=org_key,
            user_data=CBCManager_pb2.UserAccount(
                contact_id=request.form["contact_id"],
                email=request.form["email"],
                first_name=request.form["first_name"],
                last_name=request.form["last_name"],
                login_id=request.form["login_id"],
                phone=request.form["phone"],
            ),
        )
        res = cbc_stub.CreateUserAccount(req)

    if request.method == "GET" and action == "get":
        req = CBCManager_pb2.UserAccountsRequest(
            auth_token=token, cbc_api_host=api_host, org_key=org_key
        )
        res = cbc_stub.GetUserAccounts(req)

    if request.method == "PUT" and action == "change":
        req = CBCManager_pb2.UserAccountsRequest(
            auth_token=token,
            cbc_api_host=api_host,
            org_key=org_key,
        )
        res = cbc_stub.GetUserAccounts(req)

        req = CBCManager_pb2.UpdateUserAccountRequest(
            auth_token=token,
            cbc_api_host=api_host,
            org_key=org_key,
            login_name=request.form["login_name"],
            org_id=org_id,
            user_data=CBCManager_pb2.UserAccount(
                contact_id=res[1].contact_id,
                email=request.form["login_name"],
                first_name=request.form["first_name"],
                last_name=request.form["last_name"],
                login_id=res[1].login_id,
                phone=request.form["phone"],
            ),
        )
        res = cbc_stub.UpdateUserAccount(req)

    if request.method == "DELETE" and action == "delete":
        req = CBCManager_pb2.UserAccountsRequest(
            auth_token=token,
            cbc_api_host=api_host,
            org_key=org_key,
        )
        res = cbc_stub.GetUserAccounts(req)

        req = CBCManager_pb2.DeleteUserAccountRequest(
            auth_token=token,
            cbc_api_host=api_host,
            login_id=res[1].login_id,
            org_key=org_key,
        )
        res = cbc_stub.UpdateUserAccount(req)

    return jsonify(res)


@app.route("/service/cbc/retrieve/vms", methods=["GET"])
# @token_required
def get_vms():
    """
    NextGen Antivirus - Carbon Black Cloud. Recupera listado de máquinas virtuales en formato XML, purga información que puede cambiar en un futuro y se retorna en formato JSON.
    métodos: GET
    retorno: json -> status, message, json
    """

    var_org_id = request.args["org_id"] if len(request.args.getlist('org_id')) > 0 else 0
    var_org_vdc = request.args["org_vdc"] if len(request.args.getlist('org_vdc')) > 0 else None

    with open(PATH_APP + "/App/certs/python_carbonblackmanager/ca.pem", "rb") as fp:
        ca_cert = fp.read()
    creds = grpc.ssl_channel_credentials(ca_cert)
    # Canal de comunicación hacia microservicio de carbon black
    cbc_stub = CBCManager_pb2_grpc.CBCManagerServiceStub(
        grpc.secure_channel(f"{cbc_host}:{cbc_port}", credentials=creds)
    )
    # Canal de comunicación hacia microservicio de DAO
    with open(PATH_APP + "/App/certs/csharp_dao/ca.pem", "rb") as fp:
        ca_cert = fp.read()
    
    creds_dao = grpc.ssl_channel_credentials(ca_cert)

    dao_stub = cloudServiceDAO_pb2_grpc.servicioDAOServiceStub(
        grpc.secure_channel(f"{dao_host}:{dao_port}", credentials=creds_dao)
    )

    req = cloudServiceDAO_pb2.GetOrgVCDRequest(org_id=int(var_org_id))
    org = dao_stub.getOrgVCD(req)

    if not org:
        return jsonify(
            {
                "status": "ERROR",
                "message": f"No se encuentra registrada la organización indicada",
            }
        )
    elif len(org.orgs) > 1:
        # No deben de existir organizaciones con un mismo nombre
        return jsonify(
            {
                "status": "ERROR",
                "message": f"Se encontraron múltiples organizaciones con el identificador entregado al intentar validar usuario, ésto no debería de ser posible e impide reconocer el usuario al que desea hacer referencia. Reporte éste error",
            }
        )

    org = org.orgs[0]

    req = CBCManager_pb2.RetrieveVMsRequest(
        vdc_id=var_org_vdc,  # Se envía desde front
        vcd_api_host=org.host,
        auth=CBC_SA,  # Se esperan credenciales en formato basic (antes de cifrado) o bearer token. Revisar origen para obtención, posiblemente se requiera una cuenta de servicio por organización. BD -> user_vcd -> basic base64(uservcd@org:passwordh265)
        is_basic_auth=True,  # Se requiere validación o se eliminará en caso de implementar cuenta de servicio
        api_version=org.api_version,
        tenant=org.name,
    )
    
    res = cbc_stub.RetrieveVMs(req)

    if res.status == "ERROR":
        return jsonify(
            {
                "status": "ERROR",
                "message": res.message,
            }
        )
    
    return MessageToJson(res)


@app.route("/service/cbc/retrieve/sensors", methods=["GET"])
@app.route("/service/cbc/retrieve/sensors/<deviceid>", methods=["GET"])
# @token_required
def get_sensors(deviceid):
    """
    NextGen Antivirus - Carbon Black Cloud. Recupera listado de sensores de tipo Workload en formato CSV, purga información que pudiese cambiar en un futuro y se retorna en formato JSON.
    métodos: GET
    retorno: json -> status, message, json
    """
    with open("ca.pem", "rb") as fp:
        ca_cert = fp.read()
    # CAMBIO: Recuperamos sólo un sensor filtrado por device_id retornando la info en formato json
    creds = grpc.ssl_channel_credentials(ca_cert)
    ms_channel = grpc.secure_channel(f"{dao_host}:{dao_port}", credentials=creds)
    stub = cloudServiceDAO_pb2_grpc.servicioDAOServiceStub(ms_channel)

    req = cloudServiceDAO_pb2.GetLicensesRequest(
        contracted_services_id=request.form["contracted_services_id"]
    )
    res = stub.GetLicenses(req)

    if not res:
        return jsonify(
            {
                "status": "ERROR",
                "message": "No se encontraron licencias dadas de alta, revise si la información es correcta o reporte éste error",
            }
        )

    api_host = res.api_host
    token = res.token

    ms_channel = grpc.secure_channel(f"{cbc_host}:{cbc_port}", credentials=creds)
    stub = CBCManager_pb2_grpc.CBCManagerServiceStub(ms_channel)
    req = CBCManager_pb2.RetrieveSensorsRequest(
        cbc_api_host=api_host, auth_token=token, org_key=res.organization.orgKey
    )
    res = stub.RetrieveWorkloadSensors(req)
    return jsonify(res)


@app.route(
    "/service/daas/manage_contracts/<action>",
    methods=["POST", "DELETE"],
)
# @token_required
def manage_contracts(action):
    if not "request_queue_id" in request.form:
        return jsonify({"status": "ERROR", "message": "No se recibió request_queue_id"})
    # delete, pause, reactivate
    if request.method == "DELETE" and action == "delete":
        request_queue_id = int(request.form["request_queue_id"])

        result = update_contract_status(request_queue_id, STATUS.DELETED)

    if request.method == "POST" and action == "pause":
        request_queue_id = int(request.form["request_queue_id"])

        result = update_contract_status(request_queue_id, STATUS.INACTIVE)

    if request.method == "POST" and action == "reactivate":
        request_queue_id = int(request.form["request_queue_id"])

        result = update_contract_status(request_queue_id, STATUS.ACTIVE)

    return jsonify(result)

@app.route(
    "/service/daas/deployments/<id>",
    methods=["GET"],
)
# @token_required
def get_deployments(id):
    with open("ca.pem", "rb") as fp:
        ca_cert = fp.read()

    creds = grpc.ssl_channel_credentials(ca_cert)
    # canal de comunicación hacía microservicio cloudServiceDAO (BD)
    ms_channel = grpc.secure_channel(f"{dao_host}:444", credentials=creds)
    stub = cloudServiceDAO_pb2_grpc.servicioDAOServiceStub(ms_channel)

    if not id:
        return jsonify(
            {
                "status": "ERROR",
                "message": "Se requiere request_queue_id",
                "data": [],
            }
        )
    else:
        req = cloudServiceDAO_pb2.GetRequestQueueRequest(
            request_queue_id=int(id)
        )
        try:
            res = stub.getRequestQueueService(req)
        except grpc._channel._InactiveRpcError as e:
            return jsonify(
                {
                    "status": "ERROR",
                    "message": str(e),
                    "data": [],
                }
            )

    if not res.requestqueue:
        return jsonify(
            {
                "status": "ERROR",
                "message": f"No fue posible encontrar registro con request_queue_id={id}. Verifique que la información proporcionada es correcta o reporte éste error.",
                "data": [],
            }
        )

    return jsonify(
        {
            "status": "SUCCESS",
            "message": "",
            "data": [json.loads(MessageToJson(x, including_default_value_fields=True)) for x in res.requestqueue],
        }
    )

@app.route(
    "/service/daas/deployments/contract/<contracted_services_id>",
    methods=["GET"],
)
# @token_required
def get_deployment_contract(contracted_services_id):
    with open("ca.pem", "rb") as fp:
        ca_cert = fp.read()

    creds = grpc.ssl_channel_credentials(ca_cert)
    # canal de comunicación hacía microservicio cloudServiceDAO (BD)
    ms_channel = grpc.secure_channel(f"{dao_host}:444", credentials=creds)
    stub = cloudServiceDAO_pb2_grpc.servicioDAOServiceStub(ms_channel)

    if not contracted_services_id:
        return jsonify(
            {
                "status": "ERROR",
                "message": "Se requiere contracted_service_id",
                "data": [],
            }
        )
    else:
        req = cloudServiceDAO_pb2.GetRequestQueueRequest(
            contracted_services_id=int(contracted_services_id)
        )
        try:
            res = stub.getRequestQueueService(req)
        except grpc._channel._InactiveRpcError as e:
            return jsonify(
                {
                    "status": "ERROR",
                    "message": str(e),
                    "data": [],
                }
            )

    if not res.requestqueue:
        return jsonify(
            {
                "status": "ERROR",
                "message": f"No fue posible encontrar registro con contracted_service_id={contracted_services_id}. Verifique que la información proporcionada es correcta o reporte éste error.",
                "data": [],
            }
        )

    return jsonify(
        {
            "status": "SUCCESS",
            "message": "",
            "data": [json.loads(MessageToJson(x, including_default_value_fields=True)) for x in res.requestqueue],
        }
    )


def update_contract_status(request_queue_id, status: STATUS):
    with open("ca.pem", "rb") as fp:
        ca_cert = fp.read()

    creds = grpc.ssl_channel_credentials(ca_cert)
    # canal de comunicación hacía microservicio cloudServiceDAO (BD)
    ms_channel = grpc.secure_channel(f"{dao_host}:444", credentials=creds)
    stub = cloudServiceDAO_pb2_grpc.servicioDAOServiceStub(ms_channel)
    # se requiere recuperar registro de request_queue a modificar, debido al requisito de actualización de todas las columnas por registro. Requerido asi por DAO.
    req = cloudServiceDAO_pb2.GetRequestQueueRequest(request_queue_id=request_queue_id)
    res = stub.getRequestQueueService(req)

    if not res.requestqueue:
        return {
            "status": "ERROR",
            "message": f"No fue posible encontrar registro con request_queue_id={request_queue_id}. Verifique que la información proporcionada es correcta o reporte éste error.",
        }

    request_contract = res.requestqueue[0]  # registro a editar de requests_queue

    r_datetime = request_contract.request_datetime
    r_datetime = (
        r_datetime
        if r_datetime == "" or r_datetime is None
        else patch_dateformat(r_datetime)
    )
    s_datetime = request_contract.supply_datetime
    s_datetime = (
        s_datetime
        if s_datetime == "" or r_datetime is None
        else patch_dateformat(s_datetime)
    )
    d_datetime = request_contract.down_datetime
    d_datetime = (
        s_datetime
        if s_datetime == "" or r_datetime is None
        else patch_dateformat(s_datetime)
    )

    req = cloudServiceDAO_pb2.SetUpdateRequestQueueRequest(
        request_queue_id=request_queue_id,
        contracted_services_id=request_contract.contracted_services_id,
        resource_specs=request_contract.resource_specs,
        request_datetime=r_datetime,
        supply_datetime=s_datetime,
        down_datetime=d_datetime,
        supply_status_id=request_contract.supply_status_id,
        service_status_id=status.value,
        error_log=request_contract.error_log,
        identificator=request_contract.identificator,
        name_app=request_contract.name_app,
        status=request_contract.status,
        port=request_contract.port,
    )  # DELETED
    res = stub.setUpdateRequestQueue(req)

    if (
        not res.response.response
        or not "result" in res.response.response
        or json.loads(res.response.response)[0]["result"] != "exito"
    ):
        return {
            "status": "ERROR",
            "message": f"No fue posible actualizar estatus para registro con request_queue_id={request_queue_id}. Reporte éste error.",
        }

    return {
        "status": "SUCCESS",
        "message": "Se actualizó estatus correctamente. Se aplicarán los cambios de estatus en breve.",
    }


def patch_dateformat(datetime_str):
    try:
        datetime_str = datetime.strptime(datetime_str, "%m/%d/%Y %I:%M:%S %p")
    except ValueError:
        datetime_str = datetime.strptime(datetime_str, "%d/%m/%Y %H:%M:%S")

    return datetime_str.strftime("%Y-%m-%d %H:%M:%S.%f")

@app.route("/services_security",methods=["GET"])
#@token_required
def services_security():
    if request.method == "GET":
        events_ids = request.form['events_ids'] if len(request.form.getlist('events_ids')) > 0 else 'NOTFOUND'
        if events_ids == 'srv-apigateway':
            from AxwaySrv import AxwaySrv as axway
            return axway.main(request, PATH_APP)


@app.route("/wsoa",methods=["POST"])
#@token_required
def wsoa():
    if request.method == "POST":
        events_ids = request.form['events_ids'] if len(request.form.getlist('events_ids')) > 0 else 'NOTFOUND'
        if events_ids == 'srv-woa':
            from WSOA import WSOA as wsoa
            return wsoa.main(request, PATH_APP)


@app.route("/kmshsm/<action>",methods=["POST"])
#@token_required
def fortanix(action):
    if request.method == "POST":
        from Fortanix import Fortanix as frtnx
        return frtnx(request, PATH_APP, action).main()
    

@app.route("/admin/savedatavcloud",methods=["POST"])
#@token_required
def ffortanix(action):
    if request.method == "POST":
        from AdminDaaS import AdminDaaS as admds
        return admds(request, PATH_APP, action).savedatavcloud()
    

@app.route('/srvcdn/<path:remaining_path>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
# @token_required
def cdn_api(remaining_path):
    #print(remaining_path)
    from CDN import CDN as cdn_api
    return cdn_api(request, PATH_APP, remaining_path).main()


@app.route("/hello",methods=["POST", "GET"])
@token_required
def hello(token_required):
    return f"HELLO FROM FLASK {token_required}"

if __name__ == "__main__":
    app.run()
