"""
Gets and updates configuration values using ConfigParser.

The configuration is stored in a file instead of a database.
"""

import configparser
import os
import json
import logging
import fcntl
from contextlib import contextmanager
from typing import Any

logger = logging.getLogger(__name__)

DEFAULT_SECTION = "DEFAULT"

# Create config parser
config_file: str | None = None
_config = configparser.ConfigParser()

# Used to prevent reentrant locking issues
_in_set_operation = False


@contextmanager
def _file_lock(mode="r", lock_type=fcntl.LOCK_SH):
    """
    Context manager for file locking.

    Args:
        mode: File open mode ('r' for read, 'r+' for read/write, 'w' for write)
        lock_type: fcntl.LOCK_SH for shared (read) lock, fcntl.LOCK_EX for exclusive (write) lock
    """
    # Ensure config file exists before trying to lock it
    if not config_file:
        logger.warning("config is not initialized, cannot lock file")
        return

    if not os.path.exists(config_file):
        # Create the file if it doesn't exist
        os.makedirs(os.path.dirname(config_file), exist_ok=True)
        with open(config_file, "w") as f:
            # Create minimal config structure
            temp_config = configparser.ConfigParser()
            temp_config[DEFAULT_SECTION] = {}
            temp_config.write(f)

    # Open file in appropriate mode
    if mode == "w":
        file_handle = open(config_file, "w")
    else:
        file_handle = open(config_file, "r+")

    try:
        # Acquire the lock
        fcntl.flock(file_handle.fileno(), lock_type)
        yield file_handle
    finally:
        # Release the lock and close the file
        fcntl.flock(file_handle.fileno(), fcntl.LOCK_UN)
        file_handle.close()


def initialize(data_dir: str):
    """Initialize the config module with a specific data directory"""
    global config_file, _config

    if data_dir:
        config_file = os.path.join(data_dir, "config.ini")
    else:
        config_file = os.path.join(
            os.path.dirname(os.path.realpath(__file__)), "config.ini"
        )

    # Use exclusive lock for initialization
    with _file_lock(mode="r+", lock_type=fcntl.LOCK_EX):
        # Load existing config or create a new one if it doesn't exist
        if os.path.exists(config_file):
            _config.read(config_file)
        else:
            # Ensure the DEFAULT section exists
            if DEFAULT_SECTION not in _config:
                _config[DEFAULT_SECTION] = {}


def _save_config():
    """Save the current configuration to the file."""
    with open(config_file, "w") as configfile:
        _config.write(configfile)


def _reload_config():
    """Reload the configuration from disk if it exists."""
    if os.path.exists(config_file):
        _config.read(config_file)


def get(config_key: str, default_config_value: Any = None) -> Any:
    """
    Returns an object associated with the configuration key. If the key is not
    found, return None if default_config_value is not set. If
    default_config_value is set, then create the key-value pair in the config
    and return the default_config_value.
    """

    # defaults (to prevent nested lock in nested set call)
    default_needed = False

    # Use shared lock for reading
    with _file_lock(mode="r", lock_type=fcntl.LOCK_SH):
        # Reload config from disk to ensure we have the latest version
        _reload_config()

        if config_key in _config[DEFAULT_SECTION]:
            value = json.loads(_config[DEFAULT_SECTION][config_key])
            # logger.debug(f"config get {config_key} {val}")
            return value
        else:
            # Mark that we need to set a default, but do it outside the lock
            default_needed = True

    # If we need to set a default value, do it outside the current lock
    if default_needed and default_config_value is not None:
        set(config_key, default_config_value)
        return default_config_value


def set(config_key: str, config_value: Any) -> Any:
    """
    Sets the configuration key to the given value.

    Returns the original config_value.
    """

    # Prevent reentrant calls that could cause deadlocks
    global _in_set_operation
    if _in_set_operation:
        logger.warning("Reentrant call to set() detected, this could cause deadlocks")
    _in_set_operation = True

    try:
        # Use exclusive lock for writing
        with _file_lock(mode="r+", lock_type=fcntl.LOCK_EX) as file_handle:
            _reload_config()

            # Convert the value to a JSON string
            config_value_str = json.dumps(config_value)

            # Set the value
            _config[DEFAULT_SECTION][config_key] = config_value_str
            logger.debug(f"config set {config_key} {config_value}")

            # Save to file - we need to seek to beginning and truncate
            file_handle.seek(0)
            file_handle.truncate()
            _config.write(file_handle)

            return config_value
    finally:
        _in_set_operation = False


def items() -> dict[str, Any]:
    """
    Returns all key-value pairs in the configuration.
    """
    # Use shared lock for reading
    with _file_lock(mode="r", lock_type=fcntl.LOCK_SH):
        _reload_config()

        items_dict: dict[str, Any] = {}

        for key, value in _config[DEFAULT_SECTION].items():
            # Convert the JSON string back to a Python object
            items_dict[key] = json.loads(value)

    return items_dict
