using System.Net.Http.Json;
using System.Text.Json;
using Meezi.API.Models.Printing;
using Meezi.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;

namespace Meezi.API.Services;

public record PosDeviceResult(bool Success, bool Skipped, string? ErrorCode, string? Detail = null)
{
    public static PosDeviceResult Ok() => new(true, false, null);
    public static PosDeviceResult SkippedNotConfigured() => new(true, true, null);
    public static PosDeviceResult Fail(string code, string? detail = null) => new(false, false, code, detail);
}

public interface IPosDeviceService
{
    Task<PosDeviceResult> SendPaymentRequestAsync(
        string cafeId,
        string branchId,
        PosPaymentRequest request,
        CancellationToken ct = default);
}

public class PosDeviceService : IPosDeviceService
{
    private const int DefaultPort = 8088;
    private static readonly TimeSpan RequestTimeout = TimeSpan.FromSeconds(90);

    private readonly AppDbContext _db;
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly ILogger<PosDeviceService> _logger;

    public PosDeviceService(
        AppDbContext db,
        IHttpClientFactory httpClientFactory,
        ILogger<PosDeviceService> logger)
    {
        _db = db;
        _httpClientFactory = httpClientFactory;
        _logger = logger;
    }

    public async Task<PosDeviceResult> SendPaymentRequestAsync(
        string cafeId,
        string branchId,
        PosPaymentRequest request,
        CancellationToken ct = default)
    {
        if (request.Amount <= 0)
            return PosDeviceResult.Fail("INVALID_AMOUNT");

        var branch = await _db.Branches
            .AsNoTracking()
            .FirstOrDefaultAsync(b => b.Id == branchId && b.CafeId == cafeId, ct);

        if (branch is null)
            return PosDeviceResult.Fail("BRANCH_NOT_FOUND");

        if (string.IsNullOrWhiteSpace(branch.PosDeviceIp))
            return PosDeviceResult.SkippedNotConfigured();

        var port = branch.PosDevicePort is > 0 and <= 65535
            ? branch.PosDevicePort.Value
            : DefaultPort;

        var order = await _db.Orders
            .AsNoTracking()
            .FirstOrDefaultAsync(o => o.Id == request.OrderId && o.CafeId == cafeId, ct);

        if (order is null)
            return PosDeviceResult.Fail("ORDER_NOT_FOUND");

        var payload = new
        {
            amount = (long)Math.Round(request.Amount, 0, MidpointRounding.AwayFromZero),
            orderId = request.OrderId,
            branchId,
        };

        var url = $"http://{branch.PosDeviceIp!.Trim()}:{port}/pay";

        try
        {
            var client = _httpClientFactory.CreateClient(nameof(PosDeviceService));
            client.Timeout = RequestTimeout;

            using var response = await client.PostAsJsonAsync(url, payload, ct);

            if (!response.IsSuccessStatusCode)
            {
                var body = await response.Content.ReadAsStringAsync(ct);
                _logger.LogWarning(
                    "POS device returned {Status} for {Url}: {Body}",
                    (int)response.StatusCode,
                    url,
                    body.Length > 200 ? body[..200] : body);
                return PosDeviceResult.Fail(
                    "POS_DEVICE_REJECTED",
                    $"HTTP {(int)response.StatusCode}");
            }

            return PosDeviceResult.Ok();
        }
        catch (TaskCanceledException) when (!ct.IsCancellationRequested)
        {
            return PosDeviceResult.Fail("POS_DEVICE_TIMEOUT");
        }
        catch (HttpRequestException ex)
        {
            _logger.LogWarning(ex, "POS device connection failed for {Url}", url);
            return PosDeviceResult.Fail("POS_DEVICE_CONNECTION_FAILED", ex.Message);
        }
        catch (JsonException ex)
        {
            _logger.LogWarning(ex, "POS device response invalid for {Url}", url);
            return PosDeviceResult.Fail("POS_DEVICE_CONNECTION_FAILED", ex.Message);
        }
    }
}
