import * as path from 'path';
import * as fs from 'fs';
import * as crypto from 'crypto';
import { ConfigModule, ConfigService } from '@nestjs/config';

import 'reflect-metadata';
import * as zlib from 'zlib';
import { promisify } from 'util';
import sharp from 'sharp';
import exifr from 'exifr';
import heicConvert from 'heic-convert';
import { NestFactory } from '@nestjs/core';
import { Module, Controller, Get, Post, Put, Body, Param, Patch, Delete, ParseUUIDPipe, NotFoundException, BadRequestException, UnauthorizedException, UseGuards, Req, Res, Query, ForbiddenException, Injectable, UseInterceptors, UploadedFiles, Inject, HttpException, HttpStatus } from '@nestjs/common';
import { NestExpressApplication } from '@nestjs/platform-express';
import { FilesInterceptor } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayConnection, OnGatewayDisconnect } from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { PrismaService } from '../prisma/prisma.service';
import { ParticipantRole } from '@prisma/client';
import * as bcrypt from 'bcrypt';
import { AdminGuard } from './auth/admin.guard';
import * as nodemailer from 'nodemailer';
import { JwtModule, JwtService } from '@nestjs/jwt';
import { JwtAuthGuard } from './auth/jwt-auth.guard';
import { JwtAuthGuardNoAnonymous } from './auth/jwt-auth.guard';
import { JwtStrategy } from './auth/jwt.strategy';
import { Reflector } from '@nestjs/core';
import { SetMetadata, CanActivate, ExecutionContext } from '@nestjs/common';
import { CacheModule, CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
import { redisStore } from 'cache-manager-redis-yet';
import { HttpAdapterHost } from '@nestjs/core';
import { CompressCacheInterceptor } from './common/compress-cache.interceptor';

// Promisify các hàm nén để sử dụng async/await
const gzip = promisify(zlib.gzip);
const gunzip = promisify(zlib.gunzip);

// Khai báo vị trí thư mục upload cụ thể
const UPLOAD_ROOT = path.join(process.cwd(), 'uploads');

@WebSocketGateway({ cors: { origin: '*' } })
export class CommentGateway implements OnGatewayConnection {
  @WebSocketServer() server: Server;

  handleConnection(client: Socket) {
    console.log(`[WS] Client connected: ${client.id}`);
  }

  @SubscribeMessage('joinTour')
  handleJoinTour(client: Socket, tourId: string) {
    client.join(`tour_${tourId}`);
    console.log(`[WS] Client ${client.id} joined room: tour_${tourId}`);
  }

  @SubscribeMessage('joinPhoto')
  handleJoinPhoto(client: Socket, photoId: string) {
    client.join(`photo_${photoId}`);
    console.log(`[WS] Client ${client.id} joined room: photo_${photoId}`);
  }

  notifyNewComment(tourId: string, data: any) {
    // Gửi thông báo tới tất cả client trong phòng của Tour này
    this.server.to(`tour_${tourId}`).emit('commentAdded', data);
  }

  notifyNewPhotoComment(photoId: string, data: any) {
    // Gửi thông báo tới tất cả client trong phòng của Ảnh này
    this.server.to(`photo_${photoId}`).emit('photoCommentAdded', data);
  }

  @SubscribeMessage('joinUser')
  handleJoinUser(client: Socket, userId: string) {
    client.join(`user_${userId}`);
    console.log(`[WS] Client ${client.id} joined user room: user_${userId}`);
  }

  notifyNewMessage(receiverId: string, data: any) {
    this.server.to(`user_${receiverId}`).emit('messageReceived', data);
  }

  notifyConnectionAccepted(requesterId: string, data: any) {
    this.server.to(`user_${requesterId}`).emit('connectionAccepted', data);
  }

  notifyJoinRequestAccepted(userId: string, data: any) {
    this.server.to(`user_${userId}`).emit('joinRequestAccepted', data);
  }
}

// Cấu hình TTL (mili giây) cho từng loại dữ liệu
const CACHE_TTL = {
  DEFAULT: 600000,          // 10 phút mặc định
  RESOURCE_TO_TOUR: 3600000, // 1 giờ cho ánh xạ tài nguyên -> tour
  USER_ROLE: 300000,        // 5 phút cho quyền hạn người dùng
};

async function bootstrap() {

  // Chuyển sang dùng NestExpressApplication để cấu hình static assets
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  app.setGlobalPrefix('api/v1');
  // Bật CORS để cho phép Frontend kết nối API không bị chặn
  app.enableCors();

  // Tự động tạo thư mục upload nếu chưa tồn tại
  if (!fs.existsSync(UPLOAD_ROOT)) {
    fs.mkdirSync(UPLOAD_ROOT, { recursive: true });
  }

  // Khai báo vị trí để ảnh upload có thể truy cập được từ bên ngoài qua URL
  app.useStaticAssets(UPLOAD_ROOT, {
    prefix: '/uploads/',
  });

  // Tự động tạo thư mục downloads nếu chưa tồn tại
  const downloadsDir = path.join(process.cwd(), 'public/downloads');
  if (!fs.existsSync(downloadsDir)) {
    fs.mkdirSync(downloadsDir, { recursive: true });
  }

  // Khai báo thư mục lưu trữ APK tải về
  app.useStaticAssets(downloadsDir, {
    prefix: '/downloads/',
    setHeaders: (res) => {
      res.set('Content-Type', 'application/vnd.android.package-archive');
      res.set('Content-Disposition', 'attachment; filename="yotrip-latest.apk"');
    }
  });

  const prisma = app.get(PrismaService);
  startAutoCleanup(prisma);

  await app.listen(3001);
  console.log(`🚀 Server is running on: http://localhost:3001`);
}

// Define ROLES_KEY and Roles decorator
export const ROLES_KEY = 'roles';
export const Roles = (...roles: ParticipantRole[]) => SetMetadata(ROLES_KEY, roles);

// Implement TourRoleGuard (assuming it's here or similar to this)
// This guard checks if the user is a participant of the tour and has one of the required roles.
@Injectable()
export class TourRoleGuard implements CanActivate {
  constructor(
    private reflector: Reflector, 
    private prisma: PrismaService,
    @Inject(CACHE_MANAGER) private cacheManager: Cache
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const requiredRoles = this.reflector.getAllAndOverride<ParticipantRole[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    // If no specific roles are required, default to OWNER and MANAGER for editing actions
    const defaultRoles = [ParticipantRole.OWNER, ParticipantRole.MANAGER];
    const rolesToCheck = requiredRoles && requiredRoles.length > 0 ? requiredRoles : defaultRoles;

    const request = context.switchToHttp().getRequest();
    const user = request.user; // User object from JwtAuthGuard
    
    let tourId = request.params.tourId;
    const resourceId = request.params.id || request.params.legId || request.params.locationId;

    // Nếu không có tourId trực tiếp, tìm tourId thông qua các tài nguyên liên quan
    if (!tourId && resourceId) {
      const resCacheKey = `res-to-tour:${resourceId}`;
      const compressedData = await this.cacheManager.get<Buffer>(resCacheKey);
      
      if (compressedData) {
        try {
          const decompressed = await gunzip(compressedData);
          tourId = decompressed.toString();
        } catch (e) {
          console.error('Lỗi giải nén cache:', e);
        }
      } else {
      // Thử xem resourceId có phải là tourId không
      const isTour = await this.prisma.tour.findUnique({ where: { id: resourceId }, select: { id: true } });
      if (isTour) {
        tourId = resourceId;
      } else {
        // Thử xem resourceId có phải là legId không
        const leg = await this.prisma.leg.findUnique({ where: { id: resourceId }, select: { tourId: true } });
        if (leg) {
          tourId = leg.tourId;
        } else {
          // Thử xem resourceId có phải là locationId không
          const loc = await this.prisma.location.findUnique({ 
            where: { id: resourceId }, 
            include: { leg: { select: { tourId: true } } } 
          });
          if (loc) {
            tourId = loc.leg.tourId;
          } else {
            // Thử xem resourceId có phải là photoId không
            const photo = await this.prisma.photo.findUnique({ where: { id: resourceId }, select: { tourId: true, uploaderId: true } });
            if (photo) {
              if (user && photo.uploaderId === user.id) return true; // Cho phép chủ sở hữu ảnh đi qua
              if (!photo.tourId) return true; // Cho phép đi qua nếu ảnh không thuộc tour nào (ví dụ ảnh ẩn danh public)
              tourId = photo.tourId;
            }
          }
        }
      }
        // Cache ánh xạ tài nguyên -> tour trong 1 giờ để giảm tải query ngược
        if (tourId) {
          try {
            const compressed = await gzip(Buffer.from(tourId));
            await this.cacheManager.set(resCacheKey, compressed, CACHE_TTL.RESOURCE_TO_TOUR);
          } catch (e) {
            await this.cacheManager.set(resCacheKey, tourId, CACHE_TTL.RESOURCE_TO_TOUR);
          }
        }
      }
    }

    if (!user || !tourId) {
      // Nếu đây là các route công khai hoặc không liên quan đến Tour, cho phép đi qua
      // nhưng ở đây chúng ta đang áp dụng guard cho các route cần phân quyền Tour
      if (!resourceId && !request.params.tourId) return true;
      return false;
    }

    // Cache vai trò người dùng trong tour (5 phút)
    const roleCacheKey = `user-role:${user.id}:${tourId}`;
    let role = await this.cacheManager.get<ParticipantRole>(roleCacheKey);

    if (!role) {
      const participation = await this.prisma.tourParticipant.findUnique({
        where: { tourId_userId: { tourId, userId: user.id } },
      });
      if (!participation) return false;
      role = participation.role;
      await this.cacheManager.set(roleCacheKey, role, CACHE_TTL.USER_ROLE);
    }

    if (!rolesToCheck.some(r => role === r)) {
      throw new ForbiddenException('Bạn không có quyền thực hiện hành động này trong tour này.');
    }

    // Ensure tourId is attached to the request object for controllers to use
    if (tourId) {
      (request as any).tourId = tourId;
    }
    return true;
  }
}

@Injectable()
export class EmailService {
  private transporter: any;

  constructor() {
    const user = process.env.SMTP_USER;
    const pass = process.env.SMTP_PASS;

    if (!user || !pass) {
      console.warn('[EmailService] ⚠️ SMTP_USER hoặc SMTP_PASS chưa được cấu hình trong file .env. Tính năng gửi mã OTP sẽ không khả dụng.');
    } else {
      this.transporter = nodemailer.createTransport({
        host: process.env.SMTP_HOST || 'smtp.gmail.com',
        port: parseInt(process.env.SMTP_PORT || '587'),
        secure: process.env.SMTP_SECURE === 'true', // true cho cổng 465, false cho cổng 587/25
        auth: {
          user: user,
          pass: pass,
        },
      });

      // Tự động kiểm tra kết nối khi khởi tạo để phát hiện lỗi cấu hình sớm
      this.transporter.verify((error: any) => {
        if (error) {
          console.error('[EmailService] ❌ Lỗi kết nối SMTP:', error.message);
        } else {
          console.log('[EmailService] ✅ Kết nối SMTP thành công. Sẵn sàng gửi mail.');
        }
      });
    }
  }

  async sendOTP(email: string, otp: string) {
    if (!process.env.SMTP_USER || !process.env.SMTP_PASS) {
      throw new Error('Thiếu cấu hình SMTP_USER hoặc SMTP_PASS trong file .env');
    }

    const mailOptions = {
      from: `"Travel Planning Support" <${process.env.SMTP_USER}>`,
      to: email,
      subject: 'Mã OTP Xác Thực Hệ Thống',
      html: `
        <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e0e0e0; border-radius: 10px;">
          <h2 style="color: #2563eb; text-align: center;">Mã Xác Thực OTP</h2>
          <p>Xin chào,</p>
          <p>Quản trị viên hệ thống đã yêu cầu cấp và gửi mã OTP xác thực tới tài khoản của bạn. Vui lòng sử dụng mã bảo mật dưới đây:</p>
          <div style="background-color: #f3f4f6; padding: 15px; text-align: center; font-size: 24px; font-weight: bold; letter-spacing: 5px; color: #1e3a8a; margin: 20px 0; border-radius: 5px;">
            ${otp}
          </div>
          <p>Mã OTP này có hiệu lực trong vòng 5 phút. Vui lòng tuyệt đối không chia sẻ mã này cho bất kỳ ai khác.</p>
          <hr style="border: none; border-top: 1px solid #e0e0e0; margin: 20px 0;" />
          <p style="font-size: 12px; color: #6b7280; text-align: center;">Đây là email tự động từ ứng dụng hệ thống Travel Planning.</p>
        </div>
      `,
    };
    
    try {
      return await this.transporter.sendMail(mailOptions);
    } catch (error) {
      throw error;
    }
  }

  async sendTourInvitation(email: string, tourTitle: string, inviteLink: string) {
    if (!process.env.SMTP_USER || !process.env.SMTP_PASS) {
      throw new Error('Thiếu cấu hình SMTP_USER hoặc SMTP_PASS trong file .env');
    }

    const mailOptions = {
      from: `"Travel Planning Support" <${process.env.SMTP_USER}>`,
      to: email,
      subject: `Lời mời tham gia hành trình: ${tourTitle}`,
      html: `
        <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e0e0e0; border-radius: 10px;">
          <h2 style="color: #2563eb; text-align: center;">Lời Mời Tham Gia Hành Trình</h2>
          <p>Xin chào,</p>
          <p>Bạn đã được mời tham gia hành trình du lịch <strong>"${tourTitle}"</strong>.</p>
          <p>Vui lòng nhấp vào nút dưới đây để chấp nhận lời mời và gia nhập hành trình của chúng tôi:</p>
          <div style="text-align: center; margin: 30px 0;">
            <a href="${inviteLink}" style="background-color: #2563eb; color: white; padding: 12px 24px; text-decoration: none; font-weight: bold; border-radius: 8px; display: inline-block;">
              Tham Gia Hành Trình Ngay
            </a>
          </div>
          <p style="font-size: 13px; color: #4b5563;">Hoặc bạn có thể sao chép liên kết dưới đây và dán vào trình duyệt:</p>
          <p style="font-size: 13px; color: #2563eb; word-break: break-all;">${inviteLink}</p>
          <p>Lời mời này có hiệu lực trong vòng 7 ngày. Hãy nhanh tay đăng ký nhé!</p>
          <hr style="border: none; border-top: 1px solid #e0e0e0; margin: 20px 0;" />
          <p style="font-size: 12px; color: #6b7280; text-align: center;">Đây là email tự động từ ứng dụng hệ thống Travel Planning.</p>
        </div>
      `,
    };

    try {
      return await this.transporter.sendMail(mailOptions);
    } catch (error) {
      throw error;
    }
  }
}

@Controller()
class AppController {
  @Get()
  getHello(): string {
    return 'Travel Planning API is running!';
  }
}

@Controller('auth')
class AuthController {
  constructor(
    private prisma: PrismaService, 
    private jwtService: JwtService,
    // Inject ConfigService để đọc biến môi trường một cách an toàn
    // NestJS sẽ đảm bảo ConfigModule được tải trước khi AuthController được khởi tạo
    // Do đó, các biến môi trường sẽ luôn có sẵn ở đây.
    // Điều này cũng áp dụng cho EmailService.
    private configService: ConfigService,
    private emailService: EmailService,
    @Inject(CACHE_MANAGER) private cacheManager: Cache
  ) {}

  @Post('convert-guest')
  async convertGuestToOfficial(@Body() body: any) {
    const { guestId, email, password, name } = body;

    if (!guestId || !email || !password) {
      throw new BadRequestException('Vui lòng cung cấp đủ guestId, email và mật khẩu.');
    }

    // 1. Kiểm tra xem email đã tồn tại với một tài khoản chính thức khác chưa
    const existingOfficialUser = await this.prisma.user.findFirst({
      where: {
        email: email,
        isAnonymous: false,
      },
    });

    if (existingOfficialUser) {
      throw new BadRequestException('Email này đã được một tài khoản khác sử dụng.');
    }

    // 2. Tìm tài khoản khách
    const guestUser = await this.prisma.user.findUnique({
      where: { id: guestId },
    });

    if (!guestUser || !guestUser.isAnonymous) {
      throw new NotFoundException('Không tìm thấy tài khoản khách hoặc tài khoản này đã được chuyển đổi.');
    }

    // 3. Mã hóa mật khẩu và cập nhật người dùng
    const passwordHash = await bcrypt.hash(password, 10);

    const updatedUser = await this.prisma.user.update({
      where: { id: guestId },
      data: {
        email: email,
        passwordHash: passwordHash,
        name: name || guestUser.name, // Cập nhật tên mới nếu có, nếu không giữ lại tên ẩn danh cũ
        isAnonymous: false, // Đánh dấu đây là tài khoản chính thức
      },
    });

    // 4. Tạo và trả về token để người dùng đăng nhập ngay lập tức
    const payload = { email: updatedUser.email, sub: updatedUser.id };
    return {
      access_token: this.jwtService.sign(payload),
      user: {
        id: updatedUser.id,
        email: updatedUser.email,
        name: updatedUser.name,
        isAdmin: updatedUser.isAdmin,
      },
    };
  }

  @UseGuards(JwtAuthGuard)
  @Post('promote-admin')
  async promoteAdmin(@Body() body: { secretKey: string }, @Req() req: any) {
    const { secretKey } = body;
    const adminSecret = this.configService.get<string>('ADMIN_SECRET_KEY') || 'yotrip_secret_admin_key';
    if (secretKey !== adminSecret) {
      throw new BadRequestException('Mã Secret Key không hợp lệ.');
    }
    const updated = await this.prisma.user.update({
      where: { id: req.user.id },
      data: { isAdmin: true },
      select: { id: true, email: true, name: true, isAdmin: true }
    });
    return { success: true, user: updated };
  }

  @Post('create-guest')
  async createGuestUser() {
    const user = await this.prisma.user.create({
      data: {
        isAnonymous: true,
        name: `Lữ khách #${Math.floor(1000 + Math.random() * 9000)}`,
      },
    });

    const payload = { sub: user.id, isAnonymous: true };
    return {
      access_token: this.jwtService.sign(payload),
      user: {
        id: user.id,
        name: user.name,
        isAnonymous: user.isAnonymous,
      },
    };
  }

  @Get('status')
  async getStatus() {
    const userCount = await this.prisma.user.count();
    console.log(`[Status Check] Users found: ${userCount}`);
    return { isInitialSetup: userCount === 0 };
  }

  @Post('login')
  async login(@Body() body: any) {
    const { email, password } = body;

    const adminSecret = this.configService.get<string>('ADMIN_SECRET_KEY') || 'yotrip_secret_admin_key';
    if ((email === 'admin' || email === 'admin@yotrip.com') && password === adminSecret) {
      let adminUser = await this.prisma.user.findFirst({
        where: {
          OR: [
            { email: 'admin' },
            { email: 'admin@yotrip.com' }
          ]
        }
      });
      if (!adminUser) {
        adminUser = await this.prisma.user.create({
          data: {
            email: 'admin@yotrip.com',
            name: 'Administrator',
            isAdmin: true,
            isAnonymous: false,
          }
        });
      } else if (!adminUser.isAdmin) {
        adminUser = await this.prisma.user.update({
          where: { id: adminUser.id },
          data: { isAdmin: true }
        });
      }

      const payload = { email: adminUser.email, sub: adminUser.id };
      return {
        access_token: this.jwtService.sign(payload),
        user: {
          id: adminUser.id,
          email: adminUser.email,
          name: adminUser.name,
          isAdmin: adminUser.isAdmin,
        },
      };
    }

    const user = await this.prisma.user.findUnique({ where: { email } });
    
    if (!user || !user.passwordHash || !(await bcrypt.compare(password, user.passwordHash))) {
      throw new UnauthorizedException('Email hoặc mật khẩu không chính xác');
    }

    // Chặn người dùng đã bị khóa đăng nhập
    if (user.isBlocked) {
      throw new UnauthorizedException('Tài khoản của bạn đã bị khóa. Vui lòng liên hệ quản trị viên.');
    }

    const payload = { email: user.email, sub: user.id };

    return {
      access_token: this.jwtService.sign(payload),
      user: {
        id: user.id,
        email: user.email,
        name: user.name,
        isAdmin: user.isAdmin,
      },
    };
  }

  @Post('google')
  async googleLogin(@Body() body: { credential: string }) {
    const { credential } = body;
    if (!credential) {
      throw new BadRequestException('Vui lòng cung cấp Google credential.');
    }

    try {
      const response = await fetch(`https://oauth2.googleapis.com/tokeninfo?id_token=${credential}`);
      if (!response.ok) {
        throw new UnauthorizedException('Token Google không hợp lệ hoặc đã hết hạn.');
      }
      
      const payload = await response.json();
      const email = payload.email;
      const name = payload.name || email.split('@')[0];
      const avatar = payload.picture || null;

      if (!email) {
        throw new BadRequestException('Không tìm thấy địa chỉ email từ Google.');
      }

      let user = await this.prisma.user.findUnique({ where: { email } });

      if (!user) {
        user = await this.prisma.user.create({
          data: {
            email,
            name,
            avatar,
            isAnonymous: false,
          },
        });
      } else if (user.isBlocked) {
        throw new UnauthorizedException('Tài khoản của bạn đã bị khóa. Vui lòng liên hệ quản trị viên.');
      } else if (user.isAnonymous) {
        user = await this.prisma.user.update({
          where: { id: user.id },
          data: {
            isAnonymous: false,
            name: user.name || name,
            avatar: user.avatar || avatar,
          },
        });
      }

      const jwtPayload = { email: user.email, sub: user.id };
      return {
        access_token: this.jwtService.sign(jwtPayload),
        user: {
          id: user.id,
          email: user.email,
          name: user.name,
          avatar: user.avatar,
          isAdmin: user.isAdmin,
          isGoogle: true,
        },
      };
    } catch (error) {
      console.error('[Google Auth Error]:', error);
      if (error instanceof UnauthorizedException || error instanceof BadRequestException) {
        throw error;
      }
      throw new BadRequestException('Đã xảy ra lỗi khi đăng nhập bằng Google.');
    }
  }

  @Post('signup/request')
  async signupRequest(@Body() body: any) {
    const { email, password, name, phone, address } = body;
    
    const existingUser = await this.prisma.user.findUnique({ where: { email } });
    if (existingUser) throw new BadRequestException('Email đã được sử dụng');

    const otp = Math.floor(100000 + Math.random() * 900000).toString();
    
    // Lưu OTP và dữ liệu form đăng ký vào Cache trong vòng 5 phút (300000 ms)
    await this.cacheManager.set(`signup_otp:${email}`, otp, 300000);
    await this.cacheManager.set(`signup_data:${email}`, JSON.stringify({ password, name, phone, address }), 300000);

    try {
      await this.emailService.sendOTP(email, otp);
      return { success: true, message: 'Mã OTP đã được gửi đến email của bạn.' };
    } catch (error) {
      console.error('[Signup OTP] Error:', error);
      throw new BadRequestException('Không thể gửi mã xác thực tới email này. Vui lòng kiểm tra lại cấu hình SMTP.');
    }
  }

  @Post('signup/verify')
  async signupVerify(@Body() body: { email: string; otp: string; guestId?: string }) {
    const { email, otp, guestId } = body;
    
    const storedOtp = await this.cacheManager.get<string>(`signup_otp:${email}`);
    if (!storedOtp || storedOtp !== otp) {
      throw new BadRequestException('Mã OTP không chính xác hoặc đã hết hạn.');
    }

    const cachedDataStr = await this.cacheManager.get<string>(`signup_data:${email}`);
    if (!cachedDataStr) {
      throw new BadRequestException('Thông tin đăng ký đã hết hạn. Vui lòng thực hiện lại từ đầu.');
    }

    const { password, name, phone, address } = JSON.parse(cachedDataStr);
    const passwordHash = await bcrypt.hash(password, 10);

    let user;
    if (guestId) {
      // Tìm tài khoản khách
      const guestUser = await this.prisma.user.findUnique({
        where: { id: guestId }
      });
      if (guestUser && guestUser.isAnonymous) {
        // Kiểm tra xem email này đã được sử dụng bởi một tài khoản chính thức khác chưa
        const existingUser = await this.prisma.user.findFirst({
          where: { email, isAnonymous: false }
        });
        if (existingUser) {
          throw new BadRequestException('Email này đã được đăng ký bởi tài khoản khác.');
        }

        // Cập nhật thông tin của tài khoản khách
        user = await this.prisma.user.update({
          where: { id: guestId },
          data: {
            email,
            passwordHash,
            name,
            phone,
            address,
            isAnonymous: false,
          },
          select: { id: true, email: true, name: true, phone: true, address: true, isAdmin: true }
        });
      }
    }

    if (!user) {
      const userCount = await this.prisma.user.count();
      const shouldBeAdmin = userCount === 0;

      user = await this.prisma.user.create({
        data: { email, passwordHash, name, phone, address, isAdmin: shouldBeAdmin },
        select: { id: true, email: true, name: true, phone: true, address: true, isAdmin: true }
      });
    }

    // Xóa dữ liệu cache sau khi đăng ký thành công
    await Promise.all([
      this.cacheManager.del(`signup_otp:${email}`),
      this.cacheManager.del(`signup_data:${email}`)
    ]);

    return user;
  }
}

@Controller('tours') // Controller mới để xử lý các tour công khai
class PublicTourController {
  constructor(private prisma: PrismaService) {}

  @UseInterceptors(CompressCacheInterceptor) // Áp dụng Interceptor nén và cache
  @Get(':id/public')
  async getPublicTourDetails(@Param('id', ParseUUIDPipe) id: string) {
    console.log(`[PublicTour] Đang truy vấn chi tiết Tour ID: ${id}`);
    const tour = await this.prisma.tour.findUnique({
      where: { id },
      include: {
        participants: {
          include: {
            user: {
              select: { id: true, name: true, email: true }
            }
          }
        },
        photos: {
          where: { isDeleted: false },
          include: {
            uploader: {
              select: { id: true, name: true }
            }
          }
        },
        legs: {
          orderBy: { sequence: 'asc' },
          include: {
            expenses: {
              include: {
                location: { select: { name: true, plannedStart: true } },
                paidBy: { select: { id: true, name: true } }
              }
            },
            locations: { 
              orderBy: [
                { plannedStart: { sort: 'asc', nulls: 'last' } },
                { createdAt: 'asc' }
              ],
              include: { _count: { select: { comments: true } } }
            },
          },
        },
      },
    });

    if (!tour || tour.isDeleted) {
      console.warn(`[PublicTour] Không tìm thấy Tour với ID: ${id} hoặc Tour đã bị xóa`);
      throw new NotFoundException(`Không tìm thấy Tour`);
    }
    return tour;
  }
}

@Controller('tours')
class TourController {
  constructor(
    private prisma: PrismaService,
    private emailService: EmailService,
    @Inject(CACHE_MANAGER) private cacheManager: Cache,
    private commentGateway: CommentGateway
  ) {}

  @UseGuards(JwtAuthGuard)
  @Post()
  async createTour(@Body() body: any, @Req() req: any) {
    const { title, description, startDate, endDate, adultCount, childCount, childDiscount, tags, members } = body;
    const filteredTitle = await filterText(this.prisma, title || '');
    const filteredDesc = await filterText(this.prisma, description || '');
    const tour = await this.prisma.tour.create({
      data: {
        title: filteredTitle,
        description: filteredDesc,
        startDate: startDate ? new Date(startDate) : null,
        endDate: endDate ? new Date(endDate) : null,
        tags: tags || [],
        adultCount: adultCount || 1,
        childCount: childCount || 0,
        childDiscount: childDiscount || 0,
        createdById: req.user.id,
        participants: {
          create: [
            {
              userId: req.user.id,
              role: 'OWNER'
            },
            ...(members && Array.isArray(members)
              ? members.map((m: any) => ({
                  userId: m.userId || null,
                  displayName: m.displayName || null,
                  role: m.role || 'MEMBER'
                }))
              : [])
          ]
        },
        legs: {
          create: {
            sequence: 1,
            note: 'Chặng khởi đầu'
          }
        }
      },
      include: {
        participants: {
          include: { user: { select: { id: true, name: true, email: true } } }
        }
      }
    });

    const noteContent = `<h1>${filteredTitle}</h1><h2>Ghi chú chung</h2><p><em>Nội dung ghi chú tổng quan của chuyến đi...</em></p>`;

    try {
      await this.prisma.tourNote.create({
        data: {
          tourId: tour.id,
          userId: req.user.id,
          title: `Ghi chú: ${filteredTitle}`,
          content: noteContent
        }
      });
    } catch (err) {
      console.warn(`Failed to auto-create note for tour ${tour.id}:`, err.message);
    }

    await this.cacheManager.del(`/api/v1/tours/explore`);
    return tour;
  }

  @Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER, ParticipantRole.MEMBER) // Allow members to add locations
  @UseGuards(JwtAuthGuard, TourRoleGuard)
  @Post(':tourId/locations')
  async addLocation(@Param('tourId', ParseUUIDPipe) tourId: string, @Body() body: any, @Req() req: any) {
    const legId = body.legId;

    console.log(`[USER ACTION] User ${req.user.id} added a NEW VISIT point to Tour ${tourId}: "${body.name}"`);

    const leg = legId 
      ? await this.prisma.leg.findUnique({ where: { id: legId } })
      : await this.prisma.leg.findFirst({ where: { tourId } });
      
    if (!leg) throw new NotFoundException('Không tìm thấy chặng phù hợp để thêm địa điểm');
    
    return this.prisma.location.create({
      // ...
      data: {
        name: body.name,
        address: body.address,
        latitude: body.latitude,
        longitude: body.longitude,
        type: body.type,
        legId: leg.id,
        plannedStart: body.plannedStart ? new Date(body.plannedStart) : null,
        plannedEnd: body.plannedEnd ? new Date(body.plannedEnd) : null,
      }
    }).then(async (loc) => {
      if (body.expenseAmount && Number(body.expenseAmount) > 0) {
        let paidById = body.paidById || null;
        if (paidById) {
          const userExists = await this.prisma.user.findUnique({ where: { id: paidById } });
          if (!userExists) {
            paidById = null;
          }
        }
        await this.prisma.expense.create({
          data: {
            amount: Number(body.expenseAmount),
            category: body.expenseCategory || 'OTHER',
            locationId: loc.id,
            legId: loc.legId,
            description: body.expenseDescription || `Chi phí tại ${loc.name}`,
            note: body.expenseNote || null,
            paidById,
          }
        });
      }
      // Xóa triệt để các loại cache của Tour (cả key UUID và key URL của Interceptor)
      await Promise.all([
        this.cacheManager.del(tourId),
        this.cacheManager.del(`/api/v1/tours/${tourId}`),
        this.cacheManager.del(`/api/v1/tours/${tourId}/public`),
      ]);
      return loc; // Return the created location
    });
  }

  @Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Only owner/manager can set start point
  @UseGuards(JwtAuthGuard, TourRoleGuard)
  @Post(':tourId/start-point')
  async updateTourStartPoint(@Param('tourId', ParseUUIDPipe) tourId: string, @Body() body: any, @Req() req: any) {
    const { latitude, longitude, name, plannedEnd } = body;

    console.log(`[USER ACTION] User ${req.user.id} set START point for Tour ${tourId}: "${name}" at [${latitude}, ${longitude}]`);
    
    // 1. Xóa tất cả các điểm bắt đầu cũ của Tour này (được đánh dấu bằng plannedStart = 0)
    // để đảm bảo tính duy nhất và sạch sẽ của dữ liệu.
    await this.prisma.location.deleteMany({
      where: {
        leg: { tourId: tourId },
        plannedStart: new Date(0)
      }
    });

    // Tìm chặng đầu tiên của tour để ghim điểm xuất phát
    const firstLeg = await this.prisma.leg.findFirst({
      where: { tourId },
      orderBy: { sequence: 'asc' }
    });
    
    if (!firstLeg) throw new NotFoundException('Không tìm thấy chặng đầu tiên cho Tour này');

    // 2. Tạo mới điểm xuất phát tại Chặng 1
    return this.prisma.location.create({
      data: {
        name: name || 'Điểm xuất phát',
        latitude,
        longitude,
        type: 'MOVE',
        legId: firstLeg.id,
        plannedStart: new Date(0),
        plannedEnd: plannedEnd ? new Date(plannedEnd) : null,
      }
    }).then(async (loc) => {
      await Promise.all([
        this.cacheManager.del(tourId),
        this.cacheManager.del(`/api/v1/tours/${tourId}`),
        this.cacheManager.del(`/api/v1/tours/${tourId}/public`),
      ]);
      return loc;
    });
  }

  @Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Only owner/manager can set end point
  @UseGuards(JwtAuthGuard, TourRoleGuard)
  @Post(':tourId/end-point')
  async updateTourEndPoint(@Param('tourId', ParseUUIDPipe) tourId: string, @Body() body: any, @Req() req: any) {
    const { latitude, longitude, name, plannedStart } = body;

    console.log(`[USER ACTION] User ${req.user.id} set END point for Tour ${tourId}: "${name}" at [${latitude}, ${longitude}]`);
    
    // Xóa điểm kết thúc cũ (được đánh dấu bằng plannedEnd = 0) để tránh trùng lặp ghim trên bản đồ
    await this.prisma.location.deleteMany({
      where: {
        leg: { tourId: tourId },
        plannedEnd: new Date(0)
      }
    });

    // Tìm chặng cuối cùng của tour để ghim điểm kết thúc
    const lastLeg = await this.prisma.leg.findFirst({
      where: { tourId },
      orderBy: { sequence: 'desc' }
    });
    
    if (!lastLeg) throw new NotFoundException('Không tìm thấy chặng cuối cùng cho Tour này');

    return this.prisma.location.create({
      data: {
        name: name || 'Điểm kết thúc',
        latitude,
        longitude,
        type: 'MOVE',
        legId: lastLeg.id,
        plannedStart: plannedStart ? new Date(plannedStart) : null,
        plannedEnd: new Date(0), // Đánh dấu đây là điểm kết thúc đặc biệt
      }
    }).then(async (loc) => {
      await Promise.all([
        this.cacheManager.del(tourId),
        this.cacheManager.del(`/api/v1/tours/${tourId}`),
        this.cacheManager.del(`/api/v1/tours/${tourId}/public`),
      ]);
      return loc;
    });
  }

  @Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Only owner/manager can initialize legs
  @UseGuards(JwtAuthGuard, TourRoleGuard)
  @Post(':tourId/legs/batch')
  async initializeLegs(@Param('tourId', ParseUUIDPipe) tourId: string, @Body() body: { count: number }) {
    const { count } = body;
    if (count <= 0 || count > 20) throw new BadRequestException('Số lượng chặng không hợp lệ (1-20)');

    // 1. Lấy danh sách chặng hiện có
    const existingLegs = await this.prisma.leg.findMany({
      where: { tourId },
      orderBy: { sequence: 'asc' }
    });

    // 2. Tạo thêm chặng nếu số lượng hiện tại chưa đủ 'count'
    const needed = count - existingLegs.length;
    if (needed > 0) {
      const createData = Array.from({ length: needed }).map((_, i) => ({
        tourId,
        sequence: existingLegs.length + i + 1,
        note: `Chặng ${existingLegs.length + i + 1}`
      }));
      await this.prisma.leg.createMany({ data: createData });
    }

    // 3. Lấy chặng cuối cùng sau khi đã cập nhật
    const allLegs = await this.prisma.leg.findMany({
      where: { tourId },
      orderBy: { sequence: 'asc' }
    });
    const lastLeg = allLegs[allLegs.length - 1];

    // 4. Tự động di chuyển Điểm kết thúc sang Chặng cuối cùng (nếu đã khai báo điểm kết thúc)
    const endPoint = await this.prisma.location.findFirst({
      where: { leg: { tourId }, plannedEnd: new Date(0) }
    });

    if (endPoint && lastLeg && endPoint.legId !== lastLeg.id) {
      await this.prisma.location.update({
        where: { id: endPoint.id },
        data: { legId: lastLeg.id }
      });
    }

    // Invalidate cache for the tour after initializing legs
    await Promise.all([
      this.cacheManager.del(tourId),
      this.cacheManager.del(`/api/v1/tours/${tourId}`),
      this.cacheManager.del(`/api/v1/tours/${tourId}/public`),
    ]);
    return allLegs; // Return all legs
  }

  @Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Only owner/manager can add legs
  @UseGuards(JwtAuthGuard, TourRoleGuard)
  @Post(':tourId/legs')
  async addLeg(@Param('tourId', ParseUUIDPipe) tourId: string, @Body() body: any) {
    const tour = await this.prisma.tour.findUnique({
      where: { id: tourId },
      include: { legs: true }
    });
    if (!tour) throw new NotFoundException('Không tìm thấy tour');

    return this.prisma.leg.create({
      data: {
        tourId,
        sequence: tour.legs.length + 1,
        note: body.note || `Chặng ${tour.legs.length + 1}`
      }
    }).then(async (leg) => {
      await Promise.all([
        this.cacheManager.del(tourId),
        this.cacheManager.del(`/api/v1/tours/${tourId}`),
        this.cacheManager.del(`/api/v1/tours/${tourId}/public`),
      ]);
      return leg;
    });
  }

  @Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Only owner/manager can update tour details
  @UseGuards(JwtAuthGuard, TourRoleGuard)
  @Patch(':id')
  async updateTour(@Param('id', ParseUUIDPipe) id: string, @Body() body: any) {
    const updateData: any = {
      startDate: body.startDate ? new Date(body.startDate) : undefined,
      tags: body.tags,
      endDate: body.endDate ? new Date(body.endDate) : undefined,
      adultCount: body.adultCount !== undefined ? Number(body.adultCount) : undefined,
      childCount: body.childCount !== undefined ? Number(body.childCount) : undefined,
      childDiscount: body.childDiscount !== undefined ? Number(body.childDiscount) : undefined,
    };
    if (body.title !== undefined) {
      updateData.title = await filterText(this.prisma, body.title);
    }
    if (body.description !== undefined) {
      updateData.description = await filterText(this.prisma, body.description);
    }

    return this.prisma.tour.update({
      where: { id },
      data: updateData,
    }).then(async (tour) => {
      await Promise.all([
        this.cacheManager.del(id),
        this.cacheManager.del(`/api/v1/tours/${id}`),
        this.cacheManager.del(`/api/v1/tours/${id}/public`),
      ]);
      return tour;
    });
  }

  @Roles(ParticipantRole.OWNER) // Only owner can delete tour
  @UseGuards(JwtAuthGuard, TourRoleGuard)
  @Delete(':id')
  async deleteTour(@Param('id', ParseUUIDPipe) id: string) {
    // 1. Lấy thông tin chi tiết Tour cùng các tài nguyên liên quan để dọn dẹp cache và file
    const tour = await this.prisma.tour.findUnique({
      where: { id },
      include: {
        participants: true,
        photos: true,
        legs: {
          include: { locations: true }
        }
      }
    });

    if (!tour) throw new NotFoundException('Không tìm thấy tour');

    // --- BẮT ĐẦU DỌN DẸP CACHE ---
    // a. Xóa cache vai trò của tất cả thành viên trong tour này
    for (const participant of tour.participants) {
      if (participant.userId) {
        await this.cacheManager.del(`user-role:${participant.userId}:${id}`);
      }
    }

    // b. Xóa cache mapping tài nguyên (Leg, Location, Photo) về Tour này
    await this.cacheManager.del(`res-to-tour:${id}`); // Bản thân tour
    
    for (const leg of tour.legs) {
      await this.cacheManager.del(`res-to-tour:${leg.id}`);
      for (const loc of leg.locations) {
        await this.cacheManager.del(`res-to-tour:${loc.id}`);
      }
    }

    for (const photo of tour.photos) {
      await this.cacheManager.del(`res-to-tour:${photo.id}`);
    }
    // --- KẾT THÚC DỌN DẸP CACHE ---

    // Soft-delete the tour
    await this.prisma.tour.update({
      where: { id },
      data: { isDeleted: true, deletedAt: new Date() }
    });

    // Soft-delete all related photos
    await this.prisma.photo.updateMany({
      where: { tourId: id },
      data: { isDeleted: true, deletedAt: new Date() }
    });

    // Soft-delete all related notes
    await this.prisma.tourNote.updateMany({
      where: { tourId: id },
      data: { isDeleted: true, deletedAt: new Date() }
    });

    // Xóa cache explore để Tour biến mất ngay lập tức trên bản đồ cộng đồng
    await this.cacheManager.del(`/api/v1/tours/explore`);
    await this.cacheManager.del(id);
    await this.cacheManager.del(`/api/v1/tours/${id}`);
    await this.cacheManager.del(`/api/v1/tours/${id}/public`);

    return { success: true };
  }

  @UseInterceptors(CompressCacheInterceptor) // Áp dụng Interceptor nén và cache
  @UseGuards(JwtAuthGuardNoAnonymous)
  @Get('explore')
  async getPublicTours(@Req() req: any) {
    // Trả về tất cả các tour trong hệ thống để hiển thị trên bản đồ cộng đồng
    return this.prisma.tour.findMany({
      where: { isDeleted: false },
      take: 50,
      include: {
        participants: {
          select: { userId: true, role: true, displayName: true }
        },
        joinRequests: {
          where: { userId: req.user.id, status: 'PENDING' },
          select: { id: true }
        },
        photos: { take: 1 },
        legs: {
          orderBy: { sequence: 'asc' },
          include: {
            locations: { 
              orderBy: [
                { plannedStart: { sort: 'asc', nulls: 'last' } },
                { createdAt: 'asc' }
              ],
              include: { _count: { select: { comments: true } } }
            }
          }
        }
      }
    });
  }

  @Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER, ParticipantRole.MEMBER, ParticipantRole.MEMBER_NO_FINANCE, ParticipantRole.VIEWER_ONLY) // Any participant can view tour details
  @UseGuards(JwtAuthGuard, TourRoleGuard)
  @Get(':id')
  async getTourDetails(@Param('id', ParseUUIDPipe) id: string) {
    const tour = await this.prisma.tour.findUnique({
      where: { id },
      include: {
        participants: {
          include: {
            user: {
              select: { id: true, name: true, email: true }
            }
          }
        },
        photos: {
          where: { isDeleted: false },
          include: {
            uploader: {
              select: { id: true, name: true }
            }
          }
        },
        legs: {
          orderBy: { sequence: 'asc' },
          include: {
            expenses: {
              include: {
                location: { select: { name: true, plannedStart: true } },
                paidBy: { select: { id: true, name: true } }
              }
            },
            locations: { 
              orderBy: [
                { plannedStart: { sort: 'asc', nulls: 'last' } },
                { createdAt: 'asc' }
              ],
              include: { _count: { select: { comments: true } } }
            },
          },
        },
      },
    });

    if (!tour || tour.isDeleted) throw new NotFoundException(`Không tìm thấy Tour với ID ${id}`);
    return tour;
  }

  @Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Only owner/manager can add members
  @UseGuards(JwtAuthGuard, TourRoleGuard)
  @Post(':tourId/members')
  async addMember(@Param('tourId', ParseUUIDPipe) tourId: string, @Body() body: { userId?: string; displayName?: string; role?: string }, @Req() req: any) {
    const validRoles = ['OWNER', 'MANAGER', 'MEMBER', 'MEMBER_NO_FINANCE', 'VIEWER_ONLY'] as const;
    const role = validRoles.includes(body.role as any) ? body.role as 'OWNER' | 'MANAGER' | 'MEMBER' | 'MEMBER_NO_FINANCE' | 'VIEWER_ONLY' : 'MEMBER';

    if (body.displayName) {
      const participation = await this.prisma.tourParticipant.create({
        data: {
          tourId,
          role,
          displayName: body.displayName,
        },
        include: { user: { select: { id: true, name: true, email: true } } },
      });
      await Promise.all([
        this.cacheManager.del(tourId),
        this.cacheManager.del(`/api/v1/tours/${tourId}`),
        this.cacheManager.del(`/api/v1/tours/${tourId}/public`),
      ]);
      return participation;
    }

    if (!body.userId) {
      throw new BadRequestException('Vui lòng cung cấp userId hoặc displayName');
    }

    // Xóa cache khi thay đổi quyền hạn hoặc thêm thành viên mới
    await this.cacheManager.del(`user-role:${body.userId}:${tourId}`);

    const participation = await this.prisma.tourParticipant.findUnique({
      where: { tourId_userId: { tourId, userId: body.userId } },
    });
    if (participation) {
      return this.prisma.tourParticipant.update({
        where: { tourId_userId: { tourId, userId: body.userId } },
        data: { role },
        include: { user: { select: { id: true, name: true, email: true } } },
      });
    }

    let currentRole = req.user.tourParticipation?.role;
    if (!currentRole) {
      const participation = await this.prisma.tourParticipant.findUnique({
        where: { tourId_userId: { tourId, userId: req.user.id } },
      });
      currentRole = participation?.role;
    }
    if (!currentRole || !(currentRole === 'OWNER' || currentRole === 'MANAGER')) {
      const joinRequest = await this.prisma.joinRequest.create({
        data: {
          tourId,
          userId: body.userId,
          requestedById: req.user.id,
          status: 'PENDING',
        },
        include: {
          user: { select: { id: true, name: true, email: true } },
          requestedBy: { select: { id: true, name: true, email: true } },
        },
      });
      return { ...joinRequest, pendingApproval: true };
    }

    return this.prisma.tourParticipant.create({
      data: {
        tourId,
        userId: body.userId,
        role,
      },
      include: { user: { select: { id: true, name: true, email: true } } },
    });
  }

  @Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER, ParticipantRole.MEMBER, ParticipantRole.MEMBER_NO_FINANCE, ParticipantRole.VIEWER_ONLY)
  @UseGuards(JwtAuthGuard, TourRoleGuard)
  @Patch(':tourId/members/:userId')
  async updateMemberCounts(
    @Param('tourId', ParseUUIDPipe) tourId: string,
    @Param('userId', ParseUUIDPipe) userId: string,
    @Body() body: { adultCount?: number; childCount?: number; role?: string },
    @Req() req: any
  ) {
    const participant = await this.prisma.tourParticipant.findFirst({
      where: {
        OR: [
          { id: userId },
          { tourId, userId }
        ]
      }
    });
    if (!participant) {
      throw new NotFoundException('Không tìm thấy thành viên trong tour.');
    }

    const isSelf = req.user.id === participant.userId;
    
    // Tìm quyền hạn của người gửi yêu cầu trong tour này
    const requesterParticipation = await this.prisma.tourParticipant.findUnique({
      where: { tourId_userId: { tourId, userId: req.user.id } },
    });
    const canManage = requesterParticipation?.role === 'OWNER' || requesterParticipation?.role === 'MANAGER';
    
    if (!canManage && !isSelf) {
      throw new ForbiddenException('Bạn không có quyền chỉnh sửa thông tin thành viên này.');
    }

    const data: any = {};
    if (body.adultCount !== undefined) {
      if (body.adultCount < 1) throw new BadRequestException('Số lượng người lớn tối thiểu là 1.');
      data.adultCount = Number(body.adultCount);
    }
    if (body.childCount !== undefined) {
      if (body.childCount < 0) throw new BadRequestException('Số lượng trẻ em không được âm.');
      data.childCount = Number(body.childCount);
    }
    if (body.role !== undefined && canManage) {
      const validRoles = ['OWNER', 'MANAGER', 'MEMBER', 'MEMBER_NO_FINANCE', 'VIEWER_ONLY'] as const;
      if (validRoles.includes(body.role as any)) {
        data.role = body.role;
      }
    }

    // Xóa cache liên quan
    if (participant.userId) {
      await this.cacheManager.del(`user-role:${participant.userId}:${tourId}`);
    }
    await Promise.all([
      this.cacheManager.del(tourId),
      this.cacheManager.del(`/api/v1/tours/${tourId}`),
      this.cacheManager.del(`/api/v1/tours/${tourId}/public`),
    ]);

    return this.prisma.tourParticipant.update({
      where: { id: participant.id },
      data,
      include: { user: { select: { id: true, name: true, email: true } } },
    });
  }

  @Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Only owner/manager can view join requests
  @UseGuards(JwtAuthGuard, TourRoleGuard)
  @Get(':tourId/join-requests')
  async getJoinRequests(@Param('tourId', ParseUUIDPipe) tourId: string, @Req() req: any) {
    const requests = await this.prisma.joinRequest.findMany({
      where: { tourId, status: 'PENDING' },
      orderBy: { createdAt: 'desc' },
      include: {
        user: { select: { id: true, name: true, email: true } },
        requestedBy: { select: { id: true, name: true, email: true } },
      },
    });

    return requests;
  }

  @UseGuards(JwtAuthGuard)
  @Post(':tourId/join-requests')
  async createJoinRequest(@Param('tourId', ParseUUIDPipe) tourId: string, @Body() body: { userId?: string }, @Req() req: any) {
    const requestingUserId = body.userId || req.user.id;

    const existingParticipation = await this.prisma.tourParticipant.findUnique({
      where: { tourId_userId: { tourId, userId: requestingUserId } },
    });
    if (existingParticipation) {
      throw new BadRequestException('Người dùng này đã là thành viên của tour.');
    }

    const pendingRequest = await this.prisma.joinRequest.findFirst({
      where: { tourId, userId: requestingUserId, status: 'PENDING' },
    });
    if (pendingRequest) {
      return pendingRequest;
    }

    const joinRequest = await this.prisma.joinRequest.create({
      data: {
        tourId,
        userId: requestingUserId,
        requestedById: req.user.id,
        status: 'PENDING',
      },
      include: {
        user: { select: { id: true, name: true, email: true } },
        requestedBy: { select: { id: true, name: true, email: true } },
      },
    });

    return joinRequest;
  }

  @Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Only owner/manager can accept join requests
  @UseGuards(JwtAuthGuard, TourRoleGuard)
  @Post(':tourId/join-requests/:requestId/accept')
  async acceptJoinRequest(@Param('tourId', ParseUUIDPipe) tourId: string, @Param('requestId') requestId: string, @Req() req: any) {
    let role = req.user.tourParticipation?.role;
    if (!role) {
      const participation = await this.prisma.tourParticipant.findUnique({
        where: { tourId_userId: { tourId, userId: req.user.id } },
      });
      role = participation?.role;
    }
    if  (!role || !(role === 'OWNER' || role === 'MANAGER')) {
      throw new ForbiddenException('Bạn không có quyền chấp nhận yêu cầu tham gia.');
    }

    const joinRequest = await this.prisma.joinRequest.findUnique({
      where: { id: requestId },
    });
    if (!joinRequest || joinRequest.tourId !== tourId) {
      throw new NotFoundException('Không tìm thấy yêu cầu tham gia.');
    }
    if (joinRequest.status !== 'PENDING') {
      throw new BadRequestException('Yêu cầu này đã được xử lý trước đó.');
    }

    await this.cacheManager.del(`user-role:${joinRequest.userId}:${tourId}`);

    const existing = await this.prisma.tourParticipant.findUnique({
      where: { tourId_userId: { tourId, userId: joinRequest.userId } },
    });
    if (existing) {
      await this.prisma.joinRequest.update({
        where: { id: requestId },
        data: { status: 'REJECTED' },
      });
      return { success: true, message: 'Người dùng đã là thành viên, yêu cầu đã bị từ chối.' };
    }

    await this.prisma.$transaction([
      this.prisma.tourParticipant.create({
        data: {
          tourId,
          userId: joinRequest.userId,
          role: 'MEMBER',
        },
      }),
      this.prisma.joinRequest.update({
        where: { id: requestId },
        data: { status: 'ACCEPTED' },
      }),
    ]);

    const tour = await this.prisma.tour.findUnique({
      where: { id: tourId },
      select: { title: true }
    });

    this.commentGateway.notifyJoinRequestAccepted(joinRequest.userId, {
      tourId,
      tourTitle: tour?.title || 'Hành trình',
      requestId
    });

    return { success: true, message: 'Đã chấp nhận yêu cầu tham gia.' };
  }

  @Roles(ParticipantRole.OWNER) // Only owner can reject join requests
  @UseGuards(JwtAuthGuard, TourRoleGuard)
  @Post(':tourId/join-requests/:requestId/reject')
  async rejectJoinRequest(@Param('tourId', ParseUUIDPipe) tourId: string, @Param('requestId') requestId: string, @Req() req: any) {
    let role = req.user.tourParticipation?.role;
    if (!role) {
      const participation = await this.prisma.tourParticipant.findUnique({
        where: { tourId_userId: { tourId, userId: req.user.id } },
      });
      role = participation?.role;
    }
    if (!role || role !== 'OWNER') {
      throw new ForbiddenException('Bạn không có quyền từ chối yêu cầu tham gia.');
    }

    const joinRequest = await this.prisma.joinRequest.findUnique({
      where: { id: requestId },
    });
    if (!joinRequest || joinRequest.tourId !== tourId) {
      throw new NotFoundException('Không tìm thấy yêu cầu tham gia.');
    }
    if (joinRequest.status !== 'PENDING') {
      throw new BadRequestException('Yêu cầu này đã được xử lý trước đó.');
    }

    await this.prisma.joinRequest.update({
      where: { id: requestId },
      data: { status: 'REJECTED' },
    });

    return { success: true, message: 'Đã từ chối yêu cầu tham gia.' };
  }

  @Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Only owner/manager can remove members
  @UseGuards(JwtAuthGuard, TourRoleGuard)
  @Delete(':tourId/members/:userId')
  async removeMember(@Param('tourId', ParseUUIDPipe) tourId: string, @Param('userId', ParseUUIDPipe) userId: string) {
    const participant = await this.prisma.tourParticipant.findFirst({
      where: {
        OR: [
          { id: userId },
          { tourId, userId }
        ]
      }
    });
    if (!participant) {
      throw new NotFoundException('Thành viên này không có trong tour');
    }

    if (participant.userId) {
      await this.cacheManager.del(`user-role:${participant.userId}:${tourId}`);
    }
    await Promise.all([
      this.cacheManager.del(tourId),
      this.cacheManager.del(`/api/v1/tours/${tourId}`),
      this.cacheManager.del(`/api/v1/tours/${tourId}/public`),
    ]);

    await this.prisma.tourParticipant.delete({
      where: { id: participant.id },
    });
    return { message: 'Đã xóa thành viên khỏi tour' };
  }

  @Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER, ParticipantRole.MEMBER, ParticipantRole.MEMBER_NO_FINANCE) // All members can upload photos
  @UseGuards(JwtAuthGuard, TourRoleGuard) // TourRoleGuard will now check for these roles
  @Post(':tourId/photos')
  @UseInterceptors(FilesInterceptor('images', 10)) // Chuyển sang memory storage để xử lý ảnh trước khi lưu
  async uploadPhotos(@Param('tourId', ParseUUIDPipe) tourId: string, @UploadedFiles() files: any[], @Req() req: any) {
    if (!files || files.length === 0) {
      throw new BadRequestException('Vui lòng chọn ít nhất một ảnh');
    }

    const uploaderId = req.user.id;
    // Đường dẫn ảnh gốc cho từng thành viên
    const memberOriginalDir = path.join(UPLOAD_ROOT, 'members', uploaderId, 'originals');
    // Đường dẫn ảnh hiển thị chung của Tour
    const tourDisplayPath = path.join(UPLOAD_ROOT, 'tours');

    // Đảm bảo các thư mục tồn tại
    if (!fs.existsSync(memberOriginalDir)) fs.mkdirSync(memberOriginalDir, { recursive: true });
    if (!fs.existsSync(tourDisplayPath)) fs.mkdirSync(tourDisplayPath, { recursive: true });

    // Trích xuất tọa độ GPS dự phòng từ request body gửi từ frontend
    const bodyLat = req.body.latitude ? parseFloat(req.body.latitude) : undefined;
    const bodyLng = req.body.longitude ? parseFloat(req.body.longitude) : undefined;

    return Promise.all(files.map(async (file) => {
        const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
        const originalExtension = path.extname(file.originalname).toLowerCase() || '.jpg';
        const originalFilename = `${uniqueSuffix}${originalExtension}`;
        const displayFilename = `${uniqueSuffix}.jpg`;

        const originalFilePath = path.join(memberOriginalDir, originalFilename);
        const displayFilePath = path.join(tourDisplayPath, displayFilename);

        // 1. Lưu ảnh gốc nguyên bản vào thư mục riêng của thành viên
        await fs.promises.writeFile(originalFilePath, file.buffer);

        // 2. Trích xuất GPS từ EXIF
        let lat: number | undefined;
        let lng: number | undefined;
        try {
          const gps = await exifr.gps(file.buffer);
          if (gps && typeof gps.latitude === 'number' && typeof gps.longitude === 'number') {
            lat = gps.latitude;
            lng = gps.longitude;
          }
        } catch (e) {
          console.warn('[EXIF GPS] Không thể giải nén GPS từ EXIF ảnh:', e.message);
        }

        // 3. Nếu EXIF không có GPS, dùng GPS dự phòng của thiết bị gửi từ Frontend
        if (lat === undefined || lng === undefined) {
          if (bodyLat !== undefined && !isNaN(bodyLat) && bodyLng !== undefined && !isNaN(bodyLng)) {
            lat = bodyLat;
            lng = bodyLng;
          }
        }

        // 4. Nếu vẫn không có vị trí nào, ghim tại vị trí mặc định [10.7769, 106.7009]
        if (lat === undefined || lng === undefined) {
          lat = 10.7769;
          lng = 106.7009;
        }

        // 5. Xử lý ảnh để hiển thị (Độ phân giải 2K: tối đa 2560px)
        let processBuffer = file.buffer;
        const isHeic = file.originalname.toLowerCase().endsWith('.heic') || file.originalname.toLowerCase().endsWith('.heif') || file.mimetype === 'image/heic' || file.mimetype === 'image/heif';
        if (isHeic) {
          try {
            processBuffer = await heicConvert({
              buffer: file.buffer,
              format: 'JPEG',
              quality: 1
            });
            console.log(`[HEIC] Converted original HEIC image to JPEG for display`);
          } catch (e) {
            console.warn('[HEIC] Failed to convert HEIC to JPEG, attempting to process anyway:', e.message);
          }
        }

        // Ensure /uploads/tours/ directory exists
        const uploadsDir = path.dirname(displayFilePath);
        if (!fs.existsSync(uploadsDir)) {
          try {
            fs.mkdirSync(uploadsDir, { recursive: true });
            console.log(`[Photo] Created directory: ${uploadsDir}`);
          } catch (e) {
            console.error(`[Photo] Failed to create directory ${uploadsDir}:`, e);
            throw new BadRequestException(`Lỗi tạo thư mục: ${e.message}`);
          }
        }

        // Sử dụng Sharp để resize và tối ưu dung lượng ảnh
        try {
          await sharp(processBuffer)
            .rotate()
            .resize(2560, 2560, {
              fit: 'inside',          // Giữ nguyên tỷ lệ, không vượt quá khung 2K
              withoutEnlargement: true // Nếu ảnh nhỏ hơn 2K thì giữ nguyên, không làm vỡ ảnh
            })
            .jpeg({ quality: 85 })    // Tối ưu chất lượng/dung lượng
            .toFile(displayFilePath);
          console.log(`[Photo] Display image saved successfully: ${displayFilePath}`);
        } catch (e) {
          console.error(`[Photo] Failed to save display image to ${displayFilePath}:`, e);
          throw new BadRequestException(`Lỗi lưu hình ảnh: ${e.message}`);
        }

        // Verify file was created
        if (!fs.existsSync(displayFilePath)) {
          console.error(`[Photo] File verification failed at ${displayFilePath}`);
          throw new BadRequestException('Lỗi: Hình ảnh không được lưu thành công. Vui lòng kiểm tra quyền lưu trữ.');
        }

        // 6. Lưu thông tin vào Database (Lưu cả 2 đường dẫn và metadata GPS)
        const photoRecord = await this.prisma.photo.create({
          data: {
            tourId: tourId,
            uploaderId: uploaderId,
            imageUrl: `/uploads/tours/${displayFilename}`,           // URL ảnh 2K dùng để render (luôn là .jpg)
            originalUrl: `/uploads/members/${uploaderId}/originals/${originalFilename}`, // URL ảnh gốc để tải xuống
            privacy: 'TOUR_ONLY',
            metadata: {
              lat: lat,
              lng: lng
            }
          }
        });
        console.log(`[Photo] Database record created: ID=${photoRecord.id}, imageUrl=${photoRecord.imageUrl}`);
        return photoRecord;
    }));
  }

  @Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER)
  @UseGuards(JwtAuthGuard, TourRoleGuard)
  @Post(':tourId/invitations')
  async createInvitation(
    @Param('tourId', ParseUUIDPipe) tourId: string,
    @Body() body: { email: string; role?: ParticipantRole },
    @Req() req: any
  ) {
    const { email, role = ParticipantRole.MEMBER } = body;
    if (!email) {
      throw new BadRequestException('Vui lòng cung cấp email mời.');
    }

    const tour = await this.prisma.tour.findUnique({
      where: { id: tourId },
      select: { title: true }
    });
    if (!tour) {
      throw new NotFoundException('Không tìm thấy hành trình.');
    }

    const invitedUser = await this.prisma.user.findUnique({ where: { email } });
    if (invitedUser) {
      const existingParticipant = await this.prisma.tourParticipant.findUnique({
        where: { tourId_userId: { tourId, userId: invitedUser.id } }
      });
      if (existingParticipant) {
        throw new BadRequestException('Người dùng này đã là thành viên của tour.');
      }
    }

    const token = crypto.randomBytes(32).toString('hex');
    const expiredAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days

    const invitation = await this.prisma.tourInvitation.upsert({
      where: { tourId_email: { tourId, email } },
      update: { token, expiredAt, role },
      create: { tourId, email, token, expiredAt, role }
    });

    const appUrl = process.env.FRONTEND_URL || 'http://localhost:3002';
    const inviteLink = `${appUrl}/join-tour?token=${token}`;
    
    try {
      await this.emailService.sendTourInvitation(email, tour.title, inviteLink);
    } catch (e) {
      console.error('[Invitation Email Error]:', e);
      throw new BadRequestException('Không thể gửi email lời mời. Vui lòng kiểm tra cấu hình SMTP.');
    }

    return { success: true, message: 'Lời mời đã được gửi thành công!' };
  }

  @UseGuards(JwtAuthGuard)
  @Post('join-by-token')
  async joinByToken(@Body() body: { token: string }, @Req() req: any) {
    const { token } = body;
    console.log('[joinByToken] Request received, token length:', token ? token.length : 0);
    console.log('[joinByToken] Token received (full):', token);
    
    if (!token) {
      console.error('[joinByToken] No token provided');
      throw new BadRequestException('Vui lòng cung cấp token lời mời.');
    }

    console.log('[joinByToken] Searching for invitation with token:', token.substring(0, 20) + '...');
    const invitation = await this.prisma.tourInvitation.findUnique({
      where: { token },
      include: { tour: { select: { id: true, title: true } } }
    });

    if (!invitation) {
      console.error('[joinByToken] Invitation not found for token:', token.substring(0, 20) + '...');
      // Check how many invitations exist in database for debugging
      const totalInvitations = await this.prisma.tourInvitation.count();
      console.log('[joinByToken] Total invitations in database:', totalInvitations);
      
      // List all tokens in DB for comparison (only in dev)
      const allInvitations = await this.prisma.tourInvitation.findMany({ select: { token: true, email: true, tourId: true } });
      console.log('[joinByToken] All invitation tokens:', allInvitations);
      
      throw new NotFoundException('Lời mời không hợp lệ hoặc đã bị hủy.');
    }

    // Verify the invitation email matches the logged-in user's email
    const userEmail = req.user.email;
    console.log('[joinByToken] Checking email - User:', userEmail, 'Invitation:', invitation.email);
    if (invitation.email && userEmail && invitation.email !== userEmail) {
      console.error('[joinByToken] Email mismatch - User email:', userEmail, 'Invitation email:', invitation.email);
      throw new BadRequestException('Lời mời này được gửi tới một địa chỉ email khác. Vui lòng đăng nhập bằng tài khoản đúng.');
    }

    console.log('[joinByToken] Invitation found, tourId:', invitation.tourId, 'expired:', invitation.expiredAt < new Date());

    if (invitation.expiredAt < new Date()) {
      console.error('[joinByToken] Invitation expired at:', invitation.expiredAt);
      try {
        await this.prisma.tourInvitation.delete({ where: { id: invitation.id } });
      } catch (e) {}
      throw new BadRequestException('Lời mời đã hết hạn.');
    }

    const userId = req.user.id;
    console.log('[joinByToken] User attempting to join - userId:', userId, 'tourId:', invitation.tourId);

    const existingParticipation = await this.prisma.tourParticipant.findUnique({
      where: { tourId_userId: { tourId: invitation.tourId, userId } }
    });

    if (existingParticipation) {
      console.log('[joinByToken] User is already a participant');
      try {
        await this.prisma.tourInvitation.delete({ where: { id: invitation.id } });
      } catch (e) {}
      // Xóa cache để đảm bảo dữ liệu thành viên luôn mới nhất
      await Promise.all([
        this.cacheManager.del(invitation.tourId),
        this.cacheManager.del(`/api/v1/tours/${invitation.tourId}`),
        this.cacheManager.del(`/api/v1/tours/${invitation.tourId}/public`),
        this.cacheManager.del(`/api/v1/tours/explore`),
        this.cacheManager.del(`user-role:${userId}:${invitation.tourId}`),
      ]);
      return { success: true, tourId: invitation.tourId, message: 'Bạn đã là thành viên của tour này.', shouldRefresh: true };
    }

    // Xóa cache TRƯỚC để đảm bảo không có race condition
    await Promise.all([
      this.cacheManager.del(`user-role:${userId}:${invitation.tourId}`),
      this.cacheManager.del(invitation.tourId),
      this.cacheManager.del(`/api/v1/tours/${invitation.tourId}`),
      this.cacheManager.del(`/api/v1/tours/${invitation.tourId}/public`),
      this.cacheManager.del(`/api/v1/tours/explore`),
    ]);

    await this.prisma.$transaction([
      this.prisma.tourParticipant.create({
        data: {
          tourId: invitation.tourId,
          userId,
          role: invitation.role
        }
      }),
      this.prisma.tourInvitation.delete({
        where: { id: invitation.id }
      })
    ]);

    console.log('[joinByToken] Successfully joined tour:', invitation.tourId, 'userId:', userId);
    return { success: true, tourId: invitation.tourId, message: `Bạn đã gia nhập hành trình "${invitation.tour.title}"!`, shouldRefresh: true };
  }

  @Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER)
  @UseGuards(JwtAuthGuard, TourRoleGuard)
  @Post(':tourId/members/merge')
  async mergeMember(
    @Param('tourId', ParseUUIDPipe) tourId: string,
    @Body() body: { manualParticipantId: string; systemUserId: string },
    @Req() req: any
  ) {
    const { manualParticipantId, systemUserId } = body;
    if (!manualParticipantId || !systemUserId) {
      throw new BadRequestException('Thiếu thông tin manualParticipantId hoặc systemUserId.');
    }

    const manualParticipant = await this.prisma.tourParticipant.findFirst({
      where: { id: manualParticipantId, tourId, userId: null }
    });
    if (!manualParticipant) {
      throw new NotFoundException('Không tìm thấy thành viên ngoài hệ thống cần gán.');
    }

    const systemParticipant = await this.prisma.tourParticipant.findUnique({
      where: { tourId_userId: { tourId, userId: systemUserId } }
    });
    if (!systemParticipant) {
      throw new NotFoundException('Không tìm thấy thành viên hệ thống trong hành trình này.');
    }

    const nextAdult = Math.max(systemParticipant.adultCount, manualParticipant.adultCount);
    const nextChild = Math.max(systemParticipant.childCount, manualParticipant.childCount);

    await this.prisma.$transaction([
      this.prisma.tourParticipant.update({
        where: { id: systemParticipant.id },
        data: {
          adultCount: nextAdult,
          childCount: nextChild,
          role: manualParticipant.role !== ParticipantRole.MEMBER ? manualParticipant.role : systemParticipant.role
        }
      }),
      this.prisma.tourParticipant.delete({
        where: { id: manualParticipant.id }
      })
    ]);

    await this.cacheManager.del(`user-role:${systemUserId}:${tourId}`);
    await Promise.all([
      this.cacheManager.del(tourId),
      this.cacheManager.del(`/api/v1/tours/${tourId}`),
      this.cacheManager.del(`/api/v1/tours/${tourId}/public`),
      this.cacheManager.del(`/api/v1/tours/explore`)
    ]);

    return { success: true, message: 'Đã gán và hợp nhất thông tin thành viên thành công.' };
  }
}

@Controller('locations')
@UseGuards(JwtAuthGuard)
class LocationController {
  constructor(private prisma: PrismaService, @Inject(CACHE_MANAGER) private cacheManager: Cache) {}

  @Patch(':id')
  @UseGuards(TourRoleGuard) // Apply TourRoleGuard to get tourId
  async updateLocation(@Param('id', ParseUUIDPipe) id: string, @Body() body: any, @Req() req: any) {
    const { expenseAmount, expenseCategory, ...data } = body;

    const location = await this.prisma.location.update({
      where: { id },
      data: {
        name: data.name,
        address: data.address,
        latitude: data.latitude,
        longitude: data.longitude,
        type: data.type,
        plannedStart: data.plannedStart ? new Date(data.plannedStart) : undefined,
        plannedEnd: data.plannedEnd ? new Date(data.plannedEnd) : undefined,
        status: data.status,
      }
    });

    if (expenseAmount !== undefined) {
      const amount = Number(expenseAmount);
      const existingExpense = await this.prisma.expense.findFirst({
        where: { locationId: id }
      });

      if (existingExpense) {
        await this.prisma.expense.update({
          where: { id: existingExpense.id },
          data: { amount, category: expenseCategory || 'OTHER' }
        });
      } else if (amount > 0) {
        await this.prisma.expense.create({
          data: {
            amount,
            category: expenseCategory || 'OTHER',
            locationId: id,
            legId: location.legId,
            description: `Chi phí tại ${location.name}`
          }
        });
      }
    }

    if (req.tourId) {
      await Promise.all([
        this.cacheManager.del(req.tourId),
        this.cacheManager.del(`/api/v1/tours/${req.tourId}`),
        this.cacheManager.del(`/api/v1/tours/${req.tourId}/public`),
      ]);
    }
    return location;
  }

  @Delete(':id')
  @UseGuards(TourRoleGuard) // Apply TourRoleGuard to get tourId
  async deleteLocation(@Param('id', ParseUUIDPipe) id: string, @Req() req: any) {
    try {
      await this.prisma.location.delete({ where: { id } });
    } catch (e) {
      // Nếu bản ghi đã bị xóa trước đó, không ném lỗi 500 để đảm bảo tính an toàn (idempotency)
    }
    
    // Xóa mapping cache để Guard không bị đánh lừa ở lần truy cập sau
    await this.cacheManager.del(`res-to-tour:${id}`);

    if (req.tourId) {
      await Promise.all([
        this.cacheManager.del(req.tourId),
        this.cacheManager.del(`/api/v1/tours/${req.tourId}`),
        this.cacheManager.del(`/api/v1/tours/${req.tourId}/public`),
      ]);
    }
    return { success: true };
  }
}

@Controller('legs')
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Default roles for Leg operations
@UseGuards(JwtAuthGuard, TourRoleGuard)
class LegController {
  constructor(private prisma: PrismaService, @Inject(CACHE_MANAGER) private cacheManager: Cache) {}

  @Patch(':id')
  @UseGuards(TourRoleGuard) // Apply TourRoleGuard to get tourId
  async updateLeg(@Param('id', ParseUUIDPipe) id: string, @Body() body: any, @Req() req: any) {
    return this.prisma.leg.update({
      where: { id },
      data: {
        note: body.note,
        sequence: body.sequence,
        startDate: body.startDate ? new Date(body.startDate) : undefined,
        endDate: body.endDate ? new Date(body.endDate) : undefined,
        description: body.description,
      }
    }).then(async (leg) => {
      await Promise.all([
        this.cacheManager.del(req.tourId),
        this.cacheManager.del(`/api/v1/tours/${req.tourId}`),
        this.cacheManager.del(`/api/v1/tours/${req.tourId}/public`),
      ]);
      return leg;
    });
  }

  @Delete(':id')
  @UseGuards(TourRoleGuard) // Apply TourRoleGuard to get tourId
  async deleteLeg(@Param('id', ParseUUIDPipe) id: string) {
    const leg = await this.prisma.leg.findUnique({
      where: { id },
      include: { _count: { select: { locations: true } } }
    });
    
    if (leg?._count.locations && leg._count.locations > 0) {
      throw new BadRequestException('Không thể xóa chặng đang có địa điểm. Hãy xóa địa điểm trước.');
    }

    try {
      const deletedLeg = await this.prisma.leg.delete({ where: { id } });
      await this.cacheManager.del(`res-to-tour:${id}`);
      
      if (deletedLeg.tourId) {
        await Promise.all([
          this.cacheManager.del(deletedLeg.tourId),
          this.cacheManager.del(`/api/v1/tours/${deletedLeg.tourId}`),
          this.cacheManager.del(`/api/v1/tours/${deletedLeg.tourId}/public`),
        ]);
      }
    } catch (e) {
      // Idempotency
    }

    return { success: true };
  }
}

/**
 * Helper tính khoảng cách giữa 2 tọa độ (Haversine formula)
 */
function calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number) {
  const p = 0.017453292519943295; // Math.PI / 180
  const c = Math.cos;
  const a = 0.5 - c((lat2 - lat1) * p) / 2 + 
            c(lat1 * p) * c(lat2 * p) * 
            (1 - c((lon2 - lon1) * p)) / 2;
  return 12742 * Math.asin(Math.sqrt(a)); // 2 * R; R = 6371 km
}

@Controller('routing')
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Default roles for Routing operations
@UseGuards(JwtAuthGuard, TourRoleGuard)
class RoutingController {
  constructor(private prisma: PrismaService, @Inject(CACHE_MANAGER) private cacheManager: Cache) {}

  @Post('optimize/:legId')
  @UseGuards(TourRoleGuard) // Apply TourRoleGuard to get tourId
  async optimize(@Param('legId', ParseUUIDPipe) legId: string, @Req() req: any) {
    const currentLeg = await this.prisma.leg.findUnique({
      where: { id: legId },
    });
    if (!currentLeg) throw new NotFoundException('Không tìm thấy chặng');

    const locations = await this.prisma.location.findMany({
      where: { legId },
    });

    if (locations.length === 0) return { locations: [], totalDistance: 0 };

    // KHAI BÁO startAnchor ở đầu hàm để tránh lỗi scope TS2304
    let startAnchor: any = null;
    
    // Tìm địa điểm cuối cùng của chặng trước đó
    const prevLeg = await this.prisma.leg.findFirst({
      where: { 
        tourId: currentLeg.tourId,
        sequence: currentLeg.sequence - 1 
      },
      include: { locations: { orderBy: [
        { plannedStart: { sort: 'asc', nulls: 'last' } },
        { createdAt: 'asc' }
      ] } }
    });
    
    if (prevLeg?.locations?.length) {
      startAnchor = prevLeg.locations[prevLeg.locations.length - 1];
    }

    if (locations.length <= 2 && !startAnchor) return { locations, totalDistance: 0 };

    // Thuật toán Greedy TSP đơn giản để tối ưu hóa lộ trình
    const optimized = [];
    const unvisited = [...locations];
    
    // Bắt đầu với địa điểm có thời gian dự kiến sớm nhất hiện tại
    let current: any;
    if (startAnchor) {
      let nearestIdx = 0;
      let minDist = Infinity;
      for (let i = 0; i < unvisited.length; i++) {
        const d = calculateDistance(startAnchor.latitude, startAnchor.longitude, unvisited[i].latitude, unvisited[i].longitude);
        if (d < minDist) {
          minDist = d;
          nearestIdx = i;
        }
      }
      current = unvisited.splice(nearestIdx, 1)[0];
    } else {
      current = unvisited.sort((a, b) => 
        (a.plannedStart?.getTime() || 0) - (b.plannedStart?.getTime() || 0)
      ).shift()!;
    }
    optimized.push(current);

    while (unvisited.length > 0) {
      let nearestIdx = 0;
      let minDist = Infinity;

      for (let i = 0; i < unvisited.length; i++) {
        const d = calculateDistance(current.latitude, current.longitude, unvisited[i].latitude, unvisited[i].longitude);
        if (d < minDist) {
          minDist = d;
          nearestIdx = i;
        }
      }
      current = unvisited.splice(nearestIdx, 1)[0];
      optimized.push(current);
    }

    // Tính toán tổng quãng đường di chuyển của chặng (km)
    let totalDistance = 0;
    
    // Cộng thêm quãng đường từ chặng trước nối sang chặng này
    if (startAnchor) {
      totalDistance += calculateDistance(
        startAnchor.latitude, startAnchor.longitude,
        optimized[0].latitude, optimized[0].longitude
      );
    }

    for (let i = 0; i < optimized.length - 1; i++) {
      totalDistance += calculateDistance(
        optimized[i].latitude, optimized[i].longitude,
        optimized[i+1].latitude, optimized[i+1].longitude
      );
    }

    // Cập nhật lại thời gian plannedStart trong DB để phản ánh thứ tự mới (mỗi điểm cách nhau 1 giờ giả định)
    const baseTime = optimized[0].plannedStart || new Date();
    await Promise.all(optimized.map((loc, index) => 
      this.prisma.location.update({
        where: { id: loc.id },
        data: { plannedStart: new Date(baseTime.getTime() + index * 3600000) },
      })
    ));

    const updatedLocations = await this.prisma.location.findMany({
      where: { legId },
      orderBy: [
        { plannedStart: { sort: 'asc', nulls: 'last' } },
        { createdAt: 'asc' }
      ]
    });

    if (req.tourId) {
      await Promise.all([
        this.cacheManager.del(req.tourId),
        this.cacheManager.del(`/api/v1/tours/${req.tourId}`),
        this.cacheManager.del(`/api/v1/tours/${req.tourId}/public`),
      ]);
    }
    return {
      locations: updatedLocations,
      totalDistance: parseFloat(totalDistance.toFixed(2))
    };
  }
}

@Controller('photos')
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER, ParticipantRole.MEMBER, ParticipantRole.MEMBER_NO_FINANCE) // All members can delete their own photos
@UseGuards(JwtAuthGuard, TourRoleGuard)
class PhotoController {
  constructor(private prisma: PrismaService) {}

  @Post('upload-anonymous')
  @UseGuards(JwtAuthGuard) // Vẫn dùng Guard để lấy user từ token
  @UseInterceptors(FilesInterceptor('images', 1)) // Chỉ cho phép 1 ảnh mỗi lần
  async uploadAnonymousPhoto(@UploadedFiles() files: any[], @Req() req: any) {
    if (!files || files.length === 0) {
      throw new BadRequestException('Vui lòng chọn một ảnh.');
    }

    const uploaderId = req.user.id;
    // Cả người dùng đã đăng ký và khách ẩn danh đều được dùng endpoint này để chia sẻ ảnh công khai lên bản đồ

    const file = files[0];
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
    const originalExtension = path.extname(file.originalname).toLowerCase() || '.jpg';
    const originalFilename = `${uniqueSuffix}${originalExtension}`;
    const displayFilename = `${uniqueSuffix}.jpg`;

    const memberOriginalDir = path.join(UPLOAD_ROOT, 'members', uploaderId, 'originals');
    const tourDisplayPath = path.join(UPLOAD_ROOT, 'tours');
    const displayFilePath = path.join(tourDisplayPath, displayFilename);
    const originalFilePath = path.join(memberOriginalDir, originalFilename);

    // Đảm bảo thư mục lưu trữ ảnh gốc và ảnh hiển thị tồn tại
    if (!fs.existsSync(memberOriginalDir)) {
      fs.mkdirSync(memberOriginalDir, { recursive: true });
    }
    if (!fs.existsSync(tourDisplayPath)) {
      fs.mkdirSync(tourDisplayPath, { recursive: true });
    }

    // 1. Lưu ảnh gốc nguyên bản vào thư mục riêng của khách
    await fs.promises.writeFile(originalFilePath, file.buffer);

    // 2. Trích xuất GPS từ EXIF bằng exifr
    let lat: number | undefined;
    let lng: number | undefined;

    try {
      const gps = await exifr.gps(file.buffer);
      if (gps && typeof gps.latitude === 'number' && typeof gps.longitude === 'number') {
        lat = gps.latitude;
        lng = gps.longitude;
        console.log(`[EXIF GPS] Đã tìm thấy tọa độ từ EXIF: lat=${lat}, lng=${lng}`);
      }
    } catch (e) {
      console.warn('[EXIF GPS] Không thể giải nén GPS từ EXIF ảnh:', e.message);
    }

    // 3. Nếu EXIF không có tọa độ, dùng tọa độ dự phòng gửi từ frontend
    if (lat === undefined || lng === undefined) {
      const bodyLat = req.body.latitude ? parseFloat(req.body.latitude) : undefined;
      const bodyLng = req.body.longitude ? parseFloat(req.body.longitude) : undefined;

      if (bodyLat !== undefined && !isNaN(bodyLat) && bodyLng !== undefined && !isNaN(bodyLng)) {
        lat = bodyLat;
        lng = bodyLng;
        console.log(`[EXIF GPS] Sử dụng tọa độ dự phòng từ Frontend: lat=${lat}, lng=${lng}`);
      }
    }

    // Lấy tags từ request (nếu có)
    let tags: string[] = [];
    if (req.body.tags) {
      try {
        tags = JSON.parse(req.body.tags);
        if (!Array.isArray(tags)) {
          tags = [];
        }
      } catch (e) {
        const errorMsg = e instanceof Error ? e.message : 'Unknown error';
        console.warn('[TAGS] Failed to parse tags from request:', errorMsg);
      }
    }

    // 4. Nếu vẫn không có, sử dụng vị trí mặc định (TP.HCM)
    if (lat === undefined || lng === undefined) {
      lat = 10.7769;
      lng = 106.7009;
      console.log(`[EXIF GPS] Không tìm thấy tọa độ nào, ghim tại TP.HCM mặc định: lat=${lat}, lng=${lng}`);
    }

    // 5. Chuyển đổi HEIC nếu cần để lưu hiển thị
    let processBuffer = file.buffer;
    const isHeic = file.originalname.toLowerCase().endsWith('.heic') || file.originalname.toLowerCase().endsWith('.heif') || file.mimetype === 'image/heic' || file.mimetype === 'image/heif';
    if (isHeic) {
      try {
        processBuffer = await heicConvert({
          buffer: file.buffer,
          format: 'JPEG',
          quality: 1
        });
        console.log(`[HEIC] Converted anonymous HEIC image to JPEG for display`);
      } catch (e) {
        console.warn('[HEIC] Failed to convert HEIC to JPEG, attempting to process anyway:', e.message);
      }
    }

    // 6. Xử lý ảnh để hiển thị (kích thước tối đa 2K: 2560px)
    await sharp(processBuffer)
      .rotate()
      .resize(2560, 2560, { fit: 'inside', withoutEnlargement: true })
      .jpeg({ quality: 85 })
      .toFile(displayFilePath);

    return this.prisma.photo.create({
      data: {
        uploaderId: uploaderId,
        imageUrl: `/uploads/tours/${displayFilename}`,
        originalUrl: `/uploads/members/${uploaderId}/originals/${originalFilename}`,
        privacy: 'PUBLIC',
        metadata: {
          lat: lat,
          lng: lng,
          tags: tags
        }
      },
    });
  }

  @Delete(':id')
  async deletePhoto(@Param('id', ParseUUIDPipe) id: string, @Req() req: any) {
    const photo = await this.prisma.photo.findUnique({
      where: { id },
    });

    if (!photo) {
      throw new NotFoundException('Không tìm thấy ảnh.');
    }

    // Chỉ người tải lên hoặc Admin mới có quyền xóa ảnh
    if (photo.uploaderId !== req.user.id && !req.user.isAdmin) {
      throw new ForbiddenException('Bạn không có quyền xóa ảnh này.');
    }

    await this.prisma.photo.update({
      where: { id },
      data: { isDeleted: true, deletedAt: new Date() }
    });
    return { message: 'Ảnh đã được chuyển vào thùng rác.' };
  }

  @Patch(':id')
  async updatePhoto(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() body: { title?: string; description?: string; latitude?: number; longitude?: number; tags?: string[] },
    @Req() req: any
  ) {
    const photo = await this.prisma.photo.findUnique({
      where: { id }
    });

    if (!photo) {
      throw new NotFoundException('Không tìm thấy ảnh.');
    }

    // Chỉ người tải lên hoặc Admin mới có quyền sửa thông tin ảnh
    if (photo.uploaderId !== req.user.id && !req.user.isAdmin) {
      throw new ForbiddenException('Bạn không có quyền chỉnh sửa thông tin của bức ảnh này.');
    }

    const currentMetadata = (photo.metadata as any) || {};
    const updatedMetadata = {
      ...currentMetadata,
      lat: body.latitude !== undefined ? body.latitude : currentMetadata.lat,
      lng: body.longitude !== undefined ? body.longitude : currentMetadata.lng,
      title: body.title !== undefined ? body.title : currentMetadata.title,
      description: body.description !== undefined ? body.description : currentMetadata.description,
      tags: body.tags !== undefined ? body.tags : currentMetadata.tags,
    };

    return this.prisma.photo.update({
      where: { id },
      data: {
        metadata: updatedMetadata
      }
    });
  }

  @Post(':id/toggle-like')
  @UseGuards(JwtAuthGuard)
  async toggleLikePhoto(@Param('id', ParseUUIDPipe) id: string, @Req() req: any) {
    const userId = req.user.id;
    const photo = await this.prisma.photo.findUnique({
      where: { id }
    });

    if (!photo) {
      throw new NotFoundException('Không tìm thấy ảnh.');
    }

    const currentMetadata = (photo.metadata as any) || {};
    const likedUserIds = Array.isArray(currentMetadata.likedUserIds)
      ? currentMetadata.likedUserIds
      : [];

    const index = likedUserIds.indexOf(userId);
    let updatedLikedUserIds = [...likedUserIds];

    if (index > -1) {
      // Unlike
      updatedLikedUserIds.splice(index, 1);
    } else {
      // Like
      updatedLikedUserIds.push(userId);
    }

    const updatedMetadata = {
      ...currentMetadata,
      likedUserIds: updatedLikedUserIds
    };

    return this.prisma.photo.update({
      where: { id },
      data: {
        metadata: updatedMetadata
      },
      include: {
        uploader: {
          select: { id: true, name: true }
        }
      }
    });
  }
}

@UseGuards(JwtAuthGuard)
@Controller('users')
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Default roles for User management (Admin/Manager)
class UserController {
  constructor(private prisma: PrismaService) {}

  @Get()
  async getAllUsers(@Req() req: any, @Query('q') q?: string) {
    const currentUserId = req.user?.id;
    const users = await this.prisma.user.findMany({
      where: {
        isAnonymous: false,
        ...(q
          ? {
              OR: [
                { name: { contains: q, mode: 'insensitive' as any } },
                { email: { contains: q, mode: 'insensitive' as any } },
              ],
            }
          : {}),
      },
      select: { id: true, email: true, name: true, isAdmin: true, isBlocked: true, createdAt: true, phone: true, address: true }
    });
    return users.filter((u: any) => u.id !== currentUserId);
  }

  // getMyPhotos does not need TourRoleGuard as it's for the user's own photos
  @UseGuards(JwtAuthGuard)
  @Get('me/photos')
  async getMyPhotos(@Req() req: any) {
    return this.prisma.photo.findMany({
      where: { uploaderId: req.user.id, isDeleted: false },
      include: {
        tour: { select: { title: true } }
      },
      orderBy: { capturedAt: 'desc' }
    });
  }

  @Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Default roles for updating user (Admin/Manager)
  @Patch(':id')
  async updateUser(@Param('id', ParseUUIDPipe) id: string, @Body() data: any, @Req() req: any) {
    const requestingUser = req.user;
    
    // Fetch target user from DB
    const targetUser = await this.prisma.user.findUnique({ where: { id } });
    if (!targetUser) {
      throw new NotFoundException('Không tìm thấy người dùng');
    }

    // 1. If target user is an Admin, only an Admin can update them.
    // (A manager/normal user cannot reset/change password of an Admin)
    if (targetUser.isAdmin && !requestingUser.isAdmin) {
      throw new ForbiddenException('Không có quyền thay đổi thông tin hoặc reset password của Quản trị viên');
    }

    // 2. A non-admin can only update their own profile.
    if (!requestingUser.isAdmin && requestingUser.id !== id) {
      throw new ForbiddenException('Bạn không có quyền thực hiện hành động này');
    }

    if (data.password) {
      data.passwordHash = await bcrypt.hash(data.password, 10);
      delete data.password;
    }

    // Safety: prevent non-admins from promoting anyone to admin
    if (!requestingUser.isAdmin && data.isAdmin !== undefined) {
      delete data.isAdmin;
    }

    return this.prisma.user.update({
      where: { id },
      data,
      select: { id: true, email: true, name: true, isAdmin: true, isBlocked: true, avatar: true, phone: true, address: true }
    });
  }

  @Roles(ParticipantRole.OWNER) // Only owner can delete user
  @Delete(':id')
  @UseGuards(AdminGuard)
  async deleteUser(@Param('id', ParseUUIDPipe) id: string, @Req() req: any) {
    const user = await this.prisma.user.findUnique({ where: { id } });
    if (!user) throw new NotFoundException('Không tìm thấy người dùng');
    if (user.isAdmin) {
      const adminCount = await this.prisma.user.count({ where: { isAdmin: true } });
      if (adminCount <= 1) throw new BadRequestException('Không thể xóa Quản trị viên cuối cùng');
    }

    // 1. Xác định thư mục chứa ảnh gốc của thành viên
    const memberDir = path.join(UPLOAD_ROOT, 'members', id);

    // 2. Tìm tất cả ảnh của user này để dọn dẹp nốt các bản 2K còn lại trong thư mục tours
    const photos = await this.prisma.photo.findMany({
      where: { uploaderId: id }
    });

    for (const photo of photos) {
      if (photo.imageUrl) {
        const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
        if (fs.existsSync(displayFilePath)) fs.unlinkSync(displayFilePath);
      }
    }

    // 2. Xóa các ràng buộc và dữ liệu trong DB
    await this.prisma.photo.deleteMany({ where: { uploaderId: id } });
    await this.prisma.tourParticipant.deleteMany({ where: { userId: id } });
    await this.prisma.user.delete({ where: { id } });

    // 3. Xóa vật lý toàn bộ thư mục ảnh gốc
    if (fs.existsSync(memberDir)) {
      fs.rmSync(memberDir, { recursive: true, force: true });
    }

    return { message: 'Đã xóa người dùng' };
  }

  @Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Default roles for blocking user (Admin/Manager)
  @Post('block/:id')
  @UseGuards(AdminGuard)
  async toggleBlock(@Param('id', ParseUUIDPipe) id: string, @Req() req: any) {
    const user = await this.prisma.user.findUnique({ where: { id } });
    if (!user) throw new NotFoundException('Người dùng không tồn tại');
    
    // Safety check: Cannot block an Admin
    if (user.isAdmin) {
      throw new BadRequestException('Không thể khóa tài khoản Quản trị viên');
    }

    const updated = await this.prisma.user.update({
      where: { id },
      data: { isBlocked: !user.isBlocked },
      select: { id: true, email: true, name: true, isBlocked: true }
    });
    return updated;
  }
}



@Controller('locations')
class CommentController {
  constructor(
    private prisma: PrismaService,
    private commentGateway: CommentGateway
  ) {}

  @Get(':locationId/comments')
  // Cho phép khách xem bình luận mà không cần đăng nhập
  async getComments(@Param('locationId', ParseUUIDPipe) locationId: string) {
    return this.prisma.comment.findMany({
      where: { locationId },
      include: {
        user: { select: { name: true } }
      },
      orderBy: { createdAt: 'asc' }
    });
  }

  @Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER, ParticipantRole.MEMBER, ParticipantRole.MEMBER_NO_FINANCE, ParticipantRole.VIEWER_ONLY)
  @UseGuards(JwtAuthGuard, TourRoleGuard)
  @Post(':locationId/comments')
  async addComment(
    @Param('locationId', ParseUUIDPipe) locationId: string,
    @Body() body: { content: string },
    @Req() req: any
  ) {
    const filteredContent = await filterText(this.prisma, body.content);
    const comment = await this.prisma.comment.create({
      data: {
        content: filteredContent,
        locationId,
        userId: req.user.id
      },
      include: { user: { select: { name: true } } }
    });

    // Tìm tourId để gửi thông báo vào đúng phòng
    const location = await this.prisma.location.findUnique({
      where: { id: locationId },
      include: { leg: { select: { tourId: true } } }
    });

    if (location?.leg?.tourId) {
      this.commentGateway.notifyNewComment(location.leg.tourId, {
        ...comment,
        locationId // Gửi kèm locationId để UI biết điểm nào cần tăng số lượng
      });
    }

    return comment;
  }
}

@Controller('admin/otp')
@UseGuards(JwtAuthGuard, AdminGuard)
class AdminOtpController {
  constructor(
    private prisma: PrismaService,
    private emailService: EmailService,
    @Inject(CACHE_MANAGER) private cacheManager: Cache
  ) {}

  @Post('send')
  async sendOtpToUser(@Body() body: { email: string }, @Req() req: any) {
    const { email } = body;
    
    // 1. Xác định định danh: IP và Email
    const clientIp = req.ip || req.headers['x-forwarded-for'] || req.connection.remoteAddress;
    const emailLimitKey = `rl:otp:email:${email}`;
    const ipLimitKey = `rl:otp:ip:${clientIp}`;

    // 2. Kiểm tra xem IP hoặc Email này có đang bị giới hạn không (ví dụ: 1 phút 1 lần)
    const [isEmailLimited, isIpLimited] = await Promise.all([
      this.cacheManager.get(emailLimitKey),
      this.cacheManager.get(ipLimitKey)
    ]);

    if (isEmailLimited || isIpLimited) {
      throw new HttpException(
        'Thao tác quá nhanh. Vui lòng đợi 60 giây giữa mỗi lần yêu cầu gửi mã.',
        HttpStatus.TOO_MANY_REQUESTS
      );
    }

    const user = await this.prisma.user.findUnique({ where: { email } });
    if (!user) {
      throw new NotFoundException('Không tìm thấy người dùng với email này');
    }

    const otp = Math.floor(100000 + Math.random() * 900000).toString();
    const otpCacheKey = `otp:${email}`;
    
    await this.cacheManager.set(otpCacheKey, otp, 300000);

    try {
      await this.emailService.sendOTP(email, otp);
      
      // 3. Thiết lập khóa chặn sau khi gửi thành công (hết hạn sau 60 giây)
      await Promise.all([
        this.cacheManager.set(emailLimitKey, true, 60000),
        this.cacheManager.set(ipLimitKey, true, 60000)
      ]);

      return { success: true, message: `Đã gửi mã OTP tới email ${email} thành công.` };
    } catch (error) {
      console.error('[Admin OTP] SMTP Error:', error);
      throw new BadRequestException('Lỗi cấu hình SMTP hoặc không thể kết nối tới máy chủ gửi mail');
    }
  }

  @Post('verify')
  async verifyOtp(@Body() body: { email: string; otp: string }) {
    const { email, otp } = body;
    const otpCacheKey = `otp:${email}`;
    const failCountKey = `otp_fails:${email}`;
    const MAX_FAILED_ATTEMPTS = 5;

    // 1. Kiểm tra tài khoản có đang bị khóa không
    const user = await this.prisma.user.findUnique({ where: { email } });
    if (!user) throw new NotFoundException('Người dùng không tồn tại');
    if (user.isBlocked) {
      throw new ForbiddenException('Tài khoản này hiện đang bị khóa. Vui lòng liên hệ quản trị viên.');
    }

    // 2. Lấy OTP từ Cache
    const storedOtp = await this.cacheManager.get<string>(otpCacheKey);
    if (!storedOtp) {
      throw new BadRequestException('Mã OTP đã hết hạn hoặc không tồn tại. Vui lòng yêu cầu mã mới.');
    }

    // 3. So sánh mã OTP
    if (storedOtp === otp) {
      // Thành công: Xóa OTP và bộ đếm lỗi
      await Promise.all([
        this.cacheManager.del(otpCacheKey),
        this.cacheManager.del(failCountKey)
      ]);
      return { success: true, message: 'Xác thực mã OTP thành công.' };
    } else {
      // Thất bại: Tăng bộ đếm lỗi
      let fails: number = (await this.cacheManager.get<number>(failCountKey)) || 0;
      fails++;

      if (fails >= MAX_FAILED_ATTEMPTS) {
        // Khóa tài khoản trong DB
        await this.prisma.user.update({
          where: { email },
          data: { isBlocked: true }
        });
        await this.cacheManager.del(failCountKey);
        await this.cacheManager.del(otpCacheKey);
        
        throw new ForbiddenException(`Bạn đã nhập sai quá ${MAX_FAILED_ATTEMPTS} lần. Tài khoản đã bị khóa để bảo mật.`);
      } else {
        // Cập nhật số lần sai vào cache (TTL 5 phút bằng với OTP)
        await this.cacheManager.set(failCountKey, fails, 300000);
        throw new BadRequestException({
          message: `Mã OTP không chính xác. Bạn còn ${MAX_FAILED_ATTEMPTS - fails} lần thử.`,
          remainingAttempts: MAX_FAILED_ATTEMPTS - fails
        });
      }
    }
  }
}

@Controller('admin/trash-photos')
@UseGuards(JwtAuthGuard, AdminGuard)
class AdminController {
  constructor(private prisma: PrismaService) {}

  @Get()
  async getTrashPhotos() {
    // 1. Lấy tất cả ảnh trong DB
    const dbPhotos = await this.prisma.photo.findMany({
      select: {
        id: true,
        imageUrl: true,
        originalUrl: true,
        privacy: true,
        tourId: true,
      }
    });

    const dbImageUrls = new Set(
      dbPhotos.map(p => p.imageUrl).filter(Boolean)
    );
    const dbOriginalUrls = new Set(
      dbPhotos.map(p => p.originalUrl).filter(Boolean)
    );

    // 2. Tìm tất cả tệp tin vật lý trên đĩa
    const allDiskFiles: string[] = [];
    const getFilesRecursively = (dir: string) => {
      if (!fs.existsSync(dir)) return;
      const list = fs.readdirSync(dir);
      for (const file of list) {
        const fullPath = path.join(dir, file);
        const stat = fs.statSync(fullPath);
        if (stat.isDirectory()) {
          getFilesRecursively(fullPath);
        } else {
          allDiskFiles.push(fullPath);
        }
      }
    };

    getFilesRecursively(UPLOAD_ROOT);

    const trashPhotos: any[] = [];

    // 3. Kiểm tra các tệp tin trên đĩa xem có trong DB không
    for (const filePath of allDiskFiles) {
      const relativePath = '/uploads' + filePath.substring(UPLOAD_ROOT.length).replace(/\\/g, '/');
      if (path.basename(filePath).startsWith('.')) continue;

      const isUsed = dbImageUrls.has(relativePath) || dbOriginalUrls.has(relativePath);
      
      if (!isUsed) {
        let size = 0;
        try {
          size = fs.statSync(filePath).size;
        } catch (e) {}

        trashPhotos.push({
          filePath: filePath,
          url: relativePath,
          size: size,
          reason: 'Tệp tin không tồn tại trong cơ sở dữ liệu (ảnh mồ côi)',
          type: 'file_only'
        });
      }
    }

    // 4. Kiểm tra các bản ghi DB mồ côi (không có tourId và privacy không phải PUBLIC)
    const orphanedDbPhotos = dbPhotos.filter(p => !p.tourId && p.privacy !== 'PUBLIC');
    for (const dbPhoto of orphanedDbPhotos) {
      let size = 0;
      if (dbPhoto.imageUrl) {
        const fullPath = path.join(process.cwd(), dbPhoto.imageUrl.replace(/^\//, ''));
        try {
          if (fs.existsSync(fullPath)) {
            size += fs.statSync(fullPath).size;
          }
        } catch (e) {}
      }
      if (dbPhoto.originalUrl) {
        const fullPath = path.join(process.cwd(), dbPhoto.originalUrl.replace(/^\//, ''));
        try {
          if (fs.existsSync(fullPath)) {
            size += fs.statSync(fullPath).size;
          }
        } catch (e) {}
      }

      trashPhotos.push({
        id: dbPhoto.id,
        url: dbPhoto.imageUrl,
        size: size,
        reason: 'Bản ghi ảnh riêng tư không gắn với chuyến đi nào',
        type: 'db_orphaned'
      });
    }

    return trashPhotos;
  }

  @Delete('clean')
  async cleanTrashPhotos() {
    const trashList = await this.getTrashPhotos();
    let deletedCount = 0;
    let freedSpace = 0;

    for (const item of trashList) {
      if (item.type === 'file_only') {
        if (item.filePath && fs.existsSync(item.filePath)) {
          try {
            fs.unlinkSync(item.filePath);
            deletedCount++;
            freedSpace += item.size;
          } catch (e) {}
        }
      } else if (item.type === 'db_orphaned') {
        const photo = await this.prisma.photo.findUnique({ where: { id: item.id } });
        if (photo) {
          if (photo.imageUrl) {
            const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
            if (fs.existsSync(displayFilePath)) {
              try { fs.unlinkSync(displayFilePath); } catch (e) {}
            }
          }
          if (photo.originalUrl) {
            const originalFilePath = path.join(process.cwd(), photo.originalUrl.replace(/^\//, ''));
            if (fs.existsSync(originalFilePath)) {
              try { fs.unlinkSync(originalFilePath); } catch (e) {}
            }
          }
          try {
            await this.prisma.photo.delete({ where: { id: item.id } });
            deletedCount++;
            freedSpace += item.size;
          } catch (e) {}
        }
      }
    }

    return {
      success: true,
      message: `Đã dọn dẹp thành công. Đã xóa ${deletedCount} mục, giải phóng ${(freedSpace / (1024 * 1024)).toFixed(2)} MB.`,
      deletedCount,
      freedSpace
    };
  }

  @Delete('delete-single')
  async deleteSingleTrash(@Body() body: { type: 'file_only' | 'db_orphaned'; filePath?: string; id?: string }) {
    const { type, filePath, id } = body;
    
    if (type === 'file_only') {
      if (!filePath) throw new BadRequestException('Đường dẫn tệp tin không hợp lệ');
      const resolvedPath = path.resolve(filePath);
      if (!resolvedPath.startsWith(UPLOAD_ROOT)) {
        throw new ForbiddenException('Không được phép xóa tệp tin ngoài thư mục uploads');
      }

      if (fs.existsSync(resolvedPath)) {
        fs.unlinkSync(resolvedPath);
        return { success: true, message: 'Đã xóa tệp tin thành công' };
      } else {
        throw new NotFoundException('Tệp tin không tồn tại');
      }
    } else if (type === 'db_orphaned') {
      if (!id) throw new BadRequestException('ID bản ghi không hợp lệ');
      const photo = await this.prisma.photo.findUnique({ where: { id } });
      if (!photo) throw new NotFoundException('Bản ghi không tồn tại trong cơ sở dữ liệu');

      if (photo.imageUrl) {
        const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
        if (fs.existsSync(displayFilePath)) {
          try { fs.unlinkSync(displayFilePath); } catch (e) {}
        }
      }
      if (photo.originalUrl) {
        const originalFilePath = path.join(process.cwd(), photo.originalUrl.replace(/^\//, ''));
        if (fs.existsSync(originalFilePath)) {
          try { fs.unlinkSync(originalFilePath); } catch (e) {}
        }
      }

      await this.prisma.photo.delete({ where: { id } });
      return { success: true, message: 'Đã xóa bản ghi và các tệp liên quan thành công' };
    }

    throw new BadRequestException('Yêu cầu không hợp lệ');
  }
}

@Controller('public-photos')
class PublicPhotoController {
  constructor(
    private prisma: PrismaService,
    private commentGateway: CommentGateway
  ) {}

  @Get()
  async getPublicPhotos() {
    return this.prisma.photo.findMany({
      where: { privacy: 'PUBLIC', isDeleted: false },
      select: {
        id: true,
        imageUrl: true,
        originalUrl: true,
        capturedAt: true,
        metadata: true,
        uploaderId: true,
        uploader: {
          select: {
            id: true,
            name: true,
          }
        }
      },
      orderBy: { capturedAt: 'desc' }
    });
  }

  @Get(':photoId/comments')
  async getPhotoComments(@Param('photoId', ParseUUIDPipe) photoId: string) {
    return this.prisma.comment.findMany({
      where: { photoId },
      include: {
        user: {
          select: { id: true, name: true }
        }
      },
      orderBy: { createdAt: 'asc' }
    });
  }

  @UseGuards(JwtAuthGuard)
  @Post(':photoId/comments')
  async addPhotoComment(
    @Param('photoId', ParseUUIDPipe) photoId: string,
    @Body() body: { content: string },
    @Req() req: any
  ) {
    const { content } = body;
    if (!content || content.trim() === '') {
      throw new BadRequestException('Nội dung bình luận không được để trống.');
    }

    const photo = await this.prisma.photo.findUnique({
      where: { id: photoId }
    });
    if (!photo || photo.isDeleted) {
      throw new NotFoundException('Không tìm thấy ảnh.');
    }

    const filteredContent = await filterText(this.prisma, content.trim());
    const comment = await this.prisma.comment.create({
      data: {
        content: filteredContent,
        photoId,
        userId: req.user.id
      },
      include: {
        user: {
          select: { id: true, name: true }
        }
      }
    });

    // Phát tín hiệu WebSocket cho các client đang xem ảnh này
    this.commentGateway.notifyNewPhotoComment(photoId, comment);

    return comment;
  }

  @UseGuards(JwtAuthGuard)
  @Delete('comments/:id')
  async deletePhotoComment(@Param('id', ParseUUIDPipe) id: string, @Req() req: any) {
    const comment = await this.prisma.comment.findUnique({
      where: { id },
      include: { photo: true }
    });

    if (!comment) {
      throw new NotFoundException('Không tìm thấy bình luận.');
    }

    const isAuthorized = req.user.isAdmin || 
      comment.userId === req.user.id || 
      (comment.photo && comment.photo.uploaderId === req.user.id);

    if (!isAuthorized) {
      throw new ForbiddenException('Bạn không có quyền xóa bình luận này.');
    }

    await this.prisma.comment.delete({ where: { id } });

    if (comment.photoId) {
      this.commentGateway.server.to(`photo_${comment.photoId}`).emit('photoCommentDeleted', { id, photoId: comment.photoId });
    }

    return { success: true, message: 'Đã xóa bình luận thành công.' };
  }

  @Get(':photoId/share')
  async sharePhoto(
    @Param('photoId', ParseUUIDPipe) photoId: string,
    @Req() req: any,
    @Res() res: any
  ) {
    const photo = await this.prisma.photo.findUnique({
      where: { id: photoId }
    });

    if (!photo || photo.isDeleted) {
      throw new NotFoundException('Không tìm thấy ảnh.');
    }

    let host = req.headers['x-forwarded-host'] || req.headers.host || '';
    
    // Nếu host là localhost/127.0.0.1 nhưng request có header proxy (nghĩa là đang chạy qua Nginx proxy ở production)
    const isProxied = !!(req.headers['x-forwarded-proto'] || req.headers['x-forwarded-for']);
    const isLocalhost = host.includes('localhost') || host.includes('127.0.0.1');
    if (isLocalhost && isProxied) {
      host = 'yotrip.labz.io.vn';
    }

    const isLocal = host.includes('localhost') || host.includes('127.0.0.1') || host.startsWith('192.168.') || host.startsWith('10.');
    const protocol = isLocal ? (req.headers['x-forwarded-proto'] || 'http') : 'https';
    const baseUrl = `${protocol}://${host}`;

    // Lấy thông tin metadata
    let title = 'YoTrip - Xem ảnh công khai';
    let description = 'Xem hình ảnh chia sẻ công khai trên bản đồ hành trình YoTrip.';
    if (photo.metadata && typeof photo.metadata === 'object') {
      const meta = photo.metadata as any;
      if (meta.title && meta.title.trim()) {
        title = meta.title;
      }
      if (meta.description && meta.description.trim()) {
        description = meta.description;
      }
    }

    // Xây dựng đường dẫn ảnh tuyệt đối để Facebook hiển thị thumbnail
    const fullImageUrl = photo.imageUrl
      ? (photo.imageUrl.startsWith('http') ? photo.imageUrl : `${baseUrl}${photo.imageUrl}`)
      : '';

    const shareUrl = `${baseUrl}/api/v1/public-photos/${photoId}/share`;
    const redirectUrl = `${baseUrl}/?photoId=${photoId}`;

    // Lấy kích thước thực tế của ảnh bằng sharp để Facebook hiển thị chuẩn xác không cần chờ xử lý bất đồng bộ
    let imageWidth = 1200;
    let imageHeight = 630;
    try {
      if (photo.imageUrl) {
        const localImagePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
        if (fs.existsSync(localImagePath)) {
          const imageMeta = await sharp(localImagePath).metadata();
          if (imageMeta.width && imageMeta.height) {
            imageWidth = imageMeta.width;
            imageHeight = imageMeta.height;
          }
        }
      }
    } catch (err) {
      console.warn('[Share] Không thể lấy kích thước ảnh:', err.message);
    }

    const htmlContent = `<!DOCTYPE html>
<html lang="vi">
<head>
  <meta charset="UTF-8">
  <title>${title}</title>
  <meta name="description" content="${description}">
  
  <!-- Open Graph / Facebook -->
  <meta property="og:site_name" content="YoTrip">
  <meta property="og:locale" content="vi_VN">
  <meta property="og:type" content="website">
  <meta property="og:url" content="${shareUrl}">
  <meta property="og:title" content="${title}">
  <meta property="og:description" content="${description}">
  <meta property="og:image" content="${fullImageUrl}">
  <meta property="og:image:secure_url" content="${fullImageUrl}">
  <meta property="og:image:type" content="image/jpeg">
  <meta property="og:image:width" content="${imageWidth}">
  <meta property="og:image:height" content="${imageHeight}">
  
  <!-- Twitter -->
  <meta property="twitter:card" content="summary_large_image">
  <meta property="twitter:url" content="${shareUrl}">
  <meta property="twitter:title" content="${title}">
  <meta property="twitter:description" content="${description}">
  <meta property="twitter:image" content="${fullImageUrl}">

  <!-- Tự động chuyển hướng người dùng sang frontend chi tiết -->
  <script>
    window.location.href = "${redirectUrl}";
  </script>
</head>
<body>
  <div style="font-family: sans-serif; text-align: center; margin-top: 100px; color: #334155;">
    <h2>Đang chuyển hướng bạn đến YoTrip...</h2>
    <p>Nếu trang không tự động tải, <a href="${redirectUrl}">nhấn vào đây</a>.</p>
  </div>
</body>
</html>`;

    res.type('text/html').send(htmlContent);
  }

  @UseGuards(JwtAuthGuard)
  @Post(':photoId/flag')
  async flagPhoto(
    @Param('photoId', ParseUUIDPipe) photoId: string,
    @Body() body: { reason: string },
    @Req() req: any
  ) {
    const photo = await this.prisma.photo.findUnique({
      where: { id: photoId }
    });
    if (!photo) {
      throw new NotFoundException('Ảnh không tồn tại');
    }

    // Update photo as flagged
    const updatedPhoto = await this.prisma.photo.update({
      where: { id: photoId },
      data: {
        isFlagged: true,
        flaggedReason: body.reason || 'Không xác định',
        flaggedAt: new Date()
      }
    });

    return { success: true, message: 'Đã báo cáo ảnh thành công', photo: updatedPhoto };
  }
}

@UseGuards(JwtAuthGuard)
@Controller('connections')
class ConnectionController {
  constructor(
    private prisma: PrismaService,
    private commentGateway: CommentGateway
  ) {}

  @Get()
  async getConnections(@Req() req: any) {
    const currentUserId = req.user.id;
    // Get connections where status is ACCEPTED
    const connections = await this.prisma.userConnection.findMany({
      where: {
        OR: [
          { requesterId: currentUserId },
          { receiverId: currentUserId }
        ],
        status: 'ACCEPTED'
      },
      include: {
        requester: { select: { id: true, name: true, email: true, avatar: true } },
        receiver: { select: { id: true, name: true, email: true, avatar: true } }
      }
    });

    // Format connections to return the target user and relation details
    const formattedConnections = connections.map(conn => {
      const isRequester = conn.requesterId === currentUserId;
      const targetUser = isRequester ? conn.receiver : conn.requester;
      return {
        id: conn.id,
        targetUser,
        type: conn.type, // FRIEND / FAMILY
        status: conn.status,
        createdAt: conn.createdAt
      };
    });

    // Get pending requests received by current user
    const receivedRequests = await this.prisma.userConnection.findMany({
      where: {
        receiverId: currentUserId,
        status: 'PENDING'
      },
      include: {
        requester: { select: { id: true, name: true, email: true, avatar: true } }
      }
    });

    // Get pending requests sent by current user
    const sentRequests = await this.prisma.userConnection.findMany({
      where: {
        requesterId: currentUserId,
        status: 'PENDING'
      },
      include: {
        receiver: { select: { id: true, name: true, email: true, avatar: true } }
      }
    });

    return {
      connections: formattedConnections,
      receivedRequests: receivedRequests.map(r => ({ id: r.id, requester: r.requester, type: r.type, createdAt: r.createdAt })),
      sentRequests: sentRequests.map(r => ({ id: r.id, receiver: r.receiver, type: r.type, createdAt: r.createdAt }))
    };
  }

  @Post()
  async sendRequest(@Req() req: any, @Body() body: { receiverId: string }) {
    const requesterId = req.user.id;
    const { receiverId } = body;

    if (requesterId === receiverId) {
      throw new BadRequestException('Bạn không thể gửi lời mời kết nối cho chính mình.');
    }

    const receiver = await this.prisma.user.findUnique({ where: { id: receiverId } });
    if (!receiver) {
      throw new NotFoundException('Không tìm thấy người nhận.');
    }

    // Check if relation already exists
    const existing = await this.prisma.userConnection.findFirst({
      where: {
        OR: [
          { requesterId, receiverId },
          { requesterId: receiverId, receiverId }
        ]
      }
    });

    if (existing) {
      if (existing.status === 'ACCEPTED') {
        throw new BadRequestException('Hai bạn đã kết nối với nhau.');
      }
      if (existing.status === 'PENDING') {
        throw new BadRequestException('Yêu cầu kết nối đang chờ xử lý.');
      }
      // If rejected, allow sending request again by deleting/updating the old one
      await this.prisma.userConnection.delete({ where: { id: existing.id } });
    }

    const newConn = await this.prisma.userConnection.create({
      data: {
        requesterId,
        receiverId,
        status: 'PENDING',
        type: 'FRIEND' // default
      }
    });

    return { success: true, connection: newConn };
  }

  @Patch(':id')
  async updateRequest(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() body: { status?: 'ACCEPTED' | 'REJECTED'; type?: 'FRIEND' | 'FAMILY' },
    @Req() req: any
  ) {
    const currentUserId = req.user.id;
    const conn = await this.prisma.userConnection.findUnique({ where: { id } });
    if (!conn) {
      throw new NotFoundException('Không tìm thấy bản ghi kết nối.');
    }

    // Accept/Reject request
    if (body.status) {
      if (conn.receiverId !== currentUserId) {
        throw new ForbiddenException('Bạn không có quyền thực hiện hành động này.');
      }

      const updated = await this.prisma.userConnection.update({
        where: { id },
        data: { status: body.status }
      });

      if (body.status === 'ACCEPTED') {
        const receiverUser = await this.prisma.user.findUnique({
          where: { id: currentUserId },
          select: { name: true }
        });
        this.commentGateway.notifyConnectionAccepted(conn.requesterId, {
          connectionId: conn.id,
          acceptedByName: receiverUser?.name || 'Ai đó',
          acceptedById: currentUserId
        });
      }

      return { success: true, connection: updated };
    }

    // Change relationship type (FRIEND <-> FAMILY)
    if (body.type) {
      if (conn.requesterId !== currentUserId && conn.receiverId !== currentUserId) {
        throw new ForbiddenException('Bạn không có quyền thực hiện hành động này.');
      }
      if (conn.status !== 'ACCEPTED') {
        throw new BadRequestException('Chỉ có thể thay đổi phân nhóm sau khi đã chấp nhận kết nối.');
      }

      const updated = await this.prisma.userConnection.update({
        where: { id },
        data: { type: body.type }
      });
      return { success: true, connection: updated };
    }

    throw new BadRequestException('Yêu cầu không hợp lệ.');
  }

  @Delete(':id')
  async deleteConnection(@Param('id', ParseUUIDPipe) id: string, @Req() req: any) {
    const currentUserId = req.user.id;
    const conn = await this.prisma.userConnection.findUnique({ where: { id } });
    if (!conn) {
      throw new NotFoundException('Không tìm thấy bản ghi kết nối.');
    }

    if (conn.requesterId !== currentUserId && conn.receiverId !== currentUserId) {
      throw new ForbiddenException('Bạn không có quyền thực hiện hành động này.');
    }

    await this.prisma.userConnection.delete({ where: { id } });
    return { success: true, message: 'Đã hủy kết nối thành công.' };
  }
}

@UseGuards(JwtAuthGuard)
@Controller('messages')
class DirectMessageController {
  constructor(
    private prisma: PrismaService,
    private commentGateway: CommentGateway
  ) {}

  @Get(':userId')
  async getMessages(@Param('userId', ParseUUIDPipe) userId: string, @Req() req: any) {
    const currentUserId = req.user.id;

    // Check connection first to ensure they are connected
    const conn = await this.prisma.userConnection.findFirst({
      where: {
        OR: [
          { requesterId: currentUserId, receiverId: userId },
          { requesterId: userId, receiverId: currentUserId }
        ],
        status: 'ACCEPTED'
      }
    });

    if (!conn) {
      throw new ForbiddenException('Bạn chỉ có thể nhắn tin với người đã kết nối.');
    }

    const messages = await this.prisma.directMessage.findMany({
      where: {
        OR: [
          { senderId: currentUserId, receiverId: userId },
          { senderId: userId, receiverId: currentUserId }
        ]
      },
      orderBy: { createdAt: 'asc' }
    });

    return messages;
  }

  @Post()
  async sendMessage(
    @Req() req: any, 
    @Body() body: { receiverId: string; content?: string; attachmentUrl?: string; latitude?: number; longitude?: number }
  ) {
    const senderId = req.user.id;
    const { receiverId, content, attachmentUrl, latitude, longitude } = body;

    if ((!content || !content.trim()) && !attachmentUrl && latitude === undefined) {
      throw new BadRequestException('Tin nhắn phải có nội dung, ảnh đính kèm hoặc vị trí.');
    }

    // Check connection first
    const conn = await this.prisma.userConnection.findFirst({
      where: {
        OR: [
          { requesterId: senderId, receiverId: receiverId },
          { requesterId: receiverId, receiverId: senderId }
        ],
        status: 'ACCEPTED'
      }
    });

    if (!conn) {
      throw new ForbiddenException('Bạn chỉ có thể nhắn tin với người đã kết nối.');
    }

    const filteredContent = content ? await filterText(this.prisma, content) : '';
    const message = await this.prisma.directMessage.create({
      data: {
        senderId,
        receiverId,
        content: filteredContent,
        attachmentUrl,
        latitude,
        longitude
      },
      include: {
        sender: { select: { id: true, name: true, avatar: true } }
      }
    });

    // Realtime broadcast via Socket
    this.commentGateway.notifyNewMessage(receiverId, message);

    return message;
  }
}

@UseGuards(JwtAuthGuard)
@Controller('upload')
class UploadController {
  @Post()
  @UseInterceptors(FilesInterceptor('image', 1))
  async uploadAttachment(@UploadedFiles() files: any[]) {
    if (!files || files.length === 0) {
      throw new BadRequestException('Vui lòng chọn ảnh.');
    }
    const file = files[0];
    const attachmentsDir = path.join(UPLOAD_ROOT, 'attachments');
    if (!fs.existsSync(attachmentsDir)) {
      fs.mkdirSync(attachmentsDir, { recursive: true });
    }
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
    const extension = path.extname(file.originalname).toLowerCase() || '.jpg';
    const filename = `${uniqueSuffix}${extension}`;
    const filePath = path.join(attachmentsDir, filename);

    fs.writeFileSync(filePath, file.buffer);

    return {
      url: `/uploads/attachments/${filename}`
    };
  }
}

@UseGuards(JwtAuthGuard)
@Controller('tours/:tourId/messages')
class TourMessageController {
  constructor(
    private prisma: PrismaService,
    private commentGateway: CommentGateway
  ) {}

  @Get()
  async getTourMessages(@Param('tourId', ParseUUIDPipe) tourId: string, @Req() req: any) {
    const userId = req.user.id;

    // Check if user is a participant of this tour
    const participant = await this.prisma.tourParticipant.findFirst({
      where: { tourId, userId }
    });

    if (!participant) {
      throw new ForbiddenException('Bạn không phải là thành viên của hành trình này.');
    }

    const messages = await this.prisma.tourMessage.findMany({
      where: { tourId },
      include: {
        sender: { select: { id: true, name: true, avatar: true } }
      },
      orderBy: { createdAt: 'asc' }
    });

    return messages;
  }

  @Post()
  async sendTourMessage(
    @Param('tourId', ParseUUIDPipe) tourId: string,
    @Req() req: any,
    @Body() body: { content?: string; attachmentUrl?: string; latitude?: number; longitude?: number; taggedUserIds?: string[] }
  ) {
    const senderId = req.user.id;
    const { content, attachmentUrl, latitude, longitude, taggedUserIds } = body;

    console.log(`[sendTourMessage] senderId=${senderId}, tourId=${tourId}, taggedUserIds=`, taggedUserIds);

    if ((!content || !content.trim()) && !attachmentUrl && latitude === undefined) {
      throw new BadRequestException('Tin nhắn phải có nội dung, ảnh đính kèm hoặc vị trí.');
    }

    // Check if user is a participant of this tour
    const participant = await this.prisma.tourParticipant.findFirst({
      where: { tourId, userId: senderId }
    });

    if (!participant) {
      throw new ForbiddenException('Bạn không phải là thành viên của hành trình này.');
    }

    const filteredContent = content ? await filterText(this.prisma, content) : '';
    const tourMessage = await this.prisma.tourMessage.create({
      data: {
        tourId,
        senderId,
        content: filteredContent,
        attachmentUrl,
        latitude,
        longitude
      },
      include: {
        sender: { select: { id: true, name: true, avatar: true } }
      }
    });

    // Broadcast to the WebSocket room of this tour
    this.commentGateway.server.to(`tour_${tourId}`).emit('tourMessageReceived', {
      tourId,
      message: tourMessage
    });

    const hasTags = Array.isArray(taggedUserIds) && taggedUserIds.length > 0;

    // Notify other participants (via their user rooms)
    // If taggedUserIds is provided, we ONLY notify those tagged users.
    const otherParticipants = await this.prisma.tourParticipant.findMany({
      where: {
        tourId,
        userId: { 
          not: senderId,
          in: hasTags ? taggedUserIds : undefined
        },
        NOT: { userId: null }
      }
    });

    console.log(`[sendTourMessage] otherParticipants found count: ${otherParticipants.length}`, otherParticipants.map(o => o.userId));

    const tour = await this.prisma.tour.findUnique({
      where: { id: tourId },
      select: { title: true }
    });

    for (const p of otherParticipants) {
      if (p.userId) {
        const filteredContent = content ? await filterText(this.prisma, content) : '';
        const isTagged = hasTags && taggedUserIds.includes(p.userId);
        console.log(`[sendTourMessage] Emitting tourMessageNotification to user_${p.userId}, isTagged: ${isTagged}`);
        this.commentGateway.server.to(`user_${p.userId}`).emit('tourMessageNotification', {
          tourId,
          senderName: req.user.name || 'Thành viên',
          tourTitle: tour?.title || 'Hành trình',
          content: isTagged ? `@Bạn: ${filteredContent || ''}` : filteredContent || (attachmentUrl ? '[Hình ảnh]' : '[Vị trí]'),
          isTagged
        });
      }
    }

    return tourMessage;
  }
}

async function filterText(prisma: PrismaService, text: string): Promise<string> {
  if (!text) return text;
  try {
    const filters = await prisma.wordFilter.findMany();
    let result = text;
    for (const filter of filters) {
      const escapedWord = filter.word.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
      const regex = new RegExp(escapedWord, 'gi');
      result = result.replace(regex, filter.replacement);
    }
    return result;
  } catch (err) {
    console.error('Lỗi khi lọc từ cấm:', err);
    return text;
  }
}

@Controller('moderation')
class ModerationController {
  constructor(private prisma: PrismaService) {}

  @Get('settings')
  async getSettings() {
    let settings = await this.prisma.moderationSetting.findFirst();
    if (!settings) {
      settings = await this.prisma.moderationSetting.create({
        data: { blockNsfw: false, blurFaces: false }
      });
    }
    return settings;
  }
}

@Controller('admin/moderation')
@UseGuards(JwtAuthGuard, AdminGuard)
class AdminModerationController {
  constructor(private prisma: PrismaService) {}

  @Get()
  async getSettings() {
    let settings = await this.prisma.moderationSetting.findFirst();
    if (!settings) {
      settings = await this.prisma.moderationSetting.create({
        data: { blockNsfw: false, blurFaces: false }
      });
    }
    return settings;
  }

  @Post()
  async updateSettings(@Body() body: { blockNsfw?: boolean, blurFaces?: boolean }) {
    let settings = await this.prisma.moderationSetting.findFirst();
    if (!settings) {
      return this.prisma.moderationSetting.create({
        data: {
          blockNsfw: body.blockNsfw ?? false,
          blurFaces: body.blurFaces ?? false
        }
      });
    }
    return this.prisma.moderationSetting.update({
      where: { id: settings.id },
      data: {
        blockNsfw: body.blockNsfw,
        blurFaces: body.blurFaces
      }
    });
  }

  @Get('word-filters')
  async getWordFilters() {
    return this.prisma.wordFilter.findMany({
      orderBy: { createdAt: 'desc' }
    });
  }

  @Post('word-filters')
  async addWordFilter(@Body() body: { word: string, replacement: string }) {
    const { word, replacement } = body;
    if (!word || replacement === undefined) {
      throw new BadRequestException('Từ khóa và từ thay thế không được để trống.');
    }
    return this.prisma.wordFilter.create({
      data: {
        word: word.trim(),
        replacement: replacement.trim()
      }
    });
  }

  @Delete('word-filters/:id')
  async deleteWordFilter(@Param('id', ParseUUIDPipe) id: string) {
    await this.prisma.wordFilter.delete({ where: { id } });
    return { success: true };
  }
}

@Controller('admin/photos')
@UseGuards(JwtAuthGuard, AdminGuard)
class AdminPhotosController {
  constructor(private prisma: PrismaService) {}

  @Get('flagged')
  async getFlaggedPhotos() {
    return this.prisma.photo.findMany({
      where: { isFlagged: true },
      include: {
        uploader: { select: { id: true, name: true, email: true } },
        tour: { select: { id: true, title: true } }
      },
      orderBy: { flaggedAt: 'desc' }
    });
  }

  @Post(':photoId/approve')
  async approvePhoto(@Param('photoId', ParseUUIDPipe) photoId: string) {
    const photo = await this.prisma.photo.findUnique({
      where: { id: photoId }
    });
    if (!photo) {
      throw new NotFoundException('Ảnh không tồn tại');
    }

    // Unflag the photo
    const updatedPhoto = await this.prisma.photo.update({
      where: { id: photoId },
      data: {
        isFlagged: false,
        flaggedReason: null,
        flaggedAt: null
      }
    });

    return { success: true, message: 'Đã duyệt ảnh thành công', photo: updatedPhoto };
  }

  @Delete(':photoId')
  async deletePhoto(@Param('photoId', ParseUUIDPipe) photoId: string) {
    const photo = await this.prisma.photo.findUnique({
      where: { id: photoId }
    });
    if (!photo) {
      throw new NotFoundException('Ảnh không tồn tại');
    }

    // Soft delete the photo
    const deletedPhoto = await this.prisma.photo.update({
      where: { id: photoId },
      data: {
        isDeleted: true,
        deletedAt: new Date()
      }
    });

    return { success: true, message: 'Đã xóa ảnh thành công', photo: deletedPhoto };
  }
}

@Controller('reports')
class ReportsController {
  constructor(private prisma: PrismaService) {}

  @Post()
  async createReport(@Body() body: any) {
    const { type, name, phone, email, address, latitude, longitude, reason } = body;
    if (!type || !name || !reason) {
      throw new BadRequestException('Loại báo cáo, tên cơ sở và lý do không được để trống.');
    }

    const lat = latitude ? parseFloat(latitude) : null;
    const lng = longitude ? parseFloat(longitude) : null;

    return this.prisma.businessReport.create({
      data: {
        type: type.trim(),
        name: name.trim(),
        phone: phone ? phone.trim() : null,
        email: email ? email.trim() : null,
        address: address ? address.trim() : null,
        latitude: lat,
        longitude: lng,
        reason: reason.trim(),
        isBlacklisted: false,
      }
    });
  }

  @Get('blacklist')
  async getBlacklist() {
    return this.prisma.businessReport.findMany({
      where: { isBlacklisted: true },
      orderBy: { createdAt: 'desc' }
    });
  }
}

@Controller('admin/reports')
@UseGuards(JwtAuthGuard, AdminGuard)
class AdminReportsController {
  constructor(private prisma: PrismaService) {}

  @Get()
  async getAllReports() {
    return this.prisma.businessReport.findMany({
      orderBy: { createdAt: 'desc' }
    });
  }

  @Patch(':id/blacklist')
  async toggleBlacklist(@Param('id', ParseUUIDPipe) id: string, @Body() body: { isBlacklisted: boolean }) {
    const report = await this.prisma.businessReport.findUnique({ where: { id } });
    if (!report) {
      throw new NotFoundException('Không tìm thấy báo cáo.');
    }
    return this.prisma.businessReport.update({
      where: { id },
      data: { isBlacklisted: body.isBlacklisted }
    });
  }

  @Delete(':id')
  async deleteReport(@Param('id', ParseUUIDPipe) id: string) {
    const report = await this.prisma.businessReport.findUnique({ where: { id } });
    if (!report) {
      throw new NotFoundException('Không tìm thấy báo cáo.');
    }
    await this.prisma.businessReport.delete({ where: { id } });
    return { success: true };
  }
}

@Controller('users/trusted')
class TrustedUsersController {
  constructor(private prisma: PrismaService) {}

  @Get()
  async getTrustedUsers() {
    const ratings = await this.prisma.tourRating.groupBy({
      by: ['targetUserId'],
      _avg: { averageScore: true },
      _count: { id: true }
    });
    
    const users = await this.prisma.user.findMany({
      where: { id: { in: ratings.map(r => r.targetUserId) } },
      select: { id: true, name: true, avatar: true }
    });

    const result = ratings.map(r => {
      const u = users.find(user => user.id === r.targetUserId);
      return {
        id: r.targetUserId,
        name: u?.name || 'Ẩn danh',
        avatar: u?.avatar || null,
        averageScore: Math.round((r._avg.averageScore || 0) * 10) / 10,
        ratingCount: r._count.id
      };
    });

    return result.sort((a, b) => b.averageScore - a.averageScore || b.ratingCount - a.ratingCount);
  }
}

@Controller('tours/:tourId/ratings')
@UseGuards(JwtAuthGuard)
class TourRatingController {
  constructor(private prisma: PrismaService) {}

  @Get()
  async getTourRatings(@Param('tourId', ParseUUIDPipe) tourId: string) {
    return this.prisma.tourRating.findMany({
      where: { tourId },
      include: {
        raterUser: { select: { id: true, name: true, avatar: true } },
        targetUser: { select: { id: true, name: true, avatar: true } }
      },
      orderBy: { createdAt: 'desc' }
    });
  }

  @Post()
  async createTourRating(
    @Param('tourId', ParseUUIDPipe) tourId: string,
    @Body() body: {
      targetUserId: string;
      honesty: number;
      transparency: number;
      enthusiasm: number;
      cheerfulness: number;
      seriousness: number;
      planning: number;
      survival: number;
      comment?: string;
    },
    @Req() req: any
  ) {
    const raterUserId = req.user.id;
    const { targetUserId, honesty, transparency, enthusiasm, cheerfulness, seriousness, planning, survival, comment } = body;

    if (!targetUserId || honesty < 1 || honesty > 5 || transparency < 1 || transparency > 5 || enthusiasm < 1 || enthusiasm > 5 || cheerfulness < 1 || cheerfulness > 5 || seriousness < 1 || seriousness > 5 || planning < 1 || planning > 5 || survival < 1 || survival > 5) {
      throw new BadRequestException('Dữ liệu đánh giá không hợp lệ.');
    }

    const participant = await this.prisma.tourParticipant.findFirst({
      where: { tourId, userId: raterUserId }
    });
    if (!participant) {
      throw new ForbiddenException('Bạn không phải là thành viên của tour này.');
    }

    const targetParticipant = await this.prisma.tourParticipant.findFirst({
      where: { 
        tourId, 
        userId: targetUserId,
        role: { in: ['OWNER', 'MANAGER'] }
      }
    });
    if (!targetParticipant) {
      throw new BadRequestException('Chỉ có thể đánh giá người tạo tour hoặc người quản lý.');
    }

    const averageScore = (honesty + transparency + enthusiasm + cheerfulness + seriousness + planning + survival) / 7;

    const existingRating = await this.prisma.tourRating.findUnique({
      where: {
        tourId_targetUserId_raterUserId: {
          tourId,
          targetUserId,
          raterUserId
        }
      }
    });

    if (existingRating) {
      return this.prisma.tourRating.update({
        where: { id: existingRating.id },
        data: {
          honesty,
          transparency,
          enthusiasm,
          cheerfulness,
          seriousness,
          planning,
          survival,
          averageScore,
          comment,
          createdAt: new Date()
        }
      });
    }

    return this.prisma.tourRating.create({
      data: {
        tourId,
        targetUserId,
        raterUserId,
        honesty,
        transparency,
        enthusiasm,
        cheerfulness,
        seriousness,
        planning,
        survival,
        averageScore,
        comment
      }
    });
  }
}

@Controller('tours')
class PublicTourShareController {
  constructor(private prisma: PrismaService) {}

  @Get('share/:token')
  async getSharedJourney(@Param('token') token: string) {
    const tourShare = await this.prisma.tourShare.findUnique({
      where: { token, isEnabled: true },
      include: {
        tour: {
          include: {
            creator: { select: { id: true, name: true, phone: true } },
            participants: {
              select: {
                id: true,
                role: true,
                displayName: true,
                user: { select: { id: true, name: true, phone: true } }
              }
            },
            legs: {
              include: {
                locations: {
                  orderBy: { plannedStart: 'asc' }
                }
              },
              orderBy: { sequence: 'asc' }
            }
          }
        }
      }
    });

    if (!tourShare || !tourShare.tour) {
      throw new NotFoundException('Không tìm thấy hành trình chia sẻ hoặc liên kết đã bị vô hiệu hóa.');
    }

    return {
      tour: tourShare.tour,
      isEnabled: tourShare.isEnabled
    };
  }
}

@Controller('tours/:tourId/share')
@UseGuards(JwtAuthGuard)
class TourShareController {
  constructor(private prisma: PrismaService) {}

  @Get()
  async getShareStatus(@Param('tourId', ParseUUIDPipe) tourId: string) {
    let share = await this.prisma.tourShare.findUnique({
      where: { tourId }
    });
    if (!share) {
      share = await this.prisma.tourShare.create({
        data: { tourId, isEnabled: false }
      });
    }
    return share;
  }

  @Post()
  async toggleShare(
    @Param('tourId', ParseUUIDPipe) tourId: string,
    @Body() body: { isEnabled: boolean }
  ) {
    let share = await this.prisma.tourShare.findUnique({
      where: { tourId }
    });
    if (!share) {
      return this.prisma.tourShare.create({
        data: {
          tourId,
          isEnabled: body.isEnabled
        }
      });
    }
    return this.prisma.tourShare.update({
      where: { id: share.id },
      data: { isEnabled: body.isEnabled }
    });
  }
}

// ================= NEW CONTROLLERS & SERVICES =================

@Controller('tours/:tourId/notes')
@UseGuards(JwtAuthGuard, TourRoleGuard)
class TourNoteController {
  constructor(private prisma: PrismaService) {}

  @Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER, ParticipantRole.MEMBER, ParticipantRole.MEMBER_NO_FINANCE)
  @Get()
  async getNotes(@Param('tourId') tourId: string) {
    return this.prisma.tourNote.findMany({
      where: { tourId, isDeleted: false },
      orderBy: { createdAt: 'desc' }
    });
  }

  @Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER, ParticipantRole.MEMBER, ParticipantRole.MEMBER_NO_FINANCE)
  @Post()
  async createNote(
    @Param('tourId') tourId: string,
    @Body() body: { title: string; content: string },
    @Req() req: any
  ) {
    const { title, content } = body;
    const filteredTitle = await filterText(this.prisma, title || 'Ghi chú không tiêu đề');
    const filteredContent = await filterText(this.prisma, content || '');
    return this.prisma.tourNote.create({
      data: {
        tourId,
        userId: req.user.id,
        title: filteredTitle,
        content: filteredContent,
      }
    });
  }

  @Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER, ParticipantRole.MEMBER, ParticipantRole.MEMBER_NO_FINANCE)
  @Put(':noteId')
  async updateNote(
    @Param('tourId') tourId: string,
    @Param('noteId') noteId: string,
    @Body() body: { title?: string; content?: string },
    @Req() req: any
  ) {
    const { title, content } = body;
    const note = await this.prisma.tourNote.findUnique({ where: { id: noteId } });
    if (!note || note.tourId !== tourId || note.isDeleted) {
      throw new NotFoundException('Không tìm thấy ghi chú');
    }
    if (note.userId !== req.user.id && !req.user.isAdmin) {
      throw new ForbiddenException('Bạn không có quyền sửa ghi chú này');
    }

    const updateData: any = {};
    if (title !== undefined) updateData.title = await filterText(this.prisma, title);
    if (content !== undefined) updateData.content = await filterText(this.prisma, content);

    return this.prisma.tourNote.update({
      where: { id: noteId },
      data: updateData
    });
  }

  @Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER, ParticipantRole.MEMBER, ParticipantRole.MEMBER_NO_FINANCE)
  @Delete(':noteId')
  async deleteNote(
    @Param('tourId') tourId: string,
    @Param('noteId') noteId: string,
    @Req() req: any
  ) {
    const note = await this.prisma.tourNote.findUnique({ where: { id: noteId } });
    if (!note || note.tourId !== tourId || note.isDeleted) {
      throw new NotFoundException('Không tìm thấy ghi chú');
    }
    if (note.userId !== req.user.id && !req.user.isAdmin) {
      throw new ForbiddenException('Bạn không có quyền xóa ghi chú này');
    }

    await this.prisma.tourNote.update({
      where: { id: noteId },
      data: { isDeleted: true, deletedAt: new Date() }
    });
    return { success: true };
  }

  @Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER, ParticipantRole.MEMBER, ParticipantRole.MEMBER_NO_FINANCE)
  @Post('insert')
  async insertSection(
    @Param('tourId') tourId: string,
    @Body() body: { legId: string; noteSnippet: string },
    @Req() req: any
  ) {
    const tour = await this.prisma.tour.findUnique({ where: { id: tourId } });
    if (!tour) throw new NotFoundException('Không tìm thấy tour');

    const masterNote = await this.prisma.tourNote.findFirst({
      where: { tourId, title: `Ghi chú: ${tour.title}`, isDeleted: false, userId: req.user.id }
    });

    let note = masterNote;
    if (!note) {
      note = await this.prisma.tourNote.create({
        data: {
          tourId,
          userId: req.user.id,
          title: `Ghi chú: ${tour.title}`,
          content: `# ${tour.title}\n\n## Ghi chú chung\n\n*(Nội dung ghi chú tổng quan của chuyến đi...)*\n\n`
        }
      });
    }

    const leg = await this.prisma.leg.findUnique({ where: { id: body.legId } });
    const stageHeader = leg ? `<h2>Ghi chú: ${leg.note || `Chặng ${leg.sequence}`}</h2>` : '<h2>Ghi chú:</h2>';

    let content = note.content;
    const stageIndex = content.indexOf(stageHeader);

    if (stageIndex === -1) {
      content += `<br><br>${stageHeader}${body.noteSnippet}`;
    } else {
      const nextHeaderIndex = content.indexOf('<h2>', stageIndex + stageHeader.length);
      if (nextHeaderIndex !== -1) {
        content = content.slice(0, nextHeaderIndex) + body.noteSnippet + content.slice(nextHeaderIndex);
      } else {
        content += body.noteSnippet;
      }
    }

    return this.prisma.tourNote.update({
      where: { id: note.id },
      data: { content }
    });
  }
}

@Controller('admin/notes')
@UseGuards(JwtAuthGuard, AdminGuard)
class AdminNoteController {
  constructor(private prisma: PrismaService) {}

  @Get()
  async getAllActiveNotes() {
    return this.prisma.tourNote.findMany({
      where: { isDeleted: false },
      include: {
        tour: { select: { title: true } },
        user: { select: { name: true, email: true } }
      },
      orderBy: { createdAt: 'desc' }
    });
  }

  @Delete(':id')
  async softDeleteNote(@Param('id', ParseUUIDPipe) id: string) {
    await this.prisma.tourNote.update({
      where: { id },
      data: { isDeleted: true, deletedAt: new Date() }
    });
    return { success: true };
  }
}

@Controller('admin/tours')
@UseGuards(JwtAuthGuard, AdminGuard)
class AdminTourController {
  constructor(private prisma: PrismaService) {}

  @Get()
  async getAllActiveTours() {
    return this.prisma.tour.findMany({
      where: { isDeleted: false },
      include: {
        creator: { select: { name: true, email: true } },
        _count: { select: { participants: true } }
      },
      orderBy: { createdAt: 'desc' }
    });
  }

  @Delete(':id')
  async softDeleteTour(@Param('id', ParseUUIDPipe) id: string) {
    await this.prisma.tour.update({
      where: { id },
      data: { isDeleted: true, deletedAt: new Date() }
    });
    await this.prisma.photo.updateMany({
      where: { tourId: id },
      data: { isDeleted: true, deletedAt: new Date() }
    });
    await this.prisma.tourNote.updateMany({
      where: { tourId: id },
      data: { isDeleted: true, deletedAt: new Date() }
    });
    return { success: true };
  }
}

@Controller('recommendations')
class RecommendedLocationController {
  constructor(private prisma: PrismaService) {}

  @Get()
  async getApprovedRecommendations() {
    return this.prisma.recommendedLocation.findMany({
      where: { isApproved: true },
      orderBy: { createdAt: 'desc' }
    });
  }

  @UseGuards(JwtAuthGuard)
  @Post()
  async proposeRecommendation(@Body() body: any) {
    const { type, name, phone, email, address, latitude, longitude, description, stars } = body;
    if (!name || !type || !description) {
      throw new BadRequestException('Vui lòng điền đầy đủ thông tin bắt buộc.');
    }
    const filteredName = await filterText(this.prisma, name);
    const filteredDesc = await filterText(this.prisma, description);
    return this.prisma.recommendedLocation.create({
      data: {
        type,
        name: filteredName,
        phone,
        email,
        address,
        latitude: latitude ? parseFloat(latitude) : null,
        longitude: longitude ? parseFloat(longitude) : null,
        description: filteredDesc,
        stars: stars ? parseInt(stars) : 5
      }
    });
  }
}

@Controller('admin/recommendations')
@UseGuards(JwtAuthGuard, AdminGuard)
class AdminRecommendedLocationController {
  constructor(private prisma: PrismaService) {}

  @Get()
  async getAllRecommendations() {
    return this.prisma.recommendedLocation.findMany({
      orderBy: { createdAt: 'desc' }
    });
  }

  @Patch(':id/approve')
  async approveRecommendation(@Param('id', ParseUUIDPipe) id: string, @Body() body: { isApproved: boolean }) {
    return this.prisma.recommendedLocation.update({
      where: { id },
      data: { isApproved: body.isApproved }
    });
  }

  @Delete(':id')
  async deleteRecommendation(@Param('id', ParseUUIDPipe) id: string) {
    await this.prisma.recommendedLocation.delete({ where: { id } });
    return { success: true };
  }
}

@Controller('admin/trash')
@UseGuards(JwtAuthGuard, AdminGuard)
class AdminTrashController {
  constructor(private prisma: PrismaService) {}

  @Get()
  async getTrashItems() {
    const setting = await this.prisma.moderationSetting.findFirst();
    const retentionDays = setting?.trashRetentionDays ?? 30;

    const tours = await this.prisma.tour.findMany({
      where: { isDeleted: true },
      include: { creator: { select: { name: true } } },
      orderBy: { deletedAt: 'desc' }
    });

    const photos = await this.prisma.photo.findMany({
      where: { isDeleted: true },
      include: { uploader: { select: { name: true } } },
      orderBy: { deletedAt: 'desc' }
    });

    const notes = await this.prisma.tourNote.findMany({
      where: { isDeleted: true },
      include: {
        user: { select: { name: true } },
        tour: { select: { title: true } }
      },
      orderBy: { deletedAt: 'desc' }
    });

    return {
      retentionDays,
      tours,
      photos,
      notes
    };
  }

  @Patch('retention-days')
  async updateRetentionDays(@Body() body: { days: number }) {
    const { days } = body;
    if (days === undefined || days < 1) {
      throw new BadRequestException('Số ngày lưu trữ không hợp lệ.');
    }
    const setting = await this.prisma.moderationSetting.findFirst();
    if (setting) {
      return this.prisma.moderationSetting.update({
        where: { id: setting.id },
        data: { trashRetentionDays: days }
      });
    } else {
      return this.prisma.moderationSetting.create({
        data: { trashRetentionDays: days }
      });
    }
  }

  @Post('restore')
  async restoreItems(@Body() body: { type: 'tour' | 'photo' | 'note'; ids: string[] }) {
    const { type, ids } = body;
    if (!type || !ids || !Array.isArray(ids)) {
      throw new BadRequestException('Tham số không hợp lệ.');
    }

    if (type === 'tour') {
      await this.prisma.tour.updateMany({
        where: { id: { in: ids } },
        data: { isDeleted: false, deletedAt: null }
      });
      await this.prisma.photo.updateMany({
        where: { tourId: { in: ids } },
        data: { isDeleted: false, deletedAt: null }
      });
      await this.prisma.tourNote.updateMany({
        where: { tourId: { in: ids } },
        data: { isDeleted: false, deletedAt: null }
      });
    } else if (type === 'photo') {
      await this.prisma.photo.updateMany({
        where: { id: { in: ids } },
        data: { isDeleted: false, deletedAt: null }
      });
    } else if (type === 'note') {
      await this.prisma.tourNote.updateMany({
        where: { id: { in: ids } },
        data: { isDeleted: false, deletedAt: null }
      });
    }

    return { success: true };
  }

  @Post('delete-permanent')
  async deletePermanentItems(@Body() body: { type: 'tour' | 'photo' | 'note'; ids: string[] }) {
    const { type, ids } = body;
    console.log('[deletePermanent] Request received - type:', type, 'ids count:', ids.length);
    
    if (!type || !ids || !Array.isArray(ids)) {
      throw new BadRequestException('Tham số không hợp lệ.');
    }

    const deleteResults = { success: 0, failed: 0, errors: [] as string[] };

    if (type === 'tour') {
      for (const tourId of ids) {
        try {
          console.log('[deletePermanent] Deleting tour:', tourId);
          
          // First, delete all files from disk
          const photos = await this.prisma.photo.findMany({ where: { tourId } });
          console.log('[deletePermanent] Found', photos.length, 'photos for tour');
          
          for (const p of photos) {
            if (p.imageUrl) {
              const displayFilePath = path.join(process.cwd(), p.imageUrl.replace(/^\//, ''));
              console.log('[deletePermanent] Checking display file:', displayFilePath);
              if (fs.existsSync(displayFilePath)) {
                try { 
                  fs.unlinkSync(displayFilePath);
                  console.log('[deletePermanent] Deleted display file:', displayFilePath);
                } catch (e) {
                  console.error('[deletePermanent] Error deleting display file:', e);
                }
              }
            }
            if (p.originalUrl) {
              const originalFilePath = path.join(process.cwd(), p.originalUrl.replace(/^\//, ''));
              console.log('[deletePermanent] Checking original file:', originalFilePath);
              if (fs.existsSync(originalFilePath)) {
                try { 
                  fs.unlinkSync(originalFilePath);
                  console.log('[deletePermanent] Deleted original file:', originalFilePath);
                } catch (e) {
                  console.error('[deletePermanent] Error deleting original file:', e);
                }
              }
            }
          }
          
          // Delete the tour - cascade will handle related records
          await this.prisma.tour.delete({ where: { id: tourId } });
          console.log('[deletePermanent] Deleted tour from database:', tourId);
          deleteResults.success++;
        } catch (e: any) {
          console.error('[deletePermanent] Error deleting tour:', tourId, e);
          deleteResults.failed++;
          deleteResults.errors.push(`Tour ${tourId}: ${e?.message || 'Unknown error'}`);
        }
      }
    } else if (type === 'photo') {
      console.log('[deletePermanent] Deleting', ids.length, 'photos');
      console.log('[deletePermanent] Photo IDs:', JSON.stringify(ids));
      
      if (!ids || ids.length === 0) {
        console.warn('[deletePermanent] No photo IDs provided');
        return { success: true, deleted: 0, failed: 0 };
      }
      
      for (const photoId of ids) {
        try {
          console.log('[deletePermanent] ========== START Processing photo:', photoId);
          const photo = await this.prisma.photo.findUnique({ where: { id: photoId } });
          
          if (!photo) {
            console.warn('[deletePermanent] Photo not found in DB:', photoId);
            deleteResults.failed++;
            deleteResults.errors.push(`Photo ${photoId}: not found in database`);
            console.log('[deletePermanent] ========== SKIP (not found):', photoId);
            continue;
          }
          
          console.log('[deletePermanent] Found photo:', { id: photo.id, imageUrl: photo.imageUrl, isDeleted: photo.isDeleted, tourId: photo.tourId });
          
          // First, delete all comments for this photo (because of foreign key)
          console.log('[deletePermanent] Deleting comments for photo:', photoId);
          try {
            const commentResult = await this.prisma.comment.deleteMany({
              where: { photoId }
            });
            console.log('[deletePermanent] Deleted', commentResult.count, 'comments for photo');
          } catch (e: any) {
            console.warn('[deletePermanent] Error deleting comments (continuing):', e?.message);
          }
          
          // Delete files from disk
          if (photo.imageUrl) {
            const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
            console.log('[deletePermanent] Checking display file:', displayFilePath);
            if (fs.existsSync(displayFilePath)) {
              try { 
                fs.unlinkSync(displayFilePath);
                console.log('[deletePermanent] ✓ Deleted display file');
              } catch (e) {
                console.error('[deletePermanent] Error deleting display file:', e);
              }
            }
          }
          
          if (photo.originalUrl) {
            const originalFilePath = path.join(process.cwd(), photo.originalUrl.replace(/^\//, ''));
            console.log('[deletePermanent] Checking original file:', originalFilePath);
            if (fs.existsSync(originalFilePath)) {
              try { 
                fs.unlinkSync(originalFilePath);
                console.log('[deletePermanent] ✓ Deleted original file');
              } catch (e) {
                console.error('[deletePermanent] Error deleting original file:', e);
              }
            }
          }
          
          // Delete from database
          console.log('[deletePermanent] Attempting database delete for photo:', photoId);
          const result = await this.prisma.photo.delete({ where: { id: photoId } });
          console.log('[deletePermanent] ✓ Successfully deleted photo from database:', photoId);
          deleteResults.success++;
          console.log('[deletePermanent] ========== SUCCESS:', photoId);
        } catch (e: any) {
          console.error('[deletePermanent] ✗ Error deleting photo:', photoId);
          console.error('[deletePermanent] Error details:', e?.message || String(e));
          deleteResults.failed++;
          deleteResults.errors.push(`Photo ${photoId}: ${e?.message || 'Unknown error'}`);
          console.log('[deletePermanent] ========== FAILED:', photoId);
        }
      }
      
      console.log('[deletePermanent] Photos deletion complete - success:', deleteResults.success, 'failed:', deleteResults.failed);
    } else if (type === 'note') {
      console.log('[deletePermanent] Deleting', ids.length, 'notes');
      try {
        const result = await this.prisma.tourNote.deleteMany({
          where: { id: { in: ids } }
        });
        console.log('[deletePermanent] Deleted notes from database, count:', result.count);
        deleteResults.success = result.count;
      } catch (e: any) {
        console.error('[deletePermanent] Error deleting notes:', e);
        deleteResults.failed = ids.length;
        deleteResults.errors.push(`Notes deletion: ${e?.message || 'Unknown error'}`);
      }
    }

    console.log('[deletePermanent] Deletion complete - success:', deleteResults.success, 'failed:', deleteResults.failed);
    
    if (deleteResults.failed > 0) {
      console.warn('[deletePermanent] Errors:', deleteResults.errors);
    }
    
    return { 
      success: true, 
      deleted: deleteResults.success, 
      failed: deleteResults.failed,
      errors: deleteResults.errors.length > 0 ? deleteResults.errors : undefined
    };
  }

  @Post('empty-all')
  async emptyAllTrash() {
    console.log('[emptyAllTrash] Starting to empty all trash');
    const deleteResults = { success: 0, failed: 0, errors: [] as string[] };

    try {
      // Get all deleted photos
      console.log('[emptyAllTrash] Finding all deleted photos');
      const deletedPhotos = await this.prisma.photo.findMany({
        where: { isDeleted: true }
      });
      console.log('[emptyAllTrash] Found', deletedPhotos.length, 'deleted photos');

      // Delete all comments from deleted photos
      console.log('[emptyAllTrash] Deleting all comments from deleted photos');
      const commentCount = await this.prisma.comment.deleteMany({
        where: {
          photoId: { in: deletedPhotos.map(p => p.id) }
        }
      });
      console.log('[emptyAllTrash] Deleted', commentCount.count, 'comments');

      // Delete files from disk
      for (const photo of deletedPhotos) {
        if (photo.imageUrl) {
          const filePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
          if (fs.existsSync(filePath)) {
            try {
              fs.unlinkSync(filePath);
              console.log('[emptyAllTrash] Deleted display file:', filePath);
            } catch (e) {
              console.error('[emptyAllTrash] Error deleting display file:', e);
            }
          }
        }
        if (photo.originalUrl) {
          const filePath = path.join(process.cwd(), photo.originalUrl.replace(/^\//, ''));
          if (fs.existsSync(filePath)) {
            try {
              fs.unlinkSync(filePath);
              console.log('[emptyAllTrash] Deleted original file:', filePath);
            } catch (e) {
              console.error('[emptyAllTrash] Error deleting original file:', e);
            }
          }
        }
      }

      // Delete all photos
      console.log('[emptyAllTrash] Deleting all', deletedPhotos.length, 'photos from database');
      const photoResult = await this.prisma.photo.deleteMany({
        where: { isDeleted: true }
      });
      console.log('[emptyAllTrash] Deleted', photoResult.count, 'photos');
      deleteResults.success += photoResult.count;

      // Delete all deleted tours (cascade will handle related)
      console.log('[emptyAllTrash] Deleting all deleted tours');
      const tourResult = await this.prisma.tour.deleteMany({
        where: { isDeleted: true }
      });
      console.log('[emptyAllTrash] Deleted', tourResult.count, 'tours');
      deleteResults.success += tourResult.count;

      // Delete all deleted notes
      console.log('[emptyAllTrash] Deleting all deleted notes');
      const noteResult = await this.prisma.tourNote.deleteMany({
        where: { isDeleted: true }
      });
      console.log('[emptyAllTrash] Deleted', noteResult.count, 'notes');
      deleteResults.success += noteResult.count;

      console.log('[emptyAllTrash] Successfully emptied all trash - total deleted:', deleteResults.success);
    } catch (e: any) {
      console.error('[emptyAllTrash] Error emptying trash:', e?.message || e);
      deleteResults.failed++;
      deleteResults.errors.push(`Emptying trash: ${e?.message || 'Unknown error'}`);
    }

    return { 
      success: true, 
      totalDeleted: deleteResults.success, 
      errors: deleteResults.errors.length > 0 ? deleteResults.errors : undefined
    };
  }
}

// Background auto cleanup task
function startAutoCleanup(prisma: PrismaService) {
  console.log('🔄 Đang kích hoạt dịch vụ dọn dẹp thùng rác tự động...');
  
  setInterval(async () => {
    try {
      const setting = await prisma.moderationSetting.findFirst();
      const retentionDays = setting?.trashRetentionDays ?? 30;
      
      const cutoffDate = new Date();
      cutoffDate.setDate(cutoffDate.getDate() - retentionDays);

      console.log(`[Auto Cleanup] Quét dọn các tài nguyên đã bị xóa trước ngày ${cutoffDate.toISOString()}`);

      // 1. Ghi chú rác quá hạn
      const expiredNotes = await prisma.tourNote.findMany({
        where: {
          isDeleted: true,
          deletedAt: { lt: cutoffDate }
        }
      });
      if (expiredNotes.length > 0) {
        const expiredNoteIds = expiredNotes.map(n => n.id);
        await prisma.tourNote.deleteMany({
          where: { id: { in: expiredNoteIds } }
        });
        console.log(`[Auto Cleanup] Đã xóa vĩnh viễn ${expiredNoteIds.length} ghi chú quá hạn.`);
      }

      // 2. Ảnh rác quá hạn
      const expiredPhotos = await prisma.photo.findMany({
        where: {
          isDeleted: true,
          deletedAt: { lt: cutoffDate }
        }
      });
      for (const photo of expiredPhotos) {
        if (photo.imageUrl) {
          const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
          if (fs.existsSync(displayFilePath)) {
            try { fs.unlinkSync(displayFilePath); } catch (e) {}
          }
        }
        if (photo.originalUrl) {
          const originalFilePath = path.join(process.cwd(), photo.originalUrl.replace(/^\//, ''));
          if (fs.existsSync(originalFilePath)) {
            try { fs.unlinkSync(originalFilePath); } catch (e) {}
          }
        }
        await prisma.photo.delete({ where: { id: photo.id } });
      }
      if (expiredPhotos.length > 0) {
        console.log(`[Auto Cleanup] Đã xóa vĩnh viễn ${expiredPhotos.length} hình ảnh quá hạn.`);
      }

      // 3. Tour rác quá hạn
      const expiredTours = await prisma.tour.findMany({
        where: {
          isDeleted: true,
          deletedAt: { lt: cutoffDate }
        }
      });
      for (const tour of expiredTours) {
        const tourPhotos = await prisma.photo.findMany({ where: { tourId: tour.id } });
        for (const photo of tourPhotos) {
          if (photo.imageUrl) {
            const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
            if (fs.existsSync(displayFilePath)) {
              try { fs.unlinkSync(displayFilePath); } catch (e) {}
            }
          }
          if (photo.originalUrl) {
            const originalFilePath = path.join(process.cwd(), photo.originalUrl.replace(/^\//, ''));
            if (fs.existsSync(originalFilePath)) {
              try { fs.unlinkSync(originalFilePath); } catch (e) {}
            }
          }
        }
        await prisma.tour.delete({ where: { id: tour.id } });
      }
      if (expiredTours.length > 0) {
        console.log(`[Auto Cleanup] Đã xóa vĩnh viễn ${expiredTours.length} tour du lịch quá hạn.`);
      }

    } catch (error) {
      console.error('[Auto Cleanup] Lỗi khi dọn dẹp thùng rác:', error);
    }
  }, 24 * 60 * 60 * 1000); // 24 giờ
}

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true, // Giúp ConfigModule có sẵn ở mọi nơi trong ứng dụng
      // Chỉ định đường dẫn tới file .env ở thư mục gốc của dự án
      envFilePath: path.resolve(__dirname, '..', '..', '.env'),
      // Bỏ qua lỗi nếu không tìm thấy file .env (hữu ích cho môi trường production dùng biến hệ thống)
      ignoreEnvFile: process.env.NODE_ENV === 'production',
    }),
    CacheModule.registerAsync({
      isGlobal: true,
      useFactory: async () => ({
        store: await redisStore({
          url: process.env.REDIS_URL || 'redis://localhost:6379',
          ttl: CACHE_TTL.DEFAULT, // Cấu hình TTL mặc định cho toàn bộ store
        }),
      }),
    }),
    JwtModule.register({
      secret: process.env.JWT_SECRET || 'super-secret',
      signOptions: { expiresIn: '1d' },
    }) as any,
  ],
  controllers: [AppController, AuthController, PublicTourController, TourController, UserController, RoutingController, LegController, LocationController, CommentController, PhotoController, PublicPhotoController, AdminOtpController, AdminController, ConnectionController, DirectMessageController, UploadController, TourMessageController, ModerationController, AdminModerationController, TrustedUsersController, TourRatingController, PublicTourShareController, TourShareController, ReportsController, AdminReportsController, TourNoteController, AdminNoteController, AdminTourController, RecommendedLocationController, AdminRecommendedLocationController, AdminTrashController],
  providers: [PrismaService, JwtStrategy, TourRoleGuard, JwtAuthGuard, AdminGuard, CommentGateway, Reflector, EmailService, ConfigService],
  exports: [PrismaService] 
})
class AppModule {}


bootstrap().catch(err => {
  if (err.message.includes('DATABASE_URL')) {
    console.error('❌ Lỗi nghiêm trọng: Biến môi trường DATABASE_URL không được tải. Hãy chắc chắn rằng file .env tồn tại ở thư mục gốc của dự án và chứa giá trị này.');
  }
  console.error('💥 Lỗi khởi động Server:');
  console.error(err);
  process.exit(1);
});