package de.votian.services.service;

import de.votian.services.config.LegacySchedulingProperties;
import de.votian.services.config.ParameterKeys;
import de.votian.services.dto.JobAcceptanceProtocolDto;
import de.votian.services.entity.CostCenter;
import de.votian.services.entity.CostCenterAddress;
import de.votian.services.entity.Customer;
import de.votian.services.entity.GenericDataContainer;
import de.votian.services.entity.Job;
import de.votian.services.repository.CostCenterAddressRepository;
import de.votian.services.repository.CostCenterRepository;
import de.votian.services.repository.CustomerRepository;
import de.votian.services.repository.GenericDataContainerRepository;
import de.votian.services.repository.JobRepository;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;

import java.nio.charset.StandardCharsets;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;

@Service
public class AcceptanceProtocolAutomationService {

    private static final Logger log = LoggerFactory.getLogger(AcceptanceProtocolAutomationService.class);
    private static final String JOB_OBJECT_TYPE = "jb";
    private static final String MAIL_FIELD = "mail_srv_acc_prot";
    private static final String LETTER_FIELD = "letter_srv_acc_prot";
    private static final String IN_PROGRESS = "SEND_IN_PROGRESS";
    private static final String MAIL_OK = "MAIL_SENT=OK";
    private static final String MAIL_NOT_OK = "MAIL_SENT=NOT_OK";
    private static final String LETTER_OK = "LETTER_SENT=OK";
    private static final String LETTER_NOT_OK = "LETTER_SENT=NOT_OK";
    private static final String PDF_BASE_NAME = "ABNAHMEPROTOKOLL";

    private final JdbcTemplate jdbcTemplate;
    private final JobRepository jobRepository;
    private final CostCenterRepository costCenterRepository;
    private final CustomerRepository customerRepository;
    private final CostCenterAddressRepository costCenterAddressRepository;
    private final GenericDataContainerRepository genericDataContainerRepository;
    private final JobAcceptanceProtocolService jobAcceptanceProtocolService;
    private final ParameterService parameterService;
    private final LegacyFileTransferService legacyFileTransferService;
    private final ObjectProvider<JavaMailSender> mailSenderProvider;
    private final LegacySchedulingProperties schedulingProperties;
    private final AtomicBoolean mailRunning = new AtomicBoolean();
    private final AtomicBoolean letterRunning = new AtomicBoolean();

    public AcceptanceProtocolAutomationService(JdbcTemplate jdbcTemplate,
                                               JobRepository jobRepository,
                                               CostCenterRepository costCenterRepository,
                                               CustomerRepository customerRepository,
                                               CostCenterAddressRepository costCenterAddressRepository,
                                               GenericDataContainerRepository genericDataContainerRepository,
                                               JobAcceptanceProtocolService jobAcceptanceProtocolService,
                                               ParameterService parameterService,
                                               LegacyFileTransferService legacyFileTransferService,
                                               ObjectProvider<JavaMailSender> mailSenderProvider,
                                               LegacySchedulingProperties schedulingProperties) {
        this.jdbcTemplate = jdbcTemplate;
        this.jobRepository = jobRepository;
        this.costCenterRepository = costCenterRepository;
        this.customerRepository = customerRepository;
        this.costCenterAddressRepository = costCenterAddressRepository;
        this.genericDataContainerRepository = genericDataContainerRepository;
        this.jobAcceptanceProtocolService = jobAcceptanceProtocolService;
        this.parameterService = parameterService;
        this.legacyFileTransferService = legacyFileTransferService;
        this.mailSenderProvider = mailSenderProvider;
        this.schedulingProperties = schedulingProperties;
    }

    public int dispatchPendingMailProtocols() {
        if (!isCronEnabled() || !mailRunning.compareAndSet(false, true)) {
            return 0;
        }

        try {
            int processed = 0;
            for (Long jobId : loadMailCandidates()) {
                if (dispatchMail(jobId)) {
                    processed++;
                }
            }
            if (processed > 0) {
                log.info("Acceptance protocol mail automation processed {} job(s).", processed);
            }
            return processed;
        } finally {
            mailRunning.set(false);
        }
    }

    public int dispatchPendingLetterProtocols() {
        if (!isCronEnabled() || !letterRunning.compareAndSet(false, true)) {
            return 0;
        }

        try {
            int processed = 0;
            for (Long jobId : loadLetterCandidates()) {
                if (dispatchLetter(jobId)) {
                    processed++;
                }
            }
            if (processed > 0) {
                log.info("Acceptance protocol letter automation processed {} job(s).", processed);
            }
            return processed;
        } finally {
            letterRunning.set(false);
        }
    }

    private boolean dispatchMail(Long jobId) {
        DispatchContext context = resolveContext(jobId);
        if (context == null || !context.protocol().isDataAvailable() || !context.protocol().isSignatureAvailable()) {
            return false;
        }

        MailRecipients recipients = resolveRecipients(context);
        if (recipients.to().isEmpty()) {
            return false;
        }
        if (!markInProgress(jobId, MAIL_FIELD)) {
            return false;
        }

        try {
            byte[] pdf = jobAcceptanceProtocolService.createPdf(jobId);
            sendMail(context, recipients, pdf);
            updateDispatchState(jobId, MAIL_FIELD, MAIL_OK);
            return true;
        } catch (RuntimeException exception) {
            log.warn("Acceptance protocol mail dispatch failed for job {}: {}", jobId, exception.getMessage());
            updateDispatchState(jobId, MAIL_FIELD, MAIL_NOT_OK);
            return false;
        }
    }

    private boolean dispatchLetter(Long jobId) {
        DispatchContext context = resolveContext(jobId);
        if (context == null || !context.protocol().isDataAvailable() || !context.protocol().isSignatureAvailable()) {
            return false;
        }
        if (!markInProgress(jobId, LETTER_FIELD)) {
            return false;
        }

        try {
            byte[] pdf = jobAcceptanceProtocolService.createPdf(jobId);
            String fileName = PDF_BASE_NAME + "_" + jobId + ".pdf";
            legacyFileTransferService.upload(
                    schedulingProperties.getAcceptanceProtocolLetterFtpProfile(),
                    fileName,
                    pdf
            );
            updateDispatchState(jobId, LETTER_FIELD, LETTER_OK);
            return true;
        } catch (RuntimeException exception) {
            log.warn("Acceptance protocol letter dispatch failed for job {}: {}", jobId, exception.getMessage());
            updateDispatchState(jobId, LETTER_FIELD, LETTER_NOT_OK);
            return false;
        }
    }

    private DispatchContext resolveContext(Long jobId) {
        Job job = jobRepository.findById(Objects.requireNonNull(jobId)).orElse(null);
        if (job == null || job.getHeadquartersId() == null || job.getCostCenterRelatedId() == null) {
            return null;
        }

        CostCenter relatedCostCenter = costCenterRepository.findById(job.getCostCenterRelatedId()).orElse(null);
        if (relatedCostCenter == null || relatedCostCenter.getCustomerId() == null) {
            return null;
        }

        Customer customer = customerRepository.findById(relatedCostCenter.getCustomerId()).orElse(null);
        if (customer == null) {
            return null;
        }

        JobAcceptanceProtocolDto protocol = jobAcceptanceProtocolService.getProtocol(jobId);
        Long groupId = extractFirstNumericGroup(customer.getGroup());
        return new DispatchContext(job, customer, protocol, groupId);
    }

    private MailRecipients resolveRecipients(DispatchContext context) {
        long headquartersId = context.job().getHeadquartersId();
        Long customerId = context.customer().getId();
        Long groupId = context.groupId();

        String mailTo = firstNonBlank(
                parameterService.getObjectBasedParameterValue(ParameterKeys.AUTOMAILER_ACCEPTANCE_PROTOCOL_MAIL_TO + "_CS", customerId, headquartersId, 0L, true),
                groupId != null
                        ? parameterService.getObjectBasedParameterValue(ParameterKeys.AUTOMAILER_ACCEPTANCE_PROTOCOL_MAIL_TO + "_GRP", groupId, headquartersId, 0L, true)
                        : "",
                parameterService.getParameterValueWithFallback(ParameterKeys.AUTOMAILER_ACCEPTANCE_PROTOCOL_MAIL_TO, headquartersId)
        );
        String mailCc = firstNonBlank(
                parameterService.getObjectBasedParameterValue(ParameterKeys.AUTOMAILER_ACCEPTANCE_PROTOCOL_MAIL_CC + "_CS", customerId, headquartersId, 0L, true),
                groupId != null
                        ? parameterService.getObjectBasedParameterValue(ParameterKeys.AUTOMAILER_ACCEPTANCE_PROTOCOL_MAIL_CC + "_GRP", groupId, headquartersId, 0L, true)
                        : "",
                parameterService.getParameterValueWithFallback(ParameterKeys.AUTOMAILER_ACCEPTANCE_PROTOCOL_MAIL_CC, headquartersId)
        );
        String mailBcc = firstNonBlank(
                parameterService.getObjectBasedParameterValue(ParameterKeys.AUTOMAILER_ACCEPTANCE_PROTOCOL_MAIL_BCC + "_CS", customerId, headquartersId, 0L, true),
                groupId != null
                        ? parameterService.getObjectBasedParameterValue(ParameterKeys.AUTOMAILER_ACCEPTANCE_PROTOCOL_MAIL_BCC + "_GRP", groupId, headquartersId, 0L, true)
                        : "",
                parameterService.getParameterValueWithFallback(ParameterKeys.AUTOMAILER_ACCEPTANCE_PROTOCOL_MAIL_BCC, headquartersId)
        );

        LinkedHashSet<String> toRecipients = splitRecipients(mailTo);
        resolvePayerMailAddress(context.job().getCostCenterPayerId()).ifPresent(toRecipients::add);
        return new MailRecipients(
                List.copyOf(toRecipients),
                List.copyOf(splitRecipients(mailCc)),
                List.copyOf(splitRecipients(mailBcc))
        );
    }

    private Optional<String> resolvePayerMailAddress(Long payerCostCenterId) {
        if (payerCostCenterId == null) {
            return Optional.empty();
        }

        for (int addressTypeId : List.of(4, 2, 3, 1)) {
            Optional<String> address = costCenterAddressRepository.findByCostCenterIdAndAddressTypeId(payerCostCenterId, addressTypeId)
                    .map(CostCenterAddress::getEmail)
                    .map(this::normalizeRecipient)
                    .filter(value -> !value.isBlank());
            if (address.isPresent()) {
                return address;
            }
        }
        return Optional.empty();
    }

    private void sendMail(DispatchContext context, MailRecipients recipients, byte[] pdf) {
        JavaMailSender mailSender = mailSenderProvider.getIfAvailable();
        if (mailSender == null) {
            throw new IllegalStateException("Kein JavaMailSender konfiguriert.");
        }

        MimeMessage message = mailSender.createMimeMessage();
        try {
            MimeMessageHelper helper = new MimeMessageHelper(message, true, StandardCharsets.UTF_8.name());
            helper.setTo(recipients.to().toArray(String[]::new));
            if (!recipients.cc().isEmpty()) {
                helper.setCc(recipients.cc().toArray(String[]::new));
            }
            if (!recipients.bcc().isEmpty()) {
                helper.setBcc(recipients.bcc().toArray(String[]::new));
            }
            helper.setSubject(PDF_BASE_NAME + " (" + context.job().getId() + ")");
            helper.setText(buildMailBody(context), false);

            String senderAddress = parameterService.getParameterValueWithFallback(ParameterKeys.MAIL_SENDER_ADDRESS, context.job().getHeadquartersId());
            if (!senderAddress.isBlank()) {
                helper.setFrom(senderAddress);
                helper.setReplyTo(senderAddress);
            }

            helper.addAttachment(PDF_BASE_NAME + "_" + context.job().getId() + ".pdf", new ByteArrayResource(pdf));
        } catch (MessagingException exception) {
            throw new IllegalStateException("Mail konnte nicht vorbereitet werden.", exception);
        }

        try {
            mailSender.send(message);
        } catch (MailException exception) {
            throw new IllegalStateException("Mailversand fehlgeschlagen.", exception);
        }
    }

    private String buildMailBody(DispatchContext context) {
        StringBuilder body = new StringBuilder();
        body.append("Sehr geehrte Damen und Herren,").append("\n\n");
        body.append("im Anhang erhalten Sie das Abnahmeprotokoll zum Auftrag ")
                .append(context.job().getId())
                .append('.')
                .append("\n\n");
        if (context.protocol().getProtocolServiceLabel() != null && !context.protocol().getProtocolServiceLabel().isBlank()) {
            body.append("Protokoll-Service: ")
                    .append(context.protocol().getProtocolServiceLabel())
                    .append("\n");
        }
        body.append("\nMit freundlichen Gruessen");
        return body.toString();
    }

    private boolean markInProgress(Long jobId, String fieldName) {
        Optional<GenericDataContainer> existing = genericDataContainerRepository.findByTypeAndIdAndField(JOB_OBJECT_TYPE, jobId, fieldName);
        if (existing.isPresent() && contains(existing.get().getContext(), IN_PROGRESS)) {
            return false;
        }

        GenericDataContainer container = existing.orElseGet(GenericDataContainer::new);
        container.setObjectType(JOB_OBJECT_TYPE);
        container.setObjectId(jobId);
        container.setFieldName(fieldName);
        container.setContent(existing.map(GenericDataContainer::getContent).orElse(""));
        container.setContext(IN_PROGRESS);
        genericDataContainerRepository.save(container);
        return true;
    }

    private void updateDispatchState(Long jobId, String fieldName, String context) {
        GenericDataContainer container = genericDataContainerRepository.findByTypeAndIdAndField(JOB_OBJECT_TYPE, jobId, fieldName)
                .orElseGet(GenericDataContainer::new);
        container.setObjectType(JOB_OBJECT_TYPE);
        container.setObjectId(jobId);
        container.setFieldName(fieldName);
        container.setContent(container.getContent() != null ? container.getContent() : "");
        container.setContext(context);
        genericDataContainerRepository.save(container);
    }

    private List<Long> loadMailCandidates() {
        return jdbcTemplate.queryForList(
                """
                        SELECT DISTINCT jb.jb_id
                        FROM job AS jb
                        JOIN costcenteraddress AS cscad ON cscad.csc_id = jb.csc_id_payer AND cscad.adt_id = 2
                        LEFT JOIN genericdatacontainer AS gdc
                               ON gdc.gdc_obj_type = 'jb'
                              AND gdc.gdc_obj_id = jb.jb_id
                              AND gdc.gdc_gen_fieldname = ?
                        WHERE jb.jb_finishtime > ?
                          AND (jb.jb_storno IS NULL OR jb.jb_storno = '0')
                          AND jb.jb_status = 2
                          AND COALESCE(jb.jb_offer, 0) = 0
                          AND (jb.jb_service & 2) = 2
                          AND COALESCE(cscad.cscad_email, '') <> ''
                          AND (gdc.gdc_id IS NULL
                               OR (COALESCE(gdc.gdc_context, '') NOT LIKE '%MAIL_SENT=OK%'
                                   AND COALESCE(gdc.gdc_context, '') NOT LIKE '%SEND_IN_PROGRESS%'))
                        ORDER BY jb.jb_id
                        """,
                Long.class,
                MAIL_FIELD,
                Timestamp.valueOf(LocalDateTime.now().minusDays(Math.max(1, schedulingProperties.getAcceptanceProtocolMailLookbackDays())))
        );
    }

    private List<Long> loadLetterCandidates() {
        return jdbcTemplate.queryForList(
                """
                        SELECT DISTINCT jb.jb_id
                        FROM job AS jb
                        JOIN costcenteraddress AS cscad ON cscad.csc_id = jb.csc_id_payer AND cscad.adt_id = 2
                        LEFT JOIN genericdatacontainer AS gdc
                               ON gdc.gdc_obj_type = 'jb'
                              AND gdc.gdc_obj_id = jb.jb_id
                              AND gdc.gdc_gen_fieldname = ?
                        WHERE jb.jb_finishtime > ?
                          AND (jb.jb_storno IS NULL OR jb.jb_storno = '0')
                          AND jb.jb_status = 2
                          AND COALESCE(jb.jb_offer, 0) = 0
                          AND (jb.jb_service & 2) = 2
                          AND COALESCE(cscad.cscad_email, '') = ''
                          AND (gdc.gdc_id IS NULL
                               OR (COALESCE(gdc.gdc_context, '') NOT LIKE '%LETTER_SENT=OK%'
                                   AND COALESCE(gdc.gdc_context, '') NOT LIKE '%SEND_IN_PROGRESS%'))
                        ORDER BY jb.jb_id
                        """,
                Long.class,
                LETTER_FIELD,
                Timestamp.valueOf(LocalDateTime.now().minusDays(Math.max(1, schedulingProperties.getAcceptanceProtocolLetterLookbackDays())))
        );
    }

    private boolean isCronEnabled() {
        return "1".equals(parameterService.getParameterValueWithFallback(ParameterKeys.GLOBAL_CRON_ENABLED, 0L))
                && "1".equals(parameterService.getParameterValueWithFallback(
                ParameterKeys.CRON_SERVICE_ACCEPTANCE_PROTOCOL_ENABLED,
                0L
        ));
    }

    private Long extractFirstNumericGroup(String rawValue) {
        if (rawValue == null || rawValue.isBlank()) {
            return null;
        }
        for (String token : rawValue.split(",")) {
            String normalized = token != null ? token.trim() : "";
            if (normalized.matches("\\d+")) {
                return Long.valueOf(normalized);
            }
        }
        return null;
    }

    private String firstNonBlank(String... values) {
        for (String value : values) {
            if (value != null && !value.isBlank()) {
                return value;
            }
        }
        return "";
    }

    private LinkedHashSet<String> splitRecipients(String rawRecipients) {
        LinkedHashSet<String> recipients = new LinkedHashSet<>();
        if (rawRecipients == null || rawRecipients.isBlank()) {
            return recipients;
        }
        for (String token : rawRecipients.split("[,;]")) {
            String normalized = normalizeRecipient(token);
            if (!normalized.isBlank()) {
                recipients.add(normalized);
            }
        }
        return recipients;
    }

    private String normalizeRecipient(String value) {
        String normalized = value != null ? value.trim() : "";
        if (normalized.isBlank()) {
            return "";
        }
        return normalized.toLowerCase(Locale.ROOT);
    }

    private boolean contains(String value, String token) {
        return value != null && value.contains(token);
    }

    private record DispatchContext(Job job, Customer customer, JobAcceptanceProtocolDto protocol, Long groupId) {
    }

    private record MailRecipients(List<String> to, List<String> cc, List<String> bcc) {
    }
}
