import { Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DataSource, Repository } from 'typeorm';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
import { LmsTableSettingsEntity } from 'src/entities/LmsTableSettings.entity';
import { TableSettingsDto } from './dto/table-settings-dto';
import { RECORDS_PER_PAGE } from 'src/constants/urls';
import { IColumnConfig, TableSettings } from 'src/utils/interfaces/config';
import { SessionDataProps, UserData } from 'src/utils/decorators';
import { MenuGroupsEntity } from 'src/entities/MenuGroups.entity';
import { TimezoneMastersEntity } from 'src/entities/TimezoneMasters.entity';
import { MenuGroup } from 'src/constants/types/menus';
import { UserEntity } from 'src/entities/User.entity';
import { FavoriteMenuEntity } from 'src/entities/FavoriteMenu.entity';
import { CreateMenuBookmarkDto } from './dto/create-menu-bookmark.dto';
import { plainToInstance } from 'class-transformer';
import { UserAuthorizationsEntity } from 'src/entities/UserAuthorizations.entity';
import { RecentVisitedMenusDto } from './dto/create-recent-visited.dto';
import { RecentVisitedMenusEntity } from 'src/entities/RecentVisitedMenus.entity';
import { VisitedMenuItemDto } from './dto/visited-menus.dto';
import { UsersEntity } from 'src/entities/Users.entity';

/**
 * Global Service
 *
 * Main service for handling user preferences, settings, and global application features.
 * This service consolidates functionality from what might be better organized as separate services:
 * - Table settings management
 * - Menu permission filtering
 * - Timezone data retrieval
 * - Menu bookmark management
 * - Role permission verification
 *
 * The service follows the Single Responsibility Principle violation by handling multiple
 * unrelated concerns. Consider splitting into focused services for better maintainability.
 *
 * @Injectable - Marks this class as a NestJS injectable service
 */
@Injectable()
export class GlobalService {
  constructor(
    private datasource: DataSource,
    @Inject(CACHE_MANAGER) private cacheManager: Cache,
    @InjectRepository(LmsTableSettingsEntity)
    private readonly tableSettingRepo: Repository<LmsTableSettingsEntity>,
    @InjectRepository(MenuGroupsEntity)
    private readonly menuGroupRepo: Repository<MenuGroupsEntity>,
    @InjectRepository(TimezoneMastersEntity)
    private readonly timezoneMasterRepo: Repository<TimezoneMastersEntity>,
    @InjectRepository(UserEntity)
    private readonly userRepo: Repository<UserEntity>,
    @InjectRepository(FavoriteMenuEntity)
    private readonly favMenuRepo: Repository<FavoriteMenuEntity>,
    @InjectRepository(UserAuthorizationsEntity)
    private readonly userAuthRepo: Repository<UserAuthorizationsEntity>,
    @InjectRepository(RecentVisitedMenusEntity)
    private readonly recentVisitedMenusRepo: Repository<RecentVisitedMenusEntity>,
    @InjectRepository(UsersEntity)
    private readonly userRepository: Repository<UsersEntity>,
  ) {}

  /**
   * Verifies if a user's role permissions have been modified.
   *
   * This is typically used to check if a user's permissions have changed
   * since their last login, which might require re-evaluating their access
   * or prompting them to re-login.
   *
   * @param userId - User ID to check
   * @returns Boolean indicating if role permissions were modified
   *
   * @example
   * // Check if user 123 has had role modifications
   * const hasModified = await verifyRolePermissionsModification('123');
   * if (hasModified) {
   *   // Force re-login or refresh permissions
   * }
   */
  async verifyRolePermissionsModification(userId: string): Promise<boolean> {
    const user = await this.userRepo
      .createQueryBuilder('user')
      .select(['user.id', 'user.role_modified'])
      .where('user.id = :userId', { userId })
      .getOne();

    // Check if role_modified flag is set to 1 (true)
    if (user && Number(user.role_modified) === 1) {
      return true;
    }
    return false;
  }

  /**
   * Converts a simple column mapping to the full IColumnConfig format.
   *
   * Creates default column configurations with all columns visible when
   * no user-specific settings exist.
   *
   * @private
   * @param columns - Simple object mapping column keys to titles
   * @returns Array of IColumnConfig objects with all columns enabled
   *
   * @note There's a type issue: params.columns in getTableSettings is typed
   * as IColumnConfig[] but this method expects IColumnConfig (object)
   */
  private setDefaultColumns(columns: IColumnConfig): IColumnConfig[] {
    // Convert object entries to array of column configs
    return Object.entries(columns).map(([key, title]) => ({
      key,
      title,
      is_checked: true, // All columns visible by default
    }));
  }

  /**
   * Retrieves table settings for a user with caching support.
   *
   * Implements a cache-first strategy:
   * 1. Check Redis cache
   * 2. If miss, query database
   * 3. Cache result and return
   *
   * @param params - Table settings parameters
   * @param userId - Authenticated user ID (should match params.user_id)
   * @returns TableSettings object with columns and pagination
   *
   * @throws May throw if database query fails
   *
   * @note There's a type inconsistency: params.columns is IColumnConfig[]
   * but setDefaultColumns expects IColumnConfig (object)
   */
  async getTableSettings(
    params: TableSettingsDto,
    userId: number,
  ): Promise<TableSettings> {
    // Construct cache key: table_setting:{user_id}:{setting_type}
    const cacheKey = `table_setting:${params.user_id}:${params.setting_type}`;

    // Step 1: Try cache first
    const cachedData = await this.cacheManager.get<TableSettings>(cacheKey);
    if (cachedData) {
      return cachedData; // Cache hit
    }

    // Step 2: Query database for user settings
    const userSettings = await this.tableSettingRepo.findOne({
      where: {
        user_id: userId,
        setting_type: params.setting_type,
        // affiliate_user_id: params.affiliate_user_id, // Future feature
      },
    });

    // Step 3: Determine column configuration
    // FIX NEEDED: params.columns is array but setDefaultColumns expects object
    const customColumns =
      !userSettings?.setting_pack || userSettings.setting_pack === null
        ? this.setDefaultColumns(params.columns as any) // Type cast needed due to bug
        : userSettings.setting_pack;

    // Step 4: Determine pagination preference
    const recordPerPage =
      userSettings?.record_per_page && userSettings.record_per_page > 0
        ? userSettings.record_per_page // User preference
        : RECORDS_PER_PAGE; // Application default

    // Step 5: Prepare result
    const result = { columns: customColumns, record_per_page: recordPerPage };

    // Step 6: Cache for future requests
    await this.cacheManager.set(cacheKey, result);

    return result;
  }

  /**
   * Fetches menu items accessible to the user based on permissions.
   *
   * Builds a hierarchical menu structure (Groups → Menus → Actions)
   * filtered by the user's specific permissions.
   *
   * @param clientData - Session data with user info
   * @param permitted - Permission mapping: {controller: ['action1', 'action2']}
   * @returns Filtered menu hierarchy
   */
  async fetchUserAccessibleMenuItems(
    clientData: SessionDataProps,
    permitted: Record<string, string[]>,
  ) {
    // Query all active menu groups with their nested relationships
    const items = await this.menuGroupRepo
      .createQueryBuilder('group')
      .leftJoinAndSelect('group.menus', 'menu')
      .leftJoinAndSelect('menu.actions', 'actions')
      .where('group.active = :isActive', { isActive: 1 })
      .andWhere('menu.active = :isActive', { isActive: 1 })
      .andWhere('actions.active = :isActive', { isActive: 1 })
      .andWhere('actions.is_visible = :isVisible', { isVisible: 1 })
      .orderBy('actions.order_no', 'ASC')
      .addOrderBy('group.order_no', 'ASC')
      .addOrderBy('menu.order_no', 'ASC')
      .getMany();

    // Filter based on user permissions
    return this.filterMenuGroups(items as any, permitted);
  }

  /**
   * Retrieves all available timezones with country information.
   *
   * @param clientData - Session data (unused but available for extensions)
   * @returns Array of timezone entities with country relations
   */
  async fetchTimezones(
    clientData: SessionDataProps,
  ): Promise<TimezoneMastersEntity[]> {
    return await this.timezoneMasterRepo
      .createQueryBuilder('timezone')
      .leftJoinAndSelect('timezone.country', 'country')
      .orderBy('timezone.country_name', 'ASC')
      .groupBy('timezone.timezone_id')
      .getMany();
  }

  /**
   * Filters menu groups based on user permissions.
   *
   * Recursively filters the menu hierarchy:
   * 1. Actions → only include permitted actions
   * 2. Menus → remove if no permitted actions remain
   * 3. Groups → remove if no permitted menus remain
   *
   * @private
   * @param menuGroups - Full menu hierarchy
   * @param permitted - User's permission mapping
   * @returns Filtered menu hierarchy
   */
  private filterMenuGroups(
    menuGroups: MenuGroup[],
    permitted: Record<string, string[]>,
  ): MenuGroup[] {
    return menuGroups
      .map((group) => {
        // Filter menus within this group
        const filteredMenus = group.menus
          .map((menu) => {
            // Get allowed actions for this controller
            const allowedActions = permitted[menu.controller] ?? [];

            // Filter to only permitted actions
            const filteredActions = menu.actions.filter((action) =>
              allowedActions.includes(action.action),
            );

            return { ...menu, actions: filteredActions };
          })
          .filter((menu) => menu.actions.length > 0); // Remove menus with no actions

        return { ...group, menus: filteredMenus };
      })
      .filter((group) => group.menus.length > 0); // Remove groups with no menus
  }

  /**
   * Manages user menu bookmarks (add/delete operations).
   *
   * Handles bookmark lifecycle:
   * - Add new bookmarks
   * - Delete existing bookmarks
   * - Returns updated bookmark list
   *
   * @param userId - User ID
   * @param createDto - Bookmark operation details
   * @returns Updated list of user's bookmarks
   *
   * @note Action type discrepancy: DTO likely uses 'remove' but this uses 'delete'
   */
  async manageBookmarkMenu(userId: number, createDto: CreateMenuBookmarkDto) {
    const menuLink = createDto.menu_link;

    // Determine lookup condition
    const condition = createDto.id
      ? { id: createDto.id } // Specific bookmark
      : { user_id: userId, menu_link: menuLink }; // User's bookmark for this menu

    // Check if bookmark exists
    const existing = await this.favMenuRepo.findOne({
      where: condition,
    });

    // Add new bookmark
    if (!existing && createDto.action === 'add') {
      const bookmark = plainToInstance(FavoriteMenuEntity, {
        ...createDto,
        user_id: userId,
        platform: 2, // Platform identifier
      });
      await this.favMenuRepo.save(bookmark);
    }

    // Delete existing bookmark
    // BUG: Should check for 'remove' not 'delete' (depends on DTO enum)
    if (existing && createDto.action === 'delete' && createDto.id) {
      await this.favMenuRepo.delete({ id: createDto?.id });
    }

    // Return updated list
    return await this.favMenuRepo.find({
      where: { user_id: userId },
      order: { created: 'DESC' },
    });
  }
  async getUser(id: number) {
    return this.userRepository
      .createQueryBuilder('user')
      // .leftJoinAndSelect('user.center_home', 'center_home')
      // .leftJoinAndSelect('user.theme', 'theme')
      // .leftJoinAndSelect('user.designation', 'designation')
      // .leftJoinAndSelect('user.user_roles', 'ur')
      // .leftJoinAndSelect('ur.role', 'role')
      // .leftJoinAndSelect('user.favorite_menu', 'favorite_menu')
      // .leftJoinAndSelect('user.recent_visited_menus', 'recent_visited_menus')
      .where('user.id = :id', { id })
      .getOne();
  }

  /**
   * Find a single User Details by ID.
   *
   * @param id - Numeric ID of the User Details
   * @returns Single User Details entity or null
   */
  async getUserInfo(id: number) {
    return this.userRepo
      .createQueryBuilder('user')
      .leftJoinAndSelect('user.center_home', 'center_home')
      .leftJoinAndSelect('user.theme', 'theme')
      .leftJoinAndSelect('user.designation', 'designation')
      .leftJoinAndSelect('user.user_roles', 'ur')
      .leftJoinAndSelect('ur.role', 'role')
      .leftJoinAndSelect('user.favorite_menu', 'favorite_menu')
      .leftJoinAndSelect('user.recent_visited_menus', 'recent_visited_menus')
      .where('user.id = :id', { id })
      .getOne();
  }

  async authenticateUser(token: string) {
    return this.userAuthRepo
      .createQueryBuilder('auth_user')
      .where('auth_user.token = :token', { token })
      .getOne();
  }

  async updateUser(id: number, payload: Partial<UserEntity>) {
    return await this.userRepo.update({ id }, payload);
  }

  async updateRecentVisitedMenus(userId: number, newMenu: VisitedMenuItemDto) {
    const record = await this.recentVisitedMenusRepo.findOne({
      where: { user_id: userId },
    });

    const currentMenus = record?.visited_menus || [];

    const updatedMenus = this.normalizeVisitedMenus(currentMenus, newMenu);

    return this.recentVisitedMenusRepo.save({
      ...record,
      user_id: userId,
      visited_menus: updatedMenus,
    });
  }

  private normalizeVisitedMenus(
    existing: VisitedMenuItemDto[] = [],
    incoming: VisitedMenuItemDto,
  ): VisitedMenuItemDto[] {
    // Remove if already exists (by href and platform)
    const filtered = existing.filter((item) => item.href !== incoming.href);

    // Add new item at start (most recent)
    const updated = [incoming, ...filtered];

    // Keep only latest 10
    return updated.slice(0, 10);
  }
}
