
# Copyright (C) 2023-2025 Cognizant Digital Business, Evolutionary AI.
# All Rights Reserved.
# Issued under the Academic Public License.
#
# You can be released from the terms, and requirements of the Academic Public
# License by purchasing a commercial license.
# Purchase of a commercial license is mandatory for any use of the
# neuro-san SDK Software in commercial settings.
#
# END COPYRIGHT
"""
See class comment for details
"""
import http
from typing import Any
from typing import Dict
from typing import List
from typing import Tuple

import json
import os

import grpc

import tornado
from tornado.web import RequestHandler

from neuro_san.http_sidecar.logging.http_logger import HttpLogger
from neuro_san.http_sidecar.interfaces.agent_authorizer import AgentAuthorizer
from neuro_san.http_sidecar.interfaces.agents_updater import AgentsUpdater
from neuro_san.interfaces.async_agent_session import AsyncAgentSession
from neuro_san.interfaces.concierge_session import ConciergeSession
from neuro_san.session.async_grpc_service_agent_session import AsyncGrpcServiceAgentSession
from neuro_san.session.grpc_concierge_session import GrpcConciergeSession


class BaseRequestHandler(RequestHandler):
    """
    Abstract handler class for neuro-san API calls.
    Provides logic to inject neuro-san service specific data
    into local handler context.
    """
    grpc_to_http = {
        grpc.StatusCode.INVALID_ARGUMENT: 400,
        grpc.StatusCode.UNAUTHENTICATED: 401,
        grpc.StatusCode.PERMISSION_DENIED: 403,
        grpc.StatusCode.NOT_FOUND: 404,
        grpc.StatusCode.ALREADY_EXISTS: 409,
        grpc.StatusCode.INTERNAL: 500,
        grpc.StatusCode.UNAVAILABLE: 503,
        grpc.StatusCode.DEADLINE_EXCEEDED: 504
    }

    request_id: int = 0

    # pylint: disable=attribute-defined-outside-init
    # pylint: disable=too-many-arguments
    # pylint: disable=too-many-positional-arguments
    def initialize(self,
                   agent_policy: AgentAuthorizer,
                   agents_updater: AgentsUpdater,
                   port: int,
                   forwarded_request_metadata: List[str],
                   openapi_service_spec_path: str):
        """
        This method is called by Tornado framework to allow
        injecting service-specific data into local handler context.
        :param agent_policy: abstract policy for agent requests
        :param agents_updater: abstract policy for updating
                               collection of agents being served
        :param port: gRPC service port.
        :param forwarded_request_metadata: request metadata to forward.
        :param openapi_service_spec_path: file path to OpenAPI service spec.
        """

        self.agent_policy = agent_policy
        self.agents_updater = agents_updater
        self.port: int = port
        self.forwarded_request_metadata: List[str] = forwarded_request_metadata
        self.openapi_service_spec_path: str = openapi_service_spec_path
        self.logger = HttpLogger(forwarded_request_metadata)
        # Set default request_id for this request handler in case we will need it:
        BaseRequestHandler.request_id += 1

        if os.environ.get("AGENT_ALLOW_CORS_HEADERS") is not None:
            self.set_header("Access-Control-Allow-Origin", "*")
            self.set_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
            self.set_header("Access-Control-Allow-Headers", "Content-Type, Transfer-Encoding")

    def get_metadata(self) -> Dict[str, Any]:
        """
        Extract user metadata defined by self.forwarded_request_metadata list
        from incoming request.
        :return: dictionary of user request metadata; possibly empty
        """
        headers: Dict[str, Any] = self.request.headers
        result: Dict[str, Any] = {}
        for item_name in self.forwarded_request_metadata:
            if item_name in headers.keys():
                result[item_name] = headers[item_name]
            elif item_name == "request_id":
                # Generate unique id so we have some way to track this request:
                result[item_name] = f"request-{BaseRequestHandler.request_id}"
            else:
                result[item_name] = "None"
        return result

    def get_agent_grpc_session(self,
                               metadata: Dict[str, Any],
                               agent_name: str) -> AsyncAgentSession:
        """
        Build gRPC session to talk to "main" service
        :return: AgentSession to use
        """
        grpc_session: AsyncAgentSession = \
            AsyncGrpcServiceAgentSession(
                host="localhost",
                port=self.port,
                metadata=metadata,
                agent_name=agent_name)
        return grpc_session

    def get_concierge_grpc_session(self, metadata: Dict[str, Any]) -> ConciergeSession:
        """
        Build gRPC session to talk to "concierge" service
        :return: ConciergeSession to use
        """
        grpc_session: ConciergeSession = \
            GrpcConciergeSession(
                host="localhost",
                port=self.port,
                metadata=metadata)
        return grpc_session

    async def update_agents(self, metadata: Dict[str, Any]) -> bool:
        """
        Update internal agents table by executing request
        to underlying gRPC service.
        :param metadata: metadata for request from caller context.
        :return: True if update was successful
                 False otherwise
        """
        try:
            data: Dict[str, Any] = {}
            grpc_session: ConciergeSession = self.get_concierge_grpc_session(metadata)
            agents_dict: Dict[str, Any] = grpc_session.list(data)
            agents_list = agents_dict.get("agents", [])
            agents_names: List[str] = []
            for agent_dict in agents_list:
                agents_names.append(agent_dict["agent_name"])
            self.agents_updater.update_agents(agents_names)
            return True
        except Exception as exc:  # pylint: disable=broad-exception-caught
            self.process_exception(exc)
            return False

    def extract_grpc_error_info(self, exc: grpc.aio.AioRpcError) -> Tuple[int, str, str]:
        """
        Extract user-friendly information from gRPC exception
        :param exc: gRPC service exception
        :return: tuple of 3 values:
            corresponding HTTP error code;
            name of gRPC code;
            string with additional error details.
        """
        code = exc.code()
        http_code = BaseRequestHandler.grpc_to_http.get(code, 500)
        return http_code, code.name, exc.details()

    def process_exception(self, exc: Exception):
        """
        Process exception raised during request handling
        """
        if exc is None:
            return
        if isinstance(exc, json.JSONDecodeError):
            # Handle invalid JSON input
            self.set_status(400)
            self.write({"error": "Invalid JSON format"})
            self.logger.error(self.get_metadata(), "error: Invalid JSON format")
            return

        if isinstance(exc, grpc.aio.AioRpcError):
            http_status, err_name, err_details =\
                self.extract_grpc_error_info(exc)
            self.set_status(http_status)
            err_msg: str = f"status: {http_status} grpc: {err_name} details: {err_details}"
            self.write({"error": err_msg})
            self.logger.error(self.get_metadata(), "Http server error: %s", err_msg)
            return

        # General exception case:
        self.set_status(500)
        self.write({"error": "Internal server error"})
        self.logger.error(self.get_metadata(), "Internal server error: %s", str(exc))

    def data_received(self, chunk):
        """
        Method overrides abstract method of RequestHandler
        with no-op implementation.
        """
        return

    def prepare(self):
        self.logger.info(self.get_metadata(), f"[REQUEST RECEIVED] {self.request.method} {self.request.uri}")

    def do_finish(self):
        """
        Wrapper for finish() call
        with check for closed client connection.
        """
        try:
            self.finish()
        except tornado.iostream.StreamClosedError:
            self.logger.warning(self.get_metadata(), "Finish: client closed connection unexpectedly.")

    async def do_flush(self) -> bool:
        """
        Wrapper for flush() call
        with check for closed client connection.
        """
        try:
            await self.flush()
            return True
        except tornado.iostream.StreamClosedError:
            self.logger.warning(self.get_metadata(), "Flush: client closed connection unexpectedly.")
            return False

    async def options(self, *_args, **_kwargs):
        """
        Handles OPTIONS requests for CORS support
        """
        # No body needed. Tornado will return a 204 No Content by default
        self.set_status(http.HTTPStatus.NO_CONTENT)
        self.do_finish()
