using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using FootwearTest.Models;
using Microsoft.Extensions.Logging;

namespace FootwearTest.Services;

public sealed class ExcelReportService
{
    private readonly ILogger<ExcelReportService> _logger;

    public ExcelReportService(ILogger<ExcelReportService> logger)
    {
        _logger = logger;
    }

    public async Task<string> ExportAsync(TestRunRecord record)
    {
        try
        {
            var directory = Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory),
                "整鞋试验报告");
            Directory.CreateDirectory(directory);

            var fileName = $"{record.CreatedAt:yyyyMMdd_HHmmss}_{SanitizeFileName(record.Method)}_{record.Id}.xlsx";
            var path = Path.Combine(directory, fileName);
            _logger.LogInformation("Exporting Excel report. RecordId={RecordId}, Method={Method}, Path={Path}", record.Id, record.Method, path);
            await using var stream = File.Create(path);
            WriteWorkbook(stream, CreateSheets(record));
            _logger.LogInformation("Excel report exported. RecordId={RecordId}, Path={Path}", record.Id, path);
            return path;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to export Excel report. RecordId={RecordId}, Method={Method}", record.Id, record.Method);
            throw;
        }
    }

    private static IReadOnlyList<WorksheetData> CreateSheets(TestRunRecord record)
    {
        var sheets = new List<WorksheetData>
        {
            CreateCoverSheet(record),
            CreateStandardChecklistSheet(record),
        };

        using var document = JsonDocument.Parse(record.DataJson);
        var root = document.RootElement;
        if (record.Method.StartsWith("方法 A", StringComparison.Ordinal))
        {
            sheets.Add(CreateMethodAResultSheet(root));
            sheets.Add(CreateMethodASampleSheet(root));
        }
        else if (record.Method.StartsWith("方法 B", StringComparison.Ordinal))
        {
            sheets.Add(CreateMethodBResultSheet(root, record.Method));
            sheets.Add(CreateMethodBMassSheet(root));
            sheets.Add(CreateProcedureSheet(root));
        }

        sheets.Add(CreateRawJsonSheet(root));
        return sheets;
    }

    private static WorksheetData CreateCoverSheet(TestRunRecord record)
    {
        return new WorksheetData(
            "报告封面",
            [
                Row("GB/T 33393-2023 整鞋试验报告"),
                Row("报告项目", "内容"),
                Row("记录编号", record.Id),
                Row("文件编号", "GB/T 33393-2023"),
                Row("使用的试验方法", record.Method),
                Row("试样描述", record.SampleDescription),
                Row("试验条件", record.Conditions),
                Row("试验结果", record.ResultSummary),
                Row("试验日期", record.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)),
                Row("与试验方法的偏差", record.IsValid ? "无已知偏差" : "结果被标记为需复核或重测"),
            ]);
    }

    private static WorksheetData CreateStandardChecklistSheet(TestRunRecord record)
    {
        var rows = new List<IReadOnlyList<object?>>
        {
            Row("GB/T 33393-2023 核对项"),
            Row("类别", "标准要求", "本次记录"),
            Row("报告", "报告包含文件编号、方法、试样描述、试验条件、试验结果、日期和偏差", "已写入报告封面"),
        };

        if (record.Method.StartsWith("方法 A", StringComparison.Ordinal))
        {
            rows.Add(Row("方法 A 试样", "试样数量 2 只；试验前按 GB/T 22049 标准环境调节 24 h", record.SampleDescription));
            rows.Add(Row("方法 A 环境", "环境 (23±2) ℃、(50±5)% RH；空气流速 (1.00±0.15) m/s", record.Conditions));
            rows.Add(Row("方法 A 假脚", "假脚/模拟皮肤温度 (35.0±0.3) ℃；水汽化热 0.672 W·h/g", record.Conditions));
            rows.Add(Row("方法 A 采样", "每分钟记录一次，连续记录至少 30 次；结果取连续 30 次平均值", "见“方法A连续数据”"));
            rows.Add(Row("方法 A 偏差", "任一测定值与两只鞋测试结果平均值的偏差不应超过 ±10%", record.IsValid ? "通过" : "需复核或重测"));
        }
        else if (record.Method.Contains("吸湿透水汽", StringComparison.Ordinal))
        {
            rows.Add(Row("方法 B 试样", "满帮鞋同号两只；试验前在 (23±2) ℃、(50±5)% RH 标准环境调节", record.SampleDescription));
            rows.Add(Row("方法 B 供水", "泵流量 (5.0±0.3) cm3/h；测试周期 (180±1) min", record.Conditions));
            rows.Add(Row("方法 B 称重", "m11-m72 均精确到 0.01 g；m180 应在 (15±0.9) g 范围内", record.IsValid ? "m180 有效" : "m180 超限"));
            rows.Add(Row("方法 B 结果", "透水汽性能 T180*；吸湿透水汽性能 XT180*=T180*+m3*+m4*，保留两位小数", record.ResultSummary));
        }
        else
        {
            rows.Add(Row("方法 B 保暖", "假脚温度 (38±1) ℃；测试周期 (180±1) min，连续测试 2 个周期", record.Conditions));
            rows.Add(Row("方法 B 热阻", "Q=P/t×1000；R=S×(Tf-Tc)/Q，结果保留三位小数", record.ResultSummary));
        }

        return new WorksheetData("标准核对", rows);
    }

    private static WorksheetData CreateMethodAResultSheet(JsonElement root)
    {
        var result = root.GetProperty("Result");
        return new WorksheetData(
            "方法A结果",
            [
                Row("项目", "数值", "单位/说明"),
                Row("阶段", GetString(result, "Stage"), ""),
                Row("皮肤湿阻 Res", GetDouble(result, "SkinMoistureResistance"), "Pa·m2/W"),
                Row("整鞋平均湿阻 Re", GetDouble(result, "AverageMoistureResistance"), "Pa·m2/W"),
                Row("整鞋平均热阻 Rt", GetDouble(result, "AverageThermalResistance"), "m2·℃/W"),
                Row("湿阻变异系数", GetDouble(result, "MoistureCoefficientOfVariation"), "%"),
                Row("热阻变异系数", GetDouble(result, "ThermalCoefficientOfVariation"), "%"),
                Row("±10% 偏差检查", GetBool(result, "PassedDeviationCheck") ? "通过" : "需复核或重测", ""),
                Row("湿热量 He", "He=λ×Q，λ=0.672 W·h/g", "W"),
                Row("湿阻 Re", "Re=A×(Psi×RHsi-Pa×RHa)/He-Res", "Pa·m2/W"),
                Row("干热量 Hd", "Hd=Hs-He", "W"),
                Row("热阻 Rt", "Rt=A×(Ts-Ta)/Hd", "m2·℃/W"),
            ]);
    }

    private static WorksheetData CreateMethodASampleSheet(JsonElement root)
    {
        var rows = new List<IReadOnlyList<object?>>
        {
            Row("序号", "时间", "假脚温度 ℃", "环境温度 ℃", "相对湿度 %", "出汗量 g/h", "加热功率 W", "湿热量 He W", "干热量 Hd W", "湿阻 Pa·m2/W", "热阻 m2·℃/W"),
        };

        if (root.TryGetProperty("Samples", out var samples) && samples.ValueKind == JsonValueKind.Array)
        {
            foreach (var sample in samples.EnumerateArray())
            {
                var waterLoss = GetDouble(sample, "WaterLossGramsPerHour");
                var power = GetDouble(sample, "PowerWatts");
                var moistureHeat = sample.TryGetProperty("MoistureHeatWatts", out _) ? GetDouble(sample, "MoistureHeatWatts") : Math.Round(0.672 * Math.Max(waterLoss, 0.001), 3);
                var dryHeat = sample.TryGetProperty("DryHeatWatts", out _) ? GetDouble(sample, "DryHeatWatts") : Math.Round(Math.Max(power - moistureHeat, 0.001), 3);
                rows.Add(Row(
                    GetDouble(sample, "Index"),
                    GetString(sample, "Timestamp"),
                    GetDouble(sample, "FootTemperatureC"),
                    GetDouble(sample, "EnvironmentTemperatureC"),
                    GetDouble(sample, "RelativeHumidityPercent"),
                    waterLoss,
                    power,
                    moistureHeat,
                    dryHeat,
                    GetDouble(sample, "MoistureResistance"),
                    GetDouble(sample, "ThermalResistance")));
            }
        }

        return new WorksheetData("方法A连续数据", rows);
    }

    private static WorksheetData CreateMethodBResultSheet(JsonElement root, string method)
    {
        var result = root.GetProperty("Result");
        var rows = new List<IReadOnlyList<object?>>
        {
            Row("项目", "数值", "单位/说明"),
        };

        if (method.Contains("吸湿透水汽", StringComparison.Ordinal))
        {
            rows.Add(Row("m5", GetDouble(result, "M5"), "g，C1 消耗水量"));
            rows.Add(Row("m6", GetDouble(result, "M6"), "g，C2 蒸发水量"));
            rows.Add(Row("m7", GetDouble(result, "M7"), "g，C3 蒸发水量"));
            rows.Add(Row("m8", GetDouble(result, "M8"), "g，C2/C3 平均蒸发量"));
            rows.Add(Row("m180", GetDouble(result, "M180"), "g，泵进鞋腔内水总质量，15±0.9 g"));
            rows.Add(Row("m1", GetDouble(result, "M1"), "g，模拟皮肤质量变化"));
            rows.Add(Row("m2", GetDouble(result, "M2"), "g，标准长筒袜质量变化"));
            rows.Add(Row("m3", GetDouble(result, "M3"), "g，样品鞋质量变化"));
            rows.Add(Row("m4", GetDouble(result, "M4"), "g，鞋垫质量变化"));
            rows.Add(Row("T180", GetDouble(result, "T180"), "g，散发到环境中的水汽质量"));
            rows.Add(Row("m1*", GetDouble(result, "M1Corrected"), "g，按 15 g 修正"));
            rows.Add(Row("m2*", GetDouble(result, "M2Corrected"), "g，按 15 g 修正"));
            rows.Add(Row("m3*", GetDouble(result, "M3Corrected"), "g，按 15 g 修正"));
            rows.Add(Row("m4*", GetDouble(result, "M4Corrected"), "g，按 15 g 修正"));
            rows.Add(Row("T180*", GetDouble(result, "T180Corrected"), "g，透水汽性能"));
            rows.Add(Row("XT180*", GetDouble(result, "XT180Corrected"), "g，吸湿透水汽性能"));
            rows.Add(Row("有效性", GetBool(result, "IsValid") ? "有效" : "m180 超出 15±0.9 g", ""));
        }
        else
        {
            rows.Add(Row("P", GetDouble(result, "EnergyKilojoules"), "kJ，180 min 能量消耗"));
            rows.Add(Row("t", GetDouble(result, "Seconds"), "s，测试周期"));
            rows.Add(Row("Q", GetDouble(result, "HeatWatts"), "W，单位时间热量消耗"));
            rows.Add(Row("Tf", GetDouble(result, "FootTemperatureC"), "℃，假脚表面温度平均值"));
            rows.Add(Row("Tc", GetDouble(result, "ChamberTemperatureC"), "℃，测试箱环境温度平均值"));
            rows.Add(Row("R", GetDouble(result, "ThermalResistance"), "m2·℃/W，保暖性能热阻值"));
        }

        return new WorksheetData("方法B结果", rows);
    }

    private static WorksheetData CreateMethodBMassSheet(JsonElement root)
    {
        var rows = new List<IReadOnlyList<object?>>
        {
            Row("字段", "数值 g", "说明"),
        };

        if (root.TryGetProperty("Masses", out var masses))
        {
            rows.Add(Row("m11", GetDouble(masses, "M11"), "模拟皮肤测试前质量"));
            rows.Add(Row("m12", GetDouble(masses, "M12"), "模拟皮肤测试后质量"));
            rows.Add(Row("m21", GetDouble(masses, "M21"), "标准长筒袜测试前质量"));
            rows.Add(Row("m22", GetDouble(masses, "M22"), "标准长筒袜测试后质量"));
            rows.Add(Row("m31", GetDouble(masses, "M31"), "样品鞋测试前质量"));
            rows.Add(Row("m32", GetDouble(masses, "M32"), "样品鞋测试后质量"));
            rows.Add(Row("m41", GetDouble(masses, "M41"), "鞋垫测试前质量"));
            rows.Add(Row("m42", GetDouble(masses, "M42"), "鞋垫测试后质量"));
            rows.Add(Row("m51", GetDouble(masses, "M51"), "C1 测试前质量"));
            rows.Add(Row("m52", GetDouble(masses, "M52"), "C1 测试后质量"));
            rows.Add(Row("m61", GetDouble(masses, "M61"), "C2 测试前质量"));
            rows.Add(Row("m62", GetDouble(masses, "M62"), "C2 测试后质量"));
            rows.Add(Row("m71", GetDouble(masses, "M71"), "C3 测试前质量"));
            rows.Add(Row("m72", GetDouble(masses, "M72"), "C3 测试后质量"));
        }

        return new WorksheetData("方法B称重", rows);
    }

    private static WorksheetData CreateProcedureSheet(JsonElement root)
    {
        var rows = new List<IReadOnlyList<object?>>
        {
            Row("序号", "流程记录"),
        };

        if (root.TryGetProperty("ProcedureLog", out var log) && log.ValueKind == JsonValueKind.Array)
        {
            var index = 1;
            foreach (var item in log.EnumerateArray())
            {
                rows.Add(Row(index++, item.GetString() ?? ""));
            }
        }

        return new WorksheetData("流程记录", rows);
    }

    private static WorksheetData CreateRawJsonSheet(JsonElement root)
    {
        var rows = new List<IReadOnlyList<object?>>
        {
            Row("路径", "值"),
        };
        FlattenJson(rows, "", root);
        return new WorksheetData("完整原始数据", rows);
    }

    private static void FlattenJson(List<IReadOnlyList<object?>> rows, string path, JsonElement element)
    {
        switch (element.ValueKind)
        {
            case JsonValueKind.Object:
                foreach (var property in element.EnumerateObject())
                {
                    var nextPath = string.IsNullOrWhiteSpace(path) ? property.Name : $"{path}.{property.Name}";
                    FlattenJson(rows, nextPath, property.Value);
                }
                break;
            case JsonValueKind.Array:
                var index = 0;
                foreach (var item in element.EnumerateArray())
                {
                    FlattenJson(rows, $"{path}[{index++}]", item);
                }
                break;
            case JsonValueKind.Number:
                rows.Add(Row(path, element.TryGetDouble(out var number) ? number : element.GetRawText()));
                break;
            case JsonValueKind.True:
            case JsonValueKind.False:
                rows.Add(Row(path, element.GetBoolean() ? "true" : "false"));
                break;
            case JsonValueKind.String:
                rows.Add(Row(path, element.GetString() ?? ""));
                break;
            default:
                rows.Add(Row(path, ""));
                break;
        }
    }

    private static void WriteWorkbook(Stream stream, IReadOnlyList<WorksheetData> sheets)
    {
        using var archive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true);
        AddText(archive, "[Content_Types].xml", CreateContentTypes(sheets.Count));
        AddText(archive, "_rels/.rels", """
            <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
            <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
              <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
            </Relationships>
            """);
        AddText(archive, "xl/workbook.xml", CreateWorkbookXml(sheets));
        AddText(archive, "xl/_rels/workbook.xml.rels", CreateWorkbookRelationships(sheets.Count));
        AddText(archive, "xl/styles.xml", """
            <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
            <styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
              <fonts count="1"><font><sz val="11"/><name val="Microsoft YaHei"/></font></fonts>
              <fills count="1"><fill><patternFill patternType="none"/></fill></fills>
              <borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders>
              <cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>
              <cellXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/></cellXfs>
              <cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>
            </styleSheet>
            """);

        for (var i = 0; i < sheets.Count; i++)
        {
            AddText(archive, $"xl/worksheets/sheet{i + 1}.xml", CreateWorksheetXml(sheets[i]));
        }
    }

    private static string CreateContentTypes(int sheetCount)
    {
        var builder = new StringBuilder();
        builder.Append("""
            <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
            <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
              <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
              <Default Extension="xml" ContentType="application/xml"/>
              <Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
              <Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
            """);
        for (var i = 1; i <= sheetCount; i++)
        {
            builder.Append(CultureInfo.InvariantCulture, $"  <Override PartName=\"/xl/worksheets/sheet{i}.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml\"/>\n");
        }

        builder.Append("</Types>");
        return builder.ToString();
    }

    private static string CreateWorkbookXml(IReadOnlyList<WorksheetData> sheets)
    {
        var builder = new StringBuilder();
        builder.Append("""
            <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
            <workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
              <sheets>
            """);
        for (var i = 0; i < sheets.Count; i++)
        {
            builder.Append(CultureInfo.InvariantCulture, $"    <sheet name=\"{EscapeXml(TrimSheetName(sheets[i].Name))}\" sheetId=\"{i + 1}\" r:id=\"rId{i + 1}\"/>\n");
        }

        builder.Append("""
              </sheets>
            </workbook>
            """);
        return builder.ToString();
    }

    private static string CreateWorkbookRelationships(int sheetCount)
    {
        var builder = new StringBuilder();
        builder.Append("""
            <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
            <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
            """);
        for (var i = 1; i <= sheetCount; i++)
        {
            builder.Append(CultureInfo.InvariantCulture, $"  <Relationship Id=\"rId{i}\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet\" Target=\"worksheets/sheet{i}.xml\"/>\n");
        }

        builder.Append(CultureInfo.InvariantCulture, $"  <Relationship Id=\"rId{sheetCount + 1}\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles\" Target=\"styles.xml\"/>\n");
        builder.Append("</Relationships>");
        return builder.ToString();
    }

    private static string CreateWorksheetXml(WorksheetData sheet)
    {
        var builder = new StringBuilder();
        builder.Append("""
            <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
            <worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
              <sheetViews><sheetView workbookViewId="0"/></sheetViews>
              <sheetFormatPr defaultRowHeight="18"/>
              <cols>
                <col min="1" max="1" width="22" customWidth="1"/>
                <col min="2" max="20" width="24" customWidth="1"/>
              </cols>
              <sheetData>
            """);

        for (var rowIndex = 0; rowIndex < sheet.Rows.Count; rowIndex++)
        {
            var rowNumber = rowIndex + 1;
            builder.Append(CultureInfo.InvariantCulture, $"    <row r=\"{rowNumber}\">\n");
            var row = sheet.Rows[rowIndex];
            for (var columnIndex = 0; columnIndex < row.Count; columnIndex++)
            {
                AppendCell(builder, row[columnIndex], rowNumber, columnIndex + 1);
            }

            builder.Append("    </row>\n");
        }

        builder.Append("""
              </sheetData>
            </worksheet>
            """);
        return builder.ToString();
    }

    private static void AppendCell(StringBuilder builder, object? value, int row, int column)
    {
        var reference = $"{ColumnName(column)}{row}";
        switch (value)
        {
            case null:
                builder.Append(CultureInfo.InvariantCulture, $"      <c r=\"{reference}\"/>\n");
                break;
            case byte or sbyte or short or ushort or int or uint or long or ulong or float or double or decimal:
                builder.Append(CultureInfo.InvariantCulture, $"      <c r=\"{reference}\"><v>{Convert.ToString(value, CultureInfo.InvariantCulture)}</v></c>\n");
                break;
            default:
                builder.Append(CultureInfo.InvariantCulture, $"      <c r=\"{reference}\" t=\"inlineStr\"><is><t>{EscapeXml(Convert.ToString(value, CultureInfo.InvariantCulture) ?? "")}</t></is></c>\n");
                break;
        }
    }

    private static void AddText(ZipArchive archive, string name, string content)
    {
        var entry = archive.CreateEntry(name, CompressionLevel.Optimal);
        using var writer = new StreamWriter(entry.Open(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
        writer.Write(content);
    }

    private static IReadOnlyList<object?> Row(params object?[] values) => values;

    private static double GetDouble(JsonElement element, string property)
    {
        return element.TryGetProperty(property, out var value) && value.TryGetDouble(out var number) ? number : 0.0;
    }

    private static string GetString(JsonElement element, string property)
    {
        return element.TryGetProperty(property, out var value) ? value.ToString() : "";
    }

    private static bool GetBool(JsonElement element, string property)
    {
        return element.TryGetProperty(property, out var value) && value.ValueKind == JsonValueKind.True;
    }

    private static string ColumnName(int column)
    {
        var name = "";
        while (column > 0)
        {
            column--;
            name = (char)('A' + column % 26) + name;
            column /= 26;
        }

        return name;
    }

    private static string EscapeXml(string value)
    {
        return value
            .Replace("&", "&amp;", StringComparison.Ordinal)
            .Replace("<", "&lt;", StringComparison.Ordinal)
            .Replace(">", "&gt;", StringComparison.Ordinal)
            .Replace("\"", "&quot;", StringComparison.Ordinal)
            .Replace("'", "&apos;", StringComparison.Ordinal);
    }

    private static string TrimSheetName(string name)
    {
        return name.Length <= 31 ? name : name[..31];
    }

    private static string SanitizeFileName(string value)
    {
        var invalid = Path.GetInvalidFileNameChars();
        return new string(value.Select(ch => invalid.Contains(ch) ? '_' : ch).ToArray());
    }

    private sealed record WorksheetData(string Name, IReadOnlyList<IReadOnlyList<object?>> Rows);
}
