package de.assecutor.votianlt.pages.service;

import de.assecutor.votianlt.dto.JobWithRelatedDataDTO;
import de.assecutor.votianlt.model.CargoItem;
import de.assecutor.votianlt.model.DeliveryStation;
import de.assecutor.votianlt.model.Job;
import de.assecutor.votianlt.model.JobStatus;
import de.assecutor.votianlt.model.task.BaseTask;
import de.assecutor.votianlt.messaging.MessagingPublisher;
import de.assecutor.votianlt.repository.JobRepository;
import de.assecutor.votianlt.repository.TaskRepository;
import de.assecutor.votianlt.security.SecurityService;
import de.assecutor.votianlt.repository.CargoItemRepository;
import de.assecutor.votianlt.service.ClientConnectionService;
import de.assecutor.votianlt.service.JobHistoryService;
import de.assecutor.votianlt.service.EmailService;
import de.assecutor.votianlt.service.TaskAssignmentService;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.bson.types.ObjectId;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

@Service
@RequiredArgsConstructor
@Slf4j
public class AddJobService {

    private final CargoItemRepository cargoItemRepository;
    private final JobRepository jobRepository;
    private final TaskRepository taskRepository;
    private final SecurityService securityService;
    private final JobHistoryService jobHistoryService;
    private final EmailService emailService;
    private final ClientConnectionService clientConnectionService;
    private final MessagingPublisher messagingPublisher;
    private final TaskAssignmentService taskAssignmentService;

    /**
     * Speichert einen neuen Auftrag samt CargoItems und Tasks
     * 
     * @param job
     *            der Auftrag
     * @param transientCargo
     *            zugehörige, noch nicht gespeicherte CargoItems aus der View
     */
    public Job addJobWithCargo(Job job, List<CargoItem> transientCargo, List<BaseTask> transientTasks) {
        try {
            // Metadaten setzen
            LocalDateTime now = LocalDateTime.now();
            job.setCreatedAt(now);
            job.setUpdatedAt(now);
            job.setStatus(JobStatus.CREATED);
            job.setCreatedBy(securityService.getCurrentUserId().toHexString());

            // Auftragsnummer generieren, falls nicht vorhanden
            if (job.getJobNumber() == null || job.getJobNumber().isEmpty()) {
                job.setJobNumber(generateJobNumber());
            }

            ensureDeliveryStationIds(job);

            // Auftrag speichern
            Job savedJob = jobRepository.save(job);
            final ObjectId jobId = savedJob.getId();
            boolean modified = false;

            // CargoItems separat mit Referenz auf Job speichern, IDs im Job verknüpfen
            if (transientCargo != null && !transientCargo.isEmpty()) {
                List<CargoItem> itemsWithJob = transientCargo.stream().filter(Objects::nonNull)
                        .filter(ci -> ci.getDescription() != null && !ci.getDescription().isBlank()).map(ci -> {
                            CargoItem copy = new CargoItem();
                            copy.setJobId(jobId);
                            copy.setDescription(ci.getDescription());
                            copy.setQuantity(ci.getQuantity());
                            copy.setWeightKg(ci.getWeightKg());
                            copy.setLengthMm(ci.getLengthMm());
                            copy.setWidthMm(ci.getWidthMm());
                            copy.setHeightMm(ci.getHeightMm());
                            return copy;
                        }).toList();
                cargoItemRepository.saveAll(itemsWithJob);
                modified = true;
            }

            // Tasks separat speichern und referenzieren mit korrekter Nummerierung
            if (transientTasks != null && !transientTasks.isEmpty()) {
                var filteredTasks = transientTasks.stream().filter(Objects::nonNull)
                        .filter(task -> task.getTaskType() != null).toList();

                Map<Integer, Integer> taskOrderByStation = new HashMap<>();
                Map<Integer, ObjectId> stationIdByOrder = buildStationIdByOrder(savedJob);
                List<BaseTask> tasksToPersist = new ArrayList<>();

                // Setze stationId und stelle sicher, dass taskOrder je Lieferstation korrekt
                // ist
                for (BaseTask task : filteredTasks) {
                    int stationOrder = task.getStationOrder() != null ? task.getStationOrder() : 0;
                    ObjectId stationId = task.getStationId() != null ? task.getStationId()
                            : stationIdByOrder.get(stationOrder);
                    if (stationId == null) {
                        log.warn("Skipping task without resolvable stationId for job {} and stationOrder {}", jobId,
                                stationOrder);
                        continue;
                    }

                    task.setStationId(stationId);
                    task.setJobId(null);
                    if (task.getTaskOrder() == null) {
                        int nextTaskOrder = taskOrderByStation.getOrDefault(stationOrder, 0);
                        task.setTaskOrder(nextTaskOrder);
                        taskOrderByStation.put(stationOrder, nextTaskOrder + 1);
                    } else {
                        int nextTaskOrder = Math.max(taskOrderByStation.getOrDefault(stationOrder, 0),
                                task.getTaskOrder() + 1);
                        taskOrderByStation.put(stationOrder, nextTaskOrder);
                    }
                    tasksToPersist.add(task);
                }

                taskRepository.saveAll(tasksToPersist);
                attachTasksToDeliveryStations(savedJob, tasksToPersist);
                savedJob = jobRepository.save(savedJob);
                log.info("Saved {} tasks for job {} with station-based assignment", tasksToPersist.size(), jobId);
            } else if (savedJob.getDeliveryStations() != null && !savedJob.getDeliveryStations().isEmpty()) {
                attachTasksToDeliveryStations(savedJob, List.of());
                savedJob = jobRepository.save(savedJob);
            }

            if (modified) {
                savedJob = jobRepository.save(savedJob);
            }

            // History-Eintrag für Job-Erstellung
            try {
                String currentUserName = getCurrentUserName();
                jobHistoryService.logJobCreation(savedJob, currentUserName);
            } catch (Exception e) {
                log.warn("Failed to log job creation history for job {}: {}", savedJob.getIdAsString(), e.getMessage());
            }

            // E-Mail-Benachrichtigung für Job-Erstellung senden
            try {
                emailService.sendJobCreationNotification(savedJob.getId(), savedJob.getCreatedBy());
            } catch (Exception e) {
                log.warn("Failed to send job creation email notification for job {}: {}", savedJob.getIdAsString(),
                        e.getMessage());
            }

            // Benachrichtigung an Client senden, wenn online
            notifyClientJobCreated(savedJob);

            log.info("Auftrag erfolgreich gespeichert: {}", savedJob.getJobNumber());
            return savedJob;

        } catch (Exception e) {
            log.error("Fehler beim Speichern des Auftrags: {}", e.getMessage(), e);
            throw new RuntimeException("Auftrag konnte nicht gespeichert werden: " + e.getMessage());
        }
    }

    /**
     * Generiert eine eindeutige Auftragsnummer
     */
    private String generateJobNumber() {
        String prefix = "JOB";
        String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));

        // Zähle Aufträge des aktuellen Tages
        String todayPrefix = prefix + timestamp;
        long todayCount = jobRepository.findAll().stream()
                .filter(job -> job.getJobNumber() != null && job.getJobNumber().startsWith(todayPrefix)).count();

        // Generiere neue Nummer
        String jobNumber;
        do {
            todayCount++;
            jobNumber = String.format("%s%s%03d", prefix, timestamp, todayCount);
        } while (jobRepository.existsByJobNumber(jobNumber));

        return jobNumber;
    }

    /**
     * Findet den aktuellen Entwurf eines Benutzers
     */
    public Optional<Job> findCurrentDraft(String username) {
        List<Job> drafts = jobRepository.findByCreatedByAndIsDraftTrue(username);
        return drafts.isEmpty() ? Optional.empty() : Optional.of(drafts.getFirst());
    }

    /**
     * Hilfsmethode um den aktuellen Benutzernamen zu ermitteln
     */
    private String getCurrentUserName() {
        try {
            var authenticatedUserOpt = securityService.getAuthenticatedUser();
            if (authenticatedUserOpt.isPresent()) {
                var user = authenticatedUserOpt.get();
                return user.getUsername() != null ? user.getUsername() : "Unknown User";
            }
        } catch (Exception e) {
            log.debug("Could not get authenticated user: {}", e.getMessage());
        }
        return "System"; // Fallback wenn kein authentifizierter Benutzer gefunden wird
    }

    /**
     * Sendet den neu erstellten Job per WebSocket an den zugewiesenen Client, falls
     * dieser online ist.
     */
    private void notifyClientJobCreated(Job job) {
        if (!job.isDigitalProcessing()) {
            return;
        }

        String appUserId = job.getAppUser();
        if (appUserId == null || appUserId.isBlank()) {
            return;
        }

        if (!clientConnectionService.isClientConnected(appUserId)) {
            log.info("[JOB] Client {} not online, skipping job_created notification", appUserId);
            return;
        }

        try {
            // Lade CargoItems und Tasks für den Job
            List<CargoItem> cargoItems = cargoItemRepository.findByJobId(job.getId());
            List<BaseTask> tasks = taskAssignmentService.findTasksForJob(job);

            // Erstelle DTO mit allen Daten
            JobWithRelatedDataDTO jobData = new JobWithRelatedDataDTO(job, cargoItems, tasks);

            log.info("[JOB] Sending job_created to {}: jobId={}, jobNumber={}", appUserId, job.getId().toHexString(),
                    job.getJobNumber());
            messagingPublisher.publishAsJson(appUserId, "job_created", jobData);
        } catch (Exception e) {
            log.warn("[JOB] Failed to send job_created notification: {}", e.getMessage());
        }
    }

    private void attachTasksToDeliveryStations(Job job, List<BaseTask> tasks) {
        if (job.getDeliveryStations() == null || job.getDeliveryStations().isEmpty()) {
            return;
        }

        Map<String, List<BaseTask>> tasksByStationId = new HashMap<>();
        Map<Integer, List<BaseTask>> legacyTasksByStationOrder = new HashMap<>();
        for (BaseTask task : tasks) {
            if (task == null) {
                continue;
            }
            if (task.getStationId() != null) {
                tasksByStationId.computeIfAbsent(task.getStationId().toHexString(), ignored -> new ArrayList<>())
                        .add(task);
                continue;
            }

            int stationOrder = task.getStationOrder() != null ? task.getStationOrder() : 0;
            legacyTasksByStationOrder.computeIfAbsent(stationOrder, ignored -> new ArrayList<>()).add(task);
        }

        for (DeliveryStation station : job.getDeliveryStations()) {
            String stationKey = station.getStationId() != null ? station.getStationId().toHexString() : null;
            List<BaseTask> stationTasks = stationKey != null
                    ? new ArrayList<>(tasksByStationId.getOrDefault(stationKey, List.of()))
                    : new ArrayList<>(legacyTasksByStationOrder.getOrDefault(station.getStationOrder(), List.of()));
            stationTasks.sort(Comparator.comparing(task -> task.getTaskOrder() != null ? task.getTaskOrder() : 0));
            station.setTasks(stationTasks);
        }
    }

    private void ensureDeliveryStationIds(Job job) {
        if (job.getDeliveryStations() == null) {
            return;
        }

        for (DeliveryStation station : job.getDeliveryStations()) {
            if (station != null) {
                station.ensureStationId();
            }
        }
    }

    private Map<Integer, ObjectId> buildStationIdByOrder(Job job) {
        Map<Integer, ObjectId> stationIdByOrder = new HashMap<>();
        if (job.getDeliveryStations() == null) {
            return stationIdByOrder;
        }

        for (DeliveryStation station : job.getDeliveryStations()) {
            if (station != null && station.getStationId() != null) {
                stationIdByOrder.put(station.getStationOrder(), station.getStationId());
            }
        }
        return stationIdByOrder;
    }
}
