from flask import Flask, render_template, jsonify
import os
from dotenv import load_dotenv
import sys
import traceback
import logging
import platform

# Set up logging
# Set up logging configuration
if os.getenv("FIREFLY_DEBUG") == "true":
    logging.basicConfig(
        level=logging.DEBUG,
        format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
        handlers=[
            logging.FileHandler("firefly_viewer.log"),
            logging.StreamHandler(sys.stdout),
        ],
    )
else:
    # Basic configuration with less verbose logging when debug is disabled
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    )

logger = logging.getLogger("FireflyViewer")

# Load environment variables
load_dotenv()

app = Flask(__name__)

class FireflyDatabase:
    """Wrapper for Firefly database client library"""
    def __init__(self, host="localhost", port=6379, password=None):
        self.client = None
        self.lib = None
        self._load_library()
        self._connect(host, port, password)

    def _load_library(self):
        try:
            from ctypes import cdll, c_char_p, c_bool, c_void_p, c_int, POINTER
            
            # Determine the library path based on the platform
            lib_path = os.path.dirname(os.path.abspath(__file__))
            
            if platform.system() == "Windows":
                lib_file = os.path.join(lib_path, "libFireflyClient.dll")
            else:  # Linux/macOS
                lib_file = os.path.join(lib_path, "libFireflyClient.so")
                
            if not os.path.exists(lib_file):
                raise FileNotFoundError(f"Firefly library not found: {lib_file}")
                
            # Load the library
            self.lib = cdll.LoadLibrary(lib_file)
            if self.lib is None:
                raise OSError("Failed to load the Firefly library")
                
            logger.debug(f"Firefly library loaded from: {lib_file}")
            
            # Set argument and return types
            self.lib.CreateClient.restype = c_void_p
            self.lib.CreateClient.argtypes = [c_char_p, c_int]
            self.lib.DestroyClient.argtypes = [c_void_p]
            self.lib.Authenticate.restype = c_bool
            self.lib.Authenticate.argtypes = [c_void_p, c_char_p]
            self.lib.StringSet.restype = c_bool
            self.lib.StringSet.argtypes = [c_void_p, c_char_p, c_char_p]
            self.lib.StringGet.restype = c_char_p
            self.lib.StringGet.argtypes = [c_void_p, c_char_p]
            self.lib.FreeString.argtypes = [c_char_p]
            self.lib.ListLeftPush.restype = c_int
            self.lib.ListLeftPush.argtypes = [c_void_p, c_char_p, c_char_p]
            self.lib.ListRightPush.restype = c_int
            self.lib.ListRightPush.argtypes = [c_void_p, c_char_p, c_char_p]
            self.lib.ListLeftPop.restype = c_char_p
            self.lib.ListLeftPop.argtypes = [c_void_p, c_char_p]
            self.lib.ListRightPop.restype = c_char_p
            self.lib.ListRightPop.argtypes = [c_void_p, c_char_p]
            self.lib.ListRange.restype = c_char_p
            self.lib.ListRange.argtypes = [c_void_p, c_char_p, c_int, c_int]
            self.lib.ListIndex.restype = c_char_p
            self.lib.ListIndex.argtypes = [c_void_p, c_char_p, c_int]
            self.lib.ListSet.restype = c_bool
            self.lib.ListSet.argtypes = [c_void_p, c_char_p, c_int, c_char_p]
            self.lib.ListPosition.restype = c_int
            self.lib.ListPosition.argtypes = [c_void_p, c_char_p, c_char_p, c_int, c_int]
            self.lib.ListTrim.restype = c_bool
            self.lib.ListTrim.argtypes = [c_void_p, c_char_p, c_int, c_int]
            self.lib.ListRemove.restype = c_int
            self.lib.ListRemove.argtypes = [c_void_p, c_char_p, c_int, c_char_p]
            self.lib.HashSet.restype = c_bool
            self.lib.HashSet.argtypes = [c_void_p, c_char_p, c_char_p, c_char_p]
            self.lib.HashGet.restype = c_char_p
            self.lib.HashGet.argtypes = [c_void_p, c_char_p, c_char_p]
            self.lib.HashDelete.restype = c_bool
            self.lib.HashDelete.argtypes = [c_void_p, c_char_p, c_char_p]
            self.lib.HashFieldExists.restype = c_bool
            self.lib.HashFieldExists.argtypes = [c_void_p, c_char_p, c_char_p]
            self.lib.HashGetAll.restype = c_char_p
            self.lib.HashGetAll.argtypes = [c_void_p, c_char_p]
            self.lib.ExecuteCommand.restype = c_char_p
            self.lib.ExecuteCommand.argtypes = [c_void_p, c_char_p, c_char_p]
            
            logger.info("Library loaded successfully")
        except Exception as e:
            logger.error(f"Failed to load library: {e}")
            raise

    def _connect(self, host, port, password=None):
        try:
            # Convert host to bytes for C API
            host_bytes = host.encode("utf-8")
            logger.debug(f"Connecting to {host}:{port}")

            # Create client
            self.client = self.lib.CreateClient(host_bytes, port)
            if not self.client:
                raise ConnectionError(f"Failed to connect to Firefly server at {host}:{port}")
            logger.debug("Client created successfully")

            # Authenticate if password is provided
            if password:
                password_bytes = password.encode("utf-8")
                logger.debug("Authenticating...")
                if not self.lib.Authenticate(self.client, password_bytes):
                    self.close()
                    raise ConnectionError("Authentication failed")
                logger.debug("Authentication successful")
        except Exception as e:
            logger.error(f"Connection error: {e}")
            if self.client:
                self.close()
            raise

    def close(self):
        if self.client:
            self.lib.DestroyClient(self.client)
            self.client = None

    def _check_connection(self):
        if not self.client:
            raise ConnectionError("Not connected to Firefly server")
            
    def _to_bytes(self, value):
        """Convert a value to bytes for C API"""
        if isinstance(value, bytes):
            return value
        return str(value).encode("utf-8")
        
    def _from_bytes(self, value):
        """Convert bytes from C API to string"""
        try:
            if value is None:
                return None
            if not value:  # Zero value check
                return ""
                
            # Check if value is already a string
            if isinstance(value, str):
                return value
                
            # Check if value is bytes
            if isinstance(value, bytes):
                return value.decode("utf-8")
                
            # Otherwise, assume it's a pointer and use FreeString
            return value.decode("utf-8")
        except Exception as e:
            logger.error(f"Error in _from_bytes: {e}")
            return None
            
    def _free_string(self, ptr):
        """Free a string pointer allocated by the C API"""
        try:
            # Only free if it's a pointer (not bytes or string)
            if ptr and ptr != 0 and not isinstance(ptr, (bytes, str)):
                self.lib.FreeString(ptr)
        except Exception as e:
            logger.error(f"Error in _free_string: {e}")

    def _execute_command(self, command, *args):
        """Execute a command with proper error handling"""
        try:
            # Convert command to bytes
            cmd_bytes = command.encode("utf-8") if isinstance(command, str) else command
            
            # Special handling for KEYS command
            if command.upper() == "KEYS":
                # For KEYS command, we need to pass the pattern as the second argument
                # and an empty string as the third argument
                pattern = args[0] if args else "*"
                pattern_bytes = pattern.encode("utf-8") if isinstance(pattern, str) else pattern
                logger.debug(f"Executing KEYS command with pattern: {pattern}")
                logger.debug(f"Command bytes: {cmd_bytes}, Pattern bytes: {pattern_bytes}")
                
                # Log the client pointer for debugging
                logger.debug(f"Client pointer before KEYS command: {self.client}")
                
                # Try a different approach for KEYS command
                try:
                    # First try with the standard approach
                    logger.debug("Calling ExecuteCommand for KEYS (standard approach)...")
                    result = self.lib.ExecuteCommand(self.client, cmd_bytes, pattern_bytes, b"")
                    logger.debug(f"KEYS command result: {result}")
                    
                    # Check if result is None or 0
                    if result is None or result == 0:
                        logger.warning("KEYS command returned NULL or 0 pointer, trying alternative approach")
                        
                        # Try an alternative approach - use a different command format
                        # This is a fallback mechanism in case the standard approach fails
                        logger.debug("Trying alternative approach for KEYS command...")
                        alt_cmd = f"KEYS {pattern}".encode("utf-8")
                        logger.debug(f"Alternative command: {alt_cmd}")
                        result = self.lib.ExecuteCommand(self.client, alt_cmd, b"", b"")
                        logger.debug(f"Alternative KEYS command result: {result}")
                    
                    # If still no result, try one more approach
                    if result is None or result == 0:
                        logger.warning("Alternative KEYS approach also failed, trying one more approach")
                        # Try with a different parameter order
                        logger.debug("Trying with different parameter order...")
                        result = self.lib.ExecuteCommand(self.client, cmd_bytes, b"", pattern_bytes)
                        logger.debug(f"Third KEYS approach result: {result}")
                    
                    # If we still have no result, return an empty list
                    if result is None or result == 0:
                        logger.warning("All KEYS approaches failed, returning empty list")
                        return "[]"
                    
                    # Check if result is bytes or pointer
                    if isinstance(result, bytes):
                        logger.debug(f"KEYS command returned bytes: {result}")
                        try:
                            # Try to decode the bytes
                            result_str = result.decode("utf-8")
                            logger.debug(f"Decoded KEYS result: {result_str}")
                            return result_str
                        except Exception as e:
                            logger.error(f"Error decoding KEYS result bytes: {e}")
                            return "[]"
                    else:
                        # Convert result to string with error handling
                        try:
                            result_str = self._from_bytes(result)
                            logger.debug(f"KEYS command result string: {result_str}")
                            self._free_string(result)
                            return result_str
                        except Exception as e:
                            logger.error(f"Error converting KEYS result to string: {e}")
                            self._free_string(result)
                            return "[]"
                except Exception as e:
                    logger.error(f"Error in KEYS command execution: {e}")
                    logger.error(traceback.format_exc())
                    return "[]"
            else:
                # For other commands, convert args to bytes
                arg_bytes = []
                for arg in args:
                    if isinstance(arg, str):
                        arg_bytes.append(arg.encode("utf-8"))
                    elif isinstance(arg, int):
                        # For integer arguments, we need to pass them directly
                        arg_bytes.append(arg)
                    else:
                        arg_bytes.append(arg)
                
                # Execute the command
                logger.debug(f"Executing command: {command} with args: {args}")
                
                # The ExecuteCommand function requires at least 3 arguments
                # If we have no args, use empty string as the second argument
                if len(arg_bytes) == 0:
                    logger.debug(f"Command with no args: {cmd_bytes}, empty, empty")
                    result = self.lib.ExecuteCommand(self.client, cmd_bytes, b"", b"")
                elif len(arg_bytes) == 1:
                    logger.debug(f"Command with one arg: {cmd_bytes}, {arg_bytes[0]}, empty")
                    result = self.lib.ExecuteCommand(self.client, cmd_bytes, arg_bytes[0], b"")
                else:
                    # For multiple arguments, we need to join them with spaces
                    arg_str = " ".join([str(arg) for arg in args])
                    arg_bytes = arg_str.encode("utf-8")
                    logger.debug(f"Command with multiple args: {cmd_bytes}, {arg_bytes}, empty")
                    result = self.lib.ExecuteCommand(self.client, cmd_bytes, arg_bytes, b"")
                
                logger.debug(f"Command result: {result}")
            
            # Check if result is None or 0
            if result is None or result == 0:
                logger.warning(f"Command {command} returned no result")
                return None
                
            # Check if result is bytes or pointer
            if isinstance(result, bytes):
                logger.debug(f"Command {command} returned bytes: {result}")
                try:
                    # Try to decode the bytes
                    result_str = result.decode("utf-8")
                    logger.debug(f"Decoded result: {result_str}")
                    return result_str
                except Exception as e:
                    logger.error(f"Error decoding result bytes: {e}")
                    return None
            else:
                # Convert result to string
                result_str = self._from_bytes(result)
                self._free_string(result)
                
                logger.debug(f"Command {command} result: {result_str}")
                return result_str
        except Exception as e:
            logger.error(f"Error executing command {command}: {e}")
            logger.error(traceback.format_exc())
            return None

    def get_all_keys(self):
        """Get all keys from the database and categorize them by type"""
        try:
            self._check_connection()
            
            logger.debug("Executing KEYS command to get all keys")
            # Get all keys using KEYS command
            keys_str = self._execute_command("KEYS", "*")
            
            if not keys_str:
                logger.warning("No keys found in the database")
                return {
                    'strings': [],
                    'lists': [],
                    'hashes': []
                }
            
            # Handle the case where keys_str is "[]" (empty list)
            if keys_str == "[]":
                logger.info("KEYS command returned empty list")
                return {
                    'strings': [],
                    'lists': [],
                    'hashes': []
                }
                
            logger.debug(f"Found keys: {keys_str}")
            
            # Try to parse the keys string
            try:
                # Split by newlines and clean up Redis protocol prefixes
                keys = []
                for k in keys_str.strip().split('\n'):
                    if k:
                        # Remove Redis protocol prefixes (+, -, :, etc.)
                        if k.startswith(('+', '-', ':', '$', '*')):
                            k = k[1:]
                        keys.append(k)
                logger.debug(f"Parsed {len(keys)} keys")
            except Exception as e:
                logger.error(f"Error parsing keys string: {e}")
                logger.error(f"Keys string: {keys_str}")
                # Return empty lists if we can't parse the keys
                return {
                    'strings': [],
                    'lists': [],
                    'hashes': []
                }
            
            # Initialize result containers
            strings = []
            lists = []
            hashes = []
            
            # Process each key
            for key in keys:
                try:
                    logger.debug(f"Processing key: {key}")
                    
                    # Use TYPE command to determine the type of the key
                    type_str = self._execute_command("TYPE", key)
                    logger.debug(f"TYPE command result for {key}: {type_str}")
                    
                    # Remove Redis protocol prefixes and newlines
                    if type_str:
                        # Remove protocol prefixes
                        if type_str.startswith(('+', '-', ':', '$', '*')):
                            type_str = type_str[1:]
                        # Remove newlines and whitespace
                        type_str = type_str.strip()
                    
                    logger.debug(f"Cleaned type for {key}: '{type_str}'")
                    
                    # Process based on type
                    if type_str == "string":
                        # Get the string value
                        value = self._execute_command("GET", key)
                        if value is not None and not value.startswith(("$-1", "-ERR")):
                            # Remove Redis protocol prefixes
                            if value.startswith(('+', '-', ':', '$', '*')):
                                value = value[1:]
                            strings.append({"key": key, "value": value})
                            logger.debug(f"Added string key: {key} with value: {value}")
                    
                    elif type_str == "list":
                        # Get the list values
                        value_str = self._execute_command("LRANGE", key, "0", "-1")
                        if value_str is not None and not value_str.startswith(("$-1", "-ERR")):
                            values = []
                            
                            # Split into lines
                            lines = value_str.split('\n')
                            
                            # Skip the first line if it contains the count (starts with *)
                            start_idx = 1 if lines and lines[0].strip().startswith('*') else 0
                            
                            # Process remaining lines
                            for v in lines[start_idx:]:
                                v = v.strip()
                                # Skip empty lines
                                if not v:
                                    continue
                                
                                # Remove Redis protocol prefixes and quotes
                                if v.startswith(('+', '-', ':', '$')):
                                    v = v[1:]
                                v = v.strip().strip('"').strip("'")
                                
                                if v:  # Only add non-empty values
                                    values.append(v)
                            
                            # Reverse the list to show latest values at the top
                            values.reverse()
                            lists.append({"key": key, "value": values})
                            logger.debug(f"Added list key: {key} with {len(values)} items (reversed order)")
                    
                    elif type_str == "hash":
                        # Get the hash values
                        hash_str = self._execute_command("HGETALL", key)
                        if hash_str is not None and not hash_str.startswith(("$-1", "-ERR")):
                            # Parse field=value format
                            hash_data = {}
                            if hash_str:
                                # Split into lines
                                lines = hash_str.strip().split('\n')
                                logger.debug(f"Hash lines: {lines}")
                                
                                # Skip the first line if it starts with '*' (array prefix)
                                start_idx = 1 if lines and lines[0].startswith('*') else 0
                                
                                # Process pairs of lines (field, value)
                                for i in range(start_idx, len(lines), 2):
                                    if i + 1 < len(lines):
                                        # Get field and value, removing protocol prefixes
                                        field = lines[i].strip()
                                        value = lines[i + 1].strip()
                                        
                                        # Remove protocol prefixes
                                        if field.startswith(('+', '-', ':', '$', '*')):
                                            field = field[1:]
                                        if value.startswith(('+', '-', ':', '$', '*')):
                                            value = value[1:]
                                        
                                        # Handle array values (e.g., [4000,4000,0])
                                        if value.startswith('[') and value.endswith(']'):
                                            try:
                                                # Remove brackets and split by comma
                                                array_str = value[1:-1]
                                                array_values = [v.strip() for v in array_str.split(',') if v.strip()]
                                                value = array_values
                                            except Exception as e:
                                                logger.error(f"Error parsing array value: {e}")
                                        
                                        hash_data[field] = value
                                
                            logger.debug(f"Parsed hash data: {hash_data}")
                            hashes.append({"key": key, "value": hash_data})
                            logger.debug(f"Added hash key: {key} with {len(hash_data)} fields")
                    
                    else:
                        # Unknown type or error
                        logger.warning(f"Unknown type '{type_str}' for key: {key}")
                except Exception as e:
                    logger.error(f"Error processing key {key}: {e}")
                    continue
            
            logger.debug(f"Found {len(strings)} string keys, {len(lists)} list keys, and {len(hashes)} hash keys")
            return {
                'strings': strings,
                'lists': lists,
                'hashes': hashes
            }
        except Exception as e:
            logger.error(f"Error in get_all_keys: {e}")
            logger.error(traceback.format_exc())
            return {
                'strings': [],
                'lists': [],
                'hashes': []
            }

def create_firefly_client():
    try:
        return FireflyDatabase(
            host=os.getenv('FIREFLY_HOST', 'localhost'),
            port=int(os.getenv('FIREFLY_PORT', 6379)),
            password=os.getenv('FIREFLY_PASSWORD', None)
        )
    except Exception as e:
        logger.error(f"Failed to create Firefly client: {e}")
        return None

@app.route('/')
def index():
    client = create_firefly_client()
    is_connected = client is not None
    if client:
        client.close()
    return render_template('index.html', is_connected=is_connected)

@app.route('/api/keys')
def get_keys():
    logger.debug("API endpoint /api/keys called")
    client = create_firefly_client()
    
    if not client:
        logger.error("Failed to create Firefly client in /api/keys endpoint")
        return jsonify({
            'success': False,
            'error': 'Failed to connect to Firefly database',
            'connection_status': False
        })

    try:
        logger.debug("Calling get_all_keys()")
        data = client.get_all_keys()
        logger.debug("Successfully retrieved all keys")
        return jsonify({
            'success': True,
            'data': data,
            'connection_status': True
        })
    except Exception as e:
        logger.error(f"Error fetching keys: {e}")
        logger.error(traceback.format_exc())
        return jsonify({
            'success': False,
            'error': str(e),
            'connection_status': False
        })
    finally:
        if client:
            logger.debug("Closing Firefly client")
            client.close()

if __name__ == '__main__':
    print("Starting Firefly Database Viewer...")
    print("Checking connection...")
    
    client = create_firefly_client()
    if not client:
        print("Warning: Could not connect to Firefly database")
        print("Please make sure Firefly is running and the connection details in .env are correct.")
    else:
        print("Successfully connected to Firefly!")
        client.close()
    
    print("\nStarting web server at http://localhost:5000")
    app.run(debug=True, port=5000) 