using FluentValidation;
using MediatR;
using Microsoft.EntityFrameworkCore;
using SolutionErp.Application.Common.Exceptions;
using SolutionErp.Application.Common.Interfaces;
using SolutionErp.Domain.Office;

namespace SolutionErp.Application.Office;

// Phase 10.2 G-O2 (Mig 36 — S36 2026-05-28) — Phòng họp + Booking CQRS.
// Pattern 12-bis cross-module mirror Hrm/HrmConfigFeatures.cs (S35 cumulative 10×).
// Pattern 16-bis foundation 4-place modification: entity (done) + config (done)
// + controller (separate file) + menu/DbInit (DbInitializer SeedMenuTreeAsync extend).
//
// Endpoints expose qua MeetingRoomsController + MeetingBookingsController:
//   GET    /api/meeting-rooms                 — list (filter search + active)
//   POST   /api/meeting-rooms                 — create [Admin]
//   PUT    /api/meeting-rooms/{id}            — update [Admin]
//   DELETE /api/meeting-rooms/{id}            — soft disable IsActive=false [Admin]
//                                                (NOT IsDeleted vì FK Restrict Booking)
//   GET    /api/meeting-bookings              — list (room + date range + my filter)
//   GET    /api/meeting-bookings/{id}         — detail Include Attendees
//   POST   /api/meeting-bookings              — create (SERIALIZABLE tx overlap check)
//   PUT    /api/meeting-bookings/{id}         — update (SERIALIZABLE tx exclude self)
//   DELETE /api/meeting-bookings/{id}         — cancel Status=Cancelled (NOT IsDeleted — preserve history)
//
// Investigator verdict S36 pre-flight chốt:
//   - Overlap check qua System.Data.IsolationLevel.Serializable tx + EXISTS AnyAsync
//   - Attendees join table N-to-N (MeetingBookingAttendee), NOT JSON array
//   - 3-state Status enum (Confirmed=1 / Cancelled=2 / Completed=3)

// ===== DTOs =====

public record MeetingRoomDto(
    Guid Id,
    string Code,
    string Name,
    int Capacity,
    string? Location,
    string? Equipment,
    bool IsActive,
    DateTime CreatedAt);

public record MeetingBookingAttendeeDto(
    Guid UserId,
    string FullName,
    string? Email,
    string? Notes);

public record MeetingBookingDto(
    Guid Id,
    Guid RoomId,
    string RoomCode,
    string RoomName,
    Guid BookedByUserId,
    string BookedByFullName,
    DateTime StartAt,
    DateTime EndAt,
    string Title,
    string? Description,
    int Status,
    string? Note,
    DateTime CreatedAt,
    List<MeetingBookingAttendeeDto> Attendees);

public record AttendeeInput(Guid UserId, string? Notes);

// ===== Region 1: MeetingRoom CRUD =====

public record GetMeetingRoomsQuery(string? Search = null, bool? IsActiveOnly = null) : IRequest<List<MeetingRoomDto>>;
public class GetMeetingRoomsHandler(IApplicationDbContext db) : IRequestHandler<GetMeetingRoomsQuery, List<MeetingRoomDto>>
{
    public async Task<List<MeetingRoomDto>> Handle(GetMeetingRoomsQuery req, CancellationToken ct)
    {
        var q = db.MeetingRooms.AsNoTracking().Where(x => !x.IsDeleted);
        if (req.IsActiveOnly == true) q = q.Where(x => x.IsActive);
        if (!string.IsNullOrWhiteSpace(req.Search))
        {
            var s = req.Search.ToLower();
            q = q.Where(x =>
                x.Code.ToLower().Contains(s) ||
                x.Name.ToLower().Contains(s) ||
                (x.Location != null && x.Location.ToLower().Contains(s)));
        }
        return await q.OrderBy(x => x.Code)
            .Select(x => new MeetingRoomDto(
                x.Id, x.Code, x.Name, x.Capacity, x.Location, x.Equipment, x.IsActive, x.CreatedAt))
            .ToListAsync(ct);
    }
}

public record CreateMeetingRoomCommand(
    string Code,
    string Name,
    int Capacity,
    string? Location,
    string? Equipment) : IRequest<Guid>;
public class CreateMeetingRoomValidator : AbstractValidator<CreateMeetingRoomCommand>
{
    public CreateMeetingRoomValidator()
    {
        RuleFor(x => x.Code).NotEmpty().MaximumLength(20);
        RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
        RuleFor(x => x.Capacity).GreaterThanOrEqualTo(0);
        RuleFor(x => x.Location).MaximumLength(200);
        RuleFor(x => x.Equipment).MaximumLength(500);
    }
}
public class CreateMeetingRoomHandler(IApplicationDbContext db) : IRequestHandler<CreateMeetingRoomCommand, Guid>
{
    public async Task<Guid> Handle(CreateMeetingRoomCommand req, CancellationToken ct)
    {
        if (await db.MeetingRooms.AnyAsync(x => x.Code == req.Code && !x.IsDeleted, ct))
            throw new ConflictException($"Mã phòng họp '{req.Code}' đã tồn tại.");
        var entity = new MeetingRoom
        {
            Code = req.Code,
            Name = req.Name,
            Capacity = req.Capacity,
            Location = req.Location,
            Equipment = req.Equipment,
            IsActive = true,
        };
        db.MeetingRooms.Add(entity);
        await db.SaveChangesAsync(ct);
        return entity.Id;
    }
}

public record UpdateMeetingRoomCommand(
    Guid Id,
    string Code,
    string Name,
    int Capacity,
    string? Location,
    string? Equipment,
    bool IsActive) : IRequest;
public class UpdateMeetingRoomValidator : AbstractValidator<UpdateMeetingRoomCommand>
{
    public UpdateMeetingRoomValidator()
    {
        RuleFor(x => x.Code).NotEmpty().MaximumLength(20);
        RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
        RuleFor(x => x.Capacity).GreaterThanOrEqualTo(0);
        RuleFor(x => x.Location).MaximumLength(200);
        RuleFor(x => x.Equipment).MaximumLength(500);
    }
}
public class UpdateMeetingRoomHandler(IApplicationDbContext db) : IRequestHandler<UpdateMeetingRoomCommand>
{
    public async Task Handle(UpdateMeetingRoomCommand req, CancellationToken ct)
    {
        var entity = await db.MeetingRooms.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct)
            ?? throw new NotFoundException("MeetingRoom", req.Id);
        if (entity.Code != req.Code
            && await db.MeetingRooms.AnyAsync(x => x.Code == req.Code && x.Id != req.Id && !x.IsDeleted, ct))
            throw new ConflictException($"Mã phòng họp '{req.Code}' đã tồn tại.");
        entity.Code = req.Code;
        entity.Name = req.Name;
        entity.Capacity = req.Capacity;
        entity.Location = req.Location;
        entity.Equipment = req.Equipment;
        entity.IsActive = req.IsActive;
        await db.SaveChangesAsync(ct);
    }
}

public record DeleteMeetingRoomCommand(Guid Id) : IRequest;
public class DeleteMeetingRoomHandler(IApplicationDbContext db) : IRequestHandler<DeleteMeetingRoomCommand>
{
    public async Task Handle(DeleteMeetingRoomCommand req, CancellationToken ct)
    {
        var entity = await db.MeetingRooms.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct)
            ?? throw new NotFoundException("MeetingRoom", req.Id);
        // FK Restrict Booking → NOT soft delete (IsDeleted=true), chỉ set IsActive=false.
        // Preserve booking history + future query filter chỉ load room đang active.
        // Admin có thể toggle IsActive=true lại nếu muốn re-enable.
        entity.IsActive = false;
        await db.SaveChangesAsync(ct);
    }
}

// ===== Region 2: MeetingBooking CRUD =====
// Overlap check pattern SERIALIZABLE tx + EXISTS:
//   - BeginTransactionAsync(IsolationLevel.Serializable, ct)
//   - AnyAsync(b.RoomId == cmd.RoomId && b.Status == Confirmed && !b.IsDeleted
//             && b.StartAt < cmd.EndAt && b.EndAt > cmd.StartAt)
//   - Update: exclude self (b.Id != cmd.Id)
// SERIALIZABLE đảm bảo 2 concurrent insert overlap không cùng commit (range lock).

public record GetMeetingBookingsQuery(
    Guid? RoomId = null,
    DateTime? StartDate = null,
    DateTime? EndDate = null,
    bool MyOnly = false) : IRequest<List<MeetingBookingDto>>;

public class GetMeetingBookingsHandler(IApplicationDbContext db, ICurrentUser currentUser)
    : IRequestHandler<GetMeetingBookingsQuery, List<MeetingBookingDto>>
{
    public async Task<List<MeetingBookingDto>> Handle(GetMeetingBookingsQuery req, CancellationToken ct)
    {
        var q = db.MeetingBookings.AsNoTracking()
            .Include(x => x.Room)
            .Include(x => x.Attendees)
            .Where(x => !x.IsDeleted);

        if (req.RoomId.HasValue) q = q.Where(x => x.RoomId == req.RoomId.Value);
        if (req.StartDate.HasValue) q = q.Where(x => x.EndAt >= req.StartDate.Value);
        if (req.EndDate.HasValue) q = q.Where(x => x.StartAt <= req.EndDate.Value);

        if (req.MyOnly && currentUser.UserId is Guid uid)
        {
            // "My bookings" = đã đặt OR được mời (Attendee).
            q = q.Where(x => x.BookedByUserId == uid || x.Attendees.Any(a => a.UserId == uid));
        }

        return await q.OrderBy(x => x.StartAt)
            .Select(x => new MeetingBookingDto(
                x.Id,
                x.RoomId,
                x.Room.Code,
                x.Room.Name,
                x.BookedByUserId,
                x.BookedByFullName,
                x.StartAt,
                x.EndAt,
                x.Title,
                x.Description,
                (int)x.Status,
                x.Note,
                x.CreatedAt,
                x.Attendees
                    .Select(a => new MeetingBookingAttendeeDto(a.UserId, a.FullName, a.Email, a.Notes))
                    .ToList()))
            .ToListAsync(ct);
    }
}

public record GetMeetingBookingByIdQuery(Guid Id) : IRequest<MeetingBookingDto?>;
public class GetMeetingBookingByIdHandler(IApplicationDbContext db)
    : IRequestHandler<GetMeetingBookingByIdQuery, MeetingBookingDto?>
{
    public async Task<MeetingBookingDto?> Handle(GetMeetingBookingByIdQuery req, CancellationToken ct)
    {
        return await db.MeetingBookings.AsNoTracking()
            .Include(x => x.Room)
            .Include(x => x.Attendees)
            .Where(x => x.Id == req.Id && !x.IsDeleted)
            .Select(x => new MeetingBookingDto(
                x.Id,
                x.RoomId,
                x.Room.Code,
                x.Room.Name,
                x.BookedByUserId,
                x.BookedByFullName,
                x.StartAt,
                x.EndAt,
                x.Title,
                x.Description,
                (int)x.Status,
                x.Note,
                x.CreatedAt,
                x.Attendees
                    .Select(a => new MeetingBookingAttendeeDto(a.UserId, a.FullName, a.Email, a.Notes))
                    .ToList()))
            .FirstOrDefaultAsync(ct);
    }
}

public record CreateMeetingBookingCommand(
    Guid RoomId,
    DateTime StartAt,
    DateTime EndAt,
    string Title,
    string? Description,
    string? Note,
    List<AttendeeInput> Attendees) : IRequest<Guid>;

public class CreateMeetingBookingValidator : AbstractValidator<CreateMeetingBookingCommand>
{
    public CreateMeetingBookingValidator()
    {
        RuleFor(x => x.RoomId).NotEmpty();
        RuleFor(x => x.Title).NotEmpty().MaximumLength(200);
        RuleFor(x => x.Description).MaximumLength(1000);
        RuleFor(x => x.Note).MaximumLength(500);
        RuleFor(x => x.Attendees).NotEmpty().WithMessage("Cần ít nhất 1 người tham gia.");
        RuleFor(x => x).Must(c => c.EndAt > c.StartAt)
            .WithMessage("Giờ kết thúc phải sau giờ bắt đầu.");
    }
}

public class CreateMeetingBookingHandler(IApplicationDbContext db, ICurrentUser currentUser)
    : IRequestHandler<CreateMeetingBookingCommand, Guid>
{
    public async Task<Guid> Handle(CreateMeetingBookingCommand req, CancellationToken ct)
    {
        if (currentUser.UserId is not Guid userId)
            throw new UnauthorizedException();

        // Verify Room exists + IsActive (NotFoundException nếu fail)
        var roomActive = await db.MeetingRooms
            .AnyAsync(r => r.Id == req.RoomId && r.IsActive && !r.IsDeleted, ct);
        if (!roomActive)
            throw new NotFoundException("MeetingRoom", req.RoomId);

        // SERIALIZABLE transaction — range lock prevent 2 concurrent insert overlap commit.
        // EXISTS query overlap detect: tồn tại booking Confirmed cùng Room khoảng giao thời.
        await using var tx = await ((Microsoft.EntityFrameworkCore.DbContext)db)
            .Database.BeginTransactionAsync(System.Data.IsolationLevel.Serializable, ct);

        var overlap = await db.MeetingBookings.AnyAsync(b =>
            b.RoomId == req.RoomId &&
            b.Status == MeetingBookingStatus.Confirmed &&
            !b.IsDeleted &&
            b.StartAt < req.EndAt && b.EndAt > req.StartAt,
            ct);
        if (overlap)
            throw new ConflictException("Phòng đã được đặt trong khoảng thời gian này.");

        // Resolve attendee FullName + Email từ Users JOIN (denorm).
        var attendeeIds = req.Attendees.Select(a => a.UserId).Distinct().ToList();
        var attendeeUsers = await db.Users.AsNoTracking()
            .Where(u => attendeeIds.Contains(u.Id))
            .Select(u => new { u.Id, u.FullName, u.Email })
            .ToListAsync(ct);
        var attendeeLookup = attendeeUsers.ToDictionary(u => u.Id);

        var entity = new MeetingBooking
        {
            RoomId = req.RoomId,
            BookedByUserId = userId,
            BookedByFullName = currentUser.FullName ?? string.Empty,
            StartAt = req.StartAt,
            EndAt = req.EndAt,
            Title = req.Title,
            Description = req.Description,
            Note = req.Note,
            Status = MeetingBookingStatus.Confirmed,
            Attendees = req.Attendees
                .Where(a => attendeeLookup.ContainsKey(a.UserId))   // skip user không tồn tại
                .Select(a => new MeetingBookingAttendee
                {
                    UserId = a.UserId,
                    FullName = attendeeLookup[a.UserId].FullName,
                    Email = attendeeLookup[a.UserId].Email,
                    Notes = a.Notes,
                })
                .ToList(),
        };
        db.MeetingBookings.Add(entity);
        await db.SaveChangesAsync(ct);
        await tx.CommitAsync(ct);
        return entity.Id;
    }
}

public record UpdateMeetingBookingCommand(
    Guid Id,
    Guid RoomId,
    DateTime StartAt,
    DateTime EndAt,
    string Title,
    string? Description,
    string? Note,
    List<AttendeeInput> Attendees) : IRequest;

public class UpdateMeetingBookingValidator : AbstractValidator<UpdateMeetingBookingCommand>
{
    public UpdateMeetingBookingValidator()
    {
        RuleFor(x => x.RoomId).NotEmpty();
        RuleFor(x => x.Title).NotEmpty().MaximumLength(200);
        RuleFor(x => x.Description).MaximumLength(1000);
        RuleFor(x => x.Note).MaximumLength(500);
        RuleFor(x => x.Attendees).NotEmpty().WithMessage("Cần ít nhất 1 người tham gia.");
        RuleFor(x => x).Must(c => c.EndAt > c.StartAt)
            .WithMessage("Giờ kết thúc phải sau giờ bắt đầu.");
    }
}

public class UpdateMeetingBookingHandler(IApplicationDbContext db, ICurrentUser currentUser)
    : IRequestHandler<UpdateMeetingBookingCommand>
{
    public async Task Handle(UpdateMeetingBookingCommand req, CancellationToken ct)
    {
        if (currentUser.UserId is not Guid userId)
            throw new UnauthorizedException();

        var entity = await db.MeetingBookings
            .Include(x => x.Attendees)
            .FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct)
            ?? throw new NotFoundException("MeetingBooking", req.Id);

        // Authorization: owner OR Admin role
        var isOwner = entity.BookedByUserId == userId;
        var isAdmin = currentUser.Roles.Contains("Admin");
        if (!isOwner && !isAdmin)
            throw new ForbiddenException("Chỉ người đặt phòng hoặc Admin được phép sửa booking.");

        // Verify Room exists + IsActive
        var roomActive = await db.MeetingRooms
            .AnyAsync(r => r.Id == req.RoomId && r.IsActive && !r.IsDeleted, ct);
        if (!roomActive)
            throw new NotFoundException("MeetingRoom", req.RoomId);

        // SERIALIZABLE transaction — overlap check exclude SELF (b.Id != req.Id).
        await using var tx = await ((Microsoft.EntityFrameworkCore.DbContext)db)
            .Database.BeginTransactionAsync(System.Data.IsolationLevel.Serializable, ct);

        var overlap = await db.MeetingBookings.AnyAsync(b =>
            b.Id != req.Id &&
            b.RoomId == req.RoomId &&
            b.Status == MeetingBookingStatus.Confirmed &&
            !b.IsDeleted &&
            b.StartAt < req.EndAt && b.EndAt > req.StartAt,
            ct);
        if (overlap)
            throw new ConflictException("Phòng đã được đặt trong khoảng thời gian này.");

        // Update header fields
        entity.RoomId = req.RoomId;
        entity.StartAt = req.StartAt;
        entity.EndAt = req.EndAt;
        entity.Title = req.Title;
        entity.Description = req.Description;
        entity.Note = req.Note;
        // Status preserve (Cancel via separate command).

        // Recreate Attendees: delete existing + re-add (idempotent simpler than diff).
        var attendeeIds = req.Attendees.Select(a => a.UserId).Distinct().ToList();
        var attendeeUsers = await db.Users.AsNoTracking()
            .Where(u => attendeeIds.Contains(u.Id))
            .Select(u => new { u.Id, u.FullName, u.Email })
            .ToListAsync(ct);
        var attendeeLookup = attendeeUsers.ToDictionary(u => u.Id);

        // Remove old (Cascade FK on Booking.Attendees handles cleanup via collection)
        db.MeetingBookingAttendees.RemoveRange(entity.Attendees);
        entity.Attendees = req.Attendees
            .Where(a => attendeeLookup.ContainsKey(a.UserId))
            .Select(a => new MeetingBookingAttendee
            {
                BookingId = entity.Id,
                UserId = a.UserId,
                FullName = attendeeLookup[a.UserId].FullName,
                Email = attendeeLookup[a.UserId].Email,
                Notes = a.Notes,
            })
            .ToList();

        await db.SaveChangesAsync(ct);
        await tx.CommitAsync(ct);
    }
}

public record CancelMeetingBookingCommand(Guid Id) : IRequest;
public class CancelMeetingBookingHandler(IApplicationDbContext db, ICurrentUser currentUser)
    : IRequestHandler<CancelMeetingBookingCommand>
{
    public async Task Handle(CancelMeetingBookingCommand req, CancellationToken ct)
    {
        if (currentUser.UserId is not Guid userId)
            throw new UnauthorizedException();

        var entity = await db.MeetingBookings
            .FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct)
            ?? throw new NotFoundException("MeetingBooking", req.Id);

        var isOwner = entity.BookedByUserId == userId;
        var isAdmin = currentUser.Roles.Contains("Admin");
        if (!isOwner && !isAdmin)
            throw new ForbiddenException("Chỉ người đặt phòng hoặc Admin được phép huỷ booking.");

        // Status=Cancelled (NOT IsDeleted=true) — preserve history + audit trail.
        entity.Status = MeetingBookingStatus.Cancelled;
        await db.SaveChangesAsync(ct);
    }
}
