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

import 'reflect-metadata';
import * as zlib from 'zlib';
import { promisify } from 'util';
import sharp from 'sharp';
import { NestFactory } from '@nestjs/core';
import { Module, Controller, Get, Post, Body, Param, Patch, Delete, ParseUUIDPipe, NotFoundException, BadRequestException, UnauthorizedException, UseGuards, Req, 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 { 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');

// 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/',
  });

  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 } });
            if (photo) 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;
    }
  }
}

@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,
      },
    };
  }

  @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 user = await this.prisma.user.findUnique({ where: { email } });
    
    if (!user || !(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('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 }) {
    const { email, otp } = 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 userCount = await this.prisma.user.count();
    const shouldBeAdmin = userCount === 0;

    const passwordHash = await bcrypt.hash(password, 10);

    const 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: true,
        legs: {
          orderBy: { sequence: 'asc' },
          include: {
            expenses: {
              include: {
                location: { select: { name: true, plannedStart: true } },
                paidBy: { select: { name: true } }
              }
            },
            locations: { 
              orderBy: { plannedStart: 'asc' },
              include: { _count: { select: { comments: true } } }
            },
          },
        },
      },
    });

    if (!tour) {
      console.warn(`[PublicTour] Không tìm thấy Tour với ID: ${id} trong Database`);
      throw new NotFoundException(`Không tìm thấy Tour`);
    }
    return tour;
  }
}

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

  @UseGuards(JwtAuthGuard)
  @Post()
  async createTour(@Body() body: any, @Req() req: any) {
    const { title, description, startDate, endDate, adultCount, childCount, childDiscount, tags } = body;
    const tour = await this.prisma.tour.create({
      data: {
        title,
        description,
        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'
          }
        },
        legs: {
          create: {
            sequence: 1,
            note: 'Chặng khởi đầu'
          }
        }
      },
      include: {
        participants: {
          include: { user: { select: { id: true, name: true, email: true } } }
        }
      }
    });
    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) {
        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: body.paidById || null,
          }
        });
      }
      // 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) {
    // Lưu ý: Trong thực tế nên kiểm tra xem user có phải là OWNER không
    return this.prisma.tour.update({
      where: { id },
      data: {
        title: body.title,
        description: body.description,
        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,
      },
    }).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) {
      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 ---

    // 2. Xóa các file vật lý (ảnh 2K) trong thư mục uploads/tours
    for (const photo of tour.photos) {
      if (photo.imageUrl) {
        const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
        if (fs.existsSync(displayFilePath)) {
          fs.unlinkSync(displayFilePath);
        }
      }
    }

    // Cập nhật DB: Xóa link ảnh 2K vì file vật lý đã bị xóa, tourId sẽ tự động SetNull
    await this.prisma.photo.updateMany({
      where: { tourId: id },
      data: { imageUrl: null }
    });

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

    // 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`);

    return { success: true };
  }

  // getPublicTours does not need TourRoleGuard as it's for any logged-in user to see their tours
  @UseInterceptors(CompressCacheInterceptor) // Áp dụng Interceptor nén và cache
  @UseGuards(JwtAuthGuard)
  @Get('explore')
  async getPublicTours(@Req() req: any) {
    // Lọc Tour: Chỉ lấy những tour mà người dùng hiện tại là thành viên (Participant)
    return this.prisma.tour.findMany({
      where: {
        participants: {
          some: { userId: req.user.id }
        }
      },
      take: 20,
      include: {
        participants: {
          where: { userId: req.user.id },
          select: { role: true }
        },
        photos: { take: 1 },
        legs: {
          orderBy: { sequence: 'asc' },
          include: {
            locations: { 
              orderBy: { plannedStart: 'asc' },
              include: { _count: { select: { comments: true } } }
            }
          }
        }
      }
    });
  }

  @UseInterceptors(CompressCacheInterceptor) // Áp dụng Interceptor nén và cache
  @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: true,
        legs: {
          orderBy: { sequence: 'asc' },
          include: {
            expenses: {
              include: {
                location: { select: { name: true, plannedStart: true } },
                paidBy: { select: { name: true } }
              }
            },
            locations: { 
              orderBy: { plannedStart: 'asc' },
              include: { _count: { select: { comments: true } } }
            },
          },
        },
      },
    });

    if (!tour) 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; 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';

    // 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) // 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;
  }

  @Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER, ParticipantRole.MEMBER, ParticipantRole.MEMBER_NO_FINANCE, ParticipantRole.VIEWER_ONLY) // Any participant can create a join request for themselves or others (if they have permission)
  @UseGuards(JwtAuthGuard, TourRoleGuard)
  @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' },
      }),
    ]);

    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 participation = await this.prisma.tourParticipant.findUnique({
      where: { tourId_userId: { tourId, userId } },
    });
    if (!participation) {
      throw new NotFoundException('Thành viên này không có trong tour');
    }

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

    await this.prisma.tourParticipant.delete({
      where: { tourId_userId: { tourId, userId } },
    });
    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 });

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

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

        // 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. Xử lý ảnh để hiển thị (Độ phân giải 2K: tối đa 2560px)
        // Sử dụng Sharp để resize và tối ưu dung lượng ảnh
        await sharp(file.buffer)
          .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);

        // 3. Lưu thông tin vào Database (Lưu cả 2 đường dẫn)
        return this.prisma.photo.create({
          data: {
            tourId: tourId,
            uploaderId: uploaderId,
            imageUrl: `/uploads/tours/${filename}`,           // URL ảnh 2K dùng để render
            originalUrl: `/uploads/members/${uploaderId}/originals/${filename}`, // URL ảnh gốc để tải xuống
            privacy: 'TOUR_ONLY',
          }
        });
    }));
  }
}

@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: '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: '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) {}

  @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 mới có quyền xóa ảnh của họ
    if (photo.uploaderId !== req.user.id) {
      throw new ForbiddenException('Bạn không có quyền xóa ảnh này.');
    }

    // Xóa file 2K (imageUrl) nếu tồn tại
    if (photo.imageUrl) {
      const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
      if (fs.existsSync(displayFilePath)) {
        fs.unlinkSync(displayFilePath);
      }
    }

    // Xóa file gốc (originalUrl) nếu tồn tại
    if (photo.originalUrl) {
      const originalFilePath = path.join(process.cwd(), photo.originalUrl.replace(/^\//, ''));
      if (fs.existsSync(originalFilePath)) {
        fs.unlinkSync(originalFilePath);
      }
    }

    await this.prisma.photo.delete({ where: { id } });
    return { message: 'Ảnh đã được xóa thành công.' };
  }
}

@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?.sub;
    const users = await this.prisma.user.findMany({
      where: q
        ? {
            OR: [
              { name: { contains: q, mode: 'insensitive' as any } },
              { email: { contains: q, mode: 'insensitive' as any } },
            ],
          }
        : undefined,
      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 },
      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) {
    if (data.password) {
      data.passwordHash = await bcrypt.hash(data.password, 10);
      delete data.password;
    }
    return this.prisma.user.update({
      where: { id },
      data,
      select: { id: true, email: true, name: true, isAdmin: true, isBlocked: true }
    });
  }

  @Roles(ParticipantRole.OWNER) // Only owner can delete user
  @Delete(':id')
  async deleteUser(@Param('id', ParseUUIDPipe) id: string) {
    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')
  async toggleBlock(@Param('id', ParseUUIDPipe) id: string) {
    const user = await this.prisma.user.findUnique({ where: { id } });
    if (!user) throw new NotFoundException('Người dùng không tồn tại');
    const updated = await this.prisma.user.update({
      where: { id },
      data: { isBlocked: !user.isBlocked },
      select: { id: true, email: true, name: true, isBlocked: true }
    });
    return updated;
  }
}

@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}`);
  }

  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);
  }
}

@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 comment = await this.prisma.comment.create({
      data: {
        content: body.content,
        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
        });
      }
    }
  }
}

@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, AdminOtpController],
  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);
});