from neo4j import GraphDatabase
import neo4j
from analogai.foundation.settings import config
from analogai.foundation.common.logging.app_logger import app_logger
from analogai.foundation.common.design_patterns.singleton import Singleton
import time
from typing import Optional

class Neo4jGateway(metaclass=Singleton):
    def __init__(self):
        app_logger.info("initializing neo4j")
        self._validate_configuration()
        self.uri = config.get("NEO4J_URI")
        self.user = config.get("NEO4J_USER")
        self.password = config.get("NEO4J_PASSWORD")
        self.database = config.get("NEO4J_DATABASE")
        self.driver: Optional[GraphDatabase] = None
        self.max_retries = 3
        self.retry_delay = 2
        self._initialize_driver()
    
    def _validate_configuration(self):

        missing_configs = []
        
        if not config.get("NEO4J_URI"):
            missing_configs.append("NEO4J_URI")
        if not config.get("NEO4J_USER"):
            missing_configs.append("NEO4J_USER")
        if not config.get("NEO4J_PASSWORD"):
            missing_configs.append("NEO4J_PASSWORD")
        if not config.get("NEO4J_DATABASE"):
            missing_configs.append("NEO4J_DATABASE")
        
        if missing_configs:
            error_msg = f"Neo4j configuration is missing: {', '.join(missing_configs)}. Please check your environment variables."
            app_logger.error(error_msg)
            raise ValueError(error_msg)
        
        if not str(config.get("NEO4J_URI", "")).startswith(('neo4j://', 'neo4j+s://', 'bolt://', 'bolt+s://')):
            error_msg = f"Invalid Neo4j URI format: {config.get('NEO4J_URI')}. Must start with neo4j://, neo4j+s://, bolt://, or bolt+s://"
            app_logger.error(error_msg)
            raise ValueError(error_msg)

    def _initialize_driver(self):

        app_logger.debug(f"Attempting Neo4j connection with URI: {self.uri}, User: {self.user}")
        
        for attempt in range(self.max_retries):
            try:
                self._create_driver()
                self._test_connection()
                app_logger.info(f"Neo4j connection established successfully at {self.uri}")
                return
                
            except neo4j.exceptions.ServiceUnavailable as e:
                error_msg = self._format_connection_error(e, attempt)
                if attempt < self.max_retries - 1:
                    app_logger.warning(f"{error_msg} Retrying in {self.retry_delay} seconds... (attempt {attempt + 1}/{self.max_retries})")
                    time.sleep(self.retry_delay)
                else:
                    app_logger.error(f"{error_msg} All retry attempts failed.")
                    raise ConnectionError(f"Failed to connect to Neo4j after {self.max_retries} attempts: {error_msg}")
                    
            except neo4j.exceptions.AuthError as e:
                error_msg = f"Neo4j authentication failed. Check username and password. Error: {str(e)}"
                app_logger.error(error_msg)
                raise ConnectionError(error_msg)
                
            except neo4j.exceptions.ConfigurationError as e:
                error_msg = f"Neo4j configuration error: {str(e)}"
                app_logger.error(error_msg)
                raise ValueError(error_msg)
                
            except Exception as e:
                error_msg = f"Unexpected error connecting to Neo4j: {str(e)}"
                app_logger.error(error_msg)
                if attempt < self.max_retries - 1:
                    app_logger.warning(f"Retrying in {self.retry_delay} seconds... (attempt {attempt + 1}/{self.max_retries})")
                    time.sleep(self.retry_delay)
                else:
                    raise ConnectionError(error_msg)
    
    def _create_driver(self):

        driver_config = {
            "uri": self.uri,
            "auth": (self.user, self.password),
            "max_connection_lifetime": 30,
            "max_connection_pool_size": 50,
            "connection_acquisition_timeout": 10
        }
        
        if str(config.get("NEO4J_IS_ENCRYPTED", "false")).lower() == 'true':
            driver_config.update({
                "encrypted": True,
                "trusted_certificates": neo4j.TrustAll()
            })
        
        self.driver = GraphDatabase.driver(**driver_config)
    
    def _test_connection(self):

        try:
            with self.driver.session(database=self.database) as session:
                result = session.run("RETURN 1 as test")
                test_value = result.single()["test"]
                if test_value != 1:
                    raise ConnectionError("Neo4j test query returned unexpected result")
                app_logger.debug("Neo4j connection test successful")
        except Exception as e:
            app_logger.error(f"Neo4j connection test failed: {str(e)}")
            raise
    
    def _format_connection_error(self, error: Exception, attempt: int) -> str:

        error_str = str(error)
        
        if "ConnectionRefusedError" in error_str or "actively refused" in error_str:
            return f"Neo4j server is not running or not accessible at {self.uri}. Please ensure Neo4j is started and accessible."
        elif "Unable to retrieve Neo4j routing information" in error_str:
            return f"Neo4j routing failed. The server can be starting up or experiencing issues. URI: {self.uri}"
        elif "timeout" in error_str.lower():
            return f"Neo4j connection timeout. The server can be overloaded or network issues exist. URI: {self.uri}"
        else:
            return f"Neo4j connection failed: {error_str}"

    def session(self, database: Optional[str] = None):

        if self.driver is None:
            app_logger.warning("Driver is closed, reinitializing Neo4j connection")
            self._initialize_driver()
        
        try:
            object_database = database or self.database
            return self.driver.session(database=object_database)
        except neo4j.exceptions.ServiceUnavailable as e:
            app_logger.error(f"Neo4j service unavailable when creating session: {str(e)}")
            raise ConnectionError(f"Neo4j service is unavailable: {str(e)}")
        except neo4j.exceptions.AuthError as e:
            app_logger.error(f"Neo4j authentication error when creating session: {str(e)}")
            raise ConnectionError(f"Neo4j authentication failed: {str(e)}")
        except Exception as e:
            app_logger.error(f"Failed to create Neo4j session: {str(e)}")
            raise ConnectionError(f"Failed to create Neo4j session: {str(e)}")

    def close(self):

        if self.driver:
            try:
                self.driver.close()
                app_logger.info("Neo4j connection closed successfully")
                self.driver = None
                if Neo4jGateway in Singleton._instances:
                    Singleton._instances.pop(Neo4jGateway, None)
            except Exception as e:
                app_logger.error(f"Error closing Neo4j driver: {str(e)}")
                self.driver = None

    def is_connected(self) -> bool:

        if self.driver is None:
            return False
        
        try:
            with self.driver.session(database=self.database) as session:
                session.run("RETURN 1")
            return True
        except Exception:
            return False
    
    def get_connection_info(self) -> dict:

        return {
            "uri": self.uri,
            "user": self.user,
            "database": self.database,
            "is_encrypted": config.get("NEO4J_IS_ENCRYPTED", "false"),
            "is_connected": self.is_connected(),
            "driver_active": self.driver is not None
        }

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()

    def __del__(self):

        try:
            self.close()
        except Exception:
            pass  # Ignore errors during cleanup

if __name__ == "__main__":
    try:
        gateway = Neo4jGateway()
        app_logger.info(f"Connection info: {gateway.get_connection_info()}")
        
        with gateway.session() as session:
            session.run("CREATE (:Person {name: 'test name', age: 'test age'})")
            result = session.run("MATCH (n:Person {name: 'test name'}) RETURN n.age")
            value = result.single()["n.age"]
            app_logger.info(f"Test created and retrieved age: {value}")
            session.run("MATCH (n:Person {name: 'test name'}) DELETE n")
            
        app_logger.info("Neo4j test completed successfully")
        
    except Exception as e:
        app_logger.error(f"Neo4j test failed: {str(e)}")
    finally:
        if 'gateway' in locals():
            gateway.close()
