package com.ease.gsms.server.controllers.api.v1;

import com.ease.gsms.server.model.Device;
import com.ease.gsms.server.model.Dispatch;
import com.ease.gsms.server.model.Message;
import com.ease.gsms.server.model.util.MD5Hash;
import com.ease.gsms.server.services.DeviceService;
import com.ease.gsms.server.services.DispatchService;
import com.ease.gsms.server.services.dispatch.*;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVPrinter;
import org.apache.commons.lang3.builder.CompareToBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.http.ResponseEntity;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URI;
import java.security.Principal;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;


@RestController
@RequestMapping(value = "/api/v1/sms", produces = "application/json")
@Validated
public class SMSController {

    @Autowired
    private DispatchService dispatchService;

    @Autowired
    private DeviceService deviceService;

    @Autowired
    private WorkBalancingService workService;

    @Autowired
    private SimpMessagingTemplate simpMessagingTemplate;

    @PostConstruct
    private void init() {

        workService.addDispatchUpdateObserver((userName, dispatchStatusHolder) -> {

            simpMessagingTemplate.convertAndSendToUser(
                    userName,
                    String.format("/topic/dispatch/%s/status-updates", dispatchStatusHolder.getUUID()),
                    dispatchStatusHolder
            );

        });

    }

    @RequestMapping(value = "/send", method = RequestMethod.POST)
    @PreAuthorize("hasRole('SUBMITTER')")
    @Operation(
            summary = "Processes an SMSDispatchRequest",
            description = "Requires being an authenticated user with the SUBMITTER role",
            security = @SecurityRequirement(name = "api-key")
    )
    @ApiResponses(value = {
            @ApiResponse(responseCode = "201", description = "SMSDispatchRequest successfully received"),
            @ApiResponse(responseCode = "400", description = "The SMSDispatchRequest is invalid"),
            @ApiResponse(responseCode = "500", description = "Something went wrong during processing")
    })
    public ResponseEntity<UUID> sendSMS(
            @Valid @RequestBody SMSDispatchRequest request,
            Authentication authentication
    ) throws EmptyDispatchException {

        SMSMessage[] smsMessages = request.getMessages();

        int messageCount = smsMessages.length;

        Message[] messages = new Message[messageCount];

        for (int i = 0; i < messageCount; i++) {

            messages[i] = smsMessages[i].toMessage();

        }

        Dispatch dispatch = new Dispatch(
                request.getFromICCID(),
                authentication.getName(),
                request.getSendToInternationalNumbers(),
                request.getSendToNationalLandlines(),
                request.getSendToNationalMobiles(),
                request.getResendIdenticalSuccessfullyDeliveredMessages(),
                request.getResendIdenticalSuccessfullySentMessages(),
                request.getResendIdenticalUnsuccessfullySentMessages(),
                request.getResendIdenticalUnsent(),
                request.getAskForDeliveryReport(),
                request.getPauseBetweenMessagesSeconds(),
                messages
        );

        UUID uuid = dispatchService.dispatch(dispatch);

        simpMessagingTemplate.convertAndSendToUser(
                authentication.getName(),
                "/topic/dispatch/new",
                dispatch.getUUID()
        );

        return ResponseEntity
                .created(URI.create(String.format("/request/%s", uuid)))
                .body(uuid);

    }

    @RequestMapping(value = "/active-dispatches", method = RequestMethod.GET)
    @PreAuthorize("hasRole('SUBMITTER')")
    @Operation(
            summary = "Lists the ids of all active dispatches",
            description = "Requires being an authenticated user with the SUBMITTER role",
            security = @SecurityRequirement(name = "api-key")
    )
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "List of UUID"),
    })
    public ResponseEntity<Collection<UUID>> getActiveDispatches(
            Authentication authentication
    ) {

        return ResponseEntity
                .ok(
                        dispatchService
                                .getLiveDispatches(authentication.getName())
                                .stream()
                                .sorted((d1, d2) -> new CompareToBuilder().append(d1.getSubmissionDate(), d2.getSubmissionDate()).toComparison())
                                .map(Dispatch::getUUID).collect(Collectors.toList())
                );

    }

    @RequestMapping(value = "/status/{requestId}", method = RequestMethod.GET)
    @PreAuthorize("hasRole('SUBMITTER')")
    @Operation(
            summary = "Gets the status of a dispatch",
            description = "Requires being an authenticated user with the SUBMITTER role",
            security = @SecurityRequirement(name = "api-key")
    )
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "Dispatch information"),
            @ApiResponse(responseCode = "404", description = "No dispatch was found for provided ID")
    })
    public ResponseEntity<DispatchStatusHolder> status(
            @PathVariable("requestId") UUID requestId,
            Principal principal
    ) {

        DispatchStatusHolder dispatchStatus = dispatchService.getDispatchStatus(requestId, principal);

        if (dispatchStatus != null) {
            return ResponseEntity.ok(dispatchStatus);
        } else {
            return ResponseEntity.notFound().build();
        }

    }

    @RequestMapping(value = "/status-by-hash-list", method = RequestMethod.POST)
    @Operation(
            summary = "Gets the message status for a list of message hashes",
            description = "List of message hashes computed as: MD5 hash of destination number updated with content",
            security = @SecurityRequirement(name = "api-key")
    )
    public ResponseEntity<Map<String, Map<String, Integer>>> statusByHashList(
            @RequestBody List<String> hashList
    ) {

        Map<String, Map<String, Integer>> response = new LinkedHashMap<>();

        for (Map.Entry<MD5Hash, List<Message>> entry : dispatchService.getMessagesByHexHashList(hashList).entrySet()) {

            Map<String, Integer> countForStatus = new HashMap<>();

            for (Message message : entry.getValue()) {

                countForStatus.compute(message.getStatus().name(), (messageStatus, exitingCount) -> (exitingCount != null ? exitingCount : 0) + 1);

            }

            response.put(entry.getKey().toHexString(), countForStatus);

        }

        return ResponseEntity.ok(response);

    }

    @RequestMapping(value = "/messages/{fromTimestamp}/{toTimestamp}/totals", method = RequestMethod.GET)
    @Operation(
            summary = "Gets the number of messages created between two timestamps",
            description = "[fromTimestamp, toTimestamp)",
            security = @SecurityRequirement(name = "api-key")
    )
    public ResponseEntity<Long> messageCountsBetweenCreationTimestamps(
            @PathVariable("fromTimestamp") Long fromTimestamp,
            @PathVariable("toTimestamp") Long toTimestamp,
            Principal principal
    ) {

        return ResponseEntity.ok(
                dispatchService.getMessageCountsBetweenCreationTimestamps(fromTimestamp, toTimestamp, principal.getName())
        );

    }


    @RequestMapping(value = "/messages/{fromTimestamp}/{toTimestamp}/sort/{sortBy}/{descending}/{startRow}/{fetchCount}", method = RequestMethod.GET)
    @Operation(
            summary = "Gets the messages created between two timestamps",
            description = "[fromTimestamp, toTimestamp)",
            security = @SecurityRequirement(name = "api-key")
    )
    public ResponseEntity<List<Message>> messagesBetweenCreationTimestamps(
            @PathVariable("fromTimestamp") Long fromTimestamp,
            @PathVariable("toTimestamp") Long toTimestamp,
            @PathVariable("sortBy") String sortBy,
            @PathVariable("descending") Boolean descending,
            @PathVariable("startRow") Integer startRow,
            @PathVariable("fetchCount") Integer fetchCount,
            Principal principal
    ) {

        return ResponseEntity.ok(
                dispatchService.getMessagesBetweenCreationTimestamps(fromTimestamp, toTimestamp, principal.getName(), sortBy, descending, startRow, fetchCount)
        );

    }

    @RequestMapping(value = "/messages/{fromTimestamp}/{toTimestamp}/sort/{sortBy}/{descending}/{startRow}/{fetchCount}/csv", method = RequestMethod.GET)
    @Hidden
    public void messagesBetweenCreationTimestampsCSV(
            @PathVariable("fromTimestamp") Long fromTimestamp,
            @PathVariable("toTimestamp") Long toTimestamp,
            @PathVariable("sortBy") String sortBy,
            @PathVariable("descending") Boolean descending,
            @PathVariable("startRow") Integer startRow,
            @PathVariable("fetchCount") Integer fetchCount,
            Principal principal,
            HttpServletResponse response
    ) throws IOException {

        Calendar from = Calendar.getInstance(TimeZone.getTimeZone("Europe/Bucharest"));
        Calendar to   = Calendar.getInstance(TimeZone.getTimeZone("Europe/Bucharest"));

        from.setTimeInMillis(fromTimestamp);
        to.setTimeInMillis(toTimestamp);

        response.setHeader("Content-disposition", String.format(
                "attachment; filename=mesaje_%04d-%02d-%02d-%04d-%02d-%02d_%s_%s.csv",
                from.get(Calendar.YEAR),
                from.get(Calendar.MONTH) + 1,
                from.get(Calendar.DATE),
                to.get(Calendar.YEAR),
                to.get(Calendar.MONTH) + 1,
                to.get(Calendar.DATE),
                sortBy,
                descending ? "desc" : "asc",
                Locale.forLanguageTag("RO")
        ));

        List<Message> messages = dispatchService.getMessagesBetweenCreationTimestamps(fromTimestamp, toTimestamp, principal.getName(), sortBy, descending, startRow, fetchCount);

        CSVFormat format = CSVFormat.EXCEL;

        CSVPrinter csvPrinter = new CSVPrinter(response.getWriter(), format);

        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss", Locale.forLanguageTag("RO"));

        dateFormat.setTimeZone(TimeZone.getTimeZone("Europe/Bucharest"));

        Function<Date, String> dateFormatter = date -> date == null ? "" : dateFormat.format(date);

        Function<Message, String> sendingStatusFormatter = message ->
                message.getSendingStatus() == null ? "" : message.getSendingStatus() == -1 ? "Nesolicitat" : message.getSendingStatus() == 0 ? (message.getSentDate() == null ? "" : "Eșuat") : message.getSendingStatus() == 1 ? "OK" : "Problema: " + message.getSendingStatus();

        Function<Message, String> deliveryStatusFormatter = message ->
                message.getDeliveryStatus() == null ? "" : message.getDeliveryStatus() == -1 ? "Nesolicitat" : message.getDeliveryStatus() == 0 ? (message.getDeliveredDate() == null ? "" : "Eșuat") : message.getDeliveryStatus() == 1 ? "OK" : "Problema: " + message.getDeliveryStatus();

        csvPrinter.printRecord(Arrays.asList("Id", "Expeditor", "Destinatar", "Mesaj", "Data creere", "Data alocare", "Data expediere", "Data livrare", "Expediere", "Livrare"));

        int idx = 0;

        for (Message message : messages) {
            csvPrinter.printRecord(
                    Arrays.asList(
                            String.valueOf(message.getId()),
                            message.getFromICCID(),
                            message.getTo(),
                            message.getContent(),
                            dateFormatter.apply(message.getCreationDate()),
                            dateFormatter.apply(message.getAllocationDate()),
                            dateFormatter.apply(message.getSentDate()),
                            dateFormatter.apply(message.getDeliveredDate()),
                            sendingStatusFormatter.apply(message),
                            deliveryStatusFormatter.apply(message)
                    )
            );

            if (idx++ % 1000 == 0 && idx > 0) {
                csvPrinter.flush();
            }
        }

        csvPrinter.close(true);

    }

    private final Map<String, Map<String, MessageSender>> workersBySessionId = new ConcurrentHashMap<>();

    @EventListener
    public void onDisconnect(SessionDisconnectEvent event) {
        Map<String, MessageSender> disconnectedWorkers = workersBySessionId.remove(event.getSessionId());
        if (disconnectedWorkers != null && !disconnectedWorkers.isEmpty()) {
            workService.unregisterWorkers(disconnectedWorkers.keySet());
        }
    }


    @MessageMapping("/sms/workers/{id}/register")
    public void registerWorker(@DestinationVariable("id") String id, @Header("simpSessionId") String sessionId, Principal principal) {

        Device device = deviceService.getDeviceBySimSerialNumber(id);

        if (device != null && principal.getName().equals(device.getOwner())) { // user has control over given id

            MessageSender worker = new MessageSender(id, principal.getName()) {
                @Override
                public synchronized void announceAllocation(Allocation allocation) {
                    simpMessagingTemplate.convertAndSendToUser(
                            getUser(),
                            String.format("/queue/sms/%s", id),
                            new AllocationInfo(allocation)
                    );
                }

                @Override
                public void announceDeallocation(Allocation allocation) {
                    // sit on it for now
                }

                @Override
                public synchronized void sendMessage(Message message) {
                    simpMessagingTemplate.convertAndSendToUser(
                            getUser(),
                            String.format("/queue/sms/%s", id),
                            message
                    );
                }
            };

            workersBySessionId.computeIfAbsent(sessionId, workers -> new ConcurrentHashMap<>()).put(id, worker);

            workService.registerWorker(
                    worker
            );

        }

    }

    @MessageMapping("/sms/workers/{id}/unregister")
    public void unregisterWorker(@DestinationVariable("id") String id, @Header("simpSessionId") String sessionId) {

        workersBySessionId.computeIfAbsent(sessionId, workers -> new ConcurrentHashMap<>()).remove(id);

        workService.unregisterWorker(id);

    }

    @MessageMapping("/sms/workers/{id}/confirm-allocation")
    public void confirmAllocation(@DestinationVariable("id") String id, UUID allocationUuid, Principal principal) {

        MessageSender worker = workService.getWorker(id, principal.getName());

        if (worker != null) {

            worker.allocationConfirmed(allocationUuid);

        }

    }

    @MessageMapping("/sms/workers/{id}/message-sent/{messageId}")
    public void messageSent(@DestinationVariable("id") String id, MessageSendingInfo messageSendingInfo, Principal principal) {

        MessageSender worker = workService.getWorker(id, principal.getName());

        if (worker != null) {

            worker.messageSent(
                    messageSendingInfo.getAllocation(),
                    messageSendingInfo.getDispatchId(),
                    messageSendingInfo.getMessageId(),
                    messageSendingInfo.getSendingStatusId(),
                    messageSendingInfo.getSentDate()
            );

        }

    }

    @MessageMapping("/sms/workers/{id}/message-delivered/{messageId}")
    public void messageDelivered(@DestinationVariable("id") String id, MessageDeliveryInfo messageDeliveryInfo, Principal principal) {

        System.out.println(
                String.format(
                        "Message %d delivered on %s with status id %d",
                        messageDeliveryInfo.getMessageId(),
                        messageDeliveryInfo.getDeliveryDate(),
                        messageDeliveryInfo.getDeliveryStatusId()
                )
        );

        MessageSender worker = workService.getWorker(id, principal.getName());

        if (worker != null) {

            worker.messageDelivered(
                    messageDeliveryInfo.getAllocation(),
                    messageDeliveryInfo.getDispatchId(),
                    messageDeliveryInfo.getMessageId(),
                    messageDeliveryInfo.getDeliveryDate(),
                    messageDeliveryInfo.getDeliveryStatusId()
            );

        }

    }

    @MessageMapping("/sms/dispatches/{id}/pause")
    @PreAuthorize("hasRole('SUBMITTER')")
    @Operation(
            summary = "Pause a dispatch",
            description = "Requires being an authenticated user with the SUBMITTER role",
            security = @SecurityRequirement(name = "api-key")
    )
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "Dispatch information"),
            @ApiResponse(responseCode = "404", description = "No dispatch was found for provided ID")
    })
    public ResponseEntity<DispatchStatusHolder> pauseDispatch(@DestinationVariable("id") UUID id, Principal principal) {

        Dispatch liveDispatch = workService.getLiveDispatch(id, principal.getName());

        if (liveDispatch != null) {
            workService.pauseDispatch(liveDispatch);
            return ResponseEntity.ok(liveDispatch.getDispatchStatusHolder());
        } else {
            return ResponseEntity.notFound().build();
        }

    }

    @MessageMapping("/sms/dispatches/{id}/resume")
    @PreAuthorize("hasRole('SUBMITTER')")
    @Operation(
            summary = "Resume a dispatch",
            description = "Requires being an authenticated user with the SUBMITTER role",
            security = @SecurityRequirement(name = "api-key")
    )
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "Dispatch information"),
            @ApiResponse(responseCode = "404", description = "No dispatch was found for provided ID")
    })
    public ResponseEntity<DispatchStatusHolder> resumeDispatch(@DestinationVariable("id") UUID id, Principal principal) {

        Dispatch liveDispatch = workService.getLiveDispatch(id, principal.getName());

        if (liveDispatch != null) {
            workService.resumeDispatch(liveDispatch);
            return ResponseEntity.ok(liveDispatch.getDispatchStatusHolder());
        } else {
            return ResponseEntity.notFound().build();
        }

    }

    @MessageMapping("/sms/dispatches/{id}/cancel")
    @PreAuthorize("hasRole('SUBMITTER')")
    @Operation(
            summary = "Cancel a dispatch",
            description = "Requires being an authenticated user with the SUBMITTER role",
            security = @SecurityRequirement(name = "api-key")
    )
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "Dispatch information"),
            @ApiResponse(responseCode = "404", description = "No dispatch was found for provided ID")
    })
    public ResponseEntity<DispatchStatusHolder> cancelDispatch(@DestinationVariable("id") UUID id, Principal principal) {

        Dispatch liveDispatch = workService.getLiveDispatch(id, principal.getName());

        if (liveDispatch != null) {
            workService.cancel(liveDispatch);
            return ResponseEntity.ok(liveDispatch.getDispatchStatusHolder());
        } else {
            return ResponseEntity.notFound().build();
        }

    }

}