import copy
import datetime as dt
import json
import random
from passlib.hash import bcrypt, pbkdf2_sha256
import pandas as pd
from typing import Any
from DataCache_v3 import DataCache


class BaseUser:
    """
    Handles basic user data retrieval and manipulation.
    This is the base class for all user-related operations.
    """

    def __init__(self, data_cache: DataCache):
        """
        Initialize the BaseUser with caching and database interaction capabilities.

        :param data_cache: Object responsible for managing cached data and database interaction.
        """
        self.data = data_cache
        # Create a table-based cache with specified columns
        self.data.create_cache(name='users',
                               cache_type='table',
                               size_limit=100,
                               eviction_policy='deny',
                               default_expiration=dt.timedelta(hours=24),
                               columns=["id", "user_name", "status", "chart_views", "email",
                                        "active_exchanges", "configured_exchanges", "password",
                                        "api_keys", "signin_time", "active_indicators"]
                               )

    def get_id(self, user_name: str) -> int:
        """
        Retrieves the user ID based on the username.

        :param user_name: The name of the user.
        :return: The ID of the user as an integer (native Python int).
        """
        user_id = self.data.get_datacache_item(
            item_name='id',
            cache_name='users',
            filter_vals=('user_name', user_name)
        )
        # Convert numpy.int64 to native int for SQLite compatibility
        return int(user_id) if user_id is not None else None

    def get_username(self, user_id: int) -> str:
        """
        Retrieves the user username based on the ID.

        :param user_id: The id of the user.
        :return: The name of the user as a str.
        """
        return self.data.get_datacache_item(
            item_name='user_name',
            cache_name='users',
            filter_vals=('id', user_id)
        )

    def get_user_by_id(self, user_id: int) -> dict | None:
        """
        Retrieves user data as a dict based on the user ID.

        :param user_id: The ID of the user.
        :return: A dict containing the user's data, or None if not found.
        """
        try:
            user_df = self.data.get_rows_from_datacache(
                cache_name='users', filter_vals=[('id', user_id)])

            if user_df is None or user_df.empty:
                return None

            # Convert first row to dict
            return user_df.iloc[0].to_dict()
        except Exception:
            return None

    def _remove_user_from_memory(self, user_name: str) -> None:
        """
        Private method to remove a user's data from the cache (memory).

        :param user_name: The name of the user to remove from the cache.
        """
        # Remove the user from the cache only
        self.data.remove_row_from_datacache(cache_name='users',
                                            filter_vals=[('user_name', user_name)],
                                            remove_from_db=False)

    def delete_user(self, user_name: str) -> None:
        """
        Deletes the user from both the cache and the database.

        :param user_name: The name of the user to delete.
        """
        self.data.remove_row_from_datacache(filter_vals=[('user_name', user_name)],
                                            cache_name='users')

    def get_user_data(self, user_name: str) -> pd.DataFrame | None:
        """
        Retrieves user data from the cache or database. If not found in the cache,
        it loads from the database and caches it.

        :param user_name: The name of the user.
        :return: A DataFrame containing the user's data.
        :raises ValueError: If the user is not found in both the cache and the database.
        """
        # Attempt to fetch the user data from the cache or database via DataCache
        user = self.data.get_rows_from_datacache(
            cache_name='users', filter_vals=[('user_name', user_name)])

        if user is None or user.empty:
            raise ValueError(f"User '{user_name}' not found in database or cache!")

        return user

    def modify_user_data(self, username: str, field_name: str, new_data: Any) -> None:
        """
        Updates user data in both the cache and the database. Ensures consistency between cache and database.

        :param username: The name of the user whose data is being modified.
        :param field_name: The field to be updated.
        :param new_data: The new data to be set.
        """
        # Use DataCache to modify the user's data
        self.data.modify_datacache_item(
            cache_name='users',
            filter_vals=[('user_name', username)],
            field_names=(field_name,),
            new_values=(new_data,),
            overwrite='user_name'
        )


class UserAccountManagement(BaseUser):
    """
    Manages user login, logout, validation, and account creation processes.
    """

    def __init__(self, data_cache, max_guests: int = 100):
        """
        Initialize the UserAccountManagement with caching, database interaction,
        and guest management capabilities.

        :param data_cache: Object responsible for managing cached data and database interaction.
        :param max_guests: Maximum number of guests allowed in the system at one time.
        """
        super().__init__(data_cache)

        self.max_guests_factor = max_guests  # Maximum number of guests is determined using this number.

    def notify_user(self, user_id, message):
        user_name = self.get_username(user_id=user_id)
        print(f'Message for {user_name}\n{message}')

    def is_logged_in(self, user_name: str) -> bool:
        """
        Checks if the user is logged in by verifying their status in the cache or database.

        :param user_name: The name of the user.
        :return: True if the user is logged in, False otherwise.
        """
        if user_name is None:
            return False

        try:
            user = self.get_user_data(user_name)
        except ValueError:
            # User doesn't exist in database (may have been deleted)
            return False

        if user is None or user.empty:
            return False

        if user.iloc[0]['status'] == 'logged_in':
            return True

        self._remove_user_from_memory(user_name)
        return False

    def validate_password(self, username: str, password: str) -> bool:
        """
        Validates the user's password against the stored hash.

        :param username: The name of the user.
        :param password: The plain text password to validate.
        :return: True if the password is correct, False otherwise.
        """
        # Retrieve the hashed password using DataCache
        user_data = self.data.get_rows_from_datacache(cache_name='users', filter_vals=[('user_name', username)])

        if user_data is None or user_data.empty:
            return False

        hashed_pass = user_data.iloc[0].get('password')

        if not hashed_pass:
            return False

        # Verify the password using the legacy or current hash format.
        # Some existing records (e.g., default guest) use pbkdf2_sha256 while
        # newer accounts are created with bcrypt.
        try:
            return bcrypt.verify(password, hashed_pass)
        except ValueError:
            try:
                return pbkdf2_sha256.verify(password, hashed_pass)
            except ValueError:
                return False

    def log_in_user(self, username: str, password: str) -> bool:
        """
        Validates user credentials and signs in the user by updating their status.

        :param username: The name of the user.
        :param password: The unencrypted password.
        :return: True on successful login, False otherwise.
        """
        if self.validate_password(username=username, password=password):
            self.modify_user_data(username=username, field_name="status", new_data="logged_in")
            self.modify_user_data(
                username=username,
                field_name="signin_time",
                new_data=dt.datetime.now(dt.timezone.utc).timestamp()
            )
            return True
        return False

    def log_out_user(self, username: str) -> bool:
        """
        Logs out the user by updating their status and removing them from the cache.

        :param username: The name of the user.
        :return: True on successful logout.
        """
        # Update the user's status and sign-in time in both cache and database
        self.modify_user_data(username=username, field_name='status', new_data='logged_out')
        self.modify_user_data(
            username=username,
            field_name='signin_time',
            new_data=dt.datetime.now(dt.timezone.utc).timestamp()
        )

        # Remove the user's data from the cache
        self._remove_user_from_memory(user_name=username)

        return True

    def update_email(self, user_id: int, email: str) -> bool:
        """
        Updates the user's email address.

        :param user_id: The ID of the user.
        :param email: The new email address.
        :return: True on success, False on failure.
        """
        try:
            username = self.get_username(user_id)
            if not username:
                return False

            self.modify_user_data(username=username, field_name='email', new_data=email)
            return True
        except Exception:
            return False

    def update_password(self, user_id: int, current_password: str, new_password: str) -> dict:
        """
        Updates the user's password after validating the current password.

        :param user_id: The ID of the user.
        :param current_password: The current password for verification.
        :param new_password: The new password to set.
        :return: Dict with 'success' key and optionally 'error' key.
        """
        try:
            username = self.get_username(user_id)
            if not username:
                return {'success': False, 'error': 'User not found'}

            # Validate current password
            if not self.validate_password(username=username, password=current_password):
                return {'success': False, 'error': 'Current password is incorrect'}

            # Hash and store new password
            encrypted_password = self.scramble_text(new_password)
            self.modify_user_data(username=username, field_name='password', new_data=encrypted_password)
            return {'success': True}
        except Exception as e:
            return {'success': False, 'error': str(e)}

    def log_out_all_users(self, enforcement: str = 'hard') -> None:
        """
        Logs out all users by updating their status in the database and clearing the data.

        :param enforcement: 'soft' or 'hard' - Determines how strictly users are logged out.
        """
        # if enforcement == 'soft':
        #     self._soft_log_out_all_users()
        # elif enforcement == 'hard':
        #     # Clear all user-related entries from the cache
        #     for index, row in self.data.cache.iterrows():
        #         if 'user_name' in row:
        #             self._remove_user_from_memory(row['user_name'])
        #
        #     df = self.data.get_or_fetch_rows(cache_name='users', filter_vals=('status', 'logged_in'))
        #     if df is not None:
        #         df = df[df.user_name != 'guest']
        #
        #         # Update the status of all logged-in users to 'logged_out'
        #         for user_name in df.user_name.values:
        #             self.modify_user_data(username=user_name, field_name='status', new_data='logged_out')
        # else:
        #     raise ValueError("Invalid enforcement type. Use 'soft' or 'hard'.")
        pass

    def _soft_log_out_all_users(self) -> None:
        """
        Placeholder for soft logout logic. Soft logout might involve suspending logins, halting trades,
        or allowing current operations to finish. (TODO: Implement soft logout logic)
        """
        # TODO: Implement soft logout logic here
        pass

    def user_attr_is_taken(self, attr: str, val: str) -> bool:
        """
        Checks if a specific user attribute (e.g., username, email) is already taken.
        :param attr: The attribute to check (e.g., 'user_name', 'email').
        :param val: The value of the attribute to check.
        :return: True if the attribute is already taken, False otherwise.
        """
        # Use DataCache to check if the attribute is taken
        user_cache = self.data.get_rows_from_datacache('users', [(attr, val)])
        return True if not user_cache.empty else False

    def create_unique_guest_name(self) -> str | None:
        """
        Creates a unique guest username by appending a random suffix.

        :return: A unique guest username or None if the guest limit is reached.
        """
        initial_multiplier = 9  # Start with the initial multiplier
        max_attempts = 10  # Set the maximum number of attempts before increasing the multiplier
        multiplier = initial_multiplier
        attempts = 0

        while True:
            suffix = random.choice(range(0, self.max_guests_factor * multiplier))
            username = f'guest_{suffix}'

            # Check if the username already exists in the database
            if self.data.get_rows_from_datacache(cache_name='users', filter_vals=[('user_name', username)]).empty:
                return username

            attempts += 1

            # If too many attempts have been made, increase the multiplier
            if attempts >= max_attempts:
                multiplier += 1  # Increment the multiplier by 1
                attempts = 0  # Reset the attempt counter

            # Safety net to avoid runaway multipliers
            if multiplier > self.max_guests_factor:
                return None

    def create_guest(self) -> str | None:
        username = self.create_unique_guest_name()
        if username is None:
            return None

        self.create_new_user_in_db(attrs=({'user_name': username},))
        if self.log_in_user(username=username, password='password'):
            return username

        return None  # Failure to log in.

    def create_new_user_in_db(self, attrs: tuple) -> None:
        """
        Creates a new user in the database by modifying a default template.

        :param attrs: A tuple of key-value pairs representing user attributes.
        :raises ValueError: If attrs are not well-formed.
        """
        if not attrs or not all(isinstance(attr, dict) and len(attr) == 1 for attr in attrs):
            raise ValueError("Attributes must be a tuple of single key-value pair dictionaries.")

        # Retrieve the default user template from the database using DataCache
        default_user = self.data.get_rows_from_datacache(cache_name='users', filter_vals=[('user_name', 'guest')])

        if default_user is None or default_user.empty:
            raise ValueError("Default user template not found in the database.")

        # Make a deep copy of the default user to preserve the original template
        new_user = copy.deepcopy(default_user).reset_index(drop=True)

        # Modify the deep copied user template with the provided attributes
        for attr in attrs:
            key, value = next(iter(attr.items()))
            new_user.loc[0, key] = value

        # Remove the 'id' column before inserting into the database
        new_user = new_user.drop(columns='id')

        # Insert the modified user as a single row, skipping cache
        columns = tuple(new_user.columns)
        values = tuple(new_user.iloc[0])
        self.data.insert_row_into_datacache(cache_name="users", columns=columns, values=values, skip_cache=True)

    def create_new_user(self, username: str, email: str, password: str) -> bool:
        """
        Creates a new user in the system if the username and email are not already taken.

        :param username: The desired username.
        :param email: The user's email address.
        :param password: The user's password.
        :return: True if the user was successfully created, False otherwise.
        """
        if self.user_attr_is_taken('user_name', username) or self.user_attr_is_taken('email', email):
            return False
        encrypted_password = self.scramble_text(password)
        attrs = ({'user_name': username}, {'email': email}, {'password': encrypted_password})
        self.create_new_user_in_db(attrs=attrs)
        return True

    def load_or_create_user(self, username: str) -> str:
        """
        Loads existing user data or creates a guest user if the user is not logged in.

        :param username: The username to load or create.
        :return: The username of the loaded or newly created user.
        :raises ValueError: If a guest user cannot be created.
        """
        if not self.is_logged_in(user_name=username):
            username = self.create_guest()
            if not username:
                raise ValueError('GuestLimitExceeded!')
        return username

    @staticmethod
    def scramble_text(some_text: str) -> str:
        """
        Encrypts text using bcrypt with 13 rounds of hashing.

        :param some_text: The text to encrypt.
        :return: The hashed text.
        """
        return bcrypt.using(rounds=13).hash(some_text)


class UserExchangeManagement(UserAccountManagement):
    """
    Manages user exchange-related data and operations.
    """

    def get_api_keys(self, user_name: str, exchange: str) -> dict:
        """
        Retrieves the API keys for a specific exchange associated with a user.

        :param user_name: The name of the user.
        :param exchange: The name of the exchange.
        :return: A dictionary containing the API keys for the exchange.
        """
        user = self.get_user_data(user_name)
        if user is None or user.empty or 'api_keys' not in user.columns:
            return {}

        user_keys = user.iloc[0]['api_keys']
        user_keys = json.loads(user_keys) if user_keys else {}
        return user_keys.get(exchange)

    def update_api_keys(self, api_keys, exchange, user_name: str) -> None:
        """
        Updates the API keys for a specific exchange and user, and activates the exchange.

        :param api_keys: The new API keys to store.
        :param exchange: The exchange for which the keys are being updated.
        :param user_name: The name of the user.
        """
        user = self.get_user_data(user_name)
        user_keys = user.iloc[0]['api_keys']
        user_keys = json.loads(user_keys) if user_keys else {}

        user_keys.update({exchange: api_keys})
        self.modify_user_data(username=user_name,
                              field_name='api_keys',
                              new_data=json.dumps(user_keys))

        configured_exchanges = json.loads(user.iloc[0]['configured_exchanges'])
        if exchange not in configured_exchanges:
            configured_exchanges.append(exchange)
            self.modify_user_data(username=user_name,
                                  field_name='configured_exchanges',
                                  new_data=json.dumps(configured_exchanges))

        self.active_exchange(exchange=exchange, user_name=user_name, cmd='set')

    def get_exchanges(self, user_name: str, category: str) -> list:
        """
        Retrieves the list of active or configured exchanges for a given user.

        :param user_name: The name of the user.
        :param category: The category to retrieve ('active_exchanges' or 'configured_exchanges').
        :return: A list of exchanges or empty ist if user or category is not found.
        """
        try:
            user = self.get_user_data(user_name)
            exchanges = user.iloc[0][category]
            return json.loads(exchanges) if exchanges else []
        except (KeyError, IndexError, json.JSONDecodeError) as e:
            print(f"Error retrieving exchanges for user '{user_name}' and field '{category}': {str(e)}")
            return list()

    def active_exchange(self, exchange: str, user_name: str, cmd: str) -> None:
        """
        Inserts, removes, or toggles an exchange name in a list of active exchanges.

        :param cmd: 'toggle' | 'set' | 'remove' - The action to perform on the exchange.
        :param exchange: The name of the exchange.
        :param user_name: The name of the user executing this command.
        """
        user = self.get_user_data(user_name)
        active_exchanges = user.iloc[0]['active_exchanges']
        if active_exchanges is None:
            active_exchanges = []
        else:
            active_exchanges = json.loads(active_exchanges)

        actions = {
            'toggle': lambda x: active_exchanges.remove(x) if x in active_exchanges else active_exchanges.append(x),
            'set': lambda x: active_exchanges.append(x) if x not in active_exchanges else active_exchanges,
            'remove': lambda x: active_exchanges.remove(x) if x in active_exchanges else active_exchanges
        }
        action = actions.get(cmd)
        if action:
            action(exchange)

        self.modify_user_data(username=user_name, field_name='active_exchanges', new_data=json.dumps(active_exchanges))


class UserIndicatorManagement(UserExchangeManagement):
    """
    Manages user indicators and related operations.
    """

    def get_indicators(self, user_name: str) -> pd.DataFrame | None:
        """
        Retrieves all indicators for a specific user.

        :param user_name: The name of the user.
        :return: A DataFrame containing the user's indicators or None if not found.
        """
        # Retrieve the user's ID
        user_id = int(self.get_id(user_name))

        # Fetch the indicators from the database using DataCache
        df = self.data.get_rows_from_datacache(cache_name='indicators', filter_vals=[('creator', user_id)])

        # If indicators are found, process the JSON fields
        if df is not None and not df.empty:
            # Ensure that we only apply json.loads if the value is not already a dict
            df['source'] = df['source'].apply(lambda x: x if isinstance(x, dict) else json.loads(x))
            df['properties'] = df['properties'].apply(lambda x: x if isinstance(x, dict) else json.loads(x))

        return df

    # def save_indicators(self, indicators: pd.DataFrame) -> None:
    #     """
    #     Stores one or many indicators in the database.
    #
    #     :param indicators: A DataFrame containing indicator attributes and properties.
    #     """
    #     for _, indicator in indicators.iterrows():
    #         try:
    #             # Convert necessary fields to JSON strings
    #             src_string = json.dumps(indicator['source'])
    #             prop_string = json.dumps(indicator['properties'])
    #
    #             # Prepare the values and columns for insertion
    #             values = (indicator['creator'], indicator['name'], indicator['visible'],
    #                       indicator['kind'], src_string, prop_string)
    #             columns = ('creator', 'name', 'visible', 'kind', 'source', 'properties')
    #
    #             # Insert the row into the database and cache using DataCache
    #             self.data.insert_row_into_datacache(cache_name='indicators', columns=columns, values=values)
    #
    #         except Exception as e:
    #             print(f"Error saving indicator {indicator['name']} for creator {indicator['creator']}: {str(e)}")

    def get_chart_view(self, user_name: str, prop: str | None = None):
        """
        Fetches the chart view or one specific property of it for a specific user.

        :param user_name: The name of the user.
        :param prop: Optional specific property of the chart view to retrieve.
        :return: A dictionary of chart views or a specific property if specified.
        """
        user = self.get_user_data(user_name)
        if user.empty:
            return
        chart_view = json.loads(user.iloc[0]['chart_views'])

        if prop is None:
            return chart_view
        if prop == 'exchange_name':
            return chart_view.get('exchange')
        if prop == 'timeframe':
            return chart_view.get('timeframe')
        if prop == 'market':
            return chart_view.get('market')

    def set_chart_view(self, user_name: str, values: dict | str,
                       specific_property: str | None = None, default_market: str | None = None) -> None:
        """
        Sets or updates the chart view for a specific user.

        :param user_name: The name of the user.
        :param values: The values to set for the chart view.
        :param specific_property: Optional specific property of the chart view to modify.
        :param default_market: Default market to display when changing the exchange view.
        :raises ValueError: If inputs are not valid.
        """
        user = self.get_user_data(user_name)

        if specific_property is None:
            if not isinstance(values, dict) or len(values) != 3:
                raise ValueError("Values must be a dictionary with exactly 3 key-value pairs.")
            user['chart_view'] = json.dumps(values)
            return

        if not isinstance(specific_property, str):
            raise ValueError("Specific property must be a string.")

        chart_view = json.loads(user.iloc[0]['chart_views'])
        if specific_property == 'exchange_name':
            if default_market is None:
                raise ValueError("Default market must be provided when setting the exchange name.")
            chart_view['market'] = default_market
            chart_view['exchange'] = values
        elif specific_property == 'timeframe':
            chart_view['timeframe'] = values
        elif specific_property == 'market':
            chart_view['market'] = values
        else:
            raise ValueError(f'{specific_property} is not a specific property of chart_views')

        chart_view_str = json.dumps(chart_view)
        self.modify_user_data(username=user_name, field_name='chart_views', new_data=chart_view_str)


class Users(UserIndicatorManagement):
    """
    Comprehensive user management class that inherits all functionalities from UserIndicatorManagement.
    """

    def __init__(self, data_cache, max_guests: int = 100):
        """
        Initialize the Users class with caching, database interaction, and user management capabilities.

        :param data_cache: Object responsible for managing cached data and database interaction.
        :param max_guests: Maximum number of guests allowed in the system at one time.
        """
        super().__init__(data_cache, max_guests)
