using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Meezi.API.Configuration;
using Meezi.Core.Constants;
using Meezi.Core.Enums;
using Meezi.Core.Interfaces;
using Meezi.Core.Platform;
using Meezi.Infrastructure.Data;
using Meezi.Infrastructure.Services.Platform;
using StackExchange.Redis;

namespace Meezi.API.Services;

public record MenuAi3dUsageDto(int Used, int Limit, string Period);

public record MenuAi3dGenerateResultDto(string Model3dUrl, int Used, int Limit);

public interface IMenuAi3dGenerationService
{
    Task<MenuAi3dUsageDto> GetUsageAsync(string cafeId, PlanTier planTier, CancellationToken cancellationToken = default);
    Task<(MenuAi3dGenerateResultDto? Data, string? ErrorCode, string? Message)> GenerateFromItemImageAsync(
        string cafeId,
        string itemId,
        PlanTier planTier,
        CancellationToken cancellationToken = default);
}

public class MenuAi3dGenerationService : IMenuAi3dGenerationService
{
    private const string FeatureMenu3d = "menu_3d";
    private const string FeatureMenu3dAi = "menu_3d_ai";

    private static readonly JsonSerializerOptions JsonOpts = new()
    {
        PropertyNameCaseInsensitive = true,
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
    };

    private readonly AppDbContext _db;
    private readonly IPlatformCatalogService _catalog;
    private readonly IMediaStorageService _media;
    private readonly IConnectionMultiplexer _redis;
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly IPlatformRuntimeConfig _platform;
    private readonly MenuAi3dOptions _options;
    private readonly IConfiguration _configuration;
    private readonly IWebHostEnvironment _env;
    private readonly ILogger<MenuAi3dGenerationService> _logger;

    public MenuAi3dGenerationService(
        AppDbContext db,
        IPlatformCatalogService catalog,
        IMediaStorageService media,
        IConnectionMultiplexer redis,
        IHttpClientFactory httpClientFactory,
        IPlatformRuntimeConfig platform,
        IOptions<MenuAi3dOptions> options,
        IConfiguration configuration,
        IWebHostEnvironment env,
        ILogger<MenuAi3dGenerationService> logger)
    {
        _db = db;
        _catalog = catalog;
        _media = media;
        _redis = redis;
        _httpClientFactory = httpClientFactory;
        _platform = platform;
        _options = options.Value;
        _configuration = configuration;
        _env = env;
        _logger = logger;
    }

    public async Task<MenuAi3dUsageDto> GetUsageAsync(
        string cafeId,
        PlanTier planTier,
        CancellationToken cancellationToken = default)
    {
        var limit = await ResolveLimitAsync(cafeId, planTier, cancellationToken);
        var used = await GetUsedCountAsync(cafeId);
        return new MenuAi3dUsageDto(used, limit, DateTime.UtcNow.ToString("yyyy-MM"));
    }

    public async Task<(MenuAi3dGenerateResultDto? Data, string? ErrorCode, string? Message)> GenerateFromItemImageAsync(
        string cafeId,
        string itemId,
        PlanTier planTier,
        CancellationToken cancellationToken = default)
    {
        if (!await _catalog.IsFeatureEnabledForCafeAsync(cafeId, planTier, FeatureMenu3d, cancellationToken))
            return (null, "PLAN_FEATURE_DISABLED", "3D menu is not included in your plan.");

        if (!await _catalog.IsFeatureEnabledForCafeAsync(cafeId, planTier, FeatureMenu3dAi, cancellationToken))
            return (null, "PLAN_FEATURE_DISABLED", "AI 3D generation requires Business plan or higher.");

        var limit = await ResolveLimitAsync(cafeId, planTier, cancellationToken);
        if (limit <= 0)
            return (null, "PLAN_FEATURE_DISABLED", "AI 3D generation is not available on your plan.");

        var used = await GetUsedCountAsync(cafeId);
        if (used >= limit)
            return (null, "PLAN_LIMIT_REACHED", "Monthly AI 3D generation limit reached (100).");

        var item = await _db.MenuItems.FirstOrDefaultAsync(
            i => i.CafeId == cafeId && i.Id == itemId,
            cancellationToken);
        if (item is null)
            return (null, "NOT_FOUND", "Menu item not found.");

        if (string.IsNullOrWhiteSpace(item.ImageUrl))
            return (null, "NO_IMAGE", "Upload a product photo before generating a 3D model.");

        var imageUrl = ResolvePublicUrl(item.ImageUrl.Trim());
        byte[] glbBytes;
        try
        {
            glbBytes = await GenerateGlbBytesAsync(imageUrl, cancellationToken);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "AI 3D generation failed for cafe {CafeId} item {ItemId}", cafeId, itemId);
            return (null, "AI_GENERATION_FAILED", "Could not generate 3D model. Try again later.");
        }

        var modelUrl = await _media.SaveMenuModel3dFromBytesAsync(cafeId, glbBytes, cancellationToken);
        if (modelUrl is null)
            return (null, "INVALID_FILE", "Generated model could not be saved.");

        item.Model3dUrl = modelUrl;
        await _db.SaveChangesAsync(cancellationToken);

        var newUsed = await IncrementUsageAsync(cafeId);
        return (new MenuAi3dGenerateResultDto(modelUrl, newUsed, limit), null, null);
    }

    private async Task<int> ResolveLimitAsync(string cafeId, PlanTier planTier, CancellationToken cancellationToken)
    {
        if (!await _catalog.IsFeatureEnabledForCafeAsync(cafeId, planTier, FeatureMenu3dAi, cancellationToken))
            return 0;
        return PlanLimits.MaxMenuAi3dPerMonth(planTier);
    }

    private static string UsageKey(string cafeId) =>
        $"ai3d:usage:{cafeId}:{DateTime.UtcNow:yyyy-MM}";

    private async Task<int> GetUsedCountAsync(string cafeId)
    {
        var redis = _redis.GetDatabase();
        var val = await redis.StringGetAsync(UsageKey(cafeId));
        return val.HasValue && int.TryParse(val.ToString(), out var n) ? n : 0;
    }

    private async Task<int> IncrementUsageAsync(string cafeId)
    {
        var redis = _redis.GetDatabase();
        var key = UsageKey(cafeId);
        var next = (int)await redis.StringIncrementAsync(key);
        if (next == 1)
        {
            var endOfMonth = new DateTime(DateTime.UtcNow.Year, DateTime.UtcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc)
                .AddMonths(1);
            await redis.KeyExpireAsync(key, endOfMonth - DateTime.UtcNow);
        }

        return next;
    }

    private string ResolvePublicUrl(string url)
    {
        if (url.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
            || url.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
            return url;

        var baseUrl = _configuration["App:PublicBaseUrl"]?.TrimEnd('/') ?? "http://localhost:5080";
        return url.StartsWith('/') ? $"{baseUrl}{url}" : $"{baseUrl}/{url}";
    }

    private async Task<byte[]> GenerateGlbBytesAsync(string imageUrl, CancellationToken cancellationToken)
    {
        var apiKey = await ResolveMeshyApiKeyAsync(cancellationToken);
        if (!string.IsNullOrWhiteSpace(apiKey))
            return await GenerateViaMeshyAsync(imageUrl, apiKey, cancellationToken);

        if (_options.AllowDevStub && _env.IsDevelopment())
            return DevStubGlbBytes();

        throw new InvalidOperationException("AI 3D provider is not configured.");
    }

    private async Task<string?> ResolveMeshyApiKeyAsync(CancellationToken cancellationToken)
    {
        var menu3dOn = await _platform.GetAsync(PlatformIntegrationKeys.MeshyMenu3dEnabled, cancellationToken);
        if (menu3dOn is "false")
            return null;

        var enabled = await _platform.GetAsync(PlatformIntegrationKeys.MeshyEnabled, cancellationToken);
        if (enabled is "false")
            return null;

        var fromDb = await _platform.GetAsync(PlatformIntegrationKeys.MeshyApiKey, cancellationToken);
        if (!string.IsNullOrWhiteSpace(fromDb))
            return fromDb.Trim();

        return string.IsNullOrWhiteSpace(_options.ApiKey) ? null : _options.ApiKey.Trim();
    }

    private async Task<byte[]> GenerateViaMeshyAsync(
        string imageUrl,
        string apiKey,
        CancellationToken cancellationToken)
    {
        var client = _httpClientFactory.CreateClient("MenuAi3d");
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);

        var baseUrl = _options.BaseUrl.TrimEnd('/');
        var createPath = _options.ImageTo3dPath.TrimStart('/');
        var createBody = JsonSerializer.Serialize(new { image_url = imageUrl }, JsonOpts);
        using var createReq = new HttpRequestMessage(HttpMethod.Post, $"{baseUrl}/{createPath}")
        {
            Content = new StringContent(createBody, Encoding.UTF8, "application/json")
        };

        using var createRes = await client.SendAsync(createReq, cancellationToken);
        createRes.EnsureSuccessStatusCode();
        await using var createStream = await createRes.Content.ReadAsStreamAsync(cancellationToken);
        var createDoc = await JsonDocument.ParseAsync(createStream, cancellationToken: cancellationToken);
        var taskId = createDoc.RootElement.TryGetProperty("result", out var resultEl)
            ? resultEl.GetString()
            : createDoc.RootElement.TryGetProperty("id", out var idEl)
                ? idEl.GetString()
                : null;

        if (string.IsNullOrWhiteSpace(taskId))
            throw new InvalidOperationException("AI provider did not return a task id.");

        var pollUrl = $"{baseUrl}/{createPath}/{taskId}";
        var deadline = DateTime.UtcNow.AddSeconds(Math.Max(30, _options.PollTimeoutSeconds));
        var interval = TimeSpan.FromSeconds(Math.Clamp(_options.PollIntervalSeconds, 2, 30));

        while (DateTime.UtcNow < deadline)
        {
            await Task.Delay(interval, cancellationToken);
            using var pollRes = await client.GetAsync(pollUrl, cancellationToken);
            pollRes.EnsureSuccessStatusCode();
            await using var pollStream = await pollRes.Content.ReadAsStreamAsync(cancellationToken);
            var pollDoc = await JsonDocument.ParseAsync(pollStream, cancellationToken: cancellationToken);
            var status = pollDoc.RootElement.TryGetProperty("status", out var statusEl)
                ? statusEl.GetString()
                : null;

            if (string.Equals(status, "SUCCEEDED", StringComparison.OrdinalIgnoreCase)
                || string.Equals(status, "COMPLETED", StringComparison.OrdinalIgnoreCase))
            {
                var glbUrl = ExtractGlbUrl(pollDoc.RootElement);
                if (string.IsNullOrWhiteSpace(glbUrl))
                    throw new InvalidOperationException("AI provider succeeded but returned no GLB URL.");

                using var downloadRes = await client.GetAsync(glbUrl, cancellationToken);
                downloadRes.EnsureSuccessStatusCode();
                return await downloadRes.Content.ReadAsByteArrayAsync(cancellationToken);
            }

            if (string.Equals(status, "FAILED", StringComparison.OrdinalIgnoreCase)
                || string.Equals(status, "CANCELED", StringComparison.OrdinalIgnoreCase))
                throw new InvalidOperationException("AI provider task failed.");
        }

        throw new TimeoutException("AI 3D generation timed out.");
    }

    private static string? ExtractGlbUrl(JsonElement root)
    {
        if (root.TryGetProperty("model_urls", out var urls)
            && urls.TryGetProperty("glb", out var glb)
            && glb.ValueKind == JsonValueKind.String)
            return glb.GetString();

        if (root.TryGetProperty("model_url", out var single) && single.ValueKind == JsonValueKind.String)
            return single.GetString();

        return null;
    }

    /// <summary>Minimal valid GLB (empty scene) for local development without Meshy API key.</summary>
    private static byte[] DevStubGlbBytes() =>
        Convert.FromBase64String(
            "Z2xURgIAAACI3gAQAwEAAFBLQVRGT1JNUwBCeHAEAgAqBUZsAE1BVEhQAgAgAAAAAO4AAABKQwAAAAAAAJAAAAA=");
}
