using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PSTW_CentralSystem.Areas.Bookings.Models;
using PSTW_CentralSystem.DBContext;
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace PSTW_CentralSystem.Controllers.API
{
    [ApiController]
    [Route("api/BookingsApi")]
    [Authorize] // Require auth by default
    public class BookingsAPIController : ControllerBase
    {
        private readonly CentralSystemContext _db;
        private readonly ILogger<BookingsAPIController> _logger;
        private static readonly TimeZoneInfo AppTz = TimeZoneInfo.FindSystemTimeZoneById("Asia/Kuala_Lumpur");

        public BookingsAPIController(CentralSystemContext db, ILogger<BookingsAPIController> logger)
        {
            _db = db; _logger = logger;
        }

        private int? GetCurrentUserId()
        {
            var idStr = User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
            return int.TryParse(idStr, out var id) ? id : (int?)null;
        }

        private bool IsManager()
        {
            var me = GetCurrentUserId();
            if (me is null) return false;
            // Dynamic manager list (booking_managers)
            return _db.BookingManager.AsNoTracking()
                .Any(x => x.UserId == me.Value && x.IsActive);
        }


        private bool CanModify(Booking b, int? currentUserId)
        {
            if (b is null || currentUserId is null) return false;
            if (IsManager()) return true; // Managers: can modify any
            // Owner OR target may modify
            return b.RequestedByUserId == currentUserId || b.TargetUserId == currentUserId;
        }

        #region Undo helpers
        private static bool IsTruthy(string? s)
        {
            if (string.IsNullOrWhiteSpace(s)) return false;
            var v = s.Trim();
            return v == "1"
                || v.Equals("true", StringComparison.OrdinalIgnoreCase)
                || v.Equals("yes", StringComparison.OrdinalIgnoreCase)
                || v.Equals("y", StringComparison.OrdinalIgnoreCase)
                || v.Equals("on", StringComparison.OrdinalIgnoreCase);
        }

        private static bool IsSameLocalDate(DateTime aUtc, DateTime bUtc, TimeZoneInfo tz)
        {
            var a = TimeZoneInfo.ConvertTimeFromUtc(aUtc, tz).Date;
            var b = TimeZoneInfo.ConvertTimeFromUtc(bUtc, tz).Date;
            return a == b;
        }

        private bool ShouldUndo(System.Text.Json.JsonElement? body)
        {
            // query string: /api/BookingsApi?action=cancel&id=1&undo=1
            if (Request.Query.TryGetValue("undo", out var qv))
            {
                string? s = qv.Count > 0 ? qv[0].ToString() : null;
                if (IsTruthy(s)) return true;
            }

            // JSON body: { "undo": true } or { "uncancel": true } or { "restore": true }
            if (body.HasValue && body.Value.ValueKind == System.Text.Json.JsonValueKind.Object)
            {
                var root = body.Value;
                if ((root.TryGetProperty("undo", out var a) && a.ValueKind == System.Text.Json.JsonValueKind.True)
                 || (root.TryGetProperty("uncancel", out var b) && b.ValueKind == System.Text.Json.JsonValueKind.True)
                 || (root.TryGetProperty("unCancel", out var c) && c.ValueKind == System.Text.Json.JsonValueKind.True)
                 || (root.TryGetProperty("restore", out var d) && d.ValueKind == System.Text.Json.JsonValueKind.True))
                    return true;
            }
            return false;
        }
        #endregion

        private static string? GetBodyNote(in JsonElement root)
            => root.TryGetProperty("note", out var n) && n.ValueKind == JsonValueKind.String ? n.GetString() : null;

        // ------------------- Helpers -------------------
        private static DateTime CoerceToUtc(DateTime dt)
        {
            return dt.Kind switch
            {
                DateTimeKind.Utc => dt,
                DateTimeKind.Unspecified => DateTime.SpecifyKind(dt, DateTimeKind.Utc),
                _ => dt.ToUniversalTime()
            };
        }

        private static bool IsSameUtcDate(DateTime a, DateTime b) => a.Date == b.Date;

        private static JsonSerializerOptions JsonOpts = new()
        {
            PropertyNameCaseInsensitive = true
        };

        private static DateTime SnapToNearest30(DateTime utc)
        {
            var minutes = utc.Minute;
            var baseTime = new DateTime(utc.Year, utc.Month, utc.Day, utc.Hour, 0, 0, DateTimeKind.Utc);
            if (minutes < 15) return baseTime;                // round down
            if (minutes < 45) return baseTime.AddMinutes(30); // round to :30
            return baseTime.AddHours(1);                      // round up to next hour
        }

        // ------------------- DTOs -------------------
        public record CreateBookingDto(
            string Title,
            string? Purpose,
            [property: JsonPropertyName("description")] string? Description, // alias
            int RoomId,
            DateTime StartUtc,
            DateTime EndUtc,
            int RequestedByUserId,
            int? TargetUserId,
            string? Note,
            [property: JsonPropertyName("notes")] string? Notes // alias
        );

        public record UpdateBookingDto(
            string? Title,
            string? Purpose,
            [property: JsonPropertyName("description")] string? Description, // alias
            int? RoomId,
            DateTime? StartUtc,
            DateTime? EndUtc,
            string? Note,
            [property: JsonPropertyName("notes")] string? Notes // alias
        );

        public record CreateRoomDto(string RoomName, string? LocationCode, int? Capacity, bool IsActive = true);
        public record UpdateRoomDto(string? RoomName, string? LocationCode, int? Capacity, bool? IsActive);


        // GET /api/BookingsApi/users
        [HttpGet("users")]
        public async Task<IActionResult> UsersAsync()
        {
            var list = await _db.Users
                .AsNoTracking()
                .OrderBy(u => u.FullName)
                .Select(u => new { id = u.Id, name = u.FullName, email = u.Email })
                .ToListAsync();
            return Ok(list);
        }

        // GET /api/BookingsApi/managers  -> [1,2,3]
        [HttpGet("managers")]
        public async Task<IActionResult> GetManagersAsync()
        {
            var ids = await _db.BookingManager
                .AsNoTracking()
                .Where(x => x.IsActive)
                .Select(x => x.UserId)
                .ToListAsync();
            return Ok(ids);
        }

        public class SaveManagersDto { public List<int>? UserIds { get; set; } }

        // POST /api/BookingsApi/managers  body: { "userIds":[1,2,3] }
        [HttpPost("managers")]
        public async Task<IActionResult> SaveManagersAsync([FromBody] SaveManagersDto dto)
        {
            if (dto?.UserIds is null) return BadRequest("userIds required.");
            // Only an existing manager can edit the list (bootstrap by seeding one row)
            if (!IsManager()) return Unauthorized("Managers only.");

            var now = DateTime.UtcNow;
            var me = GetCurrentUserId();

            // Soft-reset approach: mark all inactive then upsert selected as active
            var all = await _db.BookingManager.ToListAsync();
            foreach (var bm in all) bm.IsActive = false;

            var selected = new HashSet<int>(dto.UserIds.Where(id => id > 0));

            foreach (var uid in selected)
            {
                var existing = all.FirstOrDefault(x => x.UserId == uid);
                if (existing is null)
                {
                    _db.BookingManager.Add(new BookingManager
                    {
                        UserId = uid,
                        IsActive = true,
                        CreatedUtc = now,
                        CreatedByUserId = me
                    });
                }
                else
                {
                    existing.IsActive = true;
                }
            }

            await _db.SaveChangesAsync();
            return Ok(new { saved = true, count = selected.Count });
        }



        // =========================================================
        // ========================= GET ===========================
        // =========================================================
        // - Bookings list:        GET /api/BookingsApi
        // - Booking details:      GET /api/BookingsApi?id=123
        // - Lookups (rooms+users) GET /api/BookingsApi?lookups=1
        // - Rooms list:           GET /api/BookingsApi?scope=rooms[&includeInactive=true]



        [HttpGet]
        public async Task<IActionResult> GetAsync(
            [FromQuery] int? id,
            [FromQuery] int? lookups,
            [FromQuery] string? scope,
            [FromQuery] bool includeInactive = true,
            [FromQuery] string? search = null,
            [FromQuery] string? status = null,
            [FromQuery] DateTime? from = null,
            [FromQuery] DateTime? to = null,
            [FromQuery] int? roomId = null,
            [FromQuery] int page = 1,
            [FromQuery] int pageSize = 450,
            [FromQuery] int? userId = null,
            [FromQuery] int? companyId = null,
            [FromQuery] int? departmentId = null
        )
        {
            // ROOMS LIST (admin tooling / open to authenticated)
            if (string.Equals(scope, "rooms", StringComparison.OrdinalIgnoreCase))
            {
                var rq = _db.Rooms.AsNoTracking().AsQueryable();
                if (!includeInactive) rq = rq.Where(r => r.IsActive);
                var rooms = await rq
                    .OrderBy(r => r.RoomName)
                    .Select(r => new
                    {
                        roomId = r.RoomId,
                        roomName = r.RoomName,
                        locationCode = r.LocationCode,
                        capacity = r.Capacity,
                        isActive = r.IsActive
                    })
                    .ToListAsync();
                return Ok(rooms);
            }

            // ---------- CALENDAR LOOKUPS (everyone sees the full lists) ----------
            // GET /api/BookingsApi?scope=calendar&lookups=1
            if (string.Equals(scope, "calendar", StringComparison.OrdinalIgnoreCase) && lookups == 1)
            {
                var roomsAll = await _db.Rooms
                    .AsNoTracking()
                    .Where(r => r.IsActive)
                    .OrderBy(r => r.RoomName)
                    .Select(r => new { roomId = r.RoomId, roomName = r.RoomName })
                    .ToListAsync();

                var usersAll = await _db.Users
                    .AsNoTracking()
                    .OrderBy(u => u.FullName)
                    .Select(u => new { u.Id, UserName = u.FullName, u.Email })
                    .ToListAsync();

                return Ok(new { rooms = roomsAll, users = usersAll });
            }

            // ---------- CALENDAR LIST (everyone sees all bookings) ----------
            // GET /api/BookingsApi?scope=calendar&from=...&to=...
            if (string.Equals(scope, "calendar", StringComparison.OrdinalIgnoreCase) && !id.HasValue && lookups != 1)
            {
                var qCal = _db.Bookings.AsNoTracking().AsQueryable();

                if (!string.IsNullOrWhiteSpace(search))
                    qCal = qCal.Where(x => x.Title.Contains(search));

                if (!string.IsNullOrWhiteSpace(status) && Enum.TryParse<BookingStatus>(status, true, out var stCal))
                    qCal = qCal.Where(x => x.CurrentStatus == stCal);

                DateTime? fromUtcCal = from.HasValue ? CoerceToUtc(from.Value) : null;
                DateTime? toUtcCal = to.HasValue ? CoerceToUtc(to.Value) : null;

                if (fromUtcCal.HasValue) qCal = qCal.Where(x => x.EndUtc > fromUtcCal.Value);
                if (toUtcCal.HasValue) qCal = qCal.Where(x => x.StartUtc < toUtcCal.Value);

                if (roomId.HasValue) qCal = qCal.Where(x => x.RoomId == roomId.Value);
                if (userId.HasValue && userId.Value > 0)
                    qCal = qCal.Where(x => x.RequestedByUserId == userId.Value || x.TargetUserId == userId.Value);

                if (companyId.HasValue && companyId.Value > 0)
                    qCal = qCal.Where(x => x.CompanyId == companyId.Value);

                if (departmentId.HasValue && departmentId.Value > 0)
                    qCal = qCal.Where(x => x.DepartmentId == departmentId.Value);

                var list = await qCal
                    .OrderBy(x => x.StartUtc)
                    .Skip((page - 1) * pageSize)
                    .Take(pageSize)
                    .Select(b => new
                    {
                        id = b.BookingId,
                        roomId = b.RoomId,
                        requestedByUserId = b.RequestedByUserId,
                        targetUserId = b.TargetUserId,
                        title = b.Title,
                        description = b.Purpose,
                        startUtc = DateTime.SpecifyKind(b.StartUtc, DateTimeKind.Utc),
                        endUtc = DateTime.SpecifyKind(b.EndUtc, DateTimeKind.Utc),
                        status = b.CurrentStatus.ToString(),
                        note = b.Note
                    })
                    .ToListAsync();

                return Ok(list);
            }

            // LOOKUPS PACK
            if (lookups == 1)
            {
                var rooms = await _db.Rooms
                    .AsNoTracking()
                    .Where(r => r.IsActive)
                    .OrderBy(r => r.RoomName)
                    .Select(r => new { roomId = r.RoomId, roomName = r.RoomName })
                    .ToListAsync();

                object usersPayload;
                if (IsManager())
                {
                    usersPayload = await _db.Users
                        .AsNoTracking()
                        .OrderBy(u => u.FullName)
                        .Select(u => new { u.Id, UserName = u.FullName, u.Email })
                        .ToListAsync();
                }
                else
                {
                    var me = GetCurrentUserId();
                    usersPayload = (me is int myId)
                        ? await _db.Users.AsNoTracking()
                            .Where(u => u.Id == myId)
                            .Select(u => new { u.Id, UserName = u.FullName, u.Email })
                            .ToListAsync()
                        : new object[0];
                }

                return Ok(new { rooms, users = usersPayload });
            }

            // BOOKING DETAILS
            if (id.HasValue)
            {
                var b = await _db.Bookings.AsNoTracking().FirstOrDefaultAsync(x => x.BookingId == id.Value);
                if (b is null) return NotFound();

                var me = GetCurrentUserId();
                if (!IsManager() && me != b.RequestedByUserId && me != b.TargetUserId)
                    return Unauthorized("Not allowed to view this booking.");

                return Ok(new
                {
                    id = b.BookingId,
                    roomId = b.RoomId,
                    requestedByUserId = b.RequestedByUserId,
                    title = b.Title,
                    description = b.Purpose,
                    startUtc = DateTime.SpecifyKind(b.StartUtc, DateTimeKind.Utc),
                    endUtc = DateTime.SpecifyKind(b.EndUtc, DateTimeKind.Utc),
                    status = b.CurrentStatus.ToString(),
                    note = b.Note
                });
            }

            // BOOKINGS LIST
            var q = _db.Bookings.AsNoTracking().AsQueryable();

            if (!string.IsNullOrWhiteSpace(search)) q = q.Where(x => x.Title.Contains(search));

            if (!string.IsNullOrWhiteSpace(status) && Enum.TryParse<BookingStatus>(status, true, out var st))
                q = q.Where(x => x.CurrentStatus == st);

            DateTime? fromUtc = from.HasValue ? CoerceToUtc(from.Value) : null;
            DateTime? toUtc = to.HasValue ? CoerceToUtc(to.Value) : null;

            if (fromUtc.HasValue) q = q.Where(x => x.EndUtc > fromUtc.Value);
            if (toUtc.HasValue) q = q.Where(x => x.StartUtc < toUtc.Value);

            if (roomId.HasValue) q = q.Where(x => x.RoomId == roomId.Value);
            if (userId.HasValue && userId.Value > 0)
                q = q.Where(x => x.RequestedByUserId == userId.Value || x.TargetUserId == userId.Value);

            if (companyId.HasValue && companyId.Value > 0)
                q = q.Where(x => x.CompanyId == companyId.Value);

            if (departmentId.HasValue && departmentId.Value > 0)
                q = q.Where(x => x.DepartmentId == departmentId.Value);

            // RBAC: non-managers only see their own
            var meList = GetCurrentUserId();
            if (!IsManager())
            {
                if (meList is int myId)
                    q = q.Where(x => x.RequestedByUserId == myId || x.TargetUserId == myId);
                else
                    return Unauthorized();
            }

            var items = await q.OrderBy(x => x.StartUtc)
                .Skip((page - 1) * pageSize)
                .Take(pageSize)
                .Select(b => new
                {
                    id = b.BookingId,
                    roomId = b.RoomId,
                    requestedByUserId = b.RequestedByUserId,
                    title = b.Title,
                    description = b.Purpose,
                    startUtc = DateTime.SpecifyKind(b.StartUtc, DateTimeKind.Utc),
                    endUtc = DateTime.SpecifyKind(b.EndUtc, DateTimeKind.Utc),
                    status = b.CurrentStatus.ToString(),
                    note = b.Note
                })
                .ToListAsync();

            return Ok(items);
        }

        // =========================================================
        // ======================== POST ===========================
        // =========================================================
        // - Create booking: POST /api/BookingsApi
        // - Cancel/Un-cancel: POST /api/BookingsApi?action=cancel&id=123  (body: { "undo": true })
        // - Create room:    POST /api/BookingsApi?scope=rooms
        [HttpPost]
        public async Task<IActionResult> PostAsync(
            [FromQuery] string? action,
            [FromQuery] int? id,
            [FromQuery] string? scope,
            [FromBody] System.Text.Json.JsonElement? body // nullable is fine
        )
        {
            var json = body?.ToString() ?? "{}";

            // ROOMS: CREATE (Managers only)
            if (string.Equals(scope, "rooms", StringComparison.OrdinalIgnoreCase))
            {
                if (!IsManager()) return Unauthorized("Managers only.");

                var dto = JsonSerializer.Deserialize<CreateRoomDto>(json, JsonOpts);
                if (dto is null || string.IsNullOrWhiteSpace(dto.RoomName))
                    return BadRequest("RoomName is required.");

                var exists = await _db.Rooms.AnyAsync(r => r.RoomName == dto.RoomName);
                if (exists) return Conflict("A room with this name already exists.");

                var room = new Room
                {
                    RoomName = dto.RoomName.Trim(),
                    LocationCode = string.IsNullOrWhiteSpace(dto.LocationCode) ? null : dto.LocationCode.Trim(),
                    Capacity = dto.Capacity,
                    IsActive = dto.IsActive
                };
                _db.Rooms.Add(room);
                await _db.SaveChangesAsync();
                return Ok(new { roomId = room.RoomId });
            }

            // BOOKING: APPROVE (Manager only)
            if (string.Equals(action, "approve", StringComparison.OrdinalIgnoreCase))
            {
                if (!id.HasValue || id <= 0) return BadRequest("Missing id.");
                if (!IsManager())
                    return Unauthorized("Not authorized to approve bookings.");

                var b = await _db.Bookings.FirstOrDefaultAsync(x => x.BookingId == id.Value);
                if (b is null) return NotFound();

                if (b.CurrentStatus == BookingStatus.Cancelled)
                    return Conflict("Cannot approve a cancelled booking.");
                if (b.CurrentStatus == BookingStatus.Approved)
                    return Ok(new { id = b.BookingId, status = b.CurrentStatus.ToString() });

                // prevent overlap with other APPROVED bookings
                var overlap = await _db.Bookings.AsNoTracking().AnyAsync(x =>
                    x.RoomId == b.RoomId &&
                    x.BookingId != b.BookingId &&
                    x.CurrentStatus == BookingStatus.Approved &&
                    x.StartUtc < b.EndUtc && b.StartUtc < x.EndUtc);

                if (overlap) return Conflict("Cannot approve: time slot overlaps an approved booking.");

                b.CurrentStatus = BookingStatus.Approved;
                b.LastUpdatedUtc = DateTime.UtcNow;
                await _db.SaveChangesAsync();
                return Ok(new { id = b.BookingId, status = b.CurrentStatus.ToString() });
            }

            // BOOKING: REJECT / UN-REJECT (Manager only; undo -> Pending)
            if (string.Equals(action, "reject", StringComparison.OrdinalIgnoreCase))
            {
                if (!id.HasValue || id <= 0) return BadRequest("Missing id.");
                if (!IsManager())
                    return Unauthorized("Not authorized to reject bookings.");

                var b = await _db.Bookings.FirstOrDefaultAsync(x => x.BookingId == id.Value);
                if (b is null) return NotFound();

                bool undo = ShouldUndo(body);

                if (!undo)
                {
                    if (b.CurrentStatus == BookingStatus.Cancelled)
                        return Conflict("Cannot reject a cancelled booking.");
                    if (body.HasValue && body.Value.ValueKind == JsonValueKind.Object)
                        b.Note = GetBodyNote(body.Value);
                    b.CurrentStatus = BookingStatus.Rejected;
                }
                else
                {
                    if (b.CurrentStatus != BookingStatus.Rejected)
                        return Conflict("Booking is not rejected.");
                    b.CurrentStatus = BookingStatus.Pending;
                }

                b.LastUpdatedUtc = DateTime.UtcNow;
                await _db.SaveChangesAsync();
                return Ok(new { id = b.BookingId, status = b.CurrentStatus.ToString() });
            }

            // ----- BOOKING: CANCEL / UN-CANCEL (Owner or Manager) -------------------
            if (string.Equals(action, "cancel", StringComparison.OrdinalIgnoreCase))
            {
                if (!id.HasValue || id <= 0) return BadRequest("Missing id.");

                var b = await _db.Bookings.FirstOrDefaultAsync(x => x.BookingId == id.Value);
                if (b is null) return NotFound();

                var me = GetCurrentUserId();
                if (!CanModify(b, me))
                    return Unauthorized("Only the creator or a manager can cancel/un-cancel this booking.");
                if (b.CurrentStatus == BookingStatus.Rejected)
                    return Conflict("Rejected bookings cannot be cancelled or un-cancelled.");

                bool undo = ShouldUndo(body);

                if (!undo)
                {
                    if (b.CurrentStatus != BookingStatus.Cancelled)
                    {
                        b.CurrentStatus = BookingStatus.Cancelled;
                        b.LastUpdatedUtc = DateTime.UtcNow;
                        await _db.SaveChangesAsync();
                    }
                    return Ok(new { id = b.BookingId, status = b.CurrentStatus.ToString() });
                }
                else
                {
                    if (b.CurrentStatus != BookingStatus.Cancelled)
                        return Conflict("Booking is not cancelled.");

                    var overlap = await _db.Bookings.AsNoTracking().AnyAsync(x =>
                        x.RoomId == b.RoomId &&
                        x.BookingId != b.BookingId &&
                        (x.CurrentStatus == BookingStatus.Pending || x.CurrentStatus == BookingStatus.Approved) &&
                        x.StartUtc < b.EndUtc && b.StartUtc < x.EndUtc);

                    if (overlap)
                        return Conflict("Cannot un-cancel: time slot now overlaps another booking.");

                    b.CurrentStatus = BookingStatus.Pending;
                    b.LastUpdatedUtc = DateTime.UtcNow;
                    await _db.SaveChangesAsync();
                    return Ok(new { id = b.BookingId, status = b.CurrentStatus.ToString() });
                }
            }

            // ------------------ BOOKING: CREATE -------------------------
            var create = JsonSerializer.Deserialize<CreateBookingDto>(json, JsonOpts);
            if (create is null) return BadRequest("Invalid payload.");

            // For non-managers, force ownership to the current user
            var meCreate = GetCurrentUserId();
            if (!IsManager())
            {
                if (meCreate is null) return Unauthorized();
                create = create with
                {
                    RequestedByUserId = meCreate.Value,
                    TargetUserId = create.TargetUserId ?? meCreate.Value
                };
            }

            var roomOk = await _db.Rooms.AnyAsync(r => r.RoomId == create.RoomId && r.IsActive);
            if (!roomOk) return BadRequest("Room not found or inactive.");

            var targetUserId = create.TargetUserId ?? create.RequestedByUserId;
            var u = await _db.Users.AsNoTracking().FirstOrDefaultAsync(x => x.Id == targetUserId);
            if (u == null) return BadRequest("Selected user not found.");

            int? departmentId = u.departmentId;
            int? companyId = null;
            if (departmentId is int depId)
            {
                companyId = await _db.Departments.AsNoTracking()
                    .Where(d => d.DepartmentId == depId)
                    .Select(d => d.CompanyId)
                    .FirstOrDefaultAsync();
            }

            var purpose = (create.Purpose ?? create.Description)?.Trim();
            var noteIn = (create.Note ?? create.Notes)?.Trim();

            // normalize + snap to 30
            var startUtc = SnapToNearest30(CoerceToUtc(create.StartUtc));
            var endUtc = SnapToNearest30(CoerceToUtc(create.EndUtc));

            // guards
            if (endUtc <= startUtc) return BadRequest("End time must be after Start time.");
            if (!IsSameLocalDate(startUtc, endUtc, AppTz))
                return BadRequest("End must be on the same calendar date as Start.");

            var localStart = TimeZoneInfo.ConvertTimeFromUtc(startUtc, AppTz);
            var localEnd = TimeZoneInfo.ConvertTimeFromUtc(endUtc, AppTz);
            if (localStart.TimeOfDay < new TimeSpan(8, 0, 0))
                return BadRequest("Start must be at or after 08:00.");
            if (localEnd.TimeOfDay > new TimeSpan(20, 0, 0))
                return BadRequest("End must be at or before 20:00.");

            // NEW: disallow creating bookings in the past (rounded up to next :00 / :30)
            var nowUtcCeil = SnapToNearest30(DateTime.UtcNow.AddSeconds(1)); // ceil to next 30-min tick
            if (startUtc < nowUtcCeil)
                return BadRequest("Start must be in the future.");


            var entity = new Booking
            {
                RequestedByUserId = create.RequestedByUserId,
                TargetUserId = create.TargetUserId ?? create.RequestedByUserId,
                DepartmentId = departmentId,
                CompanyId = companyId,
                RoomId = create.RoomId,
                Title = create.Title?.Trim() ?? "",
                Purpose = string.IsNullOrWhiteSpace(purpose) ? null : purpose,
                StartUtc = startUtc,
                EndUtc = endUtc,
                Note = string.IsNullOrWhiteSpace(noteIn) ? null : noteIn,
                CreatedUtc = DateTime.UtcNow,
                LastUpdatedUtc = DateTime.UtcNow,
                CurrentStatus = BookingStatus.Pending
            };

            // data annotations
            var vc = new ValidationContext(entity);
            var vr = new List<ValidationResult>();
            if (!Validator.TryValidateObject(entity, vc, vr, true)) return BadRequest(vr);

            // overlap (Pending/Approved)
            var overlapCreate = await _db.Bookings.AsNoTracking().AnyAsync(b =>
                b.RoomId == entity.RoomId &&
                (b.CurrentStatus == BookingStatus.Pending || b.CurrentStatus == BookingStatus.Approved) &&
                b.StartUtc < entity.EndUtc && entity.StartUtc < b.EndUtc);

            if (overlapCreate) return Conflict("This time slot overlaps an existing booking for the selected room.");

            _db.Bookings.Add(entity);
            await _db.SaveChangesAsync();

            return CreatedAtAction(nameof(GetAsync),
                new { id = entity.BookingId },
                new { id = entity.BookingId });
        }

        // =========================================================
        // ========================= PUT ===========================
        // =========================================================
        // - Update booking: PUT /api/BookingsApi?id=123
        // - Update room:    PUT /api/BookingsApi?scope=rooms&id=5
        [HttpPut]
        public async Task<IActionResult> PutAsync(
            [FromQuery] int? id,
            [FromQuery] string? scope,
            [FromBody] System.Text.Json.JsonElement? body
        )
        {
            if (!id.HasValue || id <= 0) return BadRequest("Missing id.");
            var json = body?.ToString() ?? "{}";

            // ROOMS: UPDATE (Managers only)
            if (string.Equals(scope, "rooms", StringComparison.OrdinalIgnoreCase))
            {
                if (!IsManager()) return Unauthorized("Managers only.");

                var dto = JsonSerializer.Deserialize<UpdateRoomDto>(json, JsonOpts);

                var r = await _db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id.Value);
                if (r is null) return NotFound();

                if (dto is not null)
                {
                    if (dto.RoomName is not null) r.RoomName = dto.RoomName.Trim();
                    if (dto.LocationCode is not null) r.LocationCode = string.IsNullOrWhiteSpace(dto.LocationCode) ? null : dto.LocationCode.Trim();
                    if (dto.Capacity.HasValue) r.Capacity = dto.Capacity.Value;
                    if (dto.IsActive.HasValue) r.IsActive = dto.IsActive.Value;
                }
                await _db.SaveChangesAsync();
                return Ok(new { roomId = r.RoomId });
            }

            // BOOKING: UPDATE  (Owner or Manager)
            var payload = JsonSerializer.Deserialize<UpdateBookingDto>(json, JsonOpts);

            var entity = await _db.Bookings.FirstOrDefaultAsync(x => x.BookingId == id.Value);
            if (entity is null) return NotFound();

            var me = GetCurrentUserId();
            if (!CanModify(entity, me))
                return Unauthorized("Only the creator or a manager can edit this booking.");

            if (entity.CurrentStatus is not BookingStatus.Pending and not BookingStatus.Rejected)
                return Conflict("Only Pending or Rejected bookings can be updated.");

            // optional: switch rooms
            if (payload?.RoomId is int newRoomId && newRoomId != entity.RoomId)
            {
                var roomOk2 = await _db.Rooms.AnyAsync(r => r.RoomId == newRoomId && r.IsActive);
                if (!roomOk2) return BadRequest("New room not found or inactive.");
                entity.RoomId = newRoomId;
            }

            if (payload is not null)
            {
                if (payload.Title is not null) entity.Title = payload.Title.Trim();

                var purpose = (payload.Purpose ?? payload.Description)?.Trim();
                if (payload.Purpose is not null || payload.Description is not null)
                    entity.Purpose = string.IsNullOrWhiteSpace(purpose) ? null : purpose;

                var noteIn = (payload.Note ?? payload.Notes)?.Trim();
                if (payload.Note is not null || payload.Notes is not null)
                    entity.Note = string.IsNullOrWhiteSpace(noteIn) ? null : noteIn;

                // normalize + snap to 30 using incoming or existing values
                var newStartUtc = entity.StartUtc;
                var newEndUtc = entity.EndUtc;

                if (payload.StartUtc.HasValue)
                    newStartUtc = SnapToNearest30(CoerceToUtc(payload.StartUtc.Value));
                if (payload.EndUtc.HasValue)
                    newEndUtc = SnapToNearest30(CoerceToUtc(payload.EndUtc.Value));

                // NEW: also disallow moving an existing booking into the past
                var nowUtcCeil = SnapToNearest30(DateTime.UtcNow.AddSeconds(1));
                if (newStartUtc < nowUtcCeil)
                    return BadRequest("Start must be in the future.");

                // guards
                if (newEndUtc <= newStartUtc) return BadRequest("End time must be after Start time.");
                if (!IsSameLocalDate(newStartUtc, newEndUtc, AppTz))
                    return BadRequest("End date must be the same calendar date as Start.");

                var localStart = TimeZoneInfo.ConvertTimeFromUtc(newStartUtc, AppTz);
                var localEnd = TimeZoneInfo.ConvertTimeFromUtc(newEndUtc, AppTz);
                var windowStart = new TimeSpan(8, 0, 0);
                var windowEnd = new TimeSpan(20, 0, 0);
                if (localStart.TimeOfDay < windowStart || localEnd.TimeOfDay > windowEnd)
                    return BadRequest("Bookings must be between 08:00 and 20:00 local time.");

                entity.StartUtc = newStartUtc;
                entity.EndUtc = newEndUtc;
            }

            entity.LastUpdatedUtc = DateTime.UtcNow;

            // data annotations
            var vc = new ValidationContext(entity);
            var vr = new List<ValidationResult>();
            if (!Validator.TryValidateObject(entity, vc, vr, true)) return BadRequest(vr);

            // overlap on (possibly) new room/times
            var overlap = await _db.Bookings.AsNoTracking().AnyAsync(b =>
                b.RoomId == entity.RoomId &&
                b.BookingId != entity.BookingId &&
                (b.CurrentStatus == BookingStatus.Pending || b.CurrentStatus == BookingStatus.Approved) &&
                b.StartUtc < entity.EndUtc && entity.StartUtc < b.EndUtc);

            if (overlap) return Conflict("This time slot overlaps an existing booking for the selected room.");

            await _db.SaveChangesAsync();
            return Ok(new { id = entity.BookingId });
        }

        // =========================================================
        // ======================= DELETE ==========================
        // =========================================================
        // - Delete booking: DELETE /api/BookingsApi?id=123
        // - Delete room:    DELETE /api/BookingsApi?scope=rooms&id=5
        [HttpDelete]
        public async Task<IActionResult> DeleteAsync([FromQuery] int? id, [FromQuery] string? scope)
        {
            if (!id.HasValue || id <= 0) return BadRequest("Missing id.");

            // ROOMS: HARD DELETE (Managers only)
            if (string.Equals(scope, "rooms", StringComparison.OrdinalIgnoreCase))
            {
                if (!IsManager()) return Unauthorized("Managers only.");

                var r = await _db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id.Value);
                if (r is null) return NotFound();

                try
                {
                    _db.Rooms.Remove(r);
                    await _db.SaveChangesAsync();
                    return Ok(new { deleted = true });
                }
                catch (DbUpdateException)
                {
                    // Likely foreign key in use (existing bookings) or DB constraint
                    return StatusCode(409, new { error = "Cannot delete: room is in use or active. Deactivate it instead." });
                }
                catch (Exception)
                {
                    return StatusCode(500, new { error = "Delete failed." });
                }
            }

            // BOOKINGS: DELETE (only Pending/Rejected) + permission gate
            var entity = await _db.Bookings.FirstOrDefaultAsync(x => x.BookingId == id.Value);
            if (entity is null) return NotFound();

            var me = GetCurrentUserId();
            if (!CanModify(entity, me))
                return Unauthorized("Only the creator or a manager can delete this booking.");

            // NEW: only Pending is editable
            if (entity.CurrentStatus != BookingStatus.Pending)
                return Conflict("Only Pending bookings can be edited.");


            try
            {
                _db.Bookings.Remove(entity);
                await _db.SaveChangesAsync();
                return Ok(new { deleted = true });
            }
            catch (DbUpdateException)
            {
                return StatusCode(409, new { error = "Cannot delete: booking is referenced or locked." });
            }
            catch (Exception)
            {
                return StatusCode(500, new { error = "Delete failed." });
            }
        }
    }
}
