package cn.nianxx.thhotel.workflows.reservation.service.impl;

import cn.nianxx.thhotel.integrations.storage.aliyunoss.common.request.ObjectStoragePutRequest;
import cn.nianxx.thhotel.integrations.storage.aliyunoss.common.result.ObjectStoragePutResult;
import cn.nianxx.thhotel.integrations.storage.aliyunoss.service.ObjectStorageService;
import cn.nianxx.thhotel.platform.common.time.UtcTimeFormatter;
import cn.nianxx.thhotel.platform.documentconversion.common.dto.ExcelToPdfConversionInput;
import cn.nianxx.thhotel.platform.documentconversion.common.dto.ExcelToPdfConvertedDocument;
import cn.nianxx.thhotel.platform.documentconversion.service.DocumentConversionException;
import cn.nianxx.thhotel.platform.documentconversion.service.ExcelToPdfConverter;
import cn.nianxx.thhotel.platform.hotel.service.HotelContextException;
import cn.nianxx.thhotel.platform.hotel.service.HotelContextService;
import cn.nianxx.thhotel.platform.security.common.dto.AuthenticatedUserContext;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiQueryOrderSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAuditLogDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationInvoiceGenerationDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationInvoiceTotals;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationTaskSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationInvoiceGenerationStatus;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationInvoiceSourceType;
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationInvoiceChargeRequest;
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationInvoiceManualGenerationRequest;
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationInvoicePayloadRequest;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationInvoiceGenerationResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationInvoiceTotalsResult;
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationAiWorkflowRepository;
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationInvoiceGenerationRepository;
import cn.nianxx.thhotel.workflows.reservation.service.ReservationInvoiceGenerationService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Clock;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;

/**
 * Reservation Invoice 生成服务实现。负责业务校验、模板渲染、PDF 转换、OSS 上传和生成审计。
 */
@Service
public class ReservationInvoiceGenerationServiceImpl implements ReservationInvoiceGenerationService {

    private static final String DEFAULT_TEMPLATE_CODE = "PROFORMA_INVOICE_V1";
    private static final String TEMPLATE_VERSION = "v1";
    private static final String CURRENCY = "THB";
    private static final int MAX_CHARGE_LINES = 10;
    private static final BigDecimal VAT_DIVISOR = new BigDecimal("1.07");
    private static final String EXCEL_CONTENT_TYPE =
            "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";

    private final ObjectMapper objectMapper;
    private final HotelContextService hotelContextService;
    private final ReservationAiWorkflowRepository workflowRepository;
    private final ReservationInvoiceGenerationRepository invoiceGenerationRepository;
    private final ReservationInvoiceExcelTemplateRenderer templateRenderer;
    private final ExcelToPdfConverter excelToPdfConverter;
    private final ObjectStorageService objectStorageService;

    /**
     * 注入发票生成需要的业务仓储、模板渲染器和平台文件能力。
     */
    public ReservationInvoiceGenerationServiceImpl(
            ObjectMapper objectMapper,
            HotelContextService hotelContextService,
            ReservationAiWorkflowRepository workflowRepository,
            ReservationInvoiceGenerationRepository invoiceGenerationRepository,
            ReservationInvoiceExcelTemplateRenderer templateRenderer,
            ExcelToPdfConverter excelToPdfConverter,
            ObjectStorageService objectStorageService) {
        this.objectMapper = objectMapper;
        this.hotelContextService = hotelContextService;
        this.workflowRepository = workflowRepository;
        this.invoiceGenerationRepository = invoiceGenerationRepository;
        this.templateRenderer = templateRenderer;
        this.excelToPdfConverter = excelToPdfConverter;
        this.objectStorageService = objectStorageService;
    }

    /**
     * 手工生成 Proforma Invoice。第一版同步生成并返回 PDF URL。
     */
    @Override
    public ReservationInvoiceGenerationResult generateManualInvoice(
            ReservationInvoiceManualGenerationRequest request,
            AuthenticatedUserContext actor) {
        ReservationInvoiceManualGenerationRequest normalizedRequest = normalizeAndValidate(request);
        String hotelId = resolveAccessibleHotel(normalizedRequest.hotelId());
        ContextCheckResult context = validateOptionalContext(hotelId, normalizedRequest);
        ReservationInvoiceTotals totals = calculateTotals(normalizedRequest.invoicePayload().charges());
        LocalDateTime now = nowUtc();
        String payloadJson = writeJson(normalizedRequest.invoicePayload());
        Long generationId = invoiceGenerationRepository.insertGeneration(new ReservationInvoiceGenerationDraft(
                hotelId,
                ReservationInvoiceSourceType.MANUAL.name(),
                normalizedRequest.orderId(),
                normalizedRequest.taskId(),
                context.sourceMessageId(),
                normalizedRequest.templateCode(),
                TEMPLATE_VERSION,
                payloadJson,
                ReservationInvoiceGenerationStatus.RUNNING.name(),
                actor.username(),
                now));

        try {
            byte[] excelBytes = templateRenderer.render(normalizedRequest);
            String baseObjectKey = buildObjectKeyPrefix(hotelId, generationId);
            ObjectStoragePutResult excelResult = uploadExcel(baseObjectKey, generationId, excelBytes);
            invoiceGenerationRepository.recordGeneratedExcel(generationId, excelResult.objectKey(), nowUtc());
            ExcelToPdfConvertedDocument pdfDocument = convertToPdf(generationId, excelBytes);
            ObjectStoragePutResult pdfResult = uploadPdf(baseObjectKey, pdfDocument);
            invoiceGenerationRepository.recordGeneratedPdf(
                    generationId,
                    pdfResult.objectKey(),
                    pdfResult.publicUrl(),
                    nowUtc());
            String totalsJson = writeJson(totals);
            LocalDateTime finishedAt = nowUtc();
            writeSuccessAudit(hotelId, normalizedRequest, context.sourceMessageId(), generationId, totals, actor, finishedAt);
            invoiceGenerationRepository.markSucceeded(
                    generationId,
                    totalsJson,
                    excelResult.objectKey(),
                    pdfResult.objectKey(),
                    pdfResult.publicUrl(),
                    finishedAt);
            return toResult(generationId, hotelId, normalizedRequest.templateCode(), pdfResult, excelResult, totals, now);
        } catch (ReservationInvoiceGenerationException exception) {
            invoiceGenerationRepository.markFailed(
                    generationId,
                    exception.getErrorCode(),
                    safeSummary(exception.getMessage()),
                    nowUtc());
            throw exception;
        } catch (DocumentConversionException exception) {
            invoiceGenerationRepository.markFailed(
                    generationId,
                    exception.getErrorCode(),
                    safeSummary(exception.getMessage()),
                    nowUtc());
            throw new ReservationInvoiceGenerationException(
                    exception.getStatus(),
                    exception.getErrorCode(),
                    exception.getMessage());
        } catch (RuntimeException exception) {
            invoiceGenerationRepository.markFailed(
                    generationId,
                    "RESERVATION_INVOICE_GENERATION_FAILED",
                    safeSummary(exception.getMessage()),
                    nowUtc());
            throw new ReservationInvoiceGenerationException(
                    HttpStatus.BAD_GATEWAY,
                    "RESERVATION_INVOICE_GENERATION_FAILED",
                    "Invoice PDF 生成失败，请稍后重试。");
        }
    }

    /**
     * 归一化请求并执行第一版字段校验。
     */
    private ReservationInvoiceManualGenerationRequest normalizeAndValidate(
            ReservationInvoiceManualGenerationRequest request) {
        if (request == null) {
            throw validationError(List.of("request: 请求体不能为空。"));
        }
        String sourceType = defaultIfBlank(request.sourceType(), ReservationInvoiceSourceType.MANUAL.name());
        String templateCode = defaultIfBlank(request.templateCode(), DEFAULT_TEMPLATE_CODE);
        ReservationInvoiceManualGenerationRequest normalized = new ReservationInvoiceManualGenerationRequest(
                trimToNull(request.hotelId()),
                sourceType,
                request.orderId(),
                request.taskId(),
                templateCode,
                request.invoicePayload());
        List<String> errors = validateNormalizedRequest(normalized);
        if (!errors.isEmpty()) {
            throw validationError(errors);
        }
        return normalized;
    }

    /**
     * 校验归一化后的请求字段，返回全部可一次性提示给前端的错误。
     */
    private List<String> validateNormalizedRequest(ReservationInvoiceManualGenerationRequest request) {
        List<String> errors = new ArrayList<>();
        if (!ReservationInvoiceSourceType.MANUAL.name().equals(request.sourceType())) {
            errors.add("source_type: 第一版只支持 MANUAL。");
        }
        if (!DEFAULT_TEMPLATE_CODE.equals(request.templateCode())) {
            errors.add("template_code: 第一版只支持 PROFORMA_INVOICE_V1。");
        }
        ReservationInvoicePayloadRequest payload = request.invoicePayload();
        if (payload == null) {
            errors.add("invoice_payload: 不能为空。");
            return errors;
        }
        if (payload.document() == null) {
            errors.add("invoice_payload.document: 不能为空。");
        } else {
            if (payload.document().invoiceDate() == null) {
                errors.add("invoice_payload.document.invoice_date: 必填字段缺失。");
            }
            if (payload.document().bookingDate() == null) {
                errors.add("invoice_payload.document.booking_date: 必填字段缺失。");
            }
            if (payload.document().dueDate() == null) {
                errors.add("invoice_payload.document.due_date: 必填字段缺失。");
            }
        }
        validateRecipient(payload, errors);
        validateBooking(payload, errors);
        validateCharges(payload, errors);
        return errors;
    }

    /**
     * 校验收件人字段。第一版以最终文本生成 PDF，目录 code 允许为空。
     */
    private void validateRecipient(ReservationInvoicePayloadRequest payload, List<String> errors) {
        if (payload.recipient() == null) {
            errors.add("invoice_payload.recipient: 不能为空。");
            return;
        }
        if (isBlank(payload.recipient().company())) {
            errors.add("invoice_payload.recipient.company: 必填字段缺失。");
        }
        if (isBlank(payload.recipient().attention())) {
            errors.add("invoice_payload.recipient.attention: 必填字段缺失。");
        }
        if (isBlank(payload.recipient().address())) {
            errors.add("invoice_payload.recipient.address: 必填字段缺失。");
        }
        if (isBlank(payload.recipient().telephone())) {
            errors.add("invoice_payload.recipient.telephone: 必填字段缺失。");
        }
        if (isBlank(payload.recipient().email())) {
            errors.add("invoice_payload.recipient.email: 必填字段缺失。");
        }
    }

    /**
     * 校验预订摘要字段。到店日期必须早于离店日期。
     */
    private void validateBooking(ReservationInvoicePayloadRequest payload, List<String> errors) {
        if (payload.booking() == null) {
            errors.add("invoice_payload.booking: 不能为空。");
            return;
        }
        if (isBlank(payload.booking().groupName())) {
            errors.add("invoice_payload.booking.group_name: 必填字段缺失。");
        }
        if (payload.booking().arrivalDate() == null) {
            errors.add("invoice_payload.booking.arrival_date: 必填字段缺失。");
        }
        if (payload.booking().departureDate() == null) {
            errors.add("invoice_payload.booking.departure_date: 必填字段缺失。");
        }
        if (payload.booking().arrivalDate() != null && payload.booking().departureDate() != null
                && !payload.booking().departureDate().isAfter(payload.booking().arrivalDate())) {
            errors.add("invoice_payload.booking.departure_date: 离店日期必须晚于到店日期。");
        }
    }

    /**
     * 校验费用明细行。第一版最多承载模板默认 10 行。
     */
    private void validateCharges(ReservationInvoicePayloadRequest payload, List<String> errors) {
        List<ReservationInvoiceChargeRequest> charges = payload.charges();
        if (charges == null || charges.isEmpty()) {
            errors.add("invoice_payload.charges: 至少需要一条费用明细。");
            return;
        }
        if (charges.size() > MAX_CHARGE_LINES) {
            errors.add("invoice_payload.charges: 第一版最多支持 10 条费用明细。");
        }
        for (int index = 0; index < charges.size(); index++) {
            ReservationInvoiceChargeRequest charge = charges.get(index);
            String prefix = "invoice_payload.charges." + index + ".";
            if (charge == null) {
                errors.add(prefix + "item: 明细不能为空。");
                continue;
            }
            if (isBlank(charge.description())) {
                errors.add(prefix + "description: 必填字段缺失。");
            }
            if (isBlank(charge.roomType())) {
                errors.add(prefix + "room_type: 必填字段缺失。");
            }
            validatePositive(charge.quantity(), prefix + "quantity", errors);
            validatePositive(charge.rate(), prefix + "rate", errors);
            validatePositive(charge.nights(), prefix + "nights", errors);
        }
    }

    /**
     * 校验当前用户可访问酒店。
     */
    private String resolveAccessibleHotel(String requestedHotelId) {
        try {
            return hotelContextService.requireAccessibleHotel(requestedHotelId);
        } catch (HotelContextException exception) {
            throw new ReservationInvoiceGenerationException(
                    exception.getStatus(),
                    exception.getErrorCode(),
                    exception.getMessage());
        }
    }

    /**
     * 校验可选订单 / 任务上下文归属酒店，并提取可追溯的 source_message_id。
     */
    private ContextCheckResult validateOptionalContext(
            String hotelId,
            ReservationInvoiceManualGenerationRequest request) {
        Long sourceMessageId = null;
        if (request.orderId() != null) {
            ReservationAiQueryOrderSnapshot order = workflowRepository.findAiQueryOrderById(request.orderId())
                    .orElseThrow(() -> new ReservationInvoiceGenerationException(
                            HttpStatus.NOT_FOUND,
                            "RESERVATION_INVOICE_ORDER_NOT_FOUND",
                            "关联订单不存在。"));
            if (!hotelId.equals(order.hotelId())) {
                throw new ReservationInvoiceGenerationException(
                        HttpStatus.FORBIDDEN,
                        "HOTEL_ACCESS_DENIED",
                        "当前用户无权访问该订单所属酒店。");
            }
            sourceMessageId = order.sourceMessageId();
        }
        if (request.taskId() != null) {
            ReservationTaskSnapshot task = workflowRepository.findTaskById(request.taskId())
                    .orElseThrow(() -> new ReservationInvoiceGenerationException(
                            HttpStatus.NOT_FOUND,
                            "RESERVATION_INVOICE_TASK_NOT_FOUND",
                            "关联任务不存在。"));
            if (!hotelId.equals(task.hotelId())) {
                throw new ReservationInvoiceGenerationException(
                        HttpStatus.FORBIDDEN,
                        "HOTEL_ACCESS_DENIED",
                        "当前用户无权访问该任务所属酒店。");
            }
            if (request.orderId() != null && !Objects.equals(request.orderId(), task.orderId())) {
                throw new ReservationInvoiceGenerationException(
                        HttpStatus.BAD_REQUEST,
                        "RESERVATION_INVOICE_CONTEXT_MISMATCH",
                        "关联任务不属于传入订单。");
            }
            sourceMessageId = task.sourceMessageId();
        }
        return new ContextCheckResult(sourceMessageId);
    }

    /**
     * 计算含税总额、未税金额和 VAT。
     */
    private ReservationInvoiceTotals calculateTotals(List<ReservationInvoiceChargeRequest> charges) {
        BigDecimal total = charges.stream()
                .map(this::lineAmount)
                .reduce(BigDecimal.ZERO, BigDecimal::add)
                .setScale(2, RoundingMode.HALF_UP);
        BigDecimal subtotal = total.divide(VAT_DIVISOR, 2, RoundingMode.HALF_UP);
        BigDecimal vat = total.subtract(subtotal).setScale(2, RoundingMode.HALF_UP);
        return new ReservationInvoiceTotals(subtotal, vat, total, CURRENCY);
    }

    /**
     * 计算单行金额。
     */
    private BigDecimal lineAmount(ReservationInvoiceChargeRequest charge) {
        return charge.quantity()
                .multiply(charge.rate())
                .multiply(charge.nights());
    }

    /**
     * 上传生成后的 Excel，便于后续排查 PDF 生成问题。
     */
    private ObjectStoragePutResult uploadExcel(String baseObjectKey, Long generationId, byte[] excelBytes) {
        String fileName = "proforma-invoice-" + generationId + ".xlsx";
        return objectStorageService.putObject(new ObjectStoragePutRequest(
                baseObjectKey + fileName,
                fileName,
                EXCEL_CONTENT_TYPE,
                (long) excelBytes.length,
                excelBytes));
    }

    /**
     * 调用平台 Excel 转 PDF Adapter，不直接依赖 LibreOffice 命令细节。
     */
    private ExcelToPdfConvertedDocument convertToPdf(Long generationId, byte[] excelBytes) {
        String fileName = "proforma-invoice-" + generationId + ".xlsx";
        return excelToPdfConverter.convert(new ExcelToPdfConversionInput(
                fileName,
                EXCEL_CONTENT_TYPE,
                (long) excelBytes.length,
                excelBytes));
    }

    /**
     * 上传 PDF 到 OSS，并返回可展示 URL。
     */
    private ObjectStoragePutResult uploadPdf(String baseObjectKey, ExcelToPdfConvertedDocument pdfDocument) {
        return objectStorageService.putObject(new ObjectStoragePutRequest(
                baseObjectKey + safeFileName(pdfDocument.fileName(), "proforma-invoice.pdf"),
                safeFileName(pdfDocument.fileName(), "proforma-invoice.pdf"),
                defaultIfBlank(pdfDocument.contentType(), MediaType.APPLICATION_PDF_VALUE),
                pdfDocument.sizeBytes() == null ? (long) pdfDocument.content().length : pdfDocument.sizeBytes(),
                pdfDocument.content()));
    }

    /**
     * 记录生成成功审计，只保存摘要，不保存完整发票 payload。
     */
    private void writeSuccessAudit(
            String hotelId,
            ReservationInvoiceManualGenerationRequest request,
            Long sourceMessageId,
            Long generationId,
            ReservationInvoiceTotals totals,
            AuthenticatedUserContext actor,
            LocalDateTime occurredAt) {
        workflowRepository.insertAuditLog(new ReservationAuditLogDraft(
                hotelId,
                request.orderId(),
                request.taskId(),
                null,
                "USER",
                actor.username(),
                "RESERVATION_INVOICE_GENERATE",
                "手工生成 Proforma Invoice",
                null,
                writeJson(Map.of(
                        "invoice_generation_id", String.valueOf(generationId),
                        "source_message_id", sourceMessageId == null ? "" : String.valueOf(sourceMessageId),
                        "template_code", request.templateCode(),
                        "subtotal", totals.subtotal(),
                        "vat", totals.vat(),
                        "total", totals.total(),
                        "currency", totals.currency())),
                occurredAt));
    }

    /**
     * 转换成对前端稳定的响应结构。
     */
    private ReservationInvoiceGenerationResult toResult(
            Long generationId,
            String hotelId,
            String templateCode,
            ObjectStoragePutResult pdfResult,
            ObjectStoragePutResult excelResult,
            ReservationInvoiceTotals totals,
            LocalDateTime createdAt) {
        return new ReservationInvoiceGenerationResult(
                String.valueOf(generationId),
                ReservationInvoiceGenerationStatus.SUCCEEDED.name(),
                ReservationInvoiceSourceType.MANUAL.name(),
                hotelId,
                templateCode,
                pdfResult.publicUrl(),
                pdfResult.objectKey(),
                excelResult.objectKey(),
                new ReservationInvoiceTotalsResult(totals.subtotal(), totals.vat(), totals.total(), totals.currency()),
                UtcTimeFormatter.toUtcOffsetDateTime(createdAt));
    }

    private ReservationInvoiceGenerationException validationError(List<String> details) {
        return new ReservationInvoiceGenerationException(
                HttpStatus.BAD_REQUEST,
                "RESERVATION_INVOICE_VALIDATION_FAILED",
                "Invoice 字段校验失败。",
                details);
    }

    private void validatePositive(BigDecimal value, String fieldPath, List<String> errors) {
        if (value == null) {
            errors.add(fieldPath + ": 必填字段缺失。");
            return;
        }
        if (value.compareTo(BigDecimal.ZERO) <= 0) {
            errors.add(fieldPath + ": 必须大于 0。");
        }
    }

    private String writeJson(Object value) {
        try {
            return objectMapper.writeValueAsString(value);
        } catch (JsonProcessingException exception) {
            throw new ReservationInvoiceGenerationException(
                    HttpStatus.INTERNAL_SERVER_ERROR,
                    "RESERVATION_INVOICE_JSON_SERIALIZE_FAILED",
                    "Invoice 数据序列化失败。");
        }
    }

    private String buildObjectKeyPrefix(String hotelId, Long generationId) {
        return "reservation-invoices/" + hotelId + "/" + java.time.LocalDate.now(Clock.systemUTC())
                + "/" + generationId + "/";
    }

    private LocalDateTime nowUtc() {
        return LocalDateTime.now(Clock.systemUTC());
    }

    private String safeSummary(String message) {
        if (message == null || message.isBlank()) {
            return "Invoice PDF 生成失败。";
        }
        String normalized = message.replaceAll("\\s+", " ").trim();
        return normalized.length() <= 500 ? normalized : normalized.substring(0, 500);
    }

    private String safeFileName(String fileName, String fallback) {
        String value = trimToNull(fileName);
        if (value == null) {
            return fallback;
        }
        return value.replaceAll("[^A-Za-z0-9._-]", "_");
    }

    private String defaultIfBlank(String value, String defaultValue) {
        String normalized = trimToNull(value);
        return normalized == null ? defaultValue : normalized;
    }

    private String trimToNull(String value) {
        if (value == null || value.trim().isEmpty()) {
            return null;
        }
        return value.trim();
    }

    private boolean isBlank(String value) {
        return trimToNull(value) == null;
    }

    /**
     * 订单 / 任务可选上下文校验结果。
     *
     * @param sourceMessageId 可追溯来源消息 ID
     */
    private record ContextCheckResult(Long sourceMessageId) {
    }
}
