import {
  BadRequestException,
  ConflictException,
  ForbiddenException,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import { PrismaService } from '../../database/prisma.service';
import { CreateAdminWithShopDto } from './dto/create-admin-with-shop.dto';
import bcrypt from 'bcryptjs';
import {
  Prisma,
  CouponStatus,
  CustomerCouponStatus,
  LoyaltyEarnType,
  ModerationStatus,
  ReviewSource,
  SaleStatus,
  ShopStatus,
  UserRole,
} from '@prisma/client';
import { CreateSubadminDto } from './dto/create-subadmin.dto';
import { CreateCustomerDto } from './dto/create-customer.dto';
import { CreateOwnedShopDto } from './dto/create-owned-shop.dto';
import { UploadsService } from '../uploads/uploads.service';
import { UpdateOwnedShopDto } from './dto/update-owned-shop.dto';
import { CreateSuperadminDto } from './dto/create-superadmin.dto';
import { UpdateManagedUserDto } from './dto/update-managed-user.dto';
import { UpdateMyLocationDto } from './dto/update-my-location.dto';
import { UpdateMyProfileDto } from './dto/update-my-profile.dto';
import { PlanLimitException } from '../plans/plan-limit.exception';
import { parseAddStoresLimit, parseTeamsLimit } from '../plans/plan-limits';
import { WalletService } from '../wallet/wallet.service';
import { HttpException, HttpStatus } from '@nestjs/common';
import { MailService } from '../mail/mail.service';

@Injectable()
export class UsersService {
  private readonly logger = new Logger(UsersService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly uploads: UploadsService,
    private readonly wallet: WalletService,
    private readonly mail: MailService,
  ) {}

  private queueShopApprovedEmail(args: {
    to?: string | null;
    recipientName?: string | null;
    shopName: string;
    city?: string | null;
  }) {
    if (!args.to) return;
    void this.mail
      .sendShopApproved({
        to: args.to,
        recipientName: args.recipientName,
        shopName: args.shopName,
        city: args.city,
        portalUrl:
          process.env.MERCHANT_PORTAL_URL ?? process.env.APP_PUBLIC_URL ?? null,
      })
      .catch((error: unknown) => {
        const message =
          error instanceof Error ? error.message : 'unknown mailer error';
        this.logger.warn(`Failed to send shop approval email: ${message}`);
      });
  }

  private async resolveCustomerIdsForMe(userId: string): Promise<string[]> {
    const me = await this.prisma.user.findUnique({
      where: { id: userId },
      select: { id: true, phone: true },
    });
    if (!me) return [userId];
    const phone = (me.phone ?? '').trim();
    if (!phone) return [userId];

    const candidates = new Set<string>([phone]);

    if (/^\+91\d{10}$/.test(phone)) candidates.add(phone.slice(3)); // 10-digit legacy
    if (/^\d{10}$/.test(phone)) candidates.add(`+91${phone}`); // +91 format
    if (/^91\d{10}$/.test(phone)) {
      candidates.add(`+${phone}`);
      candidates.add(phone.slice(2));
    }

    const users = await this.prisma.user.findMany({
      where: { phone: { in: Array.from(candidates) } },
      select: { id: true },
    });
    return Array.from(new Set([userId, ...users.map((u) => u.id)]));
  }

  private defaultSubadminPermissions() {
    // Default: allow core business modules, exclude Team/Plans/Settings.
    return [
      'dashboard',
      'coupons',
      'luckyDraw',
      'customers',
      'stores',
      'loyalty',
      'sales',
      'reviews',
    ];
  }

  private sharedPlanSelect() {
    return {
      planExpiresAt: true,
      scheduledPlanCode: true,
      scheduledPlanAt: true,
      plan: {
        select: {
          id: true,
          code: true,
          name: true,
          priceMonthly: true,
          currency: true,
          features: true,
        },
      },
    } as const;
  }

  private shopResponseSelect(includeOwner: boolean) {
    return {
      id: true,
      adminId: true,
      admin: {
        select: {
          ...(includeOwner
            ? {
                id: true,
                email: true,
                phone: true,
                role: true,
              }
            : {}),
          adminSubscription: {
            select: this.sharedPlanSelect(),
          },
        },
      },
      name: true,
      username: true,
      address: true,
      city: true,
      state: true,
      pincode: true,
      gstNo: true,
      upiId: true,
      businessTypeId: true,
      imageUrl: true,
      latitude: true,
      longitude: true,
      status: true,
      approvedAt: true,
      rejectedAt: true,
      rejectedReason: true,
      isActive: true,
      deactivatedAt: true,
      createdAt: true,
      updatedAt: true,
    } as const;
  }

  private mapShopResponse(
    shop: any,
    opts?: { includeOwner?: boolean; includeSharedPlanMeta?: boolean },
  ) {
    const includeOwner = !!opts?.includeOwner;
    const includeSharedPlanMeta = !!opts?.includeSharedPlanMeta;
    const subscription = shop?.admin?.adminSubscription ?? null;
    const owner =
      includeOwner && shop?.admin
        ? {
            id: shop.admin.id,
            email: shop.admin.email ?? null,
            phone: shop.admin.phone ?? null,
            role: shop.admin.role,
          }
        : false;

    return {
      ...shop,
      admin: owner,
      plan: subscription?.plan ?? null,
      ...(includeSharedPlanMeta
        ? {
            planExpiresAt: subscription?.planExpiresAt ?? null,
            scheduledPlanCode: subscription?.scheduledPlanCode ?? null,
            scheduledPlanAt: subscription?.scheduledPlanAt ?? null,
          }
        : {}),
    };
  }

  async updateMyProfile(userId: string, dto: UpdateMyProfileDto) {
    const user = await this.prisma.user.findUnique({
      where: { id: userId },
      select: { id: true, role: true },
    });
    if (!user) throw new NotFoundException('User not found');

    const data: Prisma.UserUpdateInput = {};
    if (dto.name !== undefined) {
      const n = dto.name.trim();
      if (n.length < 2)
        throw new BadRequestException('Name must be at least 2 characters');
      data.name = n;
    }
    if (dto.email !== undefined) {
      const e = dto.email.trim();
      const taken = await this.prisma.user.findFirst({
        where: { email: e, NOT: { id: userId } },
        select: { id: true },
      });
      if (taken) throw new ConflictException('Email is already in use');
      data.email = e;
    }

    if (Object.keys(data).length === 0) {
      return this.prisma.user.findUniqueOrThrow({
        where: { id: userId },
        select: { id: true, name: true, email: true, phone: true, role: true },
      });
    }

    return this.prisma.user.update({
      where: { id: userId },
      data,
      select: { id: true, name: true, email: true, phone: true, role: true },
    });
  }

  async updateMyLocation(userId: string, dto: UpdateMyLocationDto) {
    const user = await this.prisma.user.findUnique({
      where: { id: userId },
      select: { id: true, role: true },
    });
    if (!user) throw new NotFoundException('User not found');
    if (user.role !== UserRole.USER) {
      throw new ForbiddenException(
        'Only customer accounts can use this endpoint',
      );
    }
    return this.prisma.user.update({
      where: { id: userId },
      data: {
        latitude: dto.latitude,
        longitude: dto.longitude,
        ...(dto.locationAddress !== undefined
          ? {
              locationAddress: dto.locationAddress.trim()
                ? dto.locationAddress.trim()
                : null,
            }
          : {}),
      },
      select: {
        id: true,
        latitude: true,
        longitude: true,
        locationAddress: true,
      },
    });
  }

  async deactivateMe(userId: string) {
    if (!userId) throw new BadRequestException('id is required');
    const existing = await this.prisma.user.findUnique({
      where: { id: userId },
      select: { id: true, isActive: true },
    });
    if (!existing) throw new NotFoundException('User not found');
    if (!existing.isActive) return { deactivated: true };

    const updated = await this.prisma.user.update({
      where: { id: userId },
      data: {
        isActive: false,
        deactivatedAt: new Date(),
        tokenVersion: { increment: 1 },
      },
      select: { id: true, isActive: true, deactivatedAt: true },
    });
    return { deactivated: true, user: updated };
  }

  async listShopsForRequester(requester: {
    id: string;
    role: UserRole;
    shopId?: string | null;
  }) {
    if (requester.role === UserRole.SUBADMIN) {
      // Only assigned shops for this subadmin
      const links = await this.prisma.subadminShop.findMany({
        where: { userId: requester.id },
        select: { shopId: true },
      });
      const shopIds = links.map((l) => l.shopId);
      const items = await this.prisma.shop.findMany({
        where: { id: { in: shopIds } },
        orderBy: { createdAt: 'asc' },
        select: this.shopResponseSelect(false),
      });
      return items.map((item) => this.mapShopResponse(item));
    }

    const where =
      requester.role === UserRole.SUPERADMIN ? {} : { adminId: requester.id };
    if (
      requester.role !== UserRole.SUPERADMIN &&
      requester.role !== UserRole.ADMIN
    ) {
      throw new ForbiddenException('Not allowed');
    }

    const items = await this.prisma.shop.findMany({
      where,
      orderBy: { createdAt: 'asc' },
      select: this.shopResponseSelect(requester.role === UserRole.SUPERADMIN),
    });
    return items.map((item) =>
      this.mapShopResponse(item, {
        includeOwner: requester.role === UserRole.SUPERADMIN,
      }),
    );
  }

  async listShopsForRequesterPaged(
    requester: { id: string; role: UserRole; shopId?: string | null },
    args: {
      page: number;
      limit: number;
      q?: string;
      status?: string;
      isActive?: string;
      planCode?: string;
    },
  ) {
    const { page, limit } = args;
    const skip = (page - 1) * limit;
    const q = (args.q ?? '').trim();
    const wantStatus = (args.status ?? '').trim().toUpperCase();
    const wantActiveRaw = (args.isActive ?? '').trim().toLowerCase();
    const wantPlanCode = (args.planCode ?? '').trim().toUpperCase();
    const wantActive =
      wantActiveRaw === 'true'
        ? true
        : wantActiveRaw === 'false'
          ? false
          : null;

    let allowedShopIds: string[] | null = null;
    if (requester.role === UserRole.SUBADMIN) {
      const links = await this.prisma.subadminShop.findMany({
        where: { userId: requester.id },
        select: { shopId: true },
      });
      allowedShopIds = links.map((l) => l.shopId);
      if (allowedShopIds.length === 0) {
        return { page, limit, total: 0, items: [] as any[] };
      }
    }

    if (
      requester.role !== UserRole.SUPERADMIN &&
      requester.role !== UserRole.ADMIN &&
      requester.role !== UserRole.SUBADMIN
    ) {
      throw new ForbiddenException('Not allowed');
    }

    const whereBase: Prisma.ShopWhereInput =
      requester.role === UserRole.SUPERADMIN
        ? {}
        : requester.role === UserRole.ADMIN
          ? { adminId: requester.id }
          : { id: { in: allowedShopIds! } };

    const where: Prisma.ShopWhereInput = { ...whereBase };

    if (wantActive != null) where.isActive = wantActive;
    if (wantStatus === 'APPROVED') where.status = ShopStatus.APPROVED;
    else if (wantStatus === 'PENDING') where.status = ShopStatus.PENDING;
    else if (wantStatus === 'REJECTED') {
      where.status = ShopStatus.REJECTED;
    }
    if (wantPlanCode) {
      where.admin = {
        ...(where.admin as any),
        adminSubscription: { is: { plan: { code: wantPlanCode } } },
      } as any;
    }

    if (q) {
      // Avoid expensive "contains" OR scans for very short queries.
      if (q.length < 2) {
        const [total, items] = await Promise.all([
          this.prisma.shop.count({ where }),
          this.prisma.shop.findMany({
            where,
            orderBy: { createdAt: 'desc' },
            skip,
            take: limit,
            select: this.shopResponseSelect(
              requester.role === UserRole.SUPERADMIN,
            ),
          }),
        ]);

        return {
          page,
          limit,
          total,
          items: items.map((item) =>
            this.mapShopResponse(item, {
              includeOwner: requester.role === UserRole.SUPERADMIN,
            }),
          ),
        };
      }

      const qContains = { contains: q, mode: 'insensitive' as const };
      const or: Prisma.ShopWhereInput[] = [
        { id: qContains },
        { name: qContains },
        { username: qContains },
        { address: qContains },
        { city: qContains },
        { state: qContains },
        { pincode: qContains },
      ];
      if (requester.role === UserRole.SUPERADMIN) {
        or.push({ admin: { email: qContains } });
        or.push({ admin: { phone: qContains } });
      }
      where.OR = or;
    }

    const [total, items] = await Promise.all([
      this.prisma.shop.count({ where }),
      this.prisma.shop.findMany({
        where,
        orderBy: { createdAt: 'desc' },
        skip,
        take: limit,
        select: this.shopResponseSelect(requester.role === UserRole.SUPERADMIN),
      }),
    ]);

    return {
      page,
      limit,
      total,
      items: items.map((item) =>
        this.mapShopResponse(item, {
          includeOwner: requester.role === UserRole.SUPERADMIN,
        }),
      ),
    };
  }

  private normalizeShopUsername(input: string) {
    let u = (input ?? '').trim();
    if (u.startsWith('@')) u = u.slice(1);
    u = u.toLowerCase();
    if (!/^[a-z0-9_.-]{3,30}$/.test(u)) {
      throw new BadRequestException('Invalid shop username');
    }
    return u;
  }

  async createShopForAdmin(adminUserId: string, dto: CreateOwnedShopDto) {
    const admin = await this.prisma.user.findUnique({
      where: { id: adminUserId },
      select: {
        id: true,
        role: true,
        shopId: true,
        adminSubscription: {
          select: this.sharedPlanSelect(),
        },
        shop: {
          select: {
            id: true,
            adminId: true,
            status: true,
            isActive: true,
          },
        },
      },
    });
    if (!admin) throw new NotFoundException('Admin not found');
    if (admin.role !== UserRole.ADMIN)
      throw new ForbiddenException('Only admins can create shops');

    const currentShop = admin.shop;
    if (!admin.shopId || !currentShop || currentShop.adminId !== admin.id) {
      throw new ForbiddenException('Current shop is not owned by admin');
    }
    if (!currentShop.isActive) throw new ForbiddenException('Shop is inactive');

    const subscription = admin.adminSubscription;
    if (!subscription?.plan) {
      throw new ForbiddenException('Admin subscription is missing');
    }
    const limit = parseAddStoresLimit(subscription.plan.features);
    const ownedCount = await this.prisma.shop.count({
      where: { adminId: admin.id },
    });
    if (limit === 0) {
      throw new PlanLimitException({
        featureKey: 'Add Stores',
        currentPlanCode: subscription.plan.code,
        limit,
        used: ownedCount,
        period: 'ALWAYS',
        message: 'Your plan does not allow adding stores',
      });
    }
    if (limit !== 'unlimited') {
      if (ownedCount >= limit) {
        throw new PlanLimitException({
          featureKey: 'Add Stores',
          currentPlanCode: subscription.plan.code,
          limit,
          used: ownedCount,
          period: 'ALWAYS',
          message: `Store limit reached for your plan (limit: ${limit})`,
        });
      }
    }

    const bt = await this.prisma.businessType.findUnique({
      where: { id: dto.businessTypeId },
      select: { id: true, imageUrl: true },
    });
    if (!bt) throw new NotFoundException('Business type not found');

    try {
      const created = await this.prisma.shop.create({
        data: {
          name: dto.name,
          username: this.normalizeShopUsername(dto.username),
          address: dto.address,
          city: dto.city,
          state: dto.state,
          pincode: dto.pincode,
          gstNo: dto.gstNo,
          upiId: dto.upiId?.trim() || null,
          businessTypeId: bt.id,
          imageUrl: dto.imageUrl?.trim() || bt.imageUrl || null,
          latitude: dto.latitude ?? null,
          longitude: dto.longitude ?? null,
          adminId: admin.id,
          status: ShopStatus.PENDING,
          approvedAt: null,
          isActive: true,
          deactivatedAt: null,
        },
        select: this.shopResponseSelect(false),
      });
      return this.mapShopResponse(created);
    } catch (e) {
      if (
        e instanceof Prisma.PrismaClientKnownRequestError &&
        e.code === 'P2002'
      ) {
        throw new ConflictException('Shop username already taken');
      }
      throw e;
    }
  }

  async switchAdminCurrentShop(adminUserId: string, shopId: string) {
    const shop = await this.prisma.shop.findUnique({
      where: { id: shopId },
      select: { id: true, adminId: true, status: true, isActive: true },
    });
    if (!shop) throw new NotFoundException('Shop not found');
    if (shop.adminId !== adminUserId)
      throw new ForbiddenException('Not allowed');
    if (shop.status !== ShopStatus.APPROVED)
      throw new ForbiddenException('Shop is pending approval');
    if (!shop.isActive) throw new ForbiddenException('Shop is inactive');

    await this.prisma.user.update({
      where: { id: adminUserId },
      data: { shopId: shop.id },
      select: { id: true },
    });
    return { switched: true, shopId: shop.id };
  }

  async switchCurrentShop(
    requester: { id: string; role: UserRole },
    shopId: string,
  ) {
    if (requester.role === UserRole.ADMIN) {
      return this.switchAdminCurrentShop(requester.id, shopId);
    }
    if (requester.role !== UserRole.SUBADMIN)
      throw new ForbiddenException('Not allowed');

    const link = await this.prisma.subadminShop.findUnique({
      where: { userId_shopId: { userId: requester.id, shopId } },
      select: { shopId: true },
    });
    if (!link) throw new ForbiddenException('Not allowed');

    const shop = await this.prisma.shop.findUnique({
      where: { id: shopId },
      select: { id: true, status: true, isActive: true },
    });
    if (!shop) throw new NotFoundException('Shop not found');
    if (shop.status !== ShopStatus.APPROVED)
      throw new ForbiddenException('Shop is pending approval');
    if (!shop.isActive) throw new ForbiddenException('Shop is inactive');

    await this.prisma.user.update({
      where: { id: requester.id },
      data: { shopId: shop.id },
      select: { id: true },
    });
    return { switched: true, shopId: shop.id };
  }

  async setShopPlanForAdmin(
    adminUserId: string,
    shopId: string,
    dto: { planCode: string; renew?: boolean },
    portalPublicUrl?: string | null,
  ) {
    const shop = await this.prisma.shop.findUnique({
      where: { id: shopId },
      select: {
        id: true,
        adminId: true,
        status: true,
        isActive: true,
        walletBalance: true,
        admin: {
          select: {
            adminSubscription: {
              select: this.sharedPlanSelect(),
            },
          },
        },
      },
    });
    if (!shop) throw new NotFoundException('Shop not found');
    if (shop.adminId !== adminUserId)
      throw new ForbiddenException('Not allowed');
    if (!shop.isActive) throw new ForbiddenException('Shop is inactive');
    const currentSubscription = shop.admin.adminSubscription;
    if (!currentSubscription?.plan) {
      throw new ForbiddenException('Admin subscription is missing');
    }

    const code = String(dto.planCode || '')
      .trim()
      .toUpperCase();
    const plan = await this.prisma.plan.findUnique({
      where: { code },
      select: { id: true, code: true, priceMonthly: true },
    });
    if (!plan) throw new NotFoundException(`Plan ${code} not found`);

    const now = new Date();
    const currentPrice = Math.max(
      0,
      Math.floor(Number(currentSubscription.plan.priceMonthly ?? 0)),
    );
    const nextPrice = Math.max(0, Math.floor(Number(plan.priceMonthly ?? 0)));
    const currentPaidValid =
      currentPrice > 0 &&
      !!currentSubscription.planExpiresAt &&
      currentSubscription.planExpiresAt > now;
    const effectiveCurrentPrice = currentPaidValid ? currentPrice : 0;
    const getUpdatedShop = async () => {
      const updated = await this.prisma.shop.findUnique({
        where: { id: shop.id },
        select: {
          id: true,
          adminId: true,
          name: true,
          username: true,
          address: true,
          city: true,
          state: true,
          pincode: true,
          gstNo: true,
          upiId: true,
          businessTypeId: true,
          imageUrl: true,
          latitude: true,
          longitude: true,
          status: true,
          approvedAt: true,
          rejectedAt: true,
          rejectedReason: true,
          isActive: true,
          deactivatedAt: true,
          walletBalance: true,
          createdAt: true,
          updatedAt: true,
          admin: {
            select: {
              adminSubscription: {
                select: this.sharedPlanSelect(),
              },
            },
          },
        },
      });
      return updated
        ? this.mapShopResponse(updated, { includeSharedPlanMeta: true })
        : null;
    };

    // Renew/extend the validity window (charges again).
    if (dto.renew) {
      if (nextPrice <= 0)
        throw new BadRequestException('Free plan does not require renewal');
      // Renewing any plan requires a purchase, even if it is the same plan code.
      const debit = await this.wallet.debitWalletForPlanIfEnough({
        adminUserId,
        shopId: shop.id,
        planCode: plan.code,
      });
      if (!debit.ok) {
        const checkout = await this.wallet.createPlanPurchaseCheckout({
          adminUserId,
          shopId: shop.id,
          planCode: plan.code,
          amountToTopupInr: debit.needed,
          portalPublicUrl,
        });
        throw new HttpException(
          {
            statusCode: HttpStatus.PAYMENT_REQUIRED,
            code: 'WALLET_TOPUP_REQUIRED',
            featureKey: 'Plan Renewal',
            currentPlanCode: currentSubscription.plan.code ?? null,
            upgradeTo: plan.code,
            limit: null,
            used: shop.walletBalance ?? 0,
            period: 'ALWAYS',
            checkoutUrl: checkout.checkoutUrl,
            message: 'Wallet topup required to renew plan',
          },
          HttpStatus.PAYMENT_REQUIRED,
        );
      }
      const updated = await getUpdatedShop();
      return { updated: true, renewed: true, shop: updated };
    }

    // Downgrades (or same price) are allowed without wallet debit.
    if (nextPrice <= effectiveCurrentPrice) {
      // Paid → Free: schedule downgrade at end of paid cycle (keep access till expiry).
      if (nextPrice === 0 && currentPaidValid) {
        const scheduledAt = currentSubscription.planExpiresAt!;
        await this.prisma.adminSubscription.update({
          where: { adminId: adminUserId },
          data: {
            scheduledPlanCode: plan.code,
            scheduledPlanAt: scheduledAt,
          },
        });
        const updated = await getUpdatedShop();
        return {
          updated: true,
          scheduled: true,
          effectiveAt: scheduledAt,
          shop: updated,
        };
      }

      await this.prisma.adminSubscription.upsert({
        where: { adminId: adminUserId },
        create: {
          adminId: adminUserId,
          planId: plan.id,
          planExpiresAt:
            nextPrice === 0 ? null : currentSubscription.planExpiresAt,
          scheduledPlanCode: null,
          scheduledPlanAt: null,
        },
        update: {
          planId: plan.id,
          planExpiresAt:
            nextPrice === 0 ? null : currentSubscription.planExpiresAt,
          scheduledPlanCode: null,
          scheduledPlanAt: null,
        },
      });
      const updated = await getUpdatedShop();
      return { updated: true, shop: updated };
    }

    // Upgrades require wallet purchase.
    const debit = await this.wallet.debitWalletForPlanIfEnough({
      adminUserId,
      shopId: shop.id,
      planCode: plan.code,
    });
    if (!debit.ok) {
      const checkout = await this.wallet.createPlanPurchaseCheckout({
        adminUserId,
        shopId: shop.id,
        planCode: plan.code,
        amountToTopupInr: debit.needed,
        portalPublicUrl,
      });
      throw new HttpException(
        {
          statusCode: HttpStatus.PAYMENT_REQUIRED,
          code: 'WALLET_TOPUP_REQUIRED',
          featureKey: 'Plan Purchase',
          currentPlanCode: currentSubscription.plan.code ?? null,
          upgradeTo: plan.code,
          limit: null,
          used: shop.walletBalance ?? 0,
          period: 'ALWAYS',
          checkoutUrl: checkout.checkoutUrl,
          message: 'Wallet topup required to purchase plan',
        },
        HttpStatus.PAYMENT_REQUIRED,
      );
    }

    const updated = await getUpdatedShop();
    return { updated: true, shop: updated };
  }

  async updateShopForAdmin(
    adminUserId: string,
    shopId: string,
    dto: {
      name?: string;
      username?: string;
      address?: string;
      city?: string;
      state?: string;
      pincode?: string;
      gstNo?: string;
      upiId?: string;
      businessTypeId?: string;
      imageUrl?: string;
      latitude?: number;
      longitude?: number;
    },
  ) {
    const shop = await this.prisma.shop.findUnique({
      where: { id: shopId },
      select: { id: true, adminId: true, isActive: true, status: true },
    });
    if (!shop) throw new NotFoundException('Shop not found');
    if (shop.adminId !== adminUserId)
      throw new ForbiddenException('Not allowed');
    // Allow edits for rejected shops even if they are inactive (so owner can fix and resubmit).
    if (!shop.isActive && shop.status !== ShopStatus.REJECTED) {
      throw new ForbiddenException('Shop is inactive');
    }

    let nextBusinessTypeId: string | undefined;
    let nextImageUrl: string | null | undefined;

    if (dto.businessTypeId !== undefined) {
      const bt = await this.prisma.businessType.findUnique({
        where: { id: dto.businessTypeId },
        select: { id: true, imageUrl: true },
      });
      if (!bt) throw new NotFoundException('Business type not found');
      nextBusinessTypeId = bt.id;
      // If caller didn't pass imageUrl but changed business type, keep existing shop image unless they set it.
      if (dto.imageUrl === undefined) nextImageUrl = undefined;
    }

    if (dto.imageUrl !== undefined) {
      nextImageUrl = dto.imageUrl.trim() || null;
    }

    let nextUsername: string | undefined;
    if (dto.username !== undefined)
      nextUsername = this.normalizeShopUsername(dto.username);

    let updated: any;
    try {
      updated = await this.prisma.shop.update({
        where: { id: shop.id },
        data: {
          ...(dto.name !== undefined ? { name: dto.name } : {}),
          ...(nextUsername !== undefined ? { username: nextUsername } : {}),
          ...(dto.address !== undefined ? { address: dto.address } : {}),
          ...(dto.city !== undefined ? { city: dto.city } : {}),
          ...(dto.state !== undefined ? { state: dto.state } : {}),
          ...(dto.pincode !== undefined ? { pincode: dto.pincode } : {}),
          ...(dto.gstNo !== undefined ? { gstNo: dto.gstNo || null } : {}),
          ...(dto.upiId !== undefined
            ? { upiId: dto.upiId.trim() || null }
            : {}),
          ...(nextBusinessTypeId !== undefined
            ? { businessTypeId: nextBusinessTypeId }
            : {}),
          ...(nextImageUrl !== undefined ? { imageUrl: nextImageUrl } : {}),
          ...(dto.latitude !== undefined ? { latitude: dto.latitude } : {}),
          ...(dto.longitude !== undefined ? { longitude: dto.longitude } : {}),
        },
        select: this.shopResponseSelect(false),
      });
    } catch (e) {
      if (
        e instanceof Prisma.PrismaClientKnownRequestError &&
        e.code === 'P2002'
      ) {
        throw new ConflictException('Shop username already taken');
      }
      throw e;
    }

    return { updated: true, shop: this.mapShopResponse(updated) };
  }

  async updateShopForRequester(
    requester: { id: string; role: UserRole },
    shopId: string,
    dto: UpdateOwnedShopDto,
  ) {
    if (requester.role === UserRole.ADMIN) {
      return this.updateShopForAdmin(requester.id, shopId, dto as any);
    }
    if (requester.role !== UserRole.SUPERADMIN)
      throw new ForbiddenException('Not allowed');

    const existing = await this.prisma.shop.findUnique({
      where: { id: shopId },
      select: { id: true },
    });
    if (!existing) throw new NotFoundException('Shop not found');

    let nextBusinessTypeId: string | undefined;
    let nextImageUrl: string | null | undefined;

    if ((dto as any).businessTypeId !== undefined) {
      const bt = await this.prisma.businessType.findUnique({
        where: { id: (dto as any).businessTypeId },
        select: { id: true, imageUrl: true },
      });
      if (!bt) throw new NotFoundException('Business type not found');
      nextBusinessTypeId = bt.id;
      if ((dto as any).imageUrl === undefined) nextImageUrl = undefined;
    }

    if ((dto as any).imageUrl !== undefined) {
      nextImageUrl = String((dto as any).imageUrl ?? '').trim() || null;
    }

    let nextUsername: string | undefined;
    if ((dto as any).username !== undefined)
      nextUsername = this.normalizeShopUsername((dto as any).username);

    try {
      const updated = await this.prisma.shop.update({
        where: { id: existing.id },
        data: {
          ...((dto as any).name !== undefined
            ? { name: (dto as any).name }
            : {}),
          ...(nextUsername !== undefined ? { username: nextUsername } : {}),
          ...((dto as any).address !== undefined
            ? { address: (dto as any).address }
            : {}),
          ...((dto as any).city !== undefined
            ? { city: (dto as any).city }
            : {}),
          ...((dto as any).state !== undefined
            ? { state: (dto as any).state }
            : {}),
          ...((dto as any).pincode !== undefined
            ? { pincode: (dto as any).pincode }
            : {}),
          ...((dto as any).gstNo !== undefined
            ? { gstNo: (dto as any).gstNo || null }
            : {}),
          ...((dto as any).upiId !== undefined
            ? { upiId: String((dto as any).upiId ?? '').trim() || null }
            : {}),
          ...(nextBusinessTypeId !== undefined
            ? { businessTypeId: nextBusinessTypeId }
            : {}),
          ...(nextImageUrl !== undefined ? { imageUrl: nextImageUrl } : {}),
          ...((dto as any).latitude !== undefined
            ? { latitude: (dto as any).latitude }
            : {}),
          ...((dto as any).longitude !== undefined
            ? { longitude: (dto as any).longitude }
            : {}),
        },
        select: this.shopResponseSelect(true),
      });
      return {
        updated: true,
        shop: this.mapShopResponse(updated, { includeOwner: true }),
      };
    } catch (e) {
      if (
        e instanceof Prisma.PrismaClientKnownRequestError &&
        e.code === 'P2002'
      ) {
        throw new ConflictException('Shop username already taken');
      }
      throw e;
    }
  }

  async uploadShopImageForAdmin(
    adminUserId: string,
    shopId: string,
    file: { buffer: Buffer; originalname?: string; mimetype?: string },
  ) {
    const shop = await this.prisma.shop.findUnique({
      where: { id: shopId },
      select: { id: true, adminId: true, isActive: true },
    });
    if (!shop) throw new NotFoundException('Shop not found');
    if (shop.adminId !== adminUserId)
      throw new ForbiddenException('Not allowed');
    if (!shop.isActive) throw new ForbiddenException('Shop is inactive');

    const extFromName = (file.originalname ?? '').split('.').pop();
    const ext =
      extFromName && extFromName.length <= 5
        ? `.${extFromName.toLowerCase()}`
        : file.mimetype === 'image/jpeg'
          ? '.jpg'
          : '.png';

    const path = await this.uploads.saveImage({
      shopId: shop.id,
      entityType: 'shop',
      purpose: 'assets',
      ext,
      buffer: file.buffer,
    });

    const updated = await this.prisma.shop.update({
      where: { id: shop.id },
      data: { imageUrl: path },
      select: { id: true, imageUrl: true },
    });
    return { updated: true, shop: updated };
  }

  async uploadShopImageForRequester(
    requester: { id: string; role: UserRole },
    shopId: string,
    file: { buffer: Buffer; originalname?: string; mimetype?: string },
  ) {
    if (requester.role === UserRole.ADMIN) {
      return this.uploadShopImageForAdmin(requester.id, shopId, file);
    }
    if (requester.role !== UserRole.SUPERADMIN)
      throw new ForbiddenException('Not allowed');

    const shop = await this.prisma.shop.findUnique({
      where: { id: shopId },
      select: { id: true },
    });
    if (!shop) throw new NotFoundException('Shop not found');

    const extFromName = (file.originalname ?? '').split('.').pop();
    const ext =
      extFromName && extFromName.length <= 5
        ? `.${extFromName.toLowerCase()}`
        : file.mimetype === 'image/jpeg'
          ? '.jpg'
          : '.png';

    const path = await this.uploads.saveImage({
      shopId: shop.id,
      entityType: 'shop',
      purpose: 'assets',
      ext,
      buffer: file.buffer,
    });

    const updated = await this.prisma.shop.update({
      where: { id: shop.id },
      data: { imageUrl: path },
      select: { id: true, imageUrl: true },
    });
    return { updated: true, shop: updated };
  }

  async getUserById(
    requester: { id: string; role: UserRole; shopId?: string | null },
    userId: string,
  ) {
    if (requester.role === UserRole.USER && requester.id !== userId) {
      throw new ForbiddenException('Not allowed');
    }

    const user = await this.prisma.user.findUnique({
      where: { id: userId },
      select: {
        id: true,
        role: true,
        email: true,
        phone: true,
        shopId: true,
        tokenVersion: true,
        createdAt: true,
        updatedAt: true,
        shop: {
          select: {
            id: true,
            adminId: true,
            name: true,
            address: true,
            city: true,
            state: true,
            pincode: true,
            gstNo: true,
            upiId: true,
            admin: {
              select: {
                adminSubscription: {
                  select: this.sharedPlanSelect(),
                },
              },
            },
          },
        },
      },
    });
    if (!user) throw new NotFoundException('User not found');
    const mappedUser = {
      ...user,
      shop: user.shop
        ? {
            ...user.shop,
            plan: user.shop.admin?.adminSubscription?.plan ?? null,
          }
        : null,
    };

    if (requester.role === UserRole.SUPERADMIN) return mappedUser;

    if (requester.role === UserRole.ADMIN) {
      // Admin can access users across all shops they own
      if (requester.id === user.id) return mappedUser;
      if (!user.shop || user.shop.adminId !== requester.id) {
        throw new ForbiddenException('Not allowed');
      }
      return mappedUser;
    }

    // SUBADMIN/USER are scoped to their current shop (or self)
    const requesterShopId = requester.shopId ?? null;
    if (requesterShopId == null || user.shopId !== requesterShopId) {
      throw new ForbiddenException('Not allowed');
    }

    return mappedUser;
  }

  async createAdminWithShop(dto: CreateAdminWithShopDto) {
    const planCode = String(dto.shop.planCode || '')
      .trim()
      .toUpperCase();
    const plan = await this.prisma.plan.findUnique({
      where: { code: planCode },
      select: { id: true },
    });
    if (!plan) {
      throw new NotFoundException(
        `Plan ${planCode} not found. Create it first.`,
      );
    }

    const emailRaw = (dto.email ?? '').toString().trim();
    const email = emailRaw ? emailRaw.toLowerCase() : '';
    const phone = dto.phone.trim();
    const password = (dto.password ?? '').toString();

    try {
      return await this.prisma.$transaction(async (tx) => {
        // If an admin already exists (email/phone), create an additional shop for them
        // instead of failing with a unique-constraint conflict.
        const or: Prisma.UserWhereInput[] = [{ phone }];
        if (email) or.push({ email });
        const existing = await tx.user.findFirst({
          where: {
            OR: or,
          },
          select: {
            id: true,
            email: true,
            role: true,
            phone: true,
            shopId: true,
            adminSubscription: {
              select: { id: true },
            },
          },
        });

        if (existing && existing.role !== UserRole.ADMIN) {
          throw new ConflictException(
            'A user with this email/phone already exists but is not an admin',
          );
        }

        if (!existing) {
          // Creating a NEW admin requires email + password.
          if (!email) throw new BadRequestException('Admin email is required');
          if (!password || password.trim().length < 4) {
            throw new BadRequestException('Admin password is required');
          }
        }

        const admin = existing
          ? {
              id: existing.id,
              email: existing.email,
              role: existing.role,
              phone: existing.phone,
            }
          : await tx.user.create({
              data: {
                role: UserRole.ADMIN,
                email,
                phone,
                passwordHash: await bcrypt.hash(password, 12),
                adminSubscription: {
                  create: {
                    planId: plan.id,
                  },
                },
              },
              select: { id: true, email: true, role: true, phone: true },
            });

        const bt = await tx.businessType.findUnique({
          where: { id: dto.shop.businessTypeId },
          select: { id: true, imageUrl: true },
        });
        if (!bt) throw new NotFoundException('Business type not found');

        const shop = await tx.shop.create({
          data: {
            name: dto.shop.name,
            username: this.normalizeShopUsername(dto.shop.username),
            address: dto.shop.address,
            city: dto.shop.city,
            state: dto.shop.state,
            pincode: dto.shop.pincode,
            gstNo: dto.shop.gstNo,
            businessTypeId: bt.id,
            imageUrl: dto.shop.imageUrl?.trim() || bt.imageUrl || null,
            latitude: dto.shop.latitude ?? null,
            longitude: dto.shop.longitude ?? null,
            adminId: admin.id,
            status: ShopStatus.APPROVED,
            approvedAt: new Date(),
            isActive: true,
            deactivatedAt: null,
          },
          select: {
            id: true,
            name: true,
            city: true,
            state: true,
            pincode: true,
            status: true,
            approvedAt: true,
            isActive: true,
          },
        });

        // Only set current shop context if the admin doesn't already have one.
        if (!existing?.shopId) {
          await tx.user.update({
            where: { id: admin.id },
            data: { shopId: shop.id },
            select: { id: true },
          });
        }
        if (existing && !existing.adminSubscription) {
          await tx.adminSubscription.create({
            data: {
              adminId: admin.id,
              planId: plan.id,
            },
            select: { id: true },
          });
        }

        return { admin, shop, reusedAdmin: !!existing };
      });
    } catch (e) {
      if (
        e instanceof Prisma.PrismaClientKnownRequestError &&
        e.code === 'P2002'
      ) {
        throw new ConflictException(
          'Email, phone, or shop username already exists',
        );
      }
      throw e;
    }
  }

  async createSubadminForAdmin(adminUserId: string, dto: CreateSubadminDto) {
    // Determine target shops (all owned or selected subset)
    const owned = await this.prisma.shop.findMany({
      where: { adminId: adminUserId },
      select: { id: true, status: true, isActive: true },
      orderBy: { createdAt: 'asc' },
    });
    const ownedIds = owned
      .filter((s) => s.status === ShopStatus.APPROVED && s.isActive)
      .map((s) => s.id);
    if (ownedIds.length === 0)
      throw new NotFoundException('No active shops found for admin');

    // Plan limits (Teams & Roles)
    const admin = await this.prisma.user.findUnique({
      where: { id: adminUserId },
      select: {
        adminSubscription: {
          select: {
            plan: { select: { code: true, features: true } },
          },
        },
      },
    });
    const sharedPlan = admin?.adminSubscription?.plan ?? null;
    const teamsLimit = parseTeamsLimit(sharedPlan?.features);
    if (teamsLimit === 0) {
      throw new PlanLimitException({
        featureKey: 'Teams & Roles',
        currentPlanCode: sharedPlan?.code,
        limit: teamsLimit,
        used: 0,
        period: 'ALWAYS',
        message: 'Your plan does not allow Teams & Roles',
      });
    }
    if (teamsLimit !== 'unlimited') {
      const links = await this.prisma.subadminShop.findMany({
        where: { shopId: { in: ownedIds } },
        select: { userId: true },
      });
      const currentCount = new Set(links.map((l) => l.userId)).size;
      if (currentCount + 1 > teamsLimit) {
        throw new PlanLimitException({
          featureKey: 'Teams & Roles',
          currentPlanCode: sharedPlan?.code,
          limit: teamsLimit,
          used: currentCount,
          period: 'ALWAYS',
          message: `Team member limit reached for your plan (limit: ${teamsLimit})`,
        });
      }
    }

    let targets: string[] = [];
    if (dto.allShops) {
      targets = ownedIds;
    } else if (dto.shopIds && dto.shopIds.length > 0) {
      const set = new Set(ownedIds);
      targets = dto.shopIds.filter((id) => set.has(id));
    } else {
      // default: current admin shop (if valid), else first owned
      const admin = await this.prisma.user.findUnique({
        where: { id: adminUserId },
        select: { shopId: true },
      });
      const current =
        admin?.shopId && ownedIds.includes(admin.shopId) ? admin.shopId : null;
      targets = [current ?? ownedIds[0]];
    }
    if (targets.length === 0)
      throw new ForbiddenException('No allowed shops selected');

    try {
      return await this.prisma.$transaction(async (tx) => {
        const perms =
          Array.isArray(dto.permissions) && dto.permissions.length > 0
            ? dto.permissions
            : this.defaultSubadminPermissions();

        const created = await tx.user.create({
          data: {
            role: UserRole.SUBADMIN,
            email: dto.email.toLowerCase(),
            phone: dto.phone.trim(),
            passwordHash: await bcrypt.hash(dto.password, 12),
            shopId: targets[0], // current shop context
            permissions: perms as any,
          },
          select: {
            id: true,
            email: true,
            role: true,
            phone: true,
            shopId: true,
            permissions: true,
            createdAt: true,
          },
        });

        await tx.subadminShop.createMany({
          data: targets.map((shopId) => ({ userId: created.id, shopId })),
          skipDuplicates: true,
        });

        return {
          ...created,
          shops: await tx.shop.findMany({
            where: { id: { in: targets } },
            select: { id: true, name: true },
            orderBy: { createdAt: 'asc' },
          }),
        };
      });
    } catch {
      throw new ConflictException('Email or phone already exists');
    }
  }

  async listSubadminsForAdmin(adminUserId: string) {
    const shops = await this.prisma.shop.findMany({
      where: { adminId: adminUserId },
      select: { id: true },
    });
    const shopIds = shops.map((s) => s.id);
    if (shopIds.length === 0) return [];

    const links = await this.prisma.subadminShop.findMany({
      where: { shopId: { in: shopIds } },
      select: {
        user: {
          select: {
            id: true,
            email: true,
            phone: true,
            role: true,
            shopId: true,
            createdAt: true,
            permissions: true,
          },
        },
        shop: { select: { id: true, name: true } },
      },
    });
    if (links.length === 0) return [];

    const byUser = new Map<string, any>();
    for (const l of links) {
      if (!l.user || l.user.role !== UserRole.SUBADMIN) continue;
      const rec = byUser.get(l.user.id) ?? { ...l.user, shops: [] as any[] };
      rec.shops.push(l.shop);
      byUser.set(l.user.id, rec);
    }
    return Array.from(byUser.values()).sort((a, b) =>
      a.createdAt < b.createdAt ? 1 : -1,
    );
  }

  async updateSubadminPermissionsForAdmin(
    adminUserId: string,
    subadminId: string,
    dto: { permissions?: string[] },
  ) {
    // Ensure subadmin is linked to at least one of admin's shops
    const link = await this.prisma.subadminShop.findFirst({
      where: { userId: subadminId, shop: { adminId: adminUserId } },
      select: { userId: true },
    });
    if (!link) throw new ForbiddenException('Not allowed');

    const sub = await this.prisma.user.findUnique({
      where: { id: subadminId },
      select: { id: true, role: true },
    });
    if (!sub || sub.role !== UserRole.SUBADMIN)
      throw new NotFoundException('Subadmin not found');

    const perms =
      Array.isArray(dto.permissions) && dto.permissions.length > 0
        ? dto.permissions
        : this.defaultSubadminPermissions();

    await this.prisma.user.update({
      where: { id: subadminId },
      data: { permissions: perms as any },
      select: { id: true },
    });
    return { updated: true };
  }

  async updateSubadminShopsForAdmin(
    adminUserId: string,
    subadminId: string,
    dto: { allShops?: boolean; shopIds?: string[] },
  ) {
    const owned = await this.prisma.shop.findMany({
      where: { adminId: adminUserId },
      select: {
        id: true,
        status: true,
        isActive: true,
        createdAt: true,
        name: true,
      },
      orderBy: { createdAt: 'asc' },
    });
    const ownedIds = owned
      .filter((s) => s.status === ShopStatus.APPROVED && s.isActive)
      .map((s) => s.id);
    if (ownedIds.length === 0)
      throw new NotFoundException('No active shops found for admin');

    const sub = await this.prisma.user.findUnique({
      where: { id: subadminId },
      select: { id: true, role: true },
    });
    if (!sub || sub.role !== UserRole.SUBADMIN)
      throw new NotFoundException('Subadmin not found');

    let targets: string[] = [];
    if (dto.allShops) targets = ownedIds;
    else if (dto.shopIds && dto.shopIds.length > 0) {
      const set = new Set(ownedIds);
      targets = dto.shopIds.filter((id) => set.has(id));
    }
    if (targets.length === 0)
      throw new ForbiddenException('No allowed shops selected');

    await this.prisma.$transaction(async (tx) => {
      await tx.subadminShop.deleteMany({ where: { userId: subadminId } });
      await tx.subadminShop.createMany({
        data: targets.map((shopId) => ({ userId: subadminId, shopId })),
      });

      // Ensure current shopId remains valid
      const current = await tx.user.findUnique({
        where: { id: subadminId },
        select: { shopId: true },
      });
      if (!current?.shopId || !targets.includes(current.shopId)) {
        await tx.user.update({
          where: { id: subadminId },
          data: { shopId: targets[0] },
        });
      }
    });

    return { updated: true };
  }

  async deleteSubadminForAdmin(adminUserId: string, subadminId: string) {
    // Ensure subadmin is linked to at least one of admin's shops
    const shop = await this.prisma.subadminShop.findFirst({
      where: { userId: subadminId, shop: { adminId: adminUserId } },
      select: { userId: true },
    });
    if (!shop) throw new ForbiddenException('Not allowed');

    await this.prisma.user.delete({ where: { id: subadminId } });
    return { deleted: true };
  }

  async createCustomerForAdmin(adminUserId: string, dto: CreateCustomerDto) {
    const admin = await this.prisma.user.findUnique({
      where: { id: adminUserId },
      select: { id: true, role: true, shopId: true },
    });
    if (!admin) throw new NotFoundException('Admin not found');
    return this.createCustomerForShop(admin, dto);
  }

  private async assertCanAccessShop(
    user: { id: string; role: UserRole; shopId?: string | null },
    shopId: string,
  ) {
    if (user.role === UserRole.SUPERADMIN) return;
    if (user.role === UserRole.ADMIN) {
      const shop = await this.prisma.shop.findUnique({
        where: { id: shopId },
        select: { adminId: true },
      });
      if (!shop) throw new NotFoundException('Shop not found');
      if (shop.adminId !== user.id) throw new ForbiddenException('Not allowed');
      return;
    }
    if (user.role === UserRole.SUBADMIN) {
      if (!user.shopId || user.shopId !== shopId)
        throw new ForbiddenException('Not allowed');
      return;
    }
    throw new ForbiddenException('Not allowed');
  }

  async getShopIntegrations(
    requester: { id: string; role: UserRole; shopId?: string | null },
    shopId: string,
  ) {
    await this.assertCanAccessShop(requester, shopId);
    const shop = await this.prisma.shop.findUnique({
      where: { id: shopId },
      select: {
        id: true,
        googleApiKey: true,
        googlePlaceId: true,
        facebookAppId: true,
        facebookAppSecret: true,
        instagramAccessToken: true,
        instagramBusinessAccountId: true,
      },
    });
    if (!shop) throw new NotFoundException('Shop not found');
    return shop;
  }

  async updateShopIntegrations(
    requester: { id: string; role: UserRole; shopId?: string | null },
    shopId: string,
    dto: {
      googleApiKey?: string;
      googlePlaceId?: string;
      facebookAppId?: string;
      facebookAppSecret?: string;
      instagramAccessToken?: string;
      instagramBusinessAccountId?: string;
    },
  ) {
    await this.assertCanAccessShop(requester, shopId);
    const updated = await this.prisma.shop.update({
      where: { id: shopId },
      data: {
        ...(dto.googleApiKey !== undefined
          ? { googleApiKey: dto.googleApiKey.trim() || null }
          : {}),
        ...(dto.googlePlaceId !== undefined
          ? { googlePlaceId: dto.googlePlaceId.trim() || null }
          : {}),
        ...(dto.facebookAppId !== undefined
          ? { facebookAppId: dto.facebookAppId.trim() || null }
          : {}),
        ...(dto.facebookAppSecret !== undefined
          ? { facebookAppSecret: dto.facebookAppSecret.trim() || null }
          : {}),
        ...(dto.instagramAccessToken !== undefined
          ? { instagramAccessToken: dto.instagramAccessToken.trim() || null }
          : {}),
        ...(dto.instagramBusinessAccountId !== undefined
          ? {
              instagramBusinessAccountId:
                dto.instagramBusinessAccountId.trim() || null,
            }
          : {}),
      },
      select: {
        id: true,
        googleApiKey: true,
        googlePlaceId: true,
        facebookAppId: true,
        facebookAppSecret: true,
        instagramAccessToken: true,
        instagramBusinessAccountId: true,
        updatedAt: true,
      },
    });
    return { updated: true, integrations: updated };
  }

  async createCustomerForShop(
    requester: { id: string; role: UserRole; shopId?: string | null },
    dto: { phone: string; email?: string; name?: string },
  ) {
    const shopId = requester.shopId ?? null;
    if (!shopId) throw new ForbiddenException('No shop selected');
    await this.assertCanAccessShop(requester, shopId);

    const phone = dto.phone.trim();
    const existing = await this.prisma.user.findUnique({
      where: { phone },
      select: { id: true, role: true, shopId: true },
    });
    if (existing) {
      const updated = await this.prisma.user.update({
        where: { id: existing.id },
        data: {
          ...(dto.email !== undefined
            ? {
                email: dto.email?.trim()
                  ? dto.email.trim().toLowerCase()
                  : null,
              }
            : {}),
          ...(dto.name !== undefined
            ? { name: dto.name?.trim() ? dto.name.trim() : null }
            : {}),
        },
        select: {
          id: true,
          role: true,
          name: true,
          phone: true,
          email: true,
          createdAt: true,
          passwordHash: true,
        },
      });
      await this.prisma.customerShop.upsert({
        where: { userId_shopId: { userId: updated.id, shopId } },
        create: { userId: updated.id, shopId },
        update: { lastSeenAt: new Date() },
      });
      const { passwordHash, ...safe } = updated as any;
      return { ...safe, shopId, isCashiUser: !!passwordHash };
    }

    try {
      const created = await this.prisma.user.create({
        data: {
          role: UserRole.USER,
          email: dto.email?.trim() ? dto.email.trim().toLowerCase() : null,
          name: dto.name?.trim() ? dto.name.trim() : null,
          phone,
        },
        select: {
          id: true,
          role: true,
          name: true,
          phone: true,
          email: true,
          createdAt: true,
          passwordHash: true,
        },
      });
      await this.prisma.customerShop.create({
        data: { userId: created.id, shopId },
      });
      const { passwordHash, ...safe } = created as any;
      return { ...safe, shopId, isCashiUser: !!passwordHash };
    } catch {
      throw new ConflictException('Email or phone already exists');
    }
  }

  async listCustomers(
    requester: { id: string; role: UserRole; shopId?: string | null },
    shopIdQuery?: string,
  ) {
    const shopId =
      requester.role === UserRole.SUPERADMIN
        ? (shopIdQuery ?? null)
        : (requester.shopId ?? null);
    if (!shopId) throw new ForbiddenException('No shop selected');
    await this.assertCanAccessShop(requester, shopId);

    const links = await this.prisma.customerShop.findMany({
      where: { shopId },
      orderBy: { lastSeenAt: 'desc' },
      select: {
        user: {
          select: {
            id: true,
            name: true,
            phone: true,
            email: true,
            createdAt: true,
            passwordHash: true,
          },
        },
      },
    });

    const users = links
      .map((l) => l.user)
      .filter((u): u is NonNullable<typeof u> => !!u);
    const ids = users.map((u) => u.id);
    const now = new Date();

    const couponCounts = await this.prisma.customerCoupon.groupBy({
      by: ['customerId'],
      where: {
        customerId: { in: ids },
        status: CustomerCouponStatus.ASSIGNED,
        coupon: {
          shopId,
          status: CouponStatus.ACTIVE,
          OR: [{ expiresAt: null }, { expiresAt: { gt: now } }],
        },
      },
      _count: { _all: true },
    });
    const couponByCustomer = new Map(
      couponCounts.map((r) => [r.customerId, r._count._all]),
    );

    const pointsSums = await this.prisma.loyaltyPointGrant.groupBy({
      by: ['customerId'],
      where: {
        shopId,
        customerId: { in: ids },
        expiresAt: { gt: now },
        pointsRemaining: { gt: 0 },
      },
      _sum: { pointsRemaining: true },
    });
    const pointsByCustomer = new Map(
      pointsSums.map((r) => [r.customerId, r._sum.pointsRemaining ?? 0]),
    );

    return users.map(({ passwordHash, ...u }) => ({
      ...u,
      shopId,
      isCashiUser: !!passwordHash,
      linkedToShop: true,
      rewards: {
        availableCoupons: couponByCustomer.get(u.id) ?? 0,
        availablePoints: pointsByCustomer.get(u.id) ?? 0,
      },
    }));
  }

  async listCustomersPaged(
    requester: { id: string; role: UserRole; shopId?: string | null },
    args: {
      shopId?: string;
      page: number;
      limit: number;
      q?: string;
      isActive?: string;
      isCashiUser?: string;
      couponOnly?: string;
    },
  ) {
    const page = Math.max(1, Math.floor(args.page));
    const limit = Math.min(100, Math.max(1, Math.floor(args.limit)));
    const skip = (page - 1) * limit;
    const q = (args.q ?? '').trim();

    const activeRaw = (args.isActive ?? '').trim().toLowerCase();
    const wantActive =
      activeRaw === 'true' ? true : activeRaw === 'false' ? false : null;

    const cashiRaw = (args.isCashiUser ?? '').trim().toLowerCase();
    const wantCashi =
      cashiRaw === 'true' ? true : cashiRaw === 'false' ? false : null;

    const couponOnlyRaw = (args.couponOnly ?? '').trim().toLowerCase();
    const wantCouponOnly =
      couponOnlyRaw === 'true' ||
      couponOnlyRaw === '1' ||
      couponOnlyRaw === 'yes';

    const shopId =
      requester.role === UserRole.SUPERADMIN
        ? args.shopId?.trim() || null
        : (requester.shopId ?? null);

    // SUPERADMIN: if no shopId provided, list across all shops.
    if (requester.role !== UserRole.SUPERADMIN) {
      if (!shopId) throw new ForbiddenException('No shop selected');
      await this.assertCanAccessShop(requester, shopId);
    } else if (shopId) {
      await this.assertCanAccessShop(requester, shopId);
    }

    const userSearch =
      q.length >= 2
        ? {
            OR: [
              { id: { contains: q, mode: 'insensitive' as const } },
              { name: { contains: q, mode: 'insensitive' as const } },
              { email: { contains: q, mode: 'insensitive' as const } },
              { phone: { contains: q, mode: 'insensitive' as const } },
            ],
          }
        : undefined;

    const userWhere: Prisma.UserWhereInput = {
      // Anyone linked to the shop via sale/customer activity should appear here,
      // even if their account role is ADMIN or SUBADMIN.
      ...(wantActive != null ? { isActive: wantActive } : {}),
      ...(wantCashi != null
        ? wantCashi
          ? { passwordHash: { not: null } }
          : { passwordHash: null }
        : {}),
      ...(userSearch ? userSearch : {}),
    };

    // Coupon holders (not yet customers of this shop)
    if (wantCouponOnly) {
      if (!shopId)
        throw new BadRequestException('shopId is required for couponOnly');
      const now = new Date();

      const baseWhere: Prisma.CustomerCouponWhereInput = {
        status: CustomerCouponStatus.ASSIGNED,
        coupon: {
          shopId,
          status: CouponStatus.ACTIVE,
          moderationStatus: ModerationStatus.PUBLISHED,
          OR: [{ expiresAt: null }, { expiresAt: { gt: now } }],
        },
        customer: {
          ...userWhere,
          customerShops: { none: { shopId } },
        },
      };

      const [totalGroups, groups] = await Promise.all([
        this.prisma.customerCoupon.groupBy({
          by: ['customerId'],
          where: baseWhere,
        }),
        this.prisma.customerCoupon.groupBy({
          by: ['customerId'],
          where: baseWhere,
          _count: { _all: true },
          _max: { assignedAt: true },
          orderBy: { _max: { assignedAt: 'desc' } },
          skip,
          take: limit,
        }),
      ]);
      const total = totalGroups.length;

      const ids = groups.map((g) => g.customerId);
      if (ids.length === 0) return { page, limit, total, items: [] };

      const users = await this.prisma.user.findMany({
        where: { id: { in: ids } },
        select: {
          id: true,
          name: true,
          phone: true,
          email: true,
          createdAt: true,
          passwordHash: true,
          isActive: true,
          deactivatedAt: true,
        },
      });
      const byId = new Map(users.map((u) => [u.id, u]));

      const pointsSums = await this.prisma.loyaltyPointGrant.groupBy({
        by: ['customerId'],
        where: {
          shopId,
          customerId: { in: ids },
          expiresAt: { gt: now },
          pointsRemaining: { gt: 0 },
        },
        _sum: { pointsRemaining: true },
      });
      const pointsByCustomer = new Map(
        pointsSums.map((r) => [r.customerId, r._sum.pointsRemaining ?? 0]),
      );

      const items = groups
        .map((g) => {
          const u = byId.get(g.customerId);
          if (!u) return null;
          const { passwordHash, ...safe } = u as any;
          return {
            ...safe,
            isCashiUser: !!passwordHash,
            linkedToShop: false,
            shopId,
            rewards: {
              availableCoupons: g._count._all ?? 0,
              availablePoints: pointsByCustomer.get(u.id) ?? 0,
            },
            lastSeenAt: g._max.assignedAt ?? null,
          };
        })
        .filter(Boolean);

      return { page, limit, total, items };
    }

    const where: Prisma.CustomerShopWhereInput = {
      ...(shopId ? { shopId } : {}),
      user: userWhere,
    };

    const [total, links] = await Promise.all([
      this.prisma.customerShop.count({ where }),
      this.prisma.customerShop.findMany({
        where,
        orderBy: { lastSeenAt: 'desc' },
        skip,
        take: limit,
        select: {
          shopId: true,
          lastSeenAt: true,
          shop:
            requester.role === UserRole.SUPERADMIN && !shopId
              ? { select: { id: true, name: true } }
              : false,
          user: {
            select: {
              id: true,
              name: true,
              phone: true,
              email: true,
              createdAt: true,
              passwordHash: true,
              isActive: true,
              deactivatedAt: true,
            },
          },
        },
      }),
    ]);

    const users = links
      .map((l) => ({
        link: l,
        user: l.user,
      }))
      .filter((x): x is { link: any; user: NonNullable<any> } => !!x.user);

    const ids = users.map((x) => x.user.id);
    const now = new Date();

    // Rewards are shop-scoped. If SUPERADMIN is listing across all shops, skip expensive rewards.
    let couponByCustomer = new Map<string, number>();
    let pointsByCustomer = new Map<string, number>();
    if (shopId && ids.length > 0) {
      const couponCounts = await this.prisma.customerCoupon.groupBy({
        by: ['customerId'],
        where: {
          customerId: { in: ids },
          status: CustomerCouponStatus.ASSIGNED,
          coupon: {
            shopId,
            status: CouponStatus.ACTIVE,
            OR: [{ expiresAt: null }, { expiresAt: { gt: now } }],
          },
        },
        _count: { _all: true },
      });
      couponByCustomer = new Map(
        couponCounts.map((r) => [r.customerId, r._count._all]),
      );

      const pointsSums = await this.prisma.loyaltyPointGrant.groupBy({
        by: ['customerId'],
        where: {
          shopId,
          customerId: { in: ids },
          expiresAt: { gt: now },
          pointsRemaining: { gt: 0 },
        },
        _sum: { pointsRemaining: true },
      });
      pointsByCustomer = new Map(
        pointsSums.map((r) => [r.customerId, r._sum.pointsRemaining ?? 0]),
      );
    }

    const items = users.map(({ user, link }) => {
      const { passwordHash, ...safe } = user;
      return {
        ...safe,
        isCashiUser: !!passwordHash,
        linkedToShop: true,
        shopId: link.shopId,
        shop: link.shop || undefined,
        lastSeenAt: link.lastSeenAt,
        rewards: shopId
          ? {
              availableCoupons: couponByCustomer.get(user.id) ?? 0,
              availablePoints: pointsByCustomer.get(user.id) ?? 0,
            }
          : undefined,
      };
    });

    return { page, limit, total, items };
  }

  async setCustomerActive(
    requester: { id: string; role: UserRole; shopId?: string | null },
    customerId: string,
    isActive: boolean,
  ) {
    if (
      requester.role !== UserRole.SUPERADMIN &&
      requester.role !== UserRole.ADMIN &&
      requester.role !== UserRole.SUBADMIN
    ) {
      throw new ForbiddenException('Not allowed');
    }

    if (
      requester.role === UserRole.ADMIN ||
      requester.role === UserRole.SUBADMIN
    ) {
      const shopId = requester.shopId ?? null;
      if (!shopId) throw new ForbiddenException('No shop selected');
      await this.assertCanAccessShop(requester, shopId);
      const linked = await this.prisma.customerShop.findUnique({
        where: { userId_shopId: { userId: customerId, shopId } },
        select: { userId: true },
      });
      if (!linked)
        throw new NotFoundException('Customer not found for current shop');
    }

    const user = await this.prisma.user.findUnique({
      where: { id: customerId },
      select: { id: true, role: true },
    });
    if (!user) throw new NotFoundException('Customer not found');

    const updated = await this.prisma.user.update({
      where: { id: customerId },
      data: {
        isActive,
        deactivatedAt: isActive ? null : new Date(),
        // revoke tokens on deactivate
        ...(isActive ? {} : { tokenVersion: { increment: 1 } }),
      },
      select: { id: true, isActive: true, deactivatedAt: true },
    });
    return { updated: true, customer: updated };
  }

  async listUsersPaged(
    requester: { id: string; role: UserRole; shopId?: string | null },
    args: {
      page: number;
      limit: number;
      q?: string;
      role?: string;
      isActive?: string;
    },
  ) {
    if (requester.role !== UserRole.SUPERADMIN)
      throw new ForbiddenException('Not allowed');

    const page = Math.max(1, Math.floor(args.page || 1));
    const limit = Math.min(100, Math.max(1, Math.floor(args.limit || 20)));
    const skip = (page - 1) * limit;

    const q = String(args.q ?? '').trim();
    const roleRaw = String(args.role ?? '')
      .trim()
      .toUpperCase();
    const isActiveRaw =
      args.isActive != null ? String(args.isActive).trim().toLowerCase() : '';

    let role: UserRole | undefined;
    if (roleRaw && roleRaw !== 'ALL') {
      if (!Object.prototype.hasOwnProperty.call(UserRole, roleRaw)) {
        throw new BadRequestException('Invalid role filter');
      }
      role = (UserRole as any)[roleRaw] as UserRole;
    }

    let isActive: boolean | undefined;
    if (isActiveRaw === 'true') isActive = true;
    else if (isActiveRaw === 'false') isActive = false;

    const where: Prisma.UserWhereInput = {};
    if (role) where.role = role;
    if (isActive !== undefined) where.isActive = isActive;

    if (q) {
      const short = q.length < 3;
      if (short) {
        where.OR = [{ id: q }, { email: q.toLowerCase() }, { phone: q }];
      } else {
        where.OR = [
          { id: { contains: q, mode: 'insensitive' } },
          { email: { contains: q, mode: 'insensitive' } },
          { phone: { contains: q, mode: 'insensitive' } },
          { name: { contains: q, mode: 'insensitive' } },
        ];
      }
    }

    const [total, rows] = await this.prisma.$transaction([
      this.prisma.user.count({ where }),
      this.prisma.user.findMany({
        where,
        orderBy: { createdAt: 'desc' },
        skip,
        take: limit,
        select: {
          id: true,
          role: true,
          email: true,
          phone: true,
          name: true,
          isActive: true,
          deactivatedAt: true,
          tokenVersion: true,
          shopId: true,
          shop: { select: { id: true, name: true } },
          permissions: true,
          ownedShops: { select: { id: true, name: true } },
          subadminShops: {
            select: { shop: { select: { id: true, name: true } } },
          },
          createdAt: true,
          updatedAt: true,
        },
      }),
    ]);

    const items = rows.map((u) => {
      const owned = (u.ownedShops || []).map((s) => ({
        id: s.id,
        name: s.name,
      }));
      const sub = (u.subadminShops || [])
        .map((l: any) => l?.shop)
        .filter(Boolean);
      return {
        ...u,
        shops:
          u.role === UserRole.ADMIN
            ? owned
            : u.role === UserRole.SUBADMIN
              ? sub
              : [],
      };
    });

    return { page, limit, total, items };
  }

  async setUserActive(
    requester: { id: string; role: UserRole; shopId?: string | null },
    userId: string,
    isActive: boolean,
  ) {
    if (requester.role !== UserRole.SUPERADMIN)
      throw new ForbiddenException('Not allowed');
    if (!userId) throw new BadRequestException('id is required');
    if (requester.id === userId && !isActive) {
      throw new BadRequestException('Cannot deactivate your own account');
    }

    const existing = await this.prisma.user.findUnique({
      where: { id: userId },
      select: { id: true },
    });
    if (!existing) throw new NotFoundException('User not found');

    const updated = await this.prisma.user.update({
      where: { id: userId },
      data: {
        isActive,
        deactivatedAt: isActive ? null : new Date(),
        ...(isActive ? {} : { tokenVersion: { increment: 1 } }),
      },
      select: {
        id: true,
        role: true,
        isActive: true,
        deactivatedAt: true,
        tokenVersion: true,
      },
    });

    return { updated: true, user: updated };
  }

  async createSuperadmin(
    requester: { id: string; role: UserRole; shopId?: string | null },
    dto: CreateSuperadminDto,
  ) {
    if (requester.role !== UserRole.SUPERADMIN)
      throw new ForbiddenException('Not allowed');

    const email = dto.email.toLowerCase().trim();
    const phone = dto.phone ? dto.phone.trim() : null;
    const name = dto.name ? dto.name.trim() : null;
    const perms =
      Array.isArray(dto.permissions) && dto.permissions.length > 0
        ? dto.permissions
        : null;

    try {
      const created = await this.prisma.user.create({
        data: {
          role: UserRole.SUPERADMIN,
          email,
          phone,
          name,
          passwordHash: await bcrypt.hash(dto.password, 12),
          permissions: perms as any,
        },
        select: {
          id: true,
          role: true,
          email: true,
          phone: true,
          name: true,
          permissions: true,
          isActive: true,
          createdAt: true,
        },
      });
      return { created: true, user: created };
    } catch (e) {
      if (
        e instanceof Prisma.PrismaClientKnownRequestError &&
        e.code === 'P2002'
      ) {
        throw new ConflictException('Email or phone already exists');
      }
      throw e;
    }
  }

  async updateManagedUser(
    requester: { id: string; role: UserRole; shopId?: string | null },
    userId: string,
    dto: UpdateManagedUserDto,
  ) {
    if (requester.role !== UserRole.SUPERADMIN)
      throw new ForbiddenException('Not allowed');
    if (!userId) throw new BadRequestException('id is required');

    const existing = await this.prisma.user.findUnique({
      where: { id: userId },
      select: { id: true },
    });
    if (!existing) throw new NotFoundException('User not found');

    const email =
      dto.email !== undefined ? dto.email.toLowerCase().trim() : undefined;
    const phone =
      dto.phone !== undefined
        ? dto.phone
          ? dto.phone.trim()
          : null
        : undefined;
    const name =
      dto.name !== undefined ? (dto.name ? dto.name.trim() : null) : undefined;
    const perms =
      dto.permissions !== undefined
        ? Array.isArray(dto.permissions) && dto.permissions.length > 0
          ? dto.permissions
          : null
        : undefined;

    try {
      const updated = await this.prisma.user.update({
        where: { id: userId },
        data: {
          ...(email !== undefined ? { email } : {}),
          ...(phone !== undefined ? { phone } : {}),
          ...(name !== undefined ? { name } : {}),
          ...(perms !== undefined ? { permissions: perms as any } : {}),
        },
        select: {
          id: true,
          role: true,
          email: true,
          phone: true,
          name: true,
          permissions: true,
          isActive: true,
          deactivatedAt: true,
          shopId: true,
          createdAt: true,
          updatedAt: true,
        },
      });
      return { updated: true, user: updated };
    } catch (e) {
      if (
        e instanceof Prisma.PrismaClientKnownRequestError &&
        e.code === 'P2002'
      ) {
        throw new ConflictException('Email or phone already exists');
      }
      throw e;
    }
  }

  async lookupCustomerByPhone(
    requester: { id: string; role: UserRole; shopId?: string | null },
    phone: string,
    shopIdQuery?: string,
  ) {
    const shopId =
      requester.role === UserRole.SUPERADMIN
        ? (shopIdQuery ?? null)
        : (requester.shopId ?? null);
    if (!shopId) throw new ForbiddenException('No shop selected');
    await this.assertCanAccessShop(requester, shopId);

    const p = (phone ?? '').trim();
    if (!p) throw new BadRequestException('phone is required');

    const user = await this.prisma.user.findUnique({
      where: { phone: p },
      select: {
        id: true,
        role: true,
        name: true,
        phone: true,
        email: true,
        createdAt: true,
        passwordHash: true,
      },
    });
    if (!user) return null;

    const now = new Date();
    const [linked, coupons, grants, salesAgg, lastSale] =
      await this.prisma.$transaction([
        this.prisma.customerShop.findUnique({
          where: { userId_shopId: { userId: user.id, shopId } },
          select: { userId: true },
        }),
        this.prisma.customerCoupon.findMany({
          where: {
            customerId: user.id,
            status: CustomerCouponStatus.ASSIGNED,
            coupon: {
              shopId,
              status: CouponStatus.ACTIVE,
              OR: [{ expiresAt: null }, { expiresAt: { gt: new Date() } }],
            },
          },
          orderBy: { assignedAt: 'desc' },
          select: {
            coupon: {
              select: {
                id: true,
                title: true,
                valueType: true,
                valueFixed: true,
                valuePercent: true,
                minOrderValue: true,
                expiresAt: true,
                imageUrl: true,
                shopId: true,
              },
            },
            assignedAt: true,
          },
        }),
        this.prisma.loyaltyPointGrant.findMany({
          where: {
            shopId,
            customerId: user.id,
            expiresAt: { gt: now },
            pointsRemaining: { gt: 0 },
          },
          select: { pointsRemaining: true },
        }),
        this.prisma.sale.aggregate({
          where: { shopId, customerId: user.id, status: SaleStatus.COMPLETED },
          _count: { _all: true },
          _sum: { amount: true },
        }),
        this.prisma.sale.findFirst({
          where: { shopId, customerId: user.id, status: SaleStatus.COMPLETED },
          orderBy: { createdAt: 'desc' },
          select: {
            id: true,
            createdAt: true,
            amount: true,
            originalAmount: true,
            discountAmount: true,
            loyaltyPointsRedeemed: true,
            loyaltyPointsEarned: true,
            appliedCoupon: { select: { id: true, title: true } },
          },
        }),
      ]);
    const availablePoints = grants.reduce(
      (sum, g) => sum + g.pointsRemaining,
      0,
    );

    const reviewAgg = user.phone
      ? await this.prisma.review.aggregate({
          where: {
            shopId,
            source: ReviewSource.CASHI,
            customerPhone: user.phone,
          },
          _count: { _all: true },
          _avg: { rating: true },
        })
      : { _count: { _all: 0 }, _avg: { rating: null } };

    const { passwordHash, ...safe } = user as any;
    return {
      ...safe,
      shopId,
      isCashiUser: !!passwordHash,
      linkedToShop: !!linked,
      stats: {
        visits: salesAgg._count?._all ?? 0,
        totalSpent: salesAgg._sum?.amount ?? 0,
        ratingsCount: (reviewAgg as any)._count?._all ?? 0,
        ratingAvg: (reviewAgg as any)._avg?.rating ?? null,
      },
      lastSale: lastSale
        ? {
            id: lastSale.id,
            createdAt: lastSale.createdAt,
            amount: lastSale.amount,
            originalAmount: lastSale.originalAmount ?? null,
            discountAmount: lastSale.discountAmount ?? 0,
            loyaltyPointsRedeemed: lastSale.loyaltyPointsRedeemed ?? 0,
            loyaltyPointsEarned: lastSale.loyaltyPointsEarned ?? 0,
            appliedCoupon: lastSale.appliedCoupon
              ? {
                  id: lastSale.appliedCoupon.id,
                  title: lastSale.appliedCoupon.title,
                }
              : null,
          }
        : null,
      availableCoupons: coupons.map((c) => c.coupon).filter(Boolean),
      loyalty: {
        availablePoints,
      },
    };
  }

  async lookupCustomerById(
    requester: { id: string; role: UserRole; shopId?: string | null },
    userId: string,
    shopIdQuery?: string,
  ) {
    const shopId =
      requester.role === UserRole.SUPERADMIN
        ? (shopIdQuery ?? null)
        : (requester.shopId ?? null);
    if (!shopId) throw new ForbiddenException('No shop selected');
    await this.assertCanAccessShop(requester, shopId);

    const id = (userId ?? '').trim();
    if (!id) throw new BadRequestException('id is required');

    const user = await this.prisma.user.findUnique({
      where: { id },
      select: {
        id: true,
        role: true,
        name: true,
        phone: true,
        email: true,
        createdAt: true,
        passwordHash: true,
      },
    });
    if (!user) return null;

    const now = new Date();
    const [linked, coupons, grants, salesAgg, lastSale] =
      await this.prisma.$transaction([
        this.prisma.customerShop.findUnique({
          where: { userId_shopId: { userId: user.id, shopId } },
          select: { userId: true },
        }),
        this.prisma.customerCoupon.findMany({
          where: {
            customerId: user.id,
            status: CustomerCouponStatus.ASSIGNED,
            coupon: {
              shopId,
              status: CouponStatus.ACTIVE,
              OR: [{ expiresAt: null }, { expiresAt: { gt: new Date() } }],
            },
          },
          orderBy: { assignedAt: 'desc' },
          select: {
            coupon: {
              select: {
                id: true,
                title: true,
                valueType: true,
                valueFixed: true,
                valuePercent: true,
                minOrderValue: true,
                expiresAt: true,
                imageUrl: true,
                shopId: true,
              },
            },
            assignedAt: true,
          },
        }),
        this.prisma.loyaltyPointGrant.findMany({
          where: {
            shopId,
            customerId: user.id,
            expiresAt: { gt: now },
            pointsRemaining: { gt: 0 },
          },
          select: { pointsRemaining: true },
        }),
        this.prisma.sale.aggregate({
          where: { shopId, customerId: user.id, status: SaleStatus.COMPLETED },
          _count: { _all: true },
          _sum: { amount: true },
        }),
        this.prisma.sale.findFirst({
          where: { shopId, customerId: user.id, status: SaleStatus.COMPLETED },
          orderBy: { createdAt: 'desc' },
          select: {
            id: true,
            createdAt: true,
            amount: true,
            originalAmount: true,
            discountAmount: true,
            loyaltyPointsRedeemed: true,
            loyaltyPointsEarned: true,
            appliedCoupon: { select: { id: true, title: true } },
          },
        }),
      ]);
    const availablePoints = grants.reduce(
      (sum, g) => sum + g.pointsRemaining,
      0,
    );

    const reviewAgg = user.phone
      ? await this.prisma.review.aggregate({
          where: {
            shopId,
            source: ReviewSource.CASHI,
            customerPhone: user.phone,
          },
          _count: { _all: true },
          _avg: { rating: true },
        })
      : { _count: { _all: 0 }, _avg: { rating: null } };

    const { passwordHash, ...safe } = user as any;
    return {
      ...safe,
      shopId,
      isCashiUser: !!passwordHash,
      linkedToShop: !!linked,
      stats: {
        visits: salesAgg._count?._all ?? 0,
        totalSpent: salesAgg._sum?.amount ?? 0,
        ratingsCount: (reviewAgg as any)._count?._all ?? 0,
        ratingAvg: (reviewAgg as any)._avg?.rating ?? null,
      },
      lastSale: lastSale
        ? {
            id: lastSale.id,
            createdAt: lastSale.createdAt,
            amount: lastSale.amount,
            originalAmount: lastSale.originalAmount ?? null,
            discountAmount: lastSale.discountAmount ?? 0,
            loyaltyPointsRedeemed: lastSale.loyaltyPointsRedeemed ?? 0,
            loyaltyPointsEarned: lastSale.loyaltyPointsEarned ?? 0,
            appliedCoupon: lastSale.appliedCoupon
              ? {
                  id: lastSale.appliedCoupon.id,
                  title: lastSale.appliedCoupon.title,
                }
              : null,
          }
        : null,
      availableCoupons: coupons.map((c) => c.coupon).filter(Boolean),
      loyalty: {
        availablePoints,
      },
    };
  }

  async listMyCoupons(userId: string) {
    const now = new Date();
    const customerIds = await this.resolveCustomerIdsForMe(userId);
    const rows = await this.prisma.customerCoupon.findMany({
      where: { customerId: { in: customerIds } },
      orderBy: { assignedAt: 'desc' },
      select: {
        id: true,
        status: true,
        assignedAt: true,
        redeemedAt: true,
        cashiCoinsSpent: true,
        coupon: {
          select: {
            id: true,
            shopId: true,
            title: true,
            shortDescription: true,
            longDescription: true,
            valueType: true,
            valueFixed: true,
            valuePercent: true,
            minOrderValue: true,
            status: true,
            activeAt: true,
            expiresAt: true,
            imageUrl: true,
          },
        },
      },
    });

    const active: any[] = [];
    const used: any[] = [];
    const expired: any[] = [];

    for (const r of rows) {
      const c = r.coupon;
      const isRedeemed = r.status === CustomerCouponStatus.REDEEMED;
      const isExpired =
        !isRedeemed && !!c?.expiresAt && c.expiresAt.getTime() <= now.getTime();
      const isInactiveCoupon = !isRedeemed && c?.status !== CouponStatus.ACTIVE;

      const item = {
        assignmentId: r.id,
        status: r.status,
        assignedAt: r.assignedAt,
        redeemedAt: r.redeemedAt,
        cashiCoinsSpent: r.cashiCoinsSpent ?? 0,
        coupon: c,
      };

      if (isRedeemed) used.push(item);
      else if (isExpired || isInactiveCoupon) expired.push(item);
      else active.push(item);
    }

    return { active, used, expired };
  }

  private haversineKm(
    a: { lat: number; lng: number },
    b: { lat: number; lng: number },
  ) {
    const R = 6371;
    const dLat = ((b.lat - a.lat) * Math.PI) / 180;
    const dLng = ((b.lng - a.lng) * Math.PI) / 180;
    const lat1 = (a.lat * Math.PI) / 180;
    const lat2 = (b.lat * Math.PI) / 180;
    const sinDLat = Math.sin(dLat / 2);
    const sinDLng = Math.sin(dLng / 2);
    const h =
      sinDLat * sinDLat + Math.cos(lat1) * Math.cos(lat2) * sinDLng * sinDLng;
    return 2 * R * Math.asin(Math.sqrt(h));
  }

  async listMyAvailableCoupons(
    userId: string,
    args: {
      shopId?: string;
      coords?: { lat: number; lng: number };
      radiusKm: number;
      limit: number;
    },
  ) {
    const now = new Date();

    // Resolve coords: prefer explicit coords, else use last stored user coords.
    const user = await this.prisma.user.findUnique({
      where: { id: userId },
      select: { id: true, latitude: true, longitude: true },
    });
    if (!user) throw new BadRequestException('User not found');

    const coords =
      args.coords ??
      (user.latitude != null && user.longitude != null
        ? { lat: user.latitude, lng: user.longitude }
        : null);

    // If shopId is provided, skip geo filtering.
    let shopIds: string[] = [];
    if (args.shopId) {
      shopIds = [args.shopId];
    } else {
      if (!coords)
        return { items: [], radiusKm: args.radiusKm, usedCoords: false };

      const shops = await this.prisma.shop.findMany({
        where: {
          isActive: true,
          status: ShopStatus.APPROVED,
          latitude: { not: null },
          longitude: { not: null },
        },
        select: { id: true, latitude: true, longitude: true },
      });

      const nearby = shops
        .map((s) => ({
          id: s.id,
          d: this.haversineKm(coords, {
            lat: s.latitude as number,
            lng: s.longitude as number,
          }),
        }))
        .filter((x) => x.d <= args.radiusKm)
        .sort((a, b) => a.d - b.d)
        .slice(0, 200);

      shopIds = nearby.map((n) => n.id);
    }

    if (shopIds.length === 0)
      return { items: [], radiusKm: args.radiusKm, usedCoords: !!coords };

    const items = await this.prisma.coupon.findMany({
      where: {
        shopId: { in: shopIds },
        status: CouponStatus.ACTIVE,
        moderationStatus: ModerationStatus.PUBLISHED,
        AND: [
          { OR: [{ activeAt: null }, { activeAt: { lte: now } }] },
          { OR: [{ expiresAt: null }, { expiresAt: { gt: now } }] },
        ],
        // Exclude coupons already claimed (any status) by this user.
        assignments: { none: { customerId: userId } },
      },
      take: Math.min(Math.max(args.limit, 1), 100),
      orderBy: [{ publishedAt: 'desc' }, { createdAt: 'desc' }],
      select: {
        id: true,
        shopId: true,
        title: true,
        shortDescription: true,
        longDescription: true,
        valueType: true,
        valueFixed: true,
        valuePercent: true,
        minOrderValue: true,
        status: true,
        activeAt: true,
        expiresAt: true,
        imageUrl: true,
        cashiCoinsCost: true,
        totalCoupons: true,
        issuedCount: true,
        shop: {
          select: {
            id: true,
            name: true,
            username: true,
            imageUrl: true,
            city: true,
            state: true,
          },
        },
      },
    });

    const filtered = items.filter(
      (c) => c.totalCoupons == null || (c.issuedCount ?? 0) < c.totalCoupons,
    );
    return {
      items: filtered.map((c: any) => ({
        ...c,
        // Backward compatible alias for older apps.
        cashiPointsCost: c.cashiCoinsCost ?? 0,
      })),
      radiusKm: args.radiusKm,
      usedCoords: !!coords,
    };
  }

  async claimCouponWithPoints(userId: string, dto: { couponId: string }) {
    const couponId = String(dto?.couponId ?? '').trim();
    if (!couponId) throw new BadRequestException('couponId is required');
    const now = new Date();

    return this.prisma.$transaction(async (tx) => {
      const user = await tx.user.findUnique({
        where: { id: userId },
        select: { id: true, cashiCoins: true, cashiPoints: true },
      });
      if (!user) throw new BadRequestException('User not found');

      const coupon = await tx.coupon.findFirst({
        where: {
          id: couponId,
          status: CouponStatus.ACTIVE,
          moderationStatus: ModerationStatus.PUBLISHED,
          AND: [
            { OR: [{ activeAt: null }, { activeAt: { lte: now } }] },
            { OR: [{ expiresAt: null }, { expiresAt: { gt: now } }] },
          ],
          shop: { isActive: true, status: ShopStatus.APPROVED },
        },
        select: {
          id: true,
          shopId: true,
          title: true,
          shortDescription: true,
          longDescription: true,
          valueType: true,
          valueFixed: true,
          valuePercent: true,
          minOrderValue: true,
          status: true,
          activeAt: true,
          expiresAt: true,
          imageUrl: true,
          cashiCoinsCost: true,
          totalCoupons: true,
          issuedCount: true,
        },
      });
      if (!coupon) throw new BadRequestException('Coupon not available');

      if (
        coupon.totalCoupons != null &&
        coupon.issuedCount >= coupon.totalCoupons
      ) {
        throw new BadRequestException('Coupon limit reached');
      }

      const existing = await tx.customerCoupon.findUnique({
        where: {
          customerId_couponId: { customerId: userId, couponId: coupon.id },
        },
        select: {
          id: true,
          status: true,
          assignedAt: true,
          redeemedAt: true,
          cashiCoinsSpent: true,
        },
      });
      if (existing) {
        return {
          ok: true,
          assignment: {
            assignmentId: existing.id,
            status: existing.status,
            assignedAt: existing.assignedAt,
            redeemedAt: existing.redeemedAt ?? null,
            cashiCoinsSpent: existing.cashiCoinsSpent ?? 0,
            cashiPointsSpent: existing.cashiCoinsSpent ?? 0,
            coupon: {
              ...(coupon as any),
              // Backward compatible alias for older apps.
              cashiPointsCost: (coupon as any).cashiCoinsCost ?? 0,
            },
          },
          cashiCoins: { available: user.cashiCoins },
          cashiPoints: { available: user.cashiPoints },
        };
      }

      const cost = Math.max(0, Number((coupon as any).cashiCoinsCost ?? 0));
      if (cost > user.cashiCoins) {
        throw new BadRequestException('Not enough Cashi coins');
      }

      const created = await tx.customerCoupon.create({
        data: {
          customerId: userId,
          couponId: coupon.id,
          status: CustomerCouponStatus.ASSIGNED,
          cashiCoinsSpent: cost,
        },
        select: {
          id: true,
          status: true,
          assignedAt: true,
          redeemedAt: true,
          cashiCoinsSpent: true,
        },
      });

      if (cost > 0) {
        await tx.user.update({
          where: { id: userId },
          data: { cashiCoins: { decrement: cost } },
        });
      }
      await tx.coupon.update({
        where: { id: coupon.id },
        data: { issuedCount: { increment: 1 } },
      });

      const updatedUser = await tx.user.findUnique({
        where: { id: userId },
        select: { cashiCoins: true, cashiPoints: true },
      });

      return {
        ok: true,
        assignment: {
          assignmentId: created.id,
          status: created.status,
          assignedAt: created.assignedAt,
          redeemedAt: created.redeemedAt ?? null,
          cashiCoinsSpent: created.cashiCoinsSpent ?? cost,
          cashiPointsSpent: created.cashiCoinsSpent ?? cost,
          coupon: {
            ...(coupon as any),
            cashiPointsCost: (coupon as any).cashiCoinsCost ?? 0,
          },
        },
        cashiCoins: {
          available: updatedUser?.cashiCoins ?? user.cashiCoins - cost,
        },
        cashiPoints: {
          available: updatedUser?.cashiPoints ?? user.cashiPoints,
        },
      };
    });
  }

  async getMyDashboard(userId: string) {
    const now = new Date();
    const customerIds = await this.resolveCustomerIdsForMe(userId);

    const [
      me,
      cashiCoinsEarned,
      cashiCoinsSpent,
      cashiPointsEarned,
      cashiPointsRedeemed,
      savings,
      couponActive,
      couponUsed,
    ] = await Promise.all([
      this.prisma.user.findUnique({
        where: { id: userId },
        select: { cashiCoins: true, cashiPoints: true },
      }),
      this.prisma.sale.aggregate({
        where: {
          customerId: { in: customerIds },
          status: SaleStatus.COMPLETED,
        },
        _sum: { cashiCoinsEarned: true },
      }),
      this.prisma.customerCoupon.aggregate({
        where: { customerId: { in: customerIds } },
        _sum: { cashiCoinsSpent: true },
      }),
      this.prisma.sale.aggregate({
        where: { customerId: { in: customerIds }, status: SaleStatus.COMPLETED },
        _sum: { loyaltyPointsEarned: true },
      }),
      this.prisma.sale.aggregate({
        where: { customerId: { in: customerIds }, status: SaleStatus.COMPLETED },
        _sum: { loyaltyPointsRedeemed: true },
      }),
      this.prisma.sale.aggregate({
        where: {
          customerId: { in: customerIds },
          status: SaleStatus.COMPLETED,
        },
        _sum: { discountAmount: true },
      }),
      this.prisma.customerCoupon.count({
        where: {
          customerId: { in: customerIds },
          status: CustomerCouponStatus.ASSIGNED,
          coupon: {
            status: CouponStatus.ACTIVE,
            OR: [{ expiresAt: null }, { expiresAt: { gt: now } }],
          },
        },
      }),
      this.prisma.customerCoupon.count({
        where: {
          customerId: { in: customerIds },
          status: CustomerCouponStatus.REDEEMED,
        },
      }),
    ]);

    // Expired = assigned but coupon is expired/inactive
    const couponExpired = await this.prisma.customerCoupon.count({
      where: {
        customerId: { in: customerIds },
        status: CustomerCouponStatus.ASSIGNED,
        OR: [
          { coupon: { status: { not: CouponStatus.ACTIVE } } },
          { coupon: { expiresAt: { lte: now } } },
        ],
      },
    });

    return {
      cashiCoins: {
        available: me?.cashiCoins ?? 0,
        earned: cashiCoinsEarned._sum.cashiCoinsEarned ?? 0,
        spent: cashiCoinsSpent._sum.cashiCoinsSpent ?? 0,
      },
      cashiPoints: {
        available: me?.cashiPoints ?? 0,
        earned: cashiPointsEarned._sum.loyaltyPointsEarned ?? 0,
        redeemed: cashiPointsRedeemed._sum.loyaltyPointsRedeemed ?? 0,
      },
      cashback: {
        // Best available proxy for "savings" right now.
        savedAmount: savings._sum.discountAmount ?? 0,
      },
      coupons: {
        active: couponActive,
        used: couponUsed,
        expired: couponExpired,
      },
    };
  }

  async listMyEarnings(userId: string, page = 1, limit = 20) {
    const safePage = Math.max(1, Math.floor(page));
    const safeLimit = Math.min(100, Math.max(1, Math.floor(limit)));
    const skip = (safePage - 1) * safeLimit;
    const customerIds = await this.resolveCustomerIdsForMe(userId);
    const where = {
      customerId: { in: customerIds },
      status: SaleStatus.COMPLETED,
    } as const;
    const [total, sales] = await Promise.all([
      this.prisma.sale.count({ where }),
      this.prisma.sale.findMany({
        where,
        orderBy: { createdAt: 'desc' },
        skip,
        take: safeLimit,
        select: {
          id: true,
          amount: true,
          cashiCoinsEarned: true,
          originalAmount: true,
          discountAmount: true,
          loyaltyPointsEarned: true,
          loyaltyPointsRedeemed: true,
          createdAt: true,
          shop: {
            select: { id: true, name: true, username: true, imageUrl: true },
          },
        },
      }),
    ]);

    return {
      page: safePage,
      limit: safeLimit,
      total,
      items: sales.map((s) => ({
        id: s.id,
        createdAt: s.createdAt,
        amount: s.amount,
        cashiCoinsEarned: (s as any).cashiCoinsEarned ?? 0,
        cashiPointsEarned: s.loyaltyPointsEarned ?? 0,
        cashiPointsRedeemed: s.loyaltyPointsRedeemed ?? 0,
        originalAmount: s.originalAmount,
        discountAmount: s.discountAmount,
        // Backward compatible aliases (older apps used these keys).
        pointsEarned: s.loyaltyPointsEarned,
        pointsRedeemed: s.loyaltyPointsRedeemed,
        shop: s.shop,
      })),
    };
  }

  async getMyShopActivity(
    userId: string,
    shopId: string,
    page = 1,
    limit = 20,
  ) {
    const safePage = Math.max(1, Math.floor(page));
    const safeLimit = Math.min(100, Math.max(1, Math.floor(limit)));
    const skip = (safePage - 1) * safeLimit;
    const customerIds = await this.resolveCustomerIdsForMe(userId);
    const now = new Date();

    const where = {
      shopId,
      customerId: { in: customerIds },
      status: SaleStatus.COMPLETED,
    } as const;

    const [agg, cashiPointsAvailable, total, sales] =
      await this.prisma.$transaction([
      this.prisma.sale.aggregate({
        where,
        _count: { _all: true },
        _sum: {
          loyaltyPointsEarned: true,
          loyaltyPointsRedeemed: true,
          cashiCoinsEarned: true,
          discountAmount: true,
        },
      }),
      this.prisma.loyaltyPointGrant.aggregate({
        where: {
          shopId,
          customerId: { in: customerIds },
          expiresAt: { gt: now },
          pointsRemaining: { gt: 0 },
        },
        _sum: { pointsRemaining: true },
      }),
      this.prisma.sale.count({ where }),
      this.prisma.sale.findMany({
        where,
        orderBy: { createdAt: 'desc' },
        skip,
        take: safeLimit,
        select: {
          id: true,
          createdAt: true,
          amount: true,
          cashiCoinsEarned: true,
          originalAmount: true,
          discountAmount: true,
          loyaltyPointsEarned: true,
          loyaltyPointsRedeemed: true,
        },
      }),
    ]);

    return {
      shopId,
      stats: {
        visits: agg._count?._all ?? 0,
        cashiPointsAvailable: cashiPointsAvailable._sum?.pointsRemaining ?? 0,
        cashiPointsEarned: agg._sum?.loyaltyPointsEarned ?? 0,
        cashiPointsRedeemed: agg._sum?.loyaltyPointsRedeemed ?? 0,
        cashiCoinsEarned: agg._sum?.cashiCoinsEarned ?? 0,
        savedAmount: agg._sum?.discountAmount ?? 0,
      },
      page: safePage,
      limit: safeLimit,
      total,
      items: sales.map((s) => ({
        id: s.id,
        createdAt: s.createdAt,
        amount: s.amount,
        cashiCoinsEarned: (s as any).cashiCoinsEarned ?? 0,
        cashiPointsEarned: s.loyaltyPointsEarned ?? 0,
        cashiPointsRedeemed: s.loyaltyPointsRedeemed ?? 0,
        originalAmount: s.originalAmount,
        discountAmount: s.discountAmount,
        // Backward compatible aliases (older apps used these keys).
        pointsEarned: s.loyaltyPointsEarned ?? 0,
        pointsRedeemed: s.loyaltyPointsRedeemed ?? 0,
      })),
    };
  }

  async approveShop(shopId: string) {
    const shop = await this.prisma.shop.update({
      where: { id: shopId },
      data: {
        status: ShopStatus.APPROVED,
        approvedAt: new Date(),
        rejectedAt: null,
        rejectedReason: null,
        isActive: true,
        deactivatedAt: null,
      },
      select: {
        id: true,
        name: true,
        city: true,
        status: true,
        approvedAt: true,
        rejectedAt: true,
        rejectedReason: true,
        isActive: true,
        deactivatedAt: true,
        admin: {
          select: {
            email: true,
            name: true,
          },
        },
      },
    });

    this.queueShopApprovedEmail({
      to: shop.admin?.email ?? null,
      recipientName: shop.admin?.name ?? null,
      shopName: shop.name,
      city: shop.city,
    });

    const { admin: _admin, ...safeShop } = shop;
    return { approved: true, shop: safeShop };
  }

  async rejectShop(shopId: string, reason: string) {
    const shop = await this.prisma.shop.update({
      where: { id: shopId },
      data: {
        status: ShopStatus.REJECTED,
        approvedAt: null,
        rejectedAt: new Date(),
        rejectedReason: reason?.trim() || null,
        // Rejection is a moderation state, not a deactivation state.
        // Keep the shop active so "Active/Inactive" remains independent from "Approved/Pending/Rejected".
        isActive: true,
        deactivatedAt: null,
      },
      select: {
        id: true,
        name: true,
        status: true,
        approvedAt: true,
        rejectedAt: true,
        rejectedReason: true,
        isActive: true,
        deactivatedAt: true,
      },
    });
    return { rejected: true, shop };
  }

  async setShopStatus(
    shopId: string,
    dto: { status: ShopStatus; reason?: string | null; note?: string | null },
  ) {
    if (dto.status === ShopStatus.APPROVED) return this.approveShop(shopId);
    if (dto.status === ShopStatus.REJECTED)
      return this.rejectShop(shopId, dto.reason ?? '');

    // PENDING
    const updated = await this.prisma.shop.update({
      where: { id: shopId },
      data: {
        status: ShopStatus.PENDING,
        approvedAt: null,
        rejectedAt: null,
        rejectedReason: null,
        isActive: true,
        deactivatedAt: null,
      },
      select: {
        id: true,
        name: true,
        status: true,
        approvedAt: true,
        rejectedAt: true,
        rejectedReason: true,
        isActive: true,
        deactivatedAt: true,
      },
    });
    return { updated: true, shop: updated };
  }

  async setShopActive(shopId: string, isActive: boolean) {
    const shop = await this.prisma.shop.update({
      where: { id: shopId },
      data: {
        isActive,
        deactivatedAt: isActive ? null : new Date(),
      },
      select: {
        id: true,
        name: true,
        status: true,
        approvedAt: true,
        rejectedAt: true,
        rejectedReason: true,
        isActive: true,
        deactivatedAt: true,
      },
    });
    return { updated: true, shop };
  }

  async resubmitRejectedShopForAdmin(adminUserId: string, shopId: string) {
    const shop = await this.prisma.shop.findUnique({
      where: { id: shopId },
      select: { id: true, adminId: true, status: true },
    });
    if (!shop) throw new NotFoundException('Shop not found');
    if (shop.adminId !== adminUserId)
      throw new ForbiddenException('Not allowed');
    if (shop.status !== ShopStatus.REJECTED) {
      throw new BadRequestException('Only rejected shops can be resubmitted');
    }

    const updated = await this.prisma.shop.update({
      where: { id: shop.id },
      data: {
        status: ShopStatus.PENDING,
        approvedAt: null,
        rejectedAt: null,
        rejectedReason: null,
        isActive: true,
        deactivatedAt: null,
      },
      select: {
        id: true,
        name: true,
        status: true,
        approvedAt: true,
        rejectedAt: true,
        rejectedReason: true,
        isActive: true,
        deactivatedAt: true,
      },
    });

    return { resubmitted: true, shop: updated };
  }

  async archiveShop(shopId: string) {
    const shop = await this.prisma.shop.update({
      where: { id: shopId },
      data: {
        // Soft delete: deactivate and clear unique username so it can be reused.
        isActive: false,
        deactivatedAt: new Date(),
        status: ShopStatus.PENDING,
        approvedAt: null,
        rejectedAt: null,
        rejectedReason: null,
        username: null,
      },
      select: {
        id: true,
        name: true,
        status: true,
        approvedAt: true,
        rejectedAt: true,
        rejectedReason: true,
        isActive: true,
        deactivatedAt: true,
      },
    });
    return { archived: true, shop };
  }
}
