using FluentValidation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Meezi.API.Models.Public;
using Meezi.API.Models.Queue;
using Meezi.API.Security;
using Meezi.API.Services;
using Meezi.Infrastructure.Data;
using Meezi.Shared;
using Microsoft.EntityFrameworkCore;

namespace Meezi.API.Controllers;

[ApiController]
[AllowAnonymous]
[Route("api/public")]
public class PublicController : ControllerBase
{
    private readonly IPublicService _public;
    private readonly IReviewService _reviews;
    private readonly IValidator<GuestCreateOrderRequest> _orderValidator;
    private readonly IValidator<PlaceGuestOrderRequest> _qrOrderValidator;
    private readonly IValidator<CreateReservationRequest> _reservationValidator;
    private readonly IValidator<CreateCafeReviewRequest> _reviewValidator;
    private readonly IAbuseProtectionService _abuse;
    private readonly AbuseProtectionOptions _securityOptions;

    public PublicController(
        IPublicService publicService,
        IReviewService reviews,
        IValidator<GuestCreateOrderRequest> orderValidator,
        IValidator<PlaceGuestOrderRequest> qrOrderValidator,
        IValidator<CreateReservationRequest> reservationValidator,
        IValidator<CreateCafeReviewRequest> reviewValidator,
        IAbuseProtectionService abuse,
        IOptions<AbuseProtectionOptions> securityOptions)
    {
        _public = publicService;
        _reviews = reviews;
        _orderValidator = orderValidator;
        _qrOrderValidator = qrOrderValidator;
        _reservationValidator = reservationValidator;
        _reviewValidator = reviewValidator;
        _abuse = abuse;
        _securityOptions = securityOptions.Value;
    }

    [HttpGet("security-config")]
    [EnableRateLimiting("public-read")]
    public IActionResult GetSecurityConfig()
    {
        var dto = new PublicSecurityConfigDto(
            _securityOptions.Enabled,
            _abuse.CaptchaSiteKey,
            _securityOptions.RequireCaptchaOnPublicWrites && _abuse.IsCaptchaConfigured);
        return Ok(new ApiResponse<PublicSecurityConfigDto>(true, dto));
    }

    [HttpGet("discover")]
    [EnableRateLimiting("public-read")]
    public async Task<IActionResult> Discover(
        [FromQuery] string? city,
        [FromQuery] string? q,
        [FromQuery] double? minRating,
        [FromQuery] string? sort,
        [FromQuery] string? themes,
        [FromQuery] string? vibes,
        [FromQuery] string? occasions,
        [FromQuery] string? spaceFeatures,
        [FromQuery] string? noise,
        [FromQuery] string? priceTier,
        [FromQuery] string? size,
        [FromQuery] bool requireProfile = true,
        [FromQuery] bool openNow = false,
        CancellationToken ct = default)
    {
        var filters = DiscoverFilterParams.FromQuery(
            city, q, minRating, sort, themes, vibes, occasions, spaceFeatures,
            noise, priceTier, size, requireProfile, openNow);
        var data = await _public.DiscoverAsync(filters, ct);
        return Ok(new ApiResponse<IReadOnlyList<CafeDiscoverDto>>(true, data));
    }

    [HttpGet("cafes/{slug}/reviews")]
    [EnableRateLimiting("public-read")]
    public async Task<IActionResult> GetReviews(
        string slug,
        [FromQuery] int page = 1,
        [FromQuery] int pageSize = 20,
        CancellationToken ct = default)
    {
        var cafe = await _public.GetCafeAsync(slug, ct);
        if (cafe is null) return NotFound(new ApiResponse<object>(false, null, new ApiError("NOT_FOUND", "Cafe not found.")));
        var data = await _reviews.GetReviewsAsync(cafe.Id, page, pageSize, publicOnly: true, ct);
        return Ok(new ApiResponse<IReadOnlyList<CafeReviewDto>>(true, data));
    }

    [HttpPost("cafes/{slug}/reviews")]
    public async Task<IActionResult> CreateReview(
        string slug,
        [FromBody] CreateCafeReviewRequest request,
        CancellationToken ct)
    {
        var validation = await _reviewValidator.ValidateAsync(request, ct);
        if (!validation.IsValid)
        {
            var first = validation.Errors.First();
            return BadRequest(new ApiResponse<object>(false, null, new ApiError("VALIDATION_ERROR", first.ErrorMessage, first.PropertyName)));
        }

        var cafe = await _public.GetCafeAsync(slug, ct);
        if (cafe is null) return NotFound(new ApiResponse<object>(false, null, new ApiError("NOT_FOUND", "Cafe not found.")));

        var (data, code, message) = await _reviews.CreateReviewAsync(cafe.Id, request, ct);
        if (data is null)
        {
            return StatusCode(
                PublicWriteStatusCodes.ToHttpStatus(code),
                new ApiResponse<object>(false, null, new ApiError(code ?? "ERROR", message ?? "Could not submit review.")));
        }

        return Ok(new ApiResponse<CafeReviewDto>(true, data));
    }

    [HttpPost("cafes/{slug}/reviews/upload")]
    [RequestSizeLimit(20 * 1024 * 1024)]
    public async Task<IActionResult> CreateReviewWithPhotos(
        string slug,
        [FromForm] string authorName,
        [FromForm] int rating,
        [FromForm] string? comment,
        [FromForm] string? authorPhone,
        [FromForm] string? captchaToken,
        [FromForm] List<IFormFile>? photos,
        CancellationToken ct)
    {
        var request = new CreateCafeReviewRequest(
            authorName?.Trim() ?? "",
            authorPhone?.Trim(),
            rating,
            comment?.Trim(),
            captchaToken);

        var validation = await _reviewValidator.ValidateAsync(request, ct);
        if (!validation.IsValid)
        {
            var first = validation.Errors.First();
            return BadRequest(new ApiResponse<object>(false, null, new ApiError("VALIDATION_ERROR", first.ErrorMessage, first.PropertyName)));
        }

        var cafe = await _public.GetCafeAsync(slug, ct);
        if (cafe is null) return NotFound(new ApiResponse<object>(false, null, new ApiError("NOT_FOUND", "Cafe not found.")));

        var (data, code, message) = await _reviews.CreateReviewWithPhotosAsync(
            cafe.Id,
            request,
            photos ?? [],
            ct);
        if (data is null)
        {
            return StatusCode(
                PublicWriteStatusCodes.ToHttpStatus(code),
                new ApiResponse<object>(false, null, new ApiError(code ?? "ERROR", message ?? "Could not submit review.")));
        }

        return Ok(new ApiResponse<CafeReviewDto>(true, data));
    }

    [HttpGet("badge-catalog")]
    [EnableRateLimiting("public-read")]
    public IActionResult BadgeCatalog()
    {
        var data = Core.Discover.CafeBadgeCatalog.All
            .Select(b => new CafeBadgePublicDto(b.Key, b.LabelFa, b.Icon))
            .ToList();
        return Ok(new ApiResponse<IReadOnlyList<CafeBadgePublicDto>>(true, data));
    }

    [HttpGet("cafes/{slug}")]
    [EnableRateLimiting("public-read")]
    public async Task<IActionResult> GetCafe(string slug, CancellationToken ct)
    {
        var data = await _public.GetCafeAsync(slug, ct);
        if (data is null) return NotFound(new ApiResponse<object>(false, null, new ApiError("NOT_FOUND", "Cafe not found.")));
        return Ok(new ApiResponse<CafePublicDto>(true, data));
    }

    [HttpGet("cafes/{slug}/menu")]
    [EnableRateLimiting("public-read")]
    public async Task<IActionResult> GetMenu(string slug, CancellationToken ct)
    {
        var data = await _public.GetMenuAsync(slug, ct);
        if (data is null) return NotFound(new ApiResponse<object>(false, null, new ApiError("NOT_FOUND", "Cafe not found.")));
        return Ok(new ApiResponse<PublicMenuDto>(true, data));
    }

    [HttpPost("cafes/{slug}/orders")]
    public async Task<IActionResult> PlaceOrder(
        string slug,
        [FromBody] GuestCreateOrderRequest request,
        CancellationToken ct)
    {
        var validation = await _orderValidator.ValidateAsync(request, ct);
        if (!validation.IsValid)
        {
            var first = validation.Errors.First();
            return BadRequest(new ApiResponse<object>(false, null, new ApiError("VALIDATION_ERROR", first.ErrorMessage, first.PropertyName)));
        }

        var (data, code, message) = await _public.PlaceOrderAsync(slug, request, ct);
        if (data is null)
        {
            return StatusCode(
                PublicWriteStatusCodes.ToHttpStatus(code),
                new ApiResponse<object>(false, null, new ApiError(code ?? "ERROR", message ?? "Failed.")));
        }

        return Ok(new ApiResponse<GuestOrderPlacedDto>(true, data));
    }

    [HttpGet("orders/{orderId}/track")]
    [EnableRateLimiting("public-read")]
    public async Task<IActionResult> TrackOrder(
        string orderId,
        [FromQuery] string token,
        CancellationToken ct)
    {
        if (string.IsNullOrWhiteSpace(token))
            return BadRequest(new ApiResponse<object>(false, null, new ApiError("VALIDATION_ERROR", "Tracking token is required.")));

        var data = await _public.TrackOrderAsync(orderId, token, ct);
        if (data is null) return NotFound(new ApiResponse<object>(false, null, new ApiError("NOT_FOUND", "Order not found.")));
        return Ok(new ApiResponse<OrderTrackDto>(true, data));
    }

    [HttpGet("{cafeId}/branches/{branchId}/menu")]
    [EnableRateLimiting("public-read")]
    public async Task<IActionResult> GetBranchMenu(
        string cafeId,
        string branchId,
        CancellationToken ct)
    {
        var data = await _public.GetBranchMenuAsync(cafeId, branchId, ct);
        if (data is null)
            return NotFound(new ApiResponse<object>(false, null, new ApiError("NOT_FOUND", "Branch or menu not found.")));
        return Ok(new ApiResponse<PublicMenuDto>(true, data));
    }

    [HttpGet("{cafeId}/branches/{branchId}/identity")]
    [EnableRateLimiting("public-read")]
    public async Task<IActionResult> GetBranchIdentity(
        string cafeId,
        string branchId,
        [FromServices] IBranchIdentityService identity,
        CancellationToken ct)
    {
        var data = await identity.GetEffectiveIdentityAsync(cafeId, branchId, ct);
        if (data is null)
            return NotFound(new ApiResponse<object>(false, null, new ApiError("NOT_FOUND", "Branch not found.")));
        return Ok(new ApiResponse<BranchEffectiveIdentityDto>(true, data));
    }

    [HttpPost("{cafeId}/branches/{branchId}/orders")]
    public async Task<IActionResult> PlaceBranchGuestOrder(
        string cafeId,
        string branchId,
        [FromBody] PlaceGuestOrderRequest request,
        CancellationToken ct)
    {
        var validation = await _qrOrderValidator.ValidateAsync(request, ct);
        if (!validation.IsValid)
        {
            var first = validation.Errors.First();
            return BadRequest(new ApiResponse<object>(false, null,
                new ApiError("VALIDATION_ERROR", first.ErrorMessage, first.PropertyName)));
        }

        var (data, code, message) = await _public.PlaceBranchGuestOrderAsync(cafeId, branchId, request, ct);
        if (data is null)
        {
            return StatusCode(
                PublicWriteStatusCodes.ToHttpStatus(code),
                new ApiResponse<object>(false, null, new ApiError(code ?? "ERROR", message ?? "Failed.")));
        }

        return Ok(new ApiResponse<GuestQrOrderPlacedDto>(true, data));
    }

    [HttpPost("cafes/{slug}/reservations")]
    public async Task<IActionResult> CreateReservation(
        string slug,
        [FromBody] CreateReservationRequest request,
        CancellationToken ct)
    {
        var validation = await _reservationValidator.ValidateAsync(request, ct);
        if (!validation.IsValid)
        {
            var first = validation.Errors.First();
            return BadRequest(new ApiResponse<object>(false, null, new ApiError("VALIDATION_ERROR", first.ErrorMessage, first.PropertyName)));
        }

        var (data, code, message) = await _public.CreateReservationAsync(slug, request, ct);
        if (data is null)
        {
            return StatusCode(
                PublicWriteStatusCodes.ToHttpStatus(code),
                new ApiResponse<object>(false, null, new ApiError(code ?? "ERROR", message ?? "Failed.")));
        }

        return Ok(new ApiResponse<ReservationDto>(true, data));
    }

    [HttpPost("cafes/{slug}/queue/tickets")]
    [EnableRateLimiting("public-write")]
    public async Task<IActionResult> IssuePublicQueueTicket(
        string slug,
        [FromBody] IssueQueueTicketRequest request,
        [FromServices] IQueueService queue,
        [FromServices] AppDbContext db,
        CancellationToken ct)
    {
        var cafe = await db.Cafes.AsNoTracking()
            .FirstOrDefaultAsync(c => c.Slug == slug && c.DeletedAt == null, ct);
        if (cafe is null)
            return NotFound(new ApiResponse<object>(false, null, new ApiError("NOT_FOUND", "Cafe not found.")));

        var (ticket, code, message) = await queue.IssuePublicAsync(cafe.Id, cafe.PlanTier, request, ct);
        if (ticket is null)
        {
            var status = code == "PLAN_LIMIT_REACHED" ? 403 : 400;
            return StatusCode(status, new ApiResponse<object>(false, null,
                new ApiError(code ?? "ERROR", message ?? "Could not issue ticket.")));
        }

        return Ok(new ApiResponse<QueueTicketDto>(true, ticket));
    }

    [HttpPost("{cafeId}/tables/{tableId}/call-waiter")]
    [EnableRateLimiting("public-read")]
    public async Task<IActionResult> CallWaiter(
        string cafeId,
        string tableId,
        [FromServices] AppDbContext db,
        [FromServices] IOrderNotificationService notifications,
        [FromServices] IMemoryCache cache,
        CancellationToken ct)
    {
        var cooldownKey = $"call-waiter:{cafeId}:{tableId}";
        if (cache.TryGetValue(cooldownKey, out _))
            return StatusCode(StatusCodes.Status429TooManyRequests,
                new ApiResponse<object>(false, null, new ApiError("RATE_LIMITED", "Please wait 60 seconds before calling again.")));

        var table = await db.Tables.AsNoTracking()
            .Where(t => t.Id == tableId && t.CafeId == cafeId && t.DeletedAt == null && t.IsActive)
            .Select(t => new { t.Id, t.Number })
            .FirstOrDefaultAsync(ct);

        if (table is null)
            return NotFound(new ApiResponse<object>(false, null, new ApiError("NOT_FOUND", "Table not found.")));

        cache.Set(cooldownKey, true, TimeSpan.FromSeconds(60));

        await notifications.NotifyCallWaiterAsync(cafeId, tableId, table.Number, ct);

        return Ok(new ApiResponse<object>(true, null));
    }
}
