package com.ruoyi.warehouse.service.impl;

import java.io.File;
import java.io.FileInputStream;
import java.math.BigDecimal;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import org.apache.commons.io.FilenameUtils;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.service.ISysUserService;
import com.ruoyi.warehouse.domain.InRecord;
import com.ruoyi.warehouse.domain.OutRecord;
import com.ruoyi.warehouse.domain.ProductInfo;
import com.ruoyi.warehouse.domain.StockCheckRecord;
import com.ruoyi.warehouse.domain.StockInfo;
import com.ruoyi.warehouse.domain.WarehouseArea;
import com.ruoyi.warehouse.domain.WmsBatchItem;
import com.ruoyi.warehouse.domain.WmsBatchJob;
import com.ruoyi.warehouse.mapper.WmsBatchItemMapper;
import com.ruoyi.warehouse.mapper.WmsBatchJobMapper;
import com.ruoyi.warehouse.service.IInRecordService;
import com.ruoyi.warehouse.service.IOutRecordService;
import com.ruoyi.warehouse.service.IProductInfoService;
import com.ruoyi.warehouse.service.IStockCheckRecordService;
import com.ruoyi.warehouse.service.IStockInfoService;
import com.ruoyi.warehouse.service.IWarehouseAreaService;
import com.ruoyi.warehouse.service.IWmsBatchImportService;

@Service
public class WmsBatchImportServiceImpl implements IWmsBatchImportService
{
    private static final String ACTION_AREA = "create_area";
    private static final String ACTION_PRODUCT = "create_product";
    private static final String ACTION_IN = "create_in_record";
    private static final String ACTION_OUT = "create_out_record";
    private static final String ACTION_CHECK = "create_stock_check";
    private static final String ACTION_UNKNOWN = "unknown";

    private static final int JOB_UPLOADED = 0;
    private static final int JOB_PARSING = 1;
    private static final int JOB_WAIT_CONFIRM = 2;
    private static final int JOB_EXECUTING = 3;
    private static final int JOB_DONE = 4;
    private static final int JOB_FAILED = 5;

    private static final int ITEM_VALID = 1;
    private static final int ITEM_INVALID = 2;
    private static final int ITEM_DONE = 3;
    private static final int ITEM_FAILED = 4;

    @Value("${agent.openai.base-url}")
    private String baseUrl;

    @Value("${agent.openai.api-key}")
    private String apiKey;

    @Value("${agent.openai.model}")
    private String model;

    @Autowired
    private WmsBatchJobMapper jobMapper;

    @Autowired
    private WmsBatchItemMapper itemMapper;

    @Autowired
    private IWarehouseAreaService areaService;

    @Autowired
    private IProductInfoService productService;

    @Autowired
    private IInRecordService inRecordService;

    @Autowired
    private IOutRecordService outRecordService;

    @Autowired
    private IStockCheckRecordService stockCheckRecordService;

    @Autowired
    private IStockInfoService stockInfoService;

    @Autowired
    private ISysUserService userService;

    @Override
    public WmsBatchJob uploadExcel(MultipartFile file, String username) throws Exception
    {
        if (file == null || file.isEmpty())
        {
            throw new ServiceException("请选择要上传的文件");
        }
        String extension = StringUtils.defaultString(FilenameUtils.getExtension(file.getOriginalFilename())).toLowerCase(Locale.ROOT);
        if (!isExcel(extension) && !isImage(extension))
        {
            throw new ServiceException("当前支持 xls/xlsx/jpg/jpeg/png/bmp 文件");
        }

        String jobNo = "BATCH" + System.currentTimeMillis();
        String safeName = jobNo + "." + extension;
        Path dir = Paths.get(RuoYiConfig.getUploadPath(), "wms-batch").toAbsolutePath().normalize();
        Files.createDirectories(dir);
        Path savedFile = dir.resolve(safeName).normalize();
        try (var inputStream = file.getInputStream())
        {
            Files.copy(inputStream, savedFile, StandardCopyOption.REPLACE_EXISTING);
        }

        WmsBatchJob job = new WmsBatchJob();
        job.setJobNo(jobNo);
        job.setSourceType(isExcel(extension) ? "excel" : "image");
        job.setBizType("mixed");
        job.setFileName(file.getOriginalFilename());
        job.setFilePath(savedFile.toString());
        job.setStatus(JOB_UPLOADED);
        job.setProgress(0);
        job.setTotalCount(0);
        job.setSuccessCount(0);
        job.setFailCount(0);
        job.setCreateBy(username);
        jobMapper.insertWmsBatchJob(job);

        if (isExcel(extension))
        {
            CompletableFuture.runAsync(() -> parseExcelJob(job.getId(), username));
        }
        else
        {
            CompletableFuture.runAsync(() -> parseImageJob(job.getId(), username));
        }
        return job;
    }

    @Override
    public List<WmsBatchJob> selectJobList(WmsBatchJob job)
    {
        return jobMapper.selectWmsBatchJobList(job);
    }

    @Override
    public WmsBatchJob selectJobById(Long id)
    {
        return jobMapper.selectWmsBatchJobById(id);
    }

    @Override
    public List<WmsBatchItem> selectItemList(Long jobId)
    {
        WmsBatchItem query = new WmsBatchItem();
        query.setJobId(jobId);
        return itemMapper.selectWmsBatchItemList(query);
    }

    @Override
    public WmsBatchItem updateItem(WmsBatchItem item)
    {
        WmsBatchItem dbItem = itemMapper.selectWmsBatchItemById(item.getId());
        if (dbItem == null)
        {
            throw new ServiceException("明细不存在");
        }
        ensureJobEditable(dbItem.getJobId());
        WmsBatchJob job = jobMapper.selectWmsBatchJobById(dbItem.getJobId());
        item.setJobId(dbItem.getJobId());
        item.setRowNo(dbItem.getRowNo());
        item.setRawText(StringUtils.defaultIfBlank(item.getRawText(), dbItem.getRawText()));
        normalizeItem(item);
        validateItem(item, job == null ? null : job.getCreateBy());
        fillParsedJson(item);
        itemMapper.updateWmsBatchItem(item);
        refreshJobCount(item.getJobId(), JOB_WAIT_CONFIRM, 100, null);
        return itemMapper.selectWmsBatchItemById(item.getId());
    }

    @Override
    public int updateItemAction(Long jobId, String actionType, List<Long> itemIds)
    {
        ensureJobEditable(jobId);
        String parsedAction = parseAction(actionType);
        if (StringUtils.isBlank(parsedAction) || ACTION_UNKNOWN.equals(parsedAction))
        {
            throw new ServiceException("请选择有效的操作类型");
        }
        WmsBatchJob job = jobMapper.selectWmsBatchJobById(jobId);
        List<WmsBatchItem> items = selectItemList(jobId);
        int rows = 0;
        for (WmsBatchItem item : items)
        {
            if (itemIds != null && !itemIds.isEmpty() && !itemIds.contains(item.getId()))
            {
                continue;
            }
            item.setActionType(parsedAction);
            if (ACTION_IN.equals(parsedAction) && item.getTypeCode() == null)
            {
                item.setTypeCode(2);
            }
            if (ACTION_OUT.equals(parsedAction) && item.getTypeCode() == null)
            {
                item.setTypeCode(1);
            }
            normalizeItem(item);
            validateItem(item, job == null ? null : job.getCreateBy());
            fillParsedJson(item);
            rows += itemMapper.updateWmsBatchItem(item);
        }
        refreshJobCount(jobId, JOB_WAIT_CONFIRM, 100, null);
        return rows;
    }

    @Override
    public int deleteItem(Long id)
    {
        WmsBatchItem item = itemMapper.selectWmsBatchItemById(id);
        if (item == null)
        {
            return 0;
        }
        ensureJobEditable(item.getJobId());
        int rows = itemMapper.deleteWmsBatchItemById(id);
        refreshJobCount(item.getJobId(), JOB_WAIT_CONFIRM, 100, null);
        return rows;
    }

    @Override
    public WmsBatchJob confirmJob(Long jobId, String username)
    {
        WmsBatchJob job = jobMapper.selectWmsBatchJobById(jobId);
        if (job == null)
        {
            throw new ServiceException("批量任务不存在");
        }
        if (!Integer.valueOf(JOB_WAIT_CONFIRM).equals(job.getStatus()))
        {
            throw new ServiceException("当前任务还不能确认执行");
        }
        WmsBatchItem invalidQuery = new WmsBatchItem();
        invalidQuery.setJobId(jobId);
        invalidQuery.setStatus(ITEM_INVALID);
        if (itemMapper.countByJobIdAndStatus(invalidQuery) > 0)
        {
            throw new ServiceException("存在校验失败的明细，请先修改或删除后再确认");
        }

        updateJob(jobId, JOB_EXECUTING, 0, null);
        List<WmsBatchItem> items = selectItemList(jobId);
        int success = 0;
        int fail = 0;
        int total = items.size();
        for (int i = 0; i < total; i++)
        {
            WmsBatchItem item = items.get(i);
            try
            {
                executeItem(item, username);
                item.setStatus(ITEM_DONE);
                item.setErrorMsg(null);
                success++;
            }
            catch (Exception e)
            {
                item.setStatus(ITEM_FAILED);
                item.setErrorMsg(e.getMessage());
                fail++;
            }
            itemMapper.updateWmsBatchItem(item);
            updateJobProgress(jobId, JOB_EXECUTING, total, i + 1, success, fail);
        }
        WmsBatchJob finish = new WmsBatchJob();
        finish.setId(jobId);
        finish.setStatus(JOB_DONE);
        finish.setProgress(100);
        finish.setTotalCount(total);
        finish.setSuccessCount(success);
        finish.setFailCount(fail);
        finish.setErrorMsg(fail > 0 ? "部分明细执行失败，请查看明细错误原因" : null);
        jobMapper.updateWmsBatchJob(finish);
        return jobMapper.selectWmsBatchJobById(jobId);
    }

    private void parseExcelJob(Long jobId, String username)
    {
        try
        {
            WmsBatchJob job = jobMapper.selectWmsBatchJobById(jobId);
            updateJob(jobId, JOB_PARSING, 5, null);
            itemMapper.deleteWmsBatchItemByJobId(jobId);
            List<WmsBatchItem> items = parseExcel(job, username);
            int total = items.size();
            if (total == 0)
            {
                updateJob(jobId, JOB_FAILED, 100, "没有解析到有效数据行");
                return;
            }
            for (int i = 0; i < total; i++)
            {
                WmsBatchItem item = items.get(i);
                item.setJobId(jobId);
                normalizeItem(item);
                validateItem(item, username);
                fillParsedJson(item);
                itemMapper.insertWmsBatchItem(item);
                if ((i + 1) % 10 == 0 || i + 1 == total)
                {
                    int progress = 10 + (int) Math.floor((i + 1) * 80.0 / total);
                    refreshJobCount(jobId, JOB_PARSING, progress, null);
                }
            }
            refreshJobCount(jobId, JOB_WAIT_CONFIRM, 100, null);
        }
        catch (Exception e)
        {
            updateJob(jobId, JOB_FAILED, 100, StringUtils.defaultIfBlank(e.getMessage(), "解析失败"));
        }
    }

    private void parseImageJob(Long jobId, String username)
    {
        try
        {
            WmsBatchJob job = jobMapper.selectWmsBatchJobById(jobId);
            updateJob(jobId, JOB_PARSING, 10, null);
            itemMapper.deleteWmsBatchItemByJobId(jobId);
            List<WmsBatchItem> items = parseImage(job, username);
            int total = items.size();
            if (total == 0)
            {
                updateJob(jobId, JOB_FAILED, 100, "图片中没有识别到可导入的数据");
                return;
            }
            for (int i = 0; i < total; i++)
            {
                WmsBatchItem item = items.get(i);
                item.setJobId(jobId);
                normalizeItem(item);
                validateItem(item, username);
                fillParsedJson(item);
                itemMapper.insertWmsBatchItem(item);
                int progress = 20 + (int) Math.floor((i + 1) * 70.0 / total);
                refreshJobCount(jobId, JOB_PARSING, progress, null);
            }
            refreshJobCount(jobId, JOB_WAIT_CONFIRM, 100, null);
        }
        catch (Exception e)
        {
            updateJob(jobId, JOB_FAILED, 100, StringUtils.defaultIfBlank(e.getMessage(), "图片解析失败"));
        }
    }

    private List<WmsBatchItem> parseImage(WmsBatchJob job, String username) throws Exception
    {
        if (StringUtils.isBlank(apiKey))
        {
            throw new ServiceException("图片导入需要配置 OPENAI_API_KEY 环境变量");
        }
        updateJob(job.getId(), JOB_PARSING, 30, null);
        String output = callVisionModel(job);
        updateJob(job.getId(), JOB_PARSING, 70, null);
        return parseImageItems(output, username);
    }

    private List<WmsBatchItem> parseExcel(WmsBatchJob job, String username) throws Exception
    {
        List<WmsBatchItem> items = new ArrayList<>();
        DataFormatter formatter = new DataFormatter();
        try (FileInputStream inputStream = new FileInputStream(job.getFilePath()); Workbook workbook = WorkbookFactory.create(inputStream))
        {
            Sheet sheet = workbook.getSheetAt(0);
            if (sheet == null)
            {
                return items;
            }
            int headerRowIndex = findHeaderRow(sheet, formatter);
            if (headerRowIndex < 0)
            {
                throw new ServiceException("未识别到表头，请确认Excel第一行包含产品名称、库区、数量等字段");
            }
            Row headerRow = sheet.getRow(headerRowIndex);
            Map<Integer, String> headerMap = buildHeaderMap(headerRow, formatter);
            String sheetAction = inferActionFromHeaderRow(headerRow, formatter);
            for (int rowIndex = headerRowIndex + 1; rowIndex <= sheet.getLastRowNum(); rowIndex++)
            {
                Row row = sheet.getRow(rowIndex);
                if (row == null || isEmptyRow(row, formatter))
                {
                    continue;
                }
                WmsBatchItem item = rowToItem(row, headerMap, formatter, sheetAction, username);
                item.setRowNo(rowIndex + 1);
                items.add(item);
            }
        }
        return items;
    }

    private WmsBatchItem rowToItem(Row row, Map<Integer, String> headerMap, DataFormatter formatter, String sheetAction, String username)
    {
        WmsBatchItem item = new WmsBatchItem();
        item.setActionType(sheetAction);
        item.setOperatorName(resolveNickName(username));
        StringBuilder raw = new StringBuilder();
        for (Map.Entry<Integer, String> entry : headerMap.entrySet())
        {
            String field = entry.getValue();
            String value = trimToNull(formatter.formatCellValue(row.getCell(entry.getKey())));
            if (StringUtils.isBlank(value))
            {
                continue;
            }
            raw.append(field).append("=").append(value).append("; ");
            applyCellValue(item, field, value);
        }
        if (StringUtils.isBlank(item.getActionType()))
        {
            item.setActionType(inferActionFromItem(item));
        }
        item.setRawText(raw.toString());
        return item;
    }

    private List<WmsBatchItem> parseImageItems(String output, String username)
    {
        List<WmsBatchItem> items = new ArrayList<>();
        String jsonText = extractJson(output);
        JSONArray rows;
        if (jsonText.trim().startsWith("["))
        {
            rows = JSON.parseArray(jsonText);
        }
        else
        {
            JSONObject root = JSON.parseObject(jsonText);
            rows = root.getJSONArray("items");
        }
        if (rows == null)
        {
            return items;
        }
        for (int i = 0; i < rows.size(); i++)
        {
            JSONObject row = rows.getJSONObject(i);
            if (row == null)
            {
                continue;
            }
            WmsBatchItem item = new WmsBatchItem();
            item.setRowNo(i + 1);
            item.setActionType(parseAction(firstText(row, "actionType", "action", "操作类型", "业务类型")));
            item.setAreaName(trimToNull(firstText(row, "areaName", "area", "库区", "库区名称", "仓库")));
            item.setProductName(trimToNull(firstText(row, "productName", "product", "产品名称", "物料名称", "商品名称")));
            item.setBrand(trimToNull(firstText(row, "brand", "品牌")));
            item.setCategory(trimToNull(firstText(row, "category", "类别", "分类")));
            item.setSpec(trimToNull(firstText(row, "spec", "规格", "规格型号", "型号")));
            item.setUnit(trimToNull(firstText(row, "unit", "单位")));
            item.setQuantity(parseQuantity(firstText(row, "quantity", "num", "数量", "入库数量", "出库数量", "实际盘点数量")));
            item.setOperatorName(trimToNull(firstText(row, "operatorName", "operator", "入库人", "出库人", "盘点人", "操作人")));
            if (StringUtils.isBlank(item.getOperatorName()))
            {
                item.setOperatorName(resolveNickName(username));
            }
            item.setTypeCode(parseTypeCode(firstText(row, "typeCode", "inType", "outType", "类型", "入库类型", "出库类型"), item.getActionType()));
            item.setSourceNo(trimToNull(firstText(row, "sourceNo", "来源单据号", "关联单据号")));
            item.setRemark(trimToNull(firstText(row, "remark", "备注", "原因")));
            item.setRawText(row.toJSONString());
            items.add(item);
        }
        return items;
    }

    private void applyCellValue(WmsBatchItem item, String field, String value)
    {
        if ("actionType".equals(field))
        {
            item.setActionType(parseAction(value));
        }
        else if ("areaName".equals(field))
        {
            item.setAreaName(value);
        }
        else if ("productName".equals(field))
        {
            item.setProductName(value);
        }
        else if ("brand".equals(field))
        {
            item.setBrand(value);
        }
        else if ("category".equals(field))
        {
            item.setCategory(value);
        }
        else if ("spec".equals(field))
        {
            item.setSpec(value);
        }
        else if ("unit".equals(field))
        {
            item.setUnit(value);
        }
        else if ("quantity".equals(field))
        {
            item.setQuantity(parseQuantity(value));
        }
        else if ("operatorName".equals(field))
        {
            item.setOperatorName(value);
        }
        else if ("typeCode".equals(field))
        {
            item.setTypeCode(parseTypeCode(value, item.getActionType()));
        }
        else if ("sourceNo".equals(field))
        {
            item.setSourceNo(value);
        }
        else if ("remark".equals(field))
        {
            item.setRemark(value);
        }
    }

    private String callVisionModel(WmsBatchJob job) throws Exception
    {
        File image = new File(job.getFilePath());
        String extension = StringUtils.defaultString(FilenameUtils.getExtension(job.getFileName())).toLowerCase(Locale.ROOT);
        String mimeType = "jpg".equals(extension) ? "image/jpeg" : "image/" + extension;
        String encoded = Base64.getEncoder().encodeToString(Files.readAllBytes(image.toPath()));

        JSONObject body = new JSONObject();
        body.put("model", model);
        JSONArray input = new JSONArray();
        input.add(message("system", visionSystemPrompt()));

        JSONArray content = new JSONArray();
        JSONObject text = new JSONObject();
        text.put("type", "input_text");
        text.put("text", visionUserPrompt());
        content.add(text);
        JSONObject imageContent = new JSONObject();
        imageContent.put("type", "input_image");
        imageContent.put("image_url", "data:" + mimeType + ";base64," + encoded);
        content.add(imageContent);
        input.add(message("user", content));
        body.put("input", input);

        JSONObject format = new JSONObject();
        format.put("type", "json_object");
        JSONObject textFormat = new JSONObject();
        textFormat.put("format", format);
        body.put("text", textFormat);

        HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build();
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(normalizeEndpoint(baseUrl)))
                .timeout(Duration.ofSeconds(60))
                .header("Authorization", "Bearer " + apiKey)
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(body.toJSONString(), StandardCharsets.UTF_8))
                .build();
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
        if (response.statusCode() < 200 || response.statusCode() >= 300)
        {
            throw new ServiceException("图片模型解析失败，HTTP " + response.statusCode());
        }
        String output = extractOutputText(response.body());
        if (StringUtils.isBlank(output))
        {
            throw new ServiceException("图片模型未返回解析内容");
        }
        return output;
    }

    private JSONObject message(String role, Object content)
    {
        JSONObject message = new JSONObject();
        message.put("role", role);
        message.put("content", content);
        return message;
    }

    private String visionSystemPrompt()
    {
        return "你是库存管理系统的图片表格解析器，只返回JSON对象，不要解释。"
                + "从图片中识别可导入的库存业务明细，返回字段 items。"
                + "每个item字段包括 actionType, areaName, productName, brand, category, spec, unit, quantity, operatorName, typeCode, sourceNo, remark。"
                + "actionType 只能是 create_area, create_product, create_in_record, create_out_record, create_stock_check, unknown。"
                + "入库类型typeCode：1生产入库，2采购入库，3归还入库，4退货入库；出库类型typeCode：1生产出库，2退货出库，3销售出库。"
                + "如果只有库区信息、没有产品信息，actionType填create_area；如果有产品信息、没有库区信息，actionType填create_product。"
                + "如果库区和产品都有但图片没有明确操作类型，actionType填unknown，不要猜入库或出库。"
                + "如果类别缺失可以按物品合理推断简短类别；无法识别的字段填null。";
    }

    private String visionUserPrompt()
    {
        return "请解析这张图片中的库存数据，可能是表格、截图或手写/打印清单。"
                + "不要执行操作，只生成待确认明细。"
                + "返回格式：{\"items\":[{\"actionType\":\"create_in_record\",\"areaName\":\"库区\",\"productName\":\"产品名称\",\"brand\":\"品牌\",\"category\":\"类别\",\"spec\":\"规格\",\"unit\":\"单位\",\"quantity\":1,\"operatorName\":\"人员\",\"typeCode\":2,\"sourceNo\":\"单号\",\"remark\":\"备注\"}]}";
    }

    private void normalizeItem(WmsBatchItem item)
    {
        item.setActionType(parseAction(item.getActionType()));
        item.setAreaName(trimToNull(item.getAreaName()));
        item.setProductName(trimToNull(item.getProductName()));
        item.setBrand(trimToNull(item.getBrand()));
        item.setCategory(trimToNull(item.getCategory()));
        item.setSpec(trimToNull(item.getSpec()));
        item.setUnit(trimToNull(item.getUnit()));
        item.setOperatorName(trimToNull(item.getOperatorName()));
        item.setSourceNo(trimToNull(item.getSourceNo()));
        item.setRemark(trimToNull(item.getRemark()));
        if (StringUtils.isBlank(item.getActionType()) || ACTION_UNKNOWN.equals(item.getActionType()))
        {
            String inferredAction = inferActionFromItem(item);
            if (StringUtils.isNotBlank(inferredAction))
            {
                item.setActionType(inferredAction);
            }
        }
        if (StringUtils.isBlank(item.getActionType()))
        {
            item.setActionType(ACTION_UNKNOWN);
        }
        if (ACTION_IN.equals(item.getActionType()) && item.getTypeCode() == null)
        {
            item.setTypeCode(2);
        }
        if (ACTION_OUT.equals(item.getActionType()) && item.getTypeCode() == null)
        {
            item.setTypeCode(1);
        }
        if (StringUtils.isBlank(item.getCategory()) && StringUtils.isNotBlank(item.getProductName()))
        {
            ProductInfo draft = new ProductInfo();
            draft.setProductName(item.getProductName());
            draft.setBrand(item.getBrand());
            draft.setSpec(item.getSpec());
            draft.setUnit(item.getUnit());
            item.setCategory(productService.inferProductCategory(draft));
            appendWarning(item, "未提供类别，已自动分类为：" + item.getCategory());
        }
    }

    private void validateItem(WmsBatchItem item, String username)
    {
        item.setStatus(ITEM_VALID);
        item.setErrorMsg(null);
        item.setWarningMsg(trimToNull(item.getWarningMsg()));
        if (StringUtils.isBlank(item.getActionType()) || ACTION_UNKNOWN.equals(item.getActionType()))
        {
            fail(item, "未识别操作类型，请填写操作类型");
            return;
        }
        if (ACTION_AREA.equals(item.getActionType()))
        {
            validateAreaItem(item);
        }
        else if (ACTION_PRODUCT.equals(item.getActionType()))
        {
            validateProductItem(item);
        }
        else if (ACTION_IN.equals(item.getActionType()))
        {
            validateInItem(item, username);
        }
        else if (ACTION_OUT.equals(item.getActionType()))
        {
            validateOutItem(item, username);
        }
        else if (ACTION_CHECK.equals(item.getActionType()))
        {
            validateCheckItem(item, username);
        }
        else
        {
            fail(item, "暂不支持的操作类型：" + item.getActionType());
        }
    }

    private void validateAreaItem(WmsBatchItem item)
    {
        if (StringUtils.isBlank(item.getAreaName()))
        {
            fail(item, "库区名称不能为空");
            return;
        }
        if (findArea(item.getAreaName()) != null)
        {
            appendWarning(item, "库区已存在，确认执行时不会重复新增");
        }
    }

    private void validateProductItem(WmsBatchItem item)
    {
        if (StringUtils.isBlank(item.getProductName()))
        {
            fail(item, "产品名称不能为空");
            return;
        }
        if (findProduct(item) != null)
        {
            appendWarning(item, "产品已存在，确认执行时不会重复新增");
        }
    }

    private void validateInItem(WmsBatchItem item, String username)
    {
        validateAreaProductQuantity(item, true, true);
        if (ITEM_INVALID == item.getStatus())
        {
            return;
        }
        if (StringUtils.isBlank(item.getOperatorName()))
        {
            item.setOperatorName(resolveNickName(username));
        }
        if (findArea(item.getAreaName()) == null)
        {
            appendWarning(item, "库区不存在，确认执行时将自动新增");
        }
        if (findProduct(item) == null)
        {
            appendWarning(item, "产品不存在，确认执行时将自动新增");
        }
    }

    private void validateOutItem(WmsBatchItem item, String username)
    {
        validateAreaProductQuantity(item, true, true);
        if (ITEM_INVALID == item.getStatus())
        {
            return;
        }
        if (StringUtils.isBlank(item.getOperatorName()))
        {
            item.setOperatorName(resolveNickName(username));
        }
        WarehouseArea area = findArea(item.getAreaName());
        ProductInfo product = findProduct(item);
        if (area == null)
        {
            fail(item, "库区不存在，不能出库");
            return;
        }
        if (product == null)
        {
            fail(item, "产品不存在，不能出库");
            return;
        }
        StockInfo query = new StockInfo();
        query.setAreaId(area.getId());
        query.setProductId(product.getId());
        List<StockInfo> stockList = stockInfoService.selectStockInfoList(query);
        BigDecimal stockNum = stockList.isEmpty() || stockList.get(0).getStockNum() == null ? BigDecimal.ZERO : stockList.get(0).getStockNum();
        if (stockNum.compareTo(item.getQuantity()) < 0)
        {
            fail(item, "库存不足，当前库存：" + stockNum);
        }
    }

    private void validateCheckItem(WmsBatchItem item, String username)
    {
        validateAreaProductQuantity(item, true, false);
        if (ITEM_INVALID == item.getStatus())
        {
            return;
        }
        if (StringUtils.isBlank(item.getOperatorName()))
        {
            item.setOperatorName(resolveNickName(username));
        }
        if (findArea(item.getAreaName()) == null)
        {
            fail(item, "库区不存在，不能盘库");
            return;
        }
        if (findProduct(item) == null)
        {
            fail(item, "产品不存在，不能盘库");
        }
    }

    private void validateAreaProductQuantity(WmsBatchItem item, boolean productRequired, boolean positiveQuantity)
    {
        if (StringUtils.isBlank(item.getAreaName()))
        {
            fail(item, "库区名称不能为空");
            return;
        }
        if (productRequired && StringUtils.isBlank(item.getProductName()))
        {
            fail(item, "产品名称不能为空");
            return;
        }
        if (item.getQuantity() == null)
        {
            fail(item, "数量不能为空");
            return;
        }
        if (positiveQuantity && item.getQuantity().signum() <= 0)
        {
            fail(item, "数量必须大于0");
            return;
        }
        if (!positiveQuantity && item.getQuantity().signum() < 0)
        {
            fail(item, "数量不能小于0");
        }
    }

    private void executeItem(WmsBatchItem item, String username)
    {
        normalizeItem(item);
        validateItem(item, username);
        if (ITEM_INVALID == item.getStatus())
        {
            throw new ServiceException(item.getErrorMsg());
        }
        if (ACTION_AREA.equals(item.getActionType()))
        {
            WarehouseArea area = ensureArea(item.getAreaName());
            item.setRefId(area.getId());
        }
        else if (ACTION_PRODUCT.equals(item.getActionType()))
        {
            ProductInfo product = ensureProduct(item);
            item.setRefId(product.getId());
        }
        else if (ACTION_IN.equals(item.getActionType()))
        {
            WarehouseArea area = ensureArea(item.getAreaName());
            ProductInfo product = ensureProduct(item);
            InRecord record = new InRecord();
            record.setAreaId(area.getId());
            record.setProductId(product.getId());
            record.setInType(item.getTypeCode());
            record.setInNum(item.getQuantity());
            record.setInOperator(item.getOperatorName());
            record.setOperator(username);
            record.setRemark(item.getRemark());
            inRecordService.insertInRecord(record);
            item.setRefId(record.getId());
        }
        else if (ACTION_OUT.equals(item.getActionType()))
        {
            WarehouseArea area = findArea(item.getAreaName());
            ProductInfo product = findProduct(item);
            OutRecord record = new OutRecord();
            record.setAreaId(area.getId());
            record.setProductId(product.getId());
            record.setOutType(item.getTypeCode());
            record.setOutNum(item.getQuantity());
            record.setOutOperator(item.getOperatorName());
            record.setOperator(username);
            record.setSourceNo(item.getSourceNo());
            record.setRemark(item.getRemark());
            outRecordService.insertOutRecord(record);
            item.setRefId(record.getId());
        }
        else if (ACTION_CHECK.equals(item.getActionType()))
        {
            WarehouseArea area = findArea(item.getAreaName());
            ProductInfo product = findProduct(item);
            StockCheckRecord record = new StockCheckRecord();
            record.setAreaId(area.getId());
            record.setProductId(product.getId());
            record.setRealNum(item.getQuantity());
            record.setCheckUser(item.getOperatorName());
            record.setReason(item.getRemark());
            stockCheckRecordService.insertStockCheckRecord(record);
            item.setRefId(record.getId());
        }
    }

    private void ensureJobEditable(Long jobId)
    {
        WmsBatchJob job = jobMapper.selectWmsBatchJobById(jobId);
        if (job == null)
        {
            throw new ServiceException("批量任务不存在");
        }
        if (Integer.valueOf(JOB_EXECUTING).equals(job.getStatus()) || Integer.valueOf(JOB_DONE).equals(job.getStatus()))
        {
            throw new ServiceException("任务已执行，不能再修改明细");
        }
    }

    private WarehouseArea ensureArea(String areaName)
    {
        WarehouseArea area = findArea(areaName);
        if (area != null)
        {
            return area;
        }
        area = new WarehouseArea();
        area.setAreaName(areaName);
        areaService.insertWarehouseArea(area);
        return area;
    }

    private ProductInfo ensureProduct(WmsBatchItem item)
    {
        ProductInfo product = findProduct(item);
        if (product != null)
        {
            return product;
        }
        product = new ProductInfo();
        product.setProductName(item.getProductName());
        product.setBrand(item.getBrand());
        product.setCategory(item.getCategory());
        product.setSpec(item.getSpec());
        product.setUnit(item.getUnit());
        product.setRemark(item.getRemark());
        productService.insertProductInfo(product);
        return product;
    }

    private WarehouseArea findArea(String areaName)
    {
        if (StringUtils.isBlank(areaName))
        {
            return null;
        }
        for (WarehouseArea area : areaService.selectWarehouseAreaAll())
        {
            if (sameText(area.getAreaName(), areaName) || sameText(area.getAreaCode(), areaName))
            {
                return area;
            }
        }
        return null;
    }

    private ProductInfo findProduct(WmsBatchItem item)
    {
        if (StringUtils.isBlank(item.getProductName()))
        {
            return null;
        }
        for (ProductInfo product : productService.selectProductInfoAll())
        {
            if (sameText(product.getProductName(), item.getProductName())
                    && sameText(product.getBrand(), item.getBrand())
                    && sameText(product.getCategory(), item.getCategory())
                    && sameText(product.getSpec(), item.getSpec()))
            {
                return product;
            }
        }
        return null;
    }

    private void refreshJobCount(Long jobId, Integer status, Integer progress, String errorMsg)
    {
        WmsBatchItem validQuery = new WmsBatchItem();
        validQuery.setJobId(jobId);
        validQuery.setStatus(ITEM_VALID);
        WmsBatchItem invalidQuery = new WmsBatchItem();
        invalidQuery.setJobId(jobId);
        invalidQuery.setStatus(ITEM_INVALID);
        int valid = itemMapper.countByJobIdAndStatus(validQuery);
        int invalid = itemMapper.countByJobIdAndStatus(invalidQuery);

        WmsBatchJob job = new WmsBatchJob();
        job.setId(jobId);
        job.setStatus(status);
        job.setProgress(progress);
        job.setTotalCount(valid + invalid);
        job.setSuccessCount(valid);
        job.setFailCount(invalid);
        job.setErrorMsg(errorMsg);
        jobMapper.updateWmsBatchJob(job);
    }

    private void updateJob(Long jobId, Integer status, Integer progress, String errorMsg)
    {
        WmsBatchJob job = new WmsBatchJob();
        job.setId(jobId);
        job.setStatus(status);
        job.setProgress(progress);
        job.setErrorMsg(errorMsg);
        jobMapper.updateWmsBatchJob(job);
    }

    private void updateJobProgress(Long jobId, Integer status, int total, int done, int success, int fail)
    {
        WmsBatchJob job = new WmsBatchJob();
        job.setId(jobId);
        job.setStatus(status);
        job.setProgress(total == 0 ? 100 : (int) Math.floor(done * 100.0 / total));
        job.setTotalCount(total);
        job.setSuccessCount(success);
        job.setFailCount(fail);
        jobMapper.updateWmsBatchJob(job);
    }

    private int findHeaderRow(Sheet sheet, DataFormatter formatter)
    {
        int max = Math.min(sheet.getLastRowNum(), 10);
        for (int i = 0; i <= max; i++)
        {
            Row row = sheet.getRow(i);
            if (row == null)
            {
                continue;
            }
            int count = 0;
            for (Cell cell : row)
            {
                if (mapHeader(formatter.formatCellValue(cell)) != null)
                {
                    count++;
                }
            }
            if (count >= 2)
            {
                return i;
            }
        }
        return -1;
    }

    private Map<Integer, String> buildHeaderMap(Row row, DataFormatter formatter)
    {
        Map<Integer, String> map = new HashMap<>();
        for (Cell cell : row)
        {
            String field = mapHeader(formatter.formatCellValue(cell));
            if (field != null)
            {
                map.put(cell.getColumnIndex(), field);
            }
        }
        return map;
    }

    private String mapHeader(String header)
    {
        String text = normalizeHeader(header);
        if (StringUtils.isBlank(text))
        {
            return null;
        }
        if (containsAny(text, "库区", "仓库", "库位"))
        {
            return "areaName";
        }
        if (containsAny(text, "产品名称", "物料名称", "商品名称") || "产品".equals(text) || "物料".equals(text))
        {
            return "productName";
        }
        if (containsAny(text, "品牌"))
        {
            return "brand";
        }
        if (containsAny(text, "类别", "分类"))
        {
            return "category";
        }
        if (containsAny(text, "规格", "型号"))
        {
            return "spec";
        }
        if (containsAny(text, "单位"))
        {
            return "unit";
        }
        if (containsAny(text, "数量", "入库数量", "出库数量", "实际盘点数量", "实际数量"))
        {
            return "quantity";
        }
        if (containsAny(text, "入库人", "出库人", "盘点人", "操作人", "经办人"))
        {
            return "operatorName";
        }
        if (containsAny(text, "入库类型", "出库类型"))
        {
            return "typeCode";
        }
        if (containsAny(text, "操作类型", "业务类型", "动作"))
        {
            return "actionType";
        }
        if (containsAny(text, "来源单据", "关联单据", "单据号"))
        {
            return "sourceNo";
        }
        if (containsAny(text, "备注", "原因"))
        {
            return "remark";
        }
        return null;
    }

    private String inferActionFromHeaderRow(Row row, DataFormatter formatter)
    {
        StringBuilder builder = new StringBuilder();
        for (Cell cell : row)
        {
            builder.append(formatter.formatCellValue(cell)).append(' ');
        }
        String joined = builder.toString();
        if (containsAny(joined, "入库"))
        {
            return ACTION_IN;
        }
        if (containsAny(joined, "出库"))
        {
            return ACTION_OUT;
        }
        if (containsAny(joined, "盘点", "盘库"))
        {
            return ACTION_CHECK;
        }
        return null;
    }

    private String inferActionFromItem(WmsBatchItem item)
    {
        if (StringUtils.isNotBlank(item.getAreaName()) && StringUtils.isBlank(item.getProductName()))
        {
            return ACTION_AREA;
        }
        if (StringUtils.isNotBlank(item.getProductName()) && StringUtils.isBlank(item.getAreaName()))
        {
            return ACTION_PRODUCT;
        }
        return null;
    }

    private String parseAction(String value)
    {
        String text = StringUtils.defaultString(value).trim();
        if (StringUtils.isBlank(text) || text.startsWith("create_"))
        {
            return text;
        }
        if (ACTION_UNKNOWN.equals(text))
        {
            return ACTION_UNKNOWN;
        }
        if (containsAny(text, "新增库区", "库区定义", "库区维护"))
        {
            return ACTION_AREA;
        }
        if (containsAny(text, "新增产品", "产品信息", "产品维护", "商品"))
        {
            return ACTION_PRODUCT;
        }
        if (containsAny(text, "入库"))
        {
            return ACTION_IN;
        }
        if (containsAny(text, "出库"))
        {
            return ACTION_OUT;
        }
        if (containsAny(text, "盘库", "盘点"))
        {
            return ACTION_CHECK;
        }
        return text;
    }

    private boolean isExcel(String extension)
    {
        return "xls".equals(extension) || "xlsx".equals(extension);
    }

    private boolean isImage(String extension)
    {
        return "jpg".equals(extension) || "jpeg".equals(extension) || "png".equals(extension) || "bmp".equals(extension);
    }

    private String normalizeEndpoint(String configuredBaseUrl)
    {
        String url = StringUtils.defaultIfBlank(configuredBaseUrl, "http://192.168.28.10:18080").trim();
        while (url.endsWith("/"))
        {
            url = url.substring(0, url.length() - 1);
        }
        if (url.endsWith("/v1"))
        {
            return url + "/responses";
        }
        if (url.endsWith("/v1/responses"))
        {
            return url;
        }
        return url + "/v1/responses";
    }

    private String extractOutputText(String responseBody)
    {
        JSONObject root = JSON.parseObject(responseBody);
        String outputText = root.getString("output_text");
        if (StringUtils.isNotBlank(outputText))
        {
            return outputText;
        }
        StringBuilder builder = new StringBuilder();
        JSONArray output = root.getJSONArray("output");
        if (output != null)
        {
            for (int i = 0; i < output.size(); i++)
            {
                JSONObject item = output.getJSONObject(i);
                JSONArray content = item == null ? null : item.getJSONArray("content");
                if (content == null)
                {
                    continue;
                }
                for (int j = 0; j < content.size(); j++)
                {
                    JSONObject contentItem = content.getJSONObject(j);
                    if (contentItem != null && StringUtils.isNotBlank(contentItem.getString("text")))
                    {
                        builder.append(contentItem.getString("text"));
                    }
                }
            }
        }
        return builder.toString();
    }

    private String extractJson(String output)
    {
        String text = StringUtils.defaultString(output).trim();
        int objectStart = text.indexOf('{');
        int arrayStart = text.indexOf('[');
        int start;
        int end;
        if (arrayStart >= 0 && (objectStart < 0 || arrayStart < objectStart))
        {
            start = arrayStart;
            end = text.lastIndexOf(']');
        }
        else
        {
            start = objectStart;
            end = text.lastIndexOf('}');
        }
        if (start >= 0 && end > start)
        {
            return text.substring(start, end + 1);
        }
        return text;
    }

    private Integer parseTypeCode(String value, String actionType)
    {
        if (StringUtils.isBlank(value))
        {
            return null;
        }
        String text = value.trim();
        if (text.matches("\\d+"))
        {
            return Integer.valueOf(text);
        }
        if (ACTION_OUT.equals(actionType))
        {
            if (text.contains("退货")) return 2;
            if (text.contains("销售")) return 3;
            return 1;
        }
        if (text.contains("生产")) return 1;
        if (text.contains("归还")) return 3;
        if (text.contains("退货")) return 4;
        return 2;
    }

    private BigDecimal parseQuantity(String value)
    {
        if (StringUtils.isBlank(value))
        {
            return null;
        }
        String text = value.replace(",", "").trim();
        java.util.regex.Matcher matcher = java.util.regex.Pattern.compile("-?\\d+(?:\\.\\d+)?").matcher(text);
        if (matcher.find())
        {
            return new BigDecimal(matcher.group());
        }
        return null;
    }

    private String firstText(JSONObject json, String... keys)
    {
        for (String key : keys)
        {
            Object value = json.get(key);
            if (value != null && StringUtils.isNotBlank(String.valueOf(value)))
            {
                return String.valueOf(value);
            }
        }
        return null;
    }

    private boolean isEmptyRow(Row row, DataFormatter formatter)
    {
        for (Cell cell : row)
        {
            if (StringUtils.isNotBlank(formatter.formatCellValue(cell)))
            {
                return false;
            }
        }
        return true;
    }

    private void fillParsedJson(WmsBatchItem item)
    {
        JSONObject json = new JSONObject();
        json.put("actionType", item.getActionType());
        json.put("areaName", item.getAreaName());
        json.put("productName", item.getProductName());
        json.put("brand", item.getBrand());
        json.put("category", item.getCategory());
        json.put("spec", item.getSpec());
        json.put("unit", item.getUnit());
        json.put("quantity", item.getQuantity());
        json.put("operatorName", item.getOperatorName());
        json.put("typeCode", item.getTypeCode());
        json.put("sourceNo", item.getSourceNo());
        json.put("remark", item.getRemark());
        item.setParsedJson(json.toJSONString());
    }

    private void fail(WmsBatchItem item, String message)
    {
        item.setStatus(ITEM_INVALID);
        item.setErrorMsg(message);
    }

    private void appendWarning(WmsBatchItem item, String message)
    {
        if (StringUtils.isBlank(message))
        {
            return;
        }
        if (StringUtils.isBlank(item.getWarningMsg()))
        {
            item.setWarningMsg(message);
        }
        else if (!item.getWarningMsg().contains(message))
        {
            item.setWarningMsg(item.getWarningMsg() + "；" + message);
        }
    }

    private String resolveNickName(String value)
    {
        if (StringUtils.isBlank(value))
        {
            return value;
        }
        SysUser user = userService.selectUserByUserName(value);
        if (user != null && StringUtils.isNotBlank(user.getNickName()))
        {
            return user.getNickName();
        }
        return value.trim();
    }

    private boolean sameText(String left, String right)
    {
        return StringUtils.defaultString(left).trim().equalsIgnoreCase(StringUtils.defaultString(right).trim());
    }

    private String normalizeHeader(String value)
    {
        return StringUtils.defaultString(value).replaceAll("[\\s:：_\\-（）()]", "").trim();
    }

    private String trimToNull(String value)
    {
        return StringUtils.isBlank(value) ? null : value.trim();
    }

    private boolean containsAny(String text, String... keywords)
    {
        if (StringUtils.isBlank(text))
        {
            return false;
        }
        for (String keyword : keywords)
        {
            if (StringUtils.isNotBlank(keyword) && text.toLowerCase(Locale.ROOT).contains(keyword.toLowerCase(Locale.ROOT)))
            {
                return true;
            }
        }
        return false;
    }
}
