import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { FilterQuery, RequiredEntityData } from '@mikro-orm/core';
import { User } from '../entities/user.entity';
import { UserAddress } from '../entities/user-address.entity';
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
import { EntityManager } from '@mikro-orm/postgresql';
import { UpdateUserDto } from '../dto/update-user.dto';
import { FindUsersDto } from '../dto/find-user.dto';
import { FindWalletTransactionsDto } from '../dto/find-wallet-transactions.dto';
import { CreateWalletTransactionDto } from '../dto/create-wallet-transaction.dto';
import { CreateUserAddressDto } from '../dto/create-user-address.dto';
import { UpdateUserAddressDto } from '../dto/update-user-address.dto';
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
import { UserRepository } from '../repositories/user.repository';
import { normalizePhone } from '../../utils/phone.util';
import { RestRepository } from 'src/modules/restaurants/repositories/rest.repository';
import { WalletTransactionRepository } from '../repositories/wallet-transaction.repository';
import { WalletService } from './wallet.service';
import { WalletTransactionReason, WalletTransactionType } from '../interface/wallet';
import { WalletTransaction } from '../entities/wallet-transaction.entity';

@Injectable()
export class UserService {
  constructor(
    private readonly userRepository: UserRepository,
    private readonly restaurantRepository: RestRepository,
    private readonly walletTransactionRepository: WalletTransactionRepository,
    private readonly walletService: WalletService,
    private readonly em: EntityManager,
  ) { }

  async findOrCreateByPhone(phone: string): Promise<User> {
    const normalizedPhone = normalizePhone(phone);
    let user = await this.userRepository.findOne({ phone: normalizedPhone });

    if (!user) {
      // firstName is required on the entity; use phone as a sensible default
      const _raw = {
        phone: normalizedPhone,
        firstName: '[نام]',
        // keep dates undefined (no value) — cast through unknown to satisfy TS typing
        birthDate: undefined,
        marriageDate: undefined,
        wallet: 0,
        points: 0,
      };

      const createData = _raw as unknown as RequiredEntityData<User>;

      user = this.userRepository.create(createData);
      await this.em.persistAndFlush(user);
    }

    return user;
  }

  // async updateUser(userId: string, dto: UpdateUserDto): Promise<User> {
  //   const user = await this.em.findOneOrFail(User, userId, {}).catch(() => {
  //     throw new NotFoundException(`User with ID ${userId} not found.`);
  //   });

  //   this.em.assign(user, { ...dto });

  //   await this.em.flush();

  //   return user;
  // }

  async findByPhone(phone: string): Promise<User | null> {
    const normalizedPhone = normalizePhone(phone);
    const user = await this.userRepository.findOne({ phone: normalizedPhone });
    if (!user) {
      throw new NotFoundException(`User with phone ${phone} not found.`);
    }
    return user;
  }

  async findById(id: string): Promise<User | null> {
    return this.userRepository.findOne({ id });
  }
  // todo : transaction code here
  async create(phone: string, restId: string): Promise<User> {
    const normalizedPhone = normalizePhone(phone);
    const _raw = {
      phone: normalizedPhone,
      firstName: normalizedPhone,
      birthDate: undefined,
      marriageDate: undefined,
    };
    // get registerscore from restaurant
    const restaurant = await this.restaurantRepository.findOne({ id: restId });
    if (!restaurant) {
      throw new NotFoundException(`Restaurant not found.`);
    }
    const points = Number(restaurant.score?.registerScore || 0);
    const createData = _raw as unknown as RequiredEntityData<User>;

    const user = this.userRepository.create(createData);
    const newWalletTransaction = this.walletTransactionRepository.create({
      user: user,
      restaurant: restaurant,
      balance: 0,
      amount: points,
      type: WalletTransactionType.CREDIT,
      reason: WalletTransactionReason.DEPOSIT
    });
    await this.em.persistAndFlush([user, newWalletTransaction]);
    return user;
  }

  async updateUser(userId: string, restId: string, dto: UpdateUserDto): Promise<User> {
    const user = await this.userRepository.findOne({ id: userId });
    if (!user) {
      throw new NotFoundException(`User with ID ${userId} not found.`);
    }

    // Normalize date strings into Date objects if provided
    const assignData: Partial<User> = { ...dto } as Partial<User>;
    if (dto.birthDate) {
      assignData.birthDate = new Date(dto.birthDate);
    }
    if (dto.marriageDate) {
      assignData.marriageDate = new Date(dto.marriageDate);
    }
    // get restuarant
    this.em.assign(user, assignData);
    await this.em.flush();

    return user;
  }

  async findAll(restId: string, dto: FindUsersDto): Promise<PaginatedResult<User>> {
    const { page = 1, limit = 10, search, orderBy = 'createdAt', order = 'desc' } = dto;

    // 1. Calculate pagination
    const offset = (page - 1) * limit;

    // 2. Build the 'where' filter query
    const where: FilterQuery<User> = {
      orders: {
        restaurant: {
          id: restId,
        },
      },
    };

    // 4. Add 'search' logic (case-insensitive)
    if (search) {
      const searchPattern = `%${search}%`;
      const normalizedSearch = normalizePhone(search);
      const normalizedSearchPattern = `%${normalizedSearch}%`;
      // $ilike is case-insensitive (PostgreSQL). Use $like for MySQL (often CI by default)
      // Search with both original and normalized phone patterns to handle various input formats
      where.$or = [
        { firstName: { $ilike: searchPattern } },
        { lastName: { $ilike: searchPattern } },
        { phone: { $ilike: searchPattern } },
        ...(normalizedSearch !== search ? [{ phone: { $ilike: normalizedSearchPattern } }] : []),
      ];
    }

    // 5. Execute the query using findAndCount
    const [users, total] = await this.userRepository.findAndCount(where, {
      limit,
      offset,
      orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
    });

    // 6. Calculate total pages
    const totalPages = Math.ceil(total / limit);

    // 7. Return the paginated result
    return {
      data: users,
      meta: {
        total,
        page,
        limit,
        totalPages,
      },
    };
  }

  async getUserAddresses(userId: string): Promise<UserAddress[]> {
    const user = await this.userRepository.findOne({ id: userId }, { populate: ['addresses'] });
    if (!user) {
      throw new NotFoundException(`User with ID ${userId} not found.`);
    }

    // Load addresses if not already loaded
    await user.addresses.loadItems();
    return user.addresses.getItems();
  }

  async createUserAddress(userId: string, dto: CreateUserAddressDto): Promise<UserAddress> {
    const user = await this.userRepository.findOne({ id: userId });
    if (!user) {
      throw new NotFoundException(`User with ID ${userId} not found.`);
    }

    // If setting as default, unset other default addresses
    if (dto.isDefault) {
      const existingAddresses = await this.em.find(UserAddress, { user: { id: userId }, isDefault: true });
      for (const address of existingAddresses) {
        address.isDefault = false;
      }
      await this.em.flush();
    }

    // Create new address
    const address = this.em.create(UserAddress, {
      user,
      title: dto.title,
      address: dto.address,
      city: dto.city,
      province: dto.province,
      postalCode: dto.postalCode,
      latitude: dto.latitude,
      longitude: dto.longitude,
      phone: dto.phone,
      isDefault: dto.isDefault ?? false,
    });

    await this.em.persistAndFlush(address);
    return address;
  }

  async getUserAddress(userId: string, addressId: string): Promise<UserAddress> {
    const address = await this.em.findOne(UserAddress, {
      id: addressId,
      user: { id: userId },
    });

    if (!address) {
      throw new NotFoundException(`Address with ID ${addressId} not found for user ${userId}.`);
    }

    return address;
  }

  async updateUserAddress(userId: string, addressId: string, dto: UpdateUserAddressDto): Promise<UserAddress> {
    const address = await this.em.findOne(UserAddress, {
      id: addressId,
      user: { id: userId },
    });

    if (!address) {
      throw new NotFoundException(`Address with ID ${addressId} not found for user ${userId}.`);
    }

    // If setting as default, unset other default addresses
    if (dto.isDefault === true) {
      const existingAddresses = await this.em.find(UserAddress, {
        user: { id: userId },
        isDefault: true,
        id: { $ne: addressId },
      });
      for (const existingAddress of existingAddresses) {
        existingAddress.isDefault = false;
      }
      await this.em.flush();
    }

    // Update address fields
    this.em.assign(address, dto);
    await this.em.flush();

    return address;
  }

  async deleteUserAddress(userId: string, addressId: string): Promise<void> {
    const address = await this.em.findOne(UserAddress, {
      id: addressId,
      user: { id: userId },
    });

    if (!address) {
      throw new NotFoundException(`Address with ID ${addressId} not found for user ${userId}.`);
    }

    // Soft delete the address
    address.deletedAt = new Date();
    await this.em.persistAndFlush(address);
  }

  async setDefaultAddress(userId: string, addressId: string): Promise<UserAddress> {
    const address = await this.em.findOne(UserAddress, {
      id: addressId,
      user: { id: userId },
    });

    if (!address) {
      throw new NotFoundException(`Address with ID ${addressId} not found for user ${userId}.`);
    }

    // Unset all other default addresses
    const existingAddresses = await this.em.find(UserAddress, {
      user: { id: userId },
      isDefault: true,
      id: { $ne: addressId },
    });
    for (const existingAddress of existingAddresses) {
      existingAddress.isDefault = false;
    }

    // Set this address as default
    address.isDefault = true;
    await this.em.flush();

    return address;
  }

  async convertScoreToWallet(userId: string, restId: string) {
    return this.em.transactional(async em => {
      const user = await em.findOne(User, { id: userId });
      if (!user) {
        throw new NotFoundException(`User with ID ${userId} not found.`);
      }
      const restaurant = await em.findOne(Restaurant, { id: restId });
      if (!restaurant) {
        throw new NotFoundException(`Restaurant with ID ${restId} not found.`);
      }
      let walletTransaction = await em.findOne(WalletTransaction, { user: { id: userId }, restaurant: { id: restId } },
        { orderBy: { createdAt: 'DESC' } });

      if (!walletTransaction) {
        walletTransaction = em.create(WalletTransaction,
          {
            user: user,
            restaurant,
            balance: 0,
            amount: 0,
            type: WalletTransactionType.CREDIT,
            reason: WalletTransactionReason.DEPOSIT
          });
        await em.persistAndFlush(walletTransaction);
      }
      // Determine how many points to convert
      const pointsToConvert = walletTransaction.balance;

      // Validate points to convert
      if (pointsToConvert <= 0) {
        throw new BadRequestException('Points to convert must be greater than 0.');
      }

      if (!restaurant.score) {
        throw new BadRequestException('Restaurant score not found.');
      }
      if (!restaurant.score.purchaseScore) {
        throw new BadRequestException('Restaurant purchase score not found.');
      }
      if (!restaurant.score.purchaseAmount) {
        throw new BadRequestException('Restaurant purchase amount not found.');
      }
      const walletIncreaseAmount =
        (Number(pointsToConvert) * Number(restaurant.score.purchaseAmount)) / Number(restaurant.score.purchaseScore);
      // Convert points to wallet (1:1 ratio - can be made configurable)

      const newWalletTransaction = em.create(WalletTransaction, {
        user: user,
        restaurant: restaurant,
        amount: walletIncreaseAmount,
        type: WalletTransactionType.CREDIT,
        reason: WalletTransactionReason.CONVERT_SCORE_TO_WALLET,
        balance: walletTransaction.balance + walletIncreaseAmount,
      });
      // Update user's wallet and points
      walletTransaction.balance = walletTransaction.balance + walletIncreaseAmount;
      em.persist([newWalletTransaction]);

      await em.flush();

      return user;
    });
  }

  getUserWallet(userId: string, restId: string): Promise<WalletTransaction | null> {
    return this.walletTransactionRepository.findOne({ user: { id: userId }, restaurant: { id: restId } },
      { orderBy: { createdAt: 'DESC' } });
  }

  getUserWalletTransactions(userId: string, restId: string, dto: FindWalletTransactionsDto) {
    return this.walletService.getUserWalletTransactions(userId, restId, dto);
  }

  async createWalletTransaction(userId: string, restId: string, dto: CreateWalletTransactionDto) {
    return this.em.transactional(async em => {
      return this.walletService.createTransaction(em, userId, restId, dto);
    });
  }

}
