from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv())
import os
import time
from typing import Optional, Tuple, Generator
from modules.logger_tool import initialise_logger
from neo4j import GraphDatabase as gd, Driver, Session
from contextlib import contextmanager

logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True)

def _retry_with_backoff(
    func,
    max_attempts: int = 10,  # Increased from 3 to 10
    initial_delay: float = 2.0,  # Increased from 1 to 2 seconds
    max_total_wait: float = 60.0,  # Maximum total time to wait (60 seconds)
    max_delay: float = 10.0  # Maximum delay between retries
) -> any:
    """
    Helper function to retry operations with exponential backoff.

    Args:
        func: Function to retry
        max_attempts: Maximum number of retry attempts
        initial_delay: Initial delay between retries in seconds
        max_total_wait: Maximum total time to wait before giving up
        max_delay: Maximum delay between retries
    """
    attempt = 0
    delay = initial_delay
    start_time = time.time()

    while attempt < max_attempts:
        try:
            return func()
        except Exception as e:
            attempt += 1
            elapsed_time = time.time() - start_time

            # Check if we've exceeded the maximum total wait time
            if elapsed_time >= max_total_wait:
                logger.error(f"Exceeded maximum total wait time of {max_total_wait} seconds")
                raise

            if attempt == max_attempts:
                logger.error(f"Final attempt {attempt} failed: {e}")
                raise

            # Calculate next delay with exponential backoff, but cap it
            delay = min(delay * 2, max_delay)

            # If we're in a container initialization scenario, provide more context
            if "Connection refused" in str(e):
                logger.warning(
                    f"Attempt {attempt} failed: Connection refused. "
                    f"This might indicate that Neo4j is still starting up. "
                    f"Retrying in {delay:.1f} seconds... "
                    f"(Total elapsed: {elapsed_time:.1f}s)"
                )
            else:
                logger.warning(f"Attempt {attempt} failed: {e}. Retrying in {delay:.1f} seconds...")

            time.sleep(delay)

def get_driver(db_name: Optional[str] = None, url: Optional[str] = None, auth: Optional[Tuple[str, str]] = None) -> Optional[Driver]:
    if url is None:
        url = os.getenv("APP_BOLT_URL")
        username = os.getenv("USER_NEO4J")
        password = os.getenv("PASSWORD_NEO4J")
        if not username or not password:
            logger.error("Neo4j credentials not found in environment")
            return None
        auth = (username, password)

    if auth is None:
        logger.error("No authentication credentials provided")
        return None

    def create_driver():
        logger.info(f"Attempting to connect to Neo4j at {url}")
        driver = gd.driver(url, auth=auth)
        driver.verify_connectivity()
        logger.info(f"Connected to Neo4j at {url}")
        return driver

    try:
        # Use more lenient retry parameters for initial connection
        driver = _retry_with_backoff(
            create_driver,
            max_attempts=10,
            initial_delay=2.0,
            max_total_wait=60.0,
            max_delay=10.0
        )
    except Exception as e:
        logger.error(f"Failed to establish Neo4j connection after all retries: {e}")
        return None

    # Test the connection with the specific database
    if db_name and driver:
        def verify_database():
            with driver.session(database=db_name) as session:
                result = session.run("RETURN 'Connection successful' AS message")
                record = result.single()
                if not record or not record.get("message"):
                    raise Exception(f"Failed to verify database {db_name} connection")
                logger.info(f"Connection to Neo4j at {url} with database {db_name} successful")

        try:
            # Use more lenient retry parameters for database verification
            _retry_with_backoff(
                verify_database,
                max_attempts=10,
                initial_delay=2.0,
                max_total_wait=60.0,
                max_delay=10.0
            )
        except Exception as e:
            logger.error(f"Failed to connect to database {db_name} after all retries: {e}")
            driver.close()
            return None

    return driver

def close_driver(driver: Optional[Driver]) -> None:
    if driver:
        logger.info("Closing driver")
        driver.close()

# Global driver instance — None means not yet initialised, _driver_unavailable=True means connection failed
_driver: Optional[Driver] = None
_driver_unavailable: bool = False

def get_global_driver() -> Optional[Driver]:
    """Get or create the global Neo4j driver instance.

    Caches both success and failure so a broken Neo4j connection causes
    a single 60-second retry at startup, then fast-fails on every
    subsequent call instead of hanging for 60s each time.
    """
    global _driver, _driver_unavailable
    if _driver_unavailable:
        return None
    if _driver is None:
        _driver = get_driver()
        if _driver is None:
            _driver_unavailable = True
            logger.error("Neo4j driver unavailable — all subsequent Neo4j calls will fail fast until process restarts")
    return _driver

def reset_global_driver() -> None:
    """Reset the cached driver, forcing a reconnection attempt on the next call.

    Call this if Neo4j becomes available after the process started.
    """
    global _driver, _driver_unavailable
    if _driver:
        close_driver(_driver)
    _driver = None
    _driver_unavailable = False
    logger.info("Global Neo4j driver reset — will reconnect on next request")

@contextmanager
def get_session(database: Optional[str] = None) -> Generator[Session, None, None]:
    """Get a Neo4j session using the global driver."""
    driver = get_global_driver()
    if driver is None:
        raise Exception("Failed to get Neo4j driver")

    session = None
    try:
        session = driver.session(database=database)
        yield session
    finally:
        if session:
            session.close()
