package de.votian.services.service;

import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.FontFactory;
import com.lowagie.text.Image;
import com.lowagie.text.PageSize;
import com.lowagie.text.Paragraph;
import com.lowagie.text.Phrase;
import com.lowagie.text.pdf.PdfPCell;
import com.lowagie.text.pdf.PdfPTable;
import com.lowagie.text.pdf.PdfWriter;
import de.votian.services.config.ParameterKeys;
import de.votian.services.dto.AcceptanceProtocolDto;
import de.votian.services.dto.AcceptanceProtocolQuestionDto;
import de.votian.services.dto.CustomerAcceptanceProtocolConfigDto;
import de.votian.services.dto.JobAcceptanceProtocolDto;
import de.votian.services.dto.JobAcceptanceProtocolQuestionDto;
import de.votian.services.entity.GenericDataContainer;
import de.votian.services.entity.Job;
import de.votian.services.entity.MetaType;
import de.votian.services.repository.CostCenterRepository;
import de.votian.services.repository.GenericDataContainerRepository;
import de.votian.services.repository.JobRepository;
import de.votian.services.repository.MetaTypeRepository;
import jakarta.persistence.EntityManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.imageio.ImageIO;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Service
@Transactional(readOnly = true)
public class JobAcceptanceProtocolService {

    private static final String JOB_OBJECT_TYPE = "jb";
    private static final String PACKING_PIECES_FIELD = "pack_pieces";
    private static final String MOBILE_PHOTOS_FIELD = "mobile_photos";
    private static final String MAIL_STATUS_FIELD = "mail_srv_acc_prot";
    private static final String LETTER_STATUS_FIELD = "letter_srv_acc_prot";
    private static final DateTimeFormatter DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
    private static final Pattern STATIC_RETURN_RULE = Pattern.compile("(?is)^\\s*return\\s+['\"]?(\\d+)['\"]?\\s*;\\s*$");

    private final JobRepository jobRepository;
    private final CostCenterRepository costCenterRepository;
    private final GenericDataContainerRepository genericDataContainerRepository;
    private final CustomerServiceConfigService customerServiceConfigService;
    private final MetaTypeRepository metaTypeRepository;
    private final ParameterService parameterService;
    private final JobService jobService;
    private final EntityManager entityManager;

    public JobAcceptanceProtocolService(JobRepository jobRepository,
                                        CostCenterRepository costCenterRepository,
                                        GenericDataContainerRepository genericDataContainerRepository,
                                        CustomerServiceConfigService customerServiceConfigService,
                                        MetaTypeRepository metaTypeRepository,
                                        ParameterService parameterService,
                                        JobService jobService,
                                        EntityManager entityManager) {
        this.jobRepository = jobRepository;
        this.costCenterRepository = costCenterRepository;
        this.genericDataContainerRepository = genericDataContainerRepository;
        this.customerServiceConfigService = customerServiceConfigService;
        this.metaTypeRepository = metaTypeRepository;
        this.parameterService = parameterService;
        this.jobService = jobService;
        this.entityManager = entityManager;
    }

    public JobAcceptanceProtocolDto getProtocol(Long jobId) {
        return buildSnapshot(jobId).dto();
    }

    public byte[] loadSignatureImage(Long jobId) {
        ProtocolSnapshot snapshot = buildSnapshot(jobId);
        return renderSignature(snapshot.signatureRaw());
    }

    public byte[] createPdf(Long jobId) {
        ProtocolSnapshot snapshot = buildSnapshot(jobId);
        JobAcceptanceProtocolDto dto = snapshot.dto();
        Job job = requireJob(jobId);

        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            Document document = new Document(PageSize.A4, 36, 36, 36, 36);
            PdfWriter.getInstance(document, outputStream);
            document.open();

            document.add(new Paragraph("Abnahmeprotokoll", FontFactory.getFont(FontFactory.HELVETICA_BOLD, 16)));
            document.add(new Paragraph(" "));

            PdfPTable meta = new PdfPTable(new float[]{2f, 4f});
            meta.setWidthPercentage(100);
            addMeta(meta, "Auftrag", String.valueOf(jobId));
            addMeta(meta, "Auftragszeit", job.getOrderTime() != null ? DATE_TIME_FORMAT.format(job.getOrderTime()) : "");
            addMeta(meta, "Status", dto.isFeatureEnabled() ? "Aktiv" : "Per Parameter deaktiviert");
            addMeta(meta, "Services", String.join(", ", dto.getSelectedServices()));
            addMeta(meta, "Protokoll-Service", safe(dto.getProtocolServiceLabel()));
            addMeta(meta, "Packstuecke", dto.getPackingPieces() != null ? String.valueOf(dto.getPackingPieces()) : "");
            addMeta(meta, "Mobile Fotos", dto.getMobilePhotoCount() != null ? String.valueOf(dto.getMobilePhotoCount()) : "0");
            addMeta(meta, "Mailstatus", safe(dto.getMailStatus()));
            addMeta(meta, "Briefstatus", safe(dto.getLetterStatus()));
            document.add(meta);

            if (!safe(dto.getResolutionNotice()).isBlank()) {
                document.add(new Paragraph(" "));
                document.add(new Paragraph(dto.getResolutionNotice(), FontFactory.getFont(FontFactory.HELVETICA_OBLIQUE, 9)));
            }

            if (!safe(dto.getText()).isBlank()) {
                document.add(new Paragraph(" "));
                document.add(new Paragraph("Protokolltext", FontFactory.getFont(FontFactory.HELVETICA_BOLD, 12)));
                document.add(new Paragraph(dto.getText(), FontFactory.getFont(FontFactory.HELVETICA, 10)));
            }

            if (!dto.getQuestions().isEmpty()) {
                document.add(new Paragraph(" "));
                document.add(new Paragraph("Fragen", FontFactory.getFont(FontFactory.HELVETICA_BOLD, 12)));
                PdfPTable table = new PdfPTable(new float[]{0.7f, 3.2f, 1.5f, 1.8f, 2.2f, 1.6f});
                table.setWidthPercentage(100);
                addHeader(table, "Nr.");
                addHeader(table, "Frage");
                addHeader(table, "Antwort");
                addHeader(table, "Artikel");
                addHeader(table, "Bemerkung");
                addHeader(table, "Mail");
                for (JobAcceptanceProtocolQuestionDto question : dto.getQuestions()) {
                    addValue(table, question.getSort() != null ? String.valueOf(question.getSort()) : "");
                    addValue(table, question.getQuestion());
                    addValue(table, question.getAnswer());
                    addValue(table, question.getArticle());
                    addValue(table, question.getRemark());
                    addValue(table, question.getMailState());
                }
                document.add(table);
            }

            byte[] signature = renderSignature(snapshot.signatureRaw());
            if (signature.length > 0) {
                document.add(new Paragraph(" "));
                document.add(new Paragraph("Unterschrift", FontFactory.getFont(FontFactory.HELVETICA_BOLD, 12)));
                Image image = Image.getInstance(signature);
                image.scaleToFit(220, 110);
                document.add(image);
            }

            document.close();
            return outputStream.toByteArray();
        } catch (DocumentException | IOException e) {
            throw new RuntimeException("Abnahmeprotokoll-PDF konnte nicht erzeugt werden", e);
        }
    }

    private ProtocolSnapshot buildSnapshot(Long jobId) {
        Job job = requireJob(jobId);
        Map<Integer, String> serviceLabels = loadServiceLabels();
        int effectiveServiceMask = normalizeServiceMask(job.getService());

        JobAcceptanceProtocolDto dto = new JobAcceptanceProtocolDto();
        dto.setJobId(jobId);
        dto.setJobService(job.getService());
        dto.setSelectedServices(resolveSelectedServices(effectiveServiceMask, serviceLabels));
        dto.setPackingPieces(resolvePackingPieces(jobId));
        dto.setMobilePhotoCount(countTokens(loadGdcContent(jobId, MOBILE_PHOTOS_FIELD)));
        dto.setMailStatus(describeDispatchStatus(jobId, MAIL_STATUS_FIELD, "MAIL_SENT"));
        dto.setLetterStatus(describeDispatchStatus(jobId, LETTER_STATUS_FIELD, "LETTER_SENT"));

        boolean useRelatedCustomer = "1".equals(parameterService.getParameterValueWithFallback(
                ParameterKeys.GLOBAL_USE_RELATED_CUSTOMER, safeLong(job.getHeadquartersId())));
        boolean protocolButtonEnabled = "1".equals(parameterService.getParameterValueWithFallback(
                ParameterKeys.MASK_JOBDETAILS_BUTTON_ACCEPTANCE_PROTOCOL, safeLong(job.getHeadquartersId())));
        dto.setFeatureEnabled(useRelatedCustomer && protocolButtonEnabled);

        Long customerId = resolveCustomerId(job);
        if (customerId == null) {
            dto.setDataAvailable(false);
            dto.setQuestions(List.of());
            dto.setText("");
            dto.setSignatureAvailable(false);
            return new ProtocolSnapshot(dto, "");
        }

        ProtocolResolution resolution = resolveProtocolService(effectiveServiceMask, customerId);
        dto.setProtocolServiceKey(resolution.serviceKey());
        dto.setProtocolServiceLabel(serviceLabels.getOrDefault(resolution.serviceKey(), "Service " + resolution.serviceKey()));
        dto.setResolutionNotice(resolution.notice());

        CustomerAcceptanceProtocolConfigDto config = customerServiceConfigService.getAcceptanceProtocols(customerId);
        AcceptanceProtocolDto configuredProtocol = findConfiguredProtocol(config, resolution.serviceKey());
        dto.setText(configuredProtocol != null ? safe(configuredProtocol.getText()) : "");

        String captureRaw = loadAcceptanceCapture(jobId);
        ParsedCapture capture = parseCapture(captureRaw);
        dto.setSignatureAvailable(!capture.signatureRaw().isBlank());

        Map<Integer, AcceptanceProtocolQuestionDto> configuredQuestions = new LinkedHashMap<>();
        if (configuredProtocol != null) {
            for (AcceptanceProtocolQuestionDto question : configuredProtocol.getQuestions()) {
                if (question != null && question.getSort() != null) {
                    configuredQuestions.put(question.getSort(), question);
                }
            }
        }

        TreeSet<Integer> sorts = new TreeSet<>();
        sorts.addAll(configuredQuestions.keySet());
        sorts.addAll(capture.answers().keySet());
        sorts.addAll(capture.articles().keySet());
        sorts.addAll(capture.remarks().keySet());

        List<JobAcceptanceProtocolQuestionDto> questions = new ArrayList<>();
        for (Integer sort : sorts) {
            AcceptanceProtocolQuestionDto configured = configuredQuestions.get(sort);
            JobAcceptanceProtocolQuestionDto question = new JobAcceptanceProtocolQuestionDto();
            question.setSort(sort);
            question.setQuestion(configured != null && !safe(configured.getQuestion()).isBlank()
                    ? configured.getQuestion()
                    : "Frage " + sort);
            question.setMailState(configured != null ? describeMailState(configured.getMailState()) : "");
            question.setAnswer(capture.answers().getOrDefault(sort, ""));
            question.setArticle(capture.articles().getOrDefault(sort, ""));
            question.setRemark(capture.remarks().getOrDefault(sort, ""));
            questions.add(question);
        }
        questions.sort(Comparator.comparing(JobAcceptanceProtocolQuestionDto::getSort, Comparator.nullsLast(Integer::compareTo)));
        dto.setQuestions(questions);
        dto.setDataAvailable(!questions.isEmpty() || !dto.getText().isBlank() || dto.isSignatureAvailable());

        return new ProtocolSnapshot(dto, capture.signatureRaw());
    }

    private AcceptanceProtocolDto findConfiguredProtocol(CustomerAcceptanceProtocolConfigDto config, Integer serviceKey) {
        if (config == null || config.getProtocols() == null || serviceKey == null) {
            return null;
        }
        for (AcceptanceProtocolDto protocol : config.getProtocols()) {
            if (protocol != null && serviceKey.equals(protocol.getServiceKey())) {
                return protocol;
            }
        }
        return null;
    }

    private Long resolveCustomerId(Job job) {
        Long costCenterId = job.getCostCenterRelatedId() != null ? job.getCostCenterRelatedId() : job.getCostCenterPayerId();
        if (costCenterId == null) {
            return null;
        }
        return costCenterRepository.findById(costCenterId)
                .map(de.votian.services.entity.CostCenter::getCustomerId)
                .orElse(null);
    }

    private int normalizeServiceMask(Integer serviceMask) {
        return serviceMask == null || serviceMask <= 0 ? 1 : serviceMask;
    }

    private ProtocolResolution resolveProtocolService(int jobServiceMask, Long customerId) {
        String ruleKey = ParameterKeys.MTF_SERVICE_ACCEPTANCE_PROTOCOL_RULE + "CS_" + customerId;
        String ruleSource = parameterService.getParameterValueWithFallback(ruleKey, 0L);
        if (!ruleSource.isBlank()) {
            Matcher matcher = STATIC_RETURN_RULE.matcher(ruleSource);
            if (matcher.matches()) {
                return new ProtocolResolution(Integer.parseInt(matcher.group(1)),
                        "Kundenspezifische Protokollregel wurde als fixer Servicewert uebernommen.");
            }
            return new ProtocolResolution(defaultProtocolService(jobServiceMask),
                    "Kundenspezifische Protokollregel ist vorhanden. Die NG-Ansicht nutzt hier die Legacy-Defaultprioritaet.");
        }
        return new ProtocolResolution(defaultProtocolService(jobServiceMask), "");
    }

    private int defaultProtocolService(int jobServiceMask) {
        boolean service1 = hasService(jobServiceMask, 1);
        boolean service2 = hasService(jobServiceMask, 2);
        boolean service4 = hasService(jobServiceMask, 4);
        boolean service6 = hasService(jobServiceMask, 6);

        if (service1) {
            if (service6) {
                return 6;
            }
            if (service2) {
                return 2;
            }
            if (service4) {
                return 4;
            }
            return 1;
        }
        if (service6) {
            return 6;
        }
        return 0;
    }

    private boolean hasService(int serviceMask, int sort) {
        return sort >= 0 && (serviceMask & (1 << sort)) != 0;
    }

    private List<String> resolveSelectedServices(int serviceMask, Map<Integer, String> serviceLabels) {
        List<String> selected = new ArrayList<>();
        for (int sort = 0; sort < Integer.SIZE - 1; sort++) {
            if ((serviceMask & (1 << sort)) != 0) {
                selected.add(serviceLabels.getOrDefault(sort, "Service " + sort));
            }
        }
        return selected;
    }

    private Map<Integer, String> loadServiceLabels() {
        Map<Integer, String> labels = new LinkedHashMap<>();
        for (MetaType metaType : metaTypeRepository.findByTypeOrderBySort("service")) {
            if (metaType.getSort() != null) {
                String label = safe(metaType.getValue());
                if (label.isBlank()) {
                    label = safe(metaType.getMappedValue());
                }
                labels.put(metaType.getSort(), label.isBlank() ? "Service " + metaType.getSort() : label);
            }
        }
        return labels;
    }

    private Integer resolvePackingPieces(Long jobId) {
        Integer fromProtocol = parseInteger(loadGdcContent(jobId, PACKING_PIECES_FIELD));
        if (fromProtocol != null) {
            return fromProtocol;
        }
        return jobService.getTotalPackingPieces(jobId);
    }

    private String loadGdcContent(Long jobId, String fieldName) {
        return genericDataContainerRepository.findByTypeAndIdAndField(JOB_OBJECT_TYPE, jobId, fieldName)
                .map(GenericDataContainer::getContent)
                .orElse("");
    }

    private String describeDispatchStatus(Long jobId, String fieldName, String successMarker) {
        Optional<GenericDataContainer> entry = genericDataContainerRepository.findByTypeAndIdAndField(JOB_OBJECT_TYPE, jobId, fieldName);
        if (entry.isEmpty()) {
            return "Nicht angelegt";
        }

        String context = safe(entry.get().getContext());
        if (context.contains(successMarker + "=OK")) {
            return "Versendet";
        }
        if (context.contains(successMarker + "=NOT_OK")) {
            return "Fehlgeschlagen";
        }
        if (context.contains("SEND_IN_PROGRESS")) {
            return "In Bearbeitung";
        }
        if (context.contains("SEND_PLEASE")) {
            return "Zur Verarbeitung";
        }
        return context.isBlank() ? "Angelegt" : context;
    }

    private String loadAcceptanceCapture(Long jobId) {
        String reference = String.valueOf(jobId) + "12";
        String current = loadAcceptanceCapture(reference, "phoenix_log.b2b_objects");
        if (!current.isBlank()) {
            return current;
        }
        return loadAcceptanceCapture(reference, "phoenix_log.b2b_objects_2015");
    }

    private String loadAcceptanceCapture(String reference, String tableName) {
        try {
            @SuppressWarnings("unchecked")
            List<Object> rows = entityManager.createNativeQuery(
                            "SELECT bo_obj_data FROM " + tableName + " WHERE bo_ext_id2 = :reference ORDER BY bo_id DESC")
                    .setParameter("reference", reference)
                    .setMaxResults(1)
                    .getResultList();
            if (!rows.isEmpty() && rows.get(0) != null) {
                return rows.get(0).toString();
            }
        } catch (RuntimeException ignored) {
            // Keep the detail view usable even if the legacy log schema is unavailable.
        }
        return "";
    }

    private ParsedCapture parseCapture(String raw) {
        if (raw == null || raw.isBlank()) {
            return new ParsedCapture("", Map.of(), Map.of(), Map.of());
        }

        String[] tokens = raw.split("\\|", -1);
        String signature = tokens.length > 2 ? safe(tokens[2]) : "";
        Map<Integer, String> answers = new LinkedHashMap<>();
        Map<Integer, String> articles = new LinkedHashMap<>();
        Map<Integer, String> remarks = new LinkedHashMap<>();

        for (int index = 2; index < tokens.length; index++) {
            String token = safe(tokens[index]);
            if (token.endsWith("pp") && index + 2 < tokens.length) {
                Integer sort = parseInteger(tokens[index + 1]);
                if (sort != null) {
                    answers.put(sort, describeAnswer(tokens[index + 2]));
                }
            } else if (token.endsWith("pa") && index + 3 < tokens.length) {
                Integer sort = parseInteger(tokens[index + 1]);
                if (sort != null) {
                    articles.put(sort, safe(tokens[index + 2]));
                    remarks.put(sort, normalizeRemark(tokens[index + 3]));
                }
            }
        }

        return new ParsedCapture(signature, answers, articles, remarks);
    }

    private String describeAnswer(String rawAnswer) {
        String normalized = safe(rawAnswer);
        if (!normalized.isBlank()) {
            normalized = normalized.substring(0, 1).toUpperCase(Locale.ROOT);
        }
        return switch (normalized) {
            case "J" -> "Ja";
            case "N" -> "Nein";
            case "Z" -> "Nicht zutreffend";
            default -> rawAnswer != null ? rawAnswer : "";
        };
    }

    private String normalizeRemark(String rawRemark) {
        String remark = safe(rawRemark);
        return remark.endsWith("pp") ? remark.substring(0, remark.length() - 2) : remark;
    }

    private String describeMailState(String value) {
        return switch (safe(value)) {
            case "1" -> "Mail bei JA";
            case "2" -> "Mail bei NEIN";
            default -> "";
        };
    }

    private int countTokens(String raw) {
        if (raw == null || raw.isBlank()) {
            return 0;
        }
        int count = 0;
        for (String token : raw.split("\\|")) {
            if (!token.isBlank()) {
                count++;
            }
        }
        return count;
    }

    private byte[] renderSignature(String rawSignature) {
        List<List<int[]>> strokes = parseSignature(rawSignature);
        if (strokes.isEmpty()) {
            return new byte[0];
        }

        int maxX = 1;
        int maxY = 1;
        for (List<int[]> stroke : strokes) {
            for (int[] point : stroke) {
                maxX = Math.max(maxX, point[0] + 1);
                maxY = Math.max(maxY, point[1] + 1);
            }
        }

        BufferedImage image = new BufferedImage(maxX, maxY, BufferedImage.TYPE_INT_RGB);
        Graphics2D graphics = image.createGraphics();
        graphics.setColor(Color.WHITE);
        graphics.fillRect(0, 0, maxX, maxY);
        graphics.setColor(Color.BLACK);
        graphics.setStroke(new BasicStroke(2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));

        for (List<int[]> stroke : strokes) {
            for (int index = 0; index < stroke.size() - 1; index++) {
                int[] start = stroke.get(index);
                int[] end = stroke.get(index + 1);
                graphics.drawLine(start[0], start[1], end[0], end[1]);
            }
        }
        graphics.dispose();

        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            ImageIO.write(image, "png", outputStream);
            return outputStream.toByteArray();
        } catch (IOException e) {
            throw new RuntimeException("Signaturbild konnte nicht erzeugt werden", e);
        }
    }

    private List<List<int[]>> parseSignature(String rawSignature) {
        List<List<int[]>> strokes = new ArrayList<>();
        if (rawSignature == null || rawSignature.isBlank()) {
            return strokes;
        }

        for (String cluster : rawSignature.trim().split("A")) {
            if (cluster.isBlank()) {
                continue;
            }
            List<int[]> stroke = new ArrayList<>();
            for (String pair : cluster.split(";")) {
                if (pair.isBlank()) {
                    continue;
                }
                String[] coordinates = pair.split(",", -1);
                if (coordinates.length < 2) {
                    continue;
                }
                Integer x = parseInteger(coordinates[0]);
                Integer y = parseInteger(coordinates[1]);
                if (x != null && y != null) {
                    stroke.add(new int[]{x, y});
                }
            }
            if (stroke.size() > 1) {
                strokes.add(stroke);
            }
        }
        return strokes;
    }

    private void addMeta(PdfPTable table, String label, String value) {
        PdfPCell labelCell = new PdfPCell(new Phrase(label, FontFactory.getFont(FontFactory.HELVETICA_BOLD, 9)));
        labelCell.setPadding(5f);
        table.addCell(labelCell);
        PdfPCell valueCell = new PdfPCell(new Phrase(safe(value), FontFactory.getFont(FontFactory.HELVETICA, 9)));
        valueCell.setPadding(5f);
        table.addCell(valueCell);
    }

    private void addHeader(PdfPTable table, String value) {
        PdfPCell cell = new PdfPCell(new Phrase(value, FontFactory.getFont(FontFactory.HELVETICA_BOLD, 9)));
        cell.setPadding(4f);
        table.addCell(cell);
    }

    private void addValue(PdfPTable table, String value) {
        PdfPCell cell = new PdfPCell(new Phrase(safe(value), FontFactory.getFont(FontFactory.HELVETICA, 8)));
        cell.setPadding(4f);
        table.addCell(cell);
    }

    private Job requireJob(Long jobId) {
        return jobRepository.findById(Objects.requireNonNull(jobId))
                .orElseThrow(() -> new RuntimeException("Job nicht gefunden: " + jobId));
    }

    private Integer parseInteger(String raw) {
        if (raw == null || raw.isBlank()) {
            return null;
        }
        try {
            return Integer.parseInt(raw.trim());
        } catch (NumberFormatException ignored) {
            return null;
        }
    }

    private long safeLong(Long value) {
        return value != null ? value : 0L;
    }

    private String safe(String value) {
        return value != null ? value : "";
    }

    private record ProtocolResolution(int serviceKey, String notice) { }

    private record ParsedCapture(String signatureRaw,
                                 Map<Integer, String> answers,
                                 Map<Integer, String> articles,
                                 Map<Integer, String> remarks) { }

    private record ProtocolSnapshot(JobAcceptanceProtocolDto dto, String signatureRaw) { }
}
