using ClosedXML.Excel;
using MealPlan.Api.Data;
using MealPlan.Api.DTOs.Recipe;
using MealPlan.Api.Models;
using Microsoft.EntityFrameworkCore;

namespace MealPlan.Api.Services;

/// <summary>
/// Imports recipes from a three-sheet Excel workbook (Recipes, Ingredients, Steps).
/// Also generates a downloadable template in the same format.
/// </summary>
public class RecipeExcelService : IRecipeExcelService
{
    private readonly AppDbContext _db;
    private readonly ILogger<RecipeExcelService> _logger;

    public RecipeExcelService(AppDbContext db, ILogger<RecipeExcelService> logger)
    {
        _db = db;
        _logger = logger;
    }

    public byte[] GenerateTemplate()
    {
        using var workbook = new XLWorkbook();

        var recipes = workbook.Worksheets.Add("Recipes");
        recipes.Cell(1, 1).Value = "Name";
        recipes.Cell(1, 2).Value = "MealCategory";
        recipes.Cell(1, 3).Value = "Calories";
        recipes.Cell(1, 4).Value = "Protein";
        recipes.Cell(1, 5).Value = "Fat";
        recipes.Cell(1, 6).Value = "Carbs";
        recipes.Cell(1, 7).Value = "PrepTimeMinutes";
        recipes.Row(1).Style.Font.Bold = true;
        recipes.Cell(2, 1).Value = "Oatmeal with berries";
        recipes.Cell(2, 2).Value = "Breakfast";
        recipes.Cell(2, 3).Value = 320;
        recipes.Cell(2, 4).Value = 12;
        recipes.Cell(2, 5).Value = 8;
        recipes.Cell(2, 6).Value = 45;
        recipes.Cell(2, 7).Value = 10;
        recipes.Columns().AdjustToContents();

        var ingredients = workbook.Worksheets.Add("Ingredients");
        ingredients.Cell(1, 1).Value = "RecipeName";
        ingredients.Cell(1, 2).Value = "Name";
        ingredients.Cell(1, 3).Value = "AmountGrams";
        ingredients.Cell(1, 4).Value = "Unit";
        ingredients.Cell(1, 5).Value = "SortOrder";
        ingredients.Row(1).Style.Font.Bold = true;
        ingredients.Cell(2, 1).Value = "Oatmeal with berries";
        ingredients.Cell(2, 2).Value = "Rolled oats";
        ingredients.Cell(2, 3).Value = 80;
        ingredients.Cell(2, 4).Value = "g";
        ingredients.Cell(2, 5).Value = 0;
        ingredients.Cell(3, 1).Value = "Oatmeal with berries";
        ingredients.Cell(3, 2).Value = "Blueberries";
        ingredients.Cell(3, 3).Value = 100;
        ingredients.Cell(3, 4).Value = "g";
        ingredients.Cell(3, 5).Value = 1;
        ingredients.Columns().AdjustToContents();

        var steps = workbook.Worksheets.Add("Steps");
        steps.Cell(1, 1).Value = "RecipeName";
        steps.Cell(1, 2).Value = "StepNumber";
        steps.Cell(1, 3).Value = "Description";
        steps.Row(1).Style.Font.Bold = true;
        steps.Cell(2, 1).Value = "Oatmeal with berries";
        steps.Cell(2, 2).Value = 1;
        steps.Cell(2, 3).Value = "Cook oats in milk for 5 minutes.";
        steps.Cell(3, 1).Value = "Oatmeal with berries";
        steps.Cell(3, 2).Value = 2;
        steps.Cell(3, 3).Value = "Top with berries and serve.";
        steps.Columns().AdjustToContents();

        var help = workbook.Worksheets.Add("Help");
        help.Cell(1, 1).Value = "MealCategory values";
        help.Cell(2, 1).Value = "Breakfast, SecondBreakfast (or Second Breakfast), Lunch, Dinner";
        help.Cell(2, 2).Value = "Or numeric: 0, 1, 2, 3";
        help.Cell(4, 1).Value = "RecipeName in Ingredients/Steps must match Name in Recipes sheet exactly.";
        help.Columns().AdjustToContents();

        using var stream = new MemoryStream();
        workbook.SaveAs(stream);
        return stream.ToArray();
    }

    public async Task<RecipeImportResultDto> ImportAsync(Stream fileStream, CancellationToken ct = default)
    {
        var result = new RecipeImportResultDto();

        using var workbook = new XLWorkbook(fileStream);
        var recipeSheet = workbook.Worksheets.FirstOrDefault(w =>
            w.Name.Equals("Recipes", StringComparison.OrdinalIgnoreCase));
        if (recipeSheet is null)
        {
            result.Errors.Add("Worksheet 'Recipes' not found.");
            return result;
        }

        var ingredientSheet = workbook.Worksheets.FirstOrDefault(w =>
            w.Name.Equals("Ingredients", StringComparison.OrdinalIgnoreCase));
        var stepSheet = workbook.Worksheets.FirstOrDefault(w =>
            w.Name.Equals("Steps", StringComparison.OrdinalIgnoreCase));

        var ingredientRows = ParseIngredients(ingredientSheet, result.Errors);
        var stepRows = ParseSteps(stepSheet, result.Errors);

        var recipeRows = recipeSheet.RowsUsed().Skip(1);
        foreach (var row in recipeRows)
        {
            var name = row.Cell(1).GetString().Trim();
            if (string.IsNullOrEmpty(name))
            {
                continue;
            }

            if (!TryParseCategory(row.Cell(2).GetString(), out var category, out var categoryError))
            {
                result.Errors.Add($"Recipe '{name}': {categoryError}");
                result.SkippedCount++;
                continue;
            }

            var dto = new CreateRecipeDto
            {
                Name = name,
                MealCategory = category,
                Calories = ParseNullableInt(row.Cell(3)),
                Protein = ParseNullableDecimal(row.Cell(4)),
                Fat = ParseNullableDecimal(row.Cell(5)),
                Carbs = ParseNullableDecimal(row.Cell(6)),
                PrepTimeMinutes = ParseNullableInt(row.Cell(7)),
                Ingredients = ingredientRows
                    .Where(i => i.RecipeName.Equals(name, StringComparison.OrdinalIgnoreCase))
                    .Select(i => new CreateIngredientDto
                    {
                        Name = i.Name,
                        AmountGrams = i.AmountGrams,
                        Unit = i.Unit,
                        SortOrder = i.SortOrder,
                    })
                    .ToList(),
                Steps = stepRows
                    .Where(s => s.RecipeName.Equals(name, StringComparison.OrdinalIgnoreCase))
                    .OrderBy(s => s.StepNumber)
                    .Select(s => new CreateRecipeStepDto
                    {
                        StepNumber = s.StepNumber,
                        Description = s.Description,
                    })
                    .ToList(),
            };

            try
            {
                var exists = await _db.Recipes.AnyAsync(
                    r => r.IsActive && r.Name.ToLower() == name.ToLower(), ct);
                if (exists)
                {
                    result.Errors.Add($"Recipe '{name}' already exists — skipped.");
                    result.SkippedCount++;
                    continue;
                }

                var entity = RecipeManagementService.MapToEntity(dto);
                entity.CreatedAt = DateTime.UtcNow;
                entity.IsActive = true;
                _db.Recipes.Add(entity);
                await _db.SaveChangesAsync(ct);
                result.CreatedCount++;
                result.CreatedRecipeIds.Add(entity.Id);
                _logger.LogInformation("Imported recipe {RecipeId} '{Name}' from Excel", entity.Id, name);
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Failed to import recipe '{Name}'", name);
                result.Errors.Add($"Recipe '{name}': {ex.Message}");
                result.SkippedCount++;
            }
        }

        return result;
    }

    private static List<IngredientRow> ParseIngredients(IXLWorksheet? sheet, List<string> errors)
    {
        var rows = new List<IngredientRow>();
        if (sheet is null)
        {
            return rows;
        }

        foreach (var row in sheet.RowsUsed().Skip(1))
        {
            var recipeName = row.Cell(1).GetString().Trim();
            var name = row.Cell(2).GetString().Trim();
            if (string.IsNullOrEmpty(recipeName) || string.IsNullOrEmpty(name))
            {
                continue;
            }

            rows.Add(new IngredientRow(
                recipeName,
                name,
                ParseNullableDecimal(row.Cell(3)),
                NullIfEmpty(row.Cell(4).GetString()),
                ParseNullableInt(row.Cell(5)) ?? rows.Count(r =>
                    r.RecipeName.Equals(recipeName, StringComparison.OrdinalIgnoreCase))));
        }

        return rows;
    }

    private static List<StepRow> ParseSteps(IXLWorksheet? sheet, List<string> errors)
    {
        var rows = new List<StepRow>();
        if (sheet is null)
        {
            return rows;
        }

        foreach (var row in sheet.RowsUsed().Skip(1))
        {
            var recipeName = row.Cell(1).GetString().Trim();
            var stepNumber = ParseNullableInt(row.Cell(2));
            var description = row.Cell(3).GetString().Trim();
            if (string.IsNullOrEmpty(recipeName) || stepNumber is null or < 1 || string.IsNullOrEmpty(description))
            {
                continue;
            }

            rows.Add(new StepRow(recipeName, stepNumber.Value, description));
        }

        return rows;
    }

    internal static bool TryParseCategory(string raw, out int category, out string error)
    {
        error = string.Empty;
        category = 0;
        var value = raw.Trim();

        if (int.TryParse(value, out category) && category is >= 0 and <= 3)
        {
            return true;
        }

        category = value.ToLowerInvariant().Replace(" ", "") switch
        {
            "breakfast" => 0,
            "secondbreakfast" => 1,
            "lunch" => 2,
            "dinner" => 3,
            _ => -1,
        };

        if (category < 0)
        {
            error = $"Invalid MealCategory '{raw}'. Use Breakfast, SecondBreakfast, Lunch, Dinner or 0–3.";
            return false;
        }

        return true;
    }

    private static int? ParseNullableInt(IXLCell cell)
    {
        if (cell.IsEmpty())
        {
            return null;
        }

        if (cell.TryGetValue(out int intValue))
        {
            return intValue;
        }

        return int.TryParse(cell.GetString(), out intValue) ? intValue : null;
    }

    private static decimal? ParseNullableDecimal(IXLCell cell)
    {
        if (cell.IsEmpty())
        {
            return null;
        }

        if (cell.TryGetValue(out double doubleValue))
        {
            return (decimal)doubleValue;
        }

        return decimal.TryParse(cell.GetString(), out var decimalValue) ? decimalValue : null;
    }

    private static string? NullIfEmpty(string value) =>
        string.IsNullOrWhiteSpace(value) ? null : value.Trim();

    private sealed record IngredientRow(string RecipeName, string Name, decimal? AmountGrams, string? Unit, int SortOrder);

    private sealed record StepRow(string RecipeName, int StepNumber, string Description);
}
