using ClosedXML.Excel;

namespace Jiaowu.Api.Infrastructure.Excel;

public static class ExcelWorkbookHelper
{
    public const string ContentType =
        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";

    public static byte[] Create(
        string sheetName,
        IReadOnlyList<string> headers,
        IEnumerable<IReadOnlyList<object?>> rows,
        IReadOnlyList<string>? instructions = null,
        Action<IXLWorksheet, int>? configureRow = null)
    {
        using var workbook = new XLWorkbook();
        var sheet = workbook.Worksheets.Add(sheetName);

        for (var column = 0; column < headers.Count; column++)
        {
            var cell = sheet.Cell(1, column + 1);
            cell.Value = headers[column];
            cell.Style.Font.Bold = true;
            cell.Style.Font.FontColor = XLColor.White;
            cell.Style.Fill.BackgroundColor = XLColor.FromHtml("#1F3A6D");
            cell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
        }

        var rowNumber = 2;
        foreach (var row in rows)
        {
            for (var column = 0; column < headers.Count && column < row.Count; column++)
            {
                SetCellValue(sheet.Cell(rowNumber, column + 1), row[column]);
            }
            configureRow?.Invoke(sheet, rowNumber);
            rowNumber++;
        }

        sheet.SheetView.FreezeRows(1);
        sheet.RangeUsed()?.SetAutoFilter();
        sheet.Columns().AdjustToContents(10, 38);
        sheet.Rows(1, Math.Max(1, rowNumber - 1)).Style.Alignment.Vertical =
            XLAlignmentVerticalValues.Center;

        if (instructions is { Count: > 0 })
        {
            var guide = workbook.Worksheets.Add("填写说明", 1);
            guide.Cell("A1").Value = $"{sheetName} Excel 导入说明";
            guide.Cell("A1").Style.Font.Bold = true;
            guide.Cell("A1").Style.Font.FontSize = 16;
            guide.Cell("A1").Style.Font.FontColor = XLColor.FromHtml("#1F3A6D");
            for (var index = 0; index < instructions.Count; index++)
            {
                guide.Cell(index + 3, 1).Value = $"{index + 1}. {instructions[index]}";
            }
            guide.Column(1).Width = 92;
            guide.Style.Alignment.WrapText = true;
        }

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

    public static async Task<IReadOnlyList<ExcelRow>> ReadAsync(
        IFormFile file,
        IReadOnlyCollection<string> requiredHeaders,
        CancellationToken cancellationToken)
    {
        if (file.Length == 0)
            throw new InvalidDataException("请选择包含数据的 Excel 文件。");
        if (file.Length > 10 * 1024 * 1024)
            throw new InvalidDataException("Excel 文件不能超过 10 MB。");
        if (!string.Equals(Path.GetExtension(file.FileName), ".xlsx",
                StringComparison.OrdinalIgnoreCase))
            throw new InvalidDataException("仅支持 .xlsx 格式，请先下载模板填写。");

        await using var stream = new MemoryStream();
        await file.CopyToAsync(stream, cancellationToken);
        stream.Position = 0;

        try
        {
            using var workbook = new XLWorkbook(stream);
            var sheet = workbook.Worksheets
                .FirstOrDefault(x => !string.Equals(
                    x.Name,
                    "填写说明",
                    StringComparison.OrdinalIgnoreCase));
            if (sheet is null)
                throw new InvalidDataException("Excel 中没有可导入的数据工作表。");

            var lastColumn = sheet.LastColumnUsed()?.ColumnNumber() ?? 0;
            var headerMap = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
            for (var column = 1; column <= lastColumn; column++)
            {
                var header = sheet.Cell(1, column).GetString().Trim();
                if (header.Length > 0) headerMap.TryAdd(header, column);
            }

            var missing = requiredHeaders.Where(x => !headerMap.ContainsKey(x)).ToArray();
            if (missing.Length > 0)
                throw new InvalidDataException($"缺少必需列：{string.Join("、", missing)}。");

            var lastRow = sheet.LastRowUsed()?.RowNumber() ?? 1;
            if (lastRow > 2001)
                throw new InvalidDataException("单次最多导入 2000 条数据。");

            var result = new List<ExcelRow>();
            for (var rowNumber = 2; rowNumber <= lastRow; rowNumber++)
            {
                var values = headerMap.ToDictionary(
                    pair => pair.Key,
                    pair => GetCellText(sheet.Cell(rowNumber, pair.Value)),
                    StringComparer.OrdinalIgnoreCase);
                if (values.Values.All(string.IsNullOrWhiteSpace)) continue;
                result.Add(new ExcelRow(rowNumber, values));
            }
            return result;
        }
        catch (InvalidDataException)
        {
            throw;
        }
        catch (Exception exception)
        {
            throw new InvalidDataException(
                "无法读取 Excel 文件，请确认文件未损坏且使用系统模板填写。",
                exception);
        }
    }

    private static string GetCellText(IXLCell cell)
    {
        if (cell.DataType == XLDataType.DateTime)
            return cell.GetDateTime().ToString("yyyy-MM-dd");
        if (cell.DataType == XLDataType.Number)
            return cell.GetDouble().ToString("0.################");
        return cell.GetFormattedString().Trim();
    }

    private static void SetCellValue(IXLCell cell, object? value)
    {
        switch (value)
        {
            case null:
                cell.Value = string.Empty;
                break;
            case DateOnly date:
                cell.Value = date.ToDateTime(TimeOnly.MinValue);
                cell.Style.DateFormat.Format = "yyyy-mm-dd";
                break;
            case DateTime dateTime:
                cell.Value = dateTime;
                cell.Style.DateFormat.Format = "yyyy-mm-dd hh:mm";
                break;
            case bool boolean:
                cell.Value = boolean ? "是" : "否";
                break;
            case int integer:
                cell.Value = integer;
                break;
            case long integer:
                cell.Value = integer;
                break;
            case decimal number:
                cell.Value = number;
                break;
            case double number:
                cell.Value = number;
                break;
            default:
                cell.Value = value.ToString() ?? string.Empty;
                break;
        }
    }
}

public sealed record ExcelRow(
    int RowNumber,
    IReadOnlyDictionary<string, string> Values)
{
    public string this[string header] =>
        Values.TryGetValue(header, out var value) ? value.Trim() : string.Empty;
}

public sealed record ExcelImportResult(int Created, int Updated, int Total);
