package io.hosuaby.restful.services;

import io.hosuaby.restful.domain.TeapotMessage;
import io.hosuaby.restful.repositories.WebSocketSessionRepository;
import io.hosuaby.restful.services.exceptions.teapots.TeapotInternalErrorException;
import io.hosuaby.restful.services.exceptions.teapots.TeapotNotConnectedException;

import java.io.IOException;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.async.DeferredResult;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;

import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * Implementation of the {@link TeapotCommandService}.
 */
@Service
// TODO: implement appropriate thread synchronization
public class TeapotCommandServiceImpl implements TeapotCommandService {

    /**
     * Pattern for request id. Id must start with sequence "req-".
     */
    private static final Pattern REQUEST_ID_PATTERN = Pattern.compile("^req-.*");

    /** Repository for websocket sessions of teapots */
    @Autowired
    @Qualifier("teapotSessionsRepository")
    private WebSocketSessionRepository sessionRepository;

    /** Jackson object mapper */
    @Autowired
    private ObjectMapper jacksonMapper;

    /** Map of session ids by teapot ids */
    private Map<String, String> mapTeapotIdSessionId;

    /** Map of teapot ids by session ids */
    private Map<String, String> mapSessionIdTeapotId;

    /** Deferred results for command executions in Ajax mode */
    private Map<String, DeferredResult<String>> results;

    /**
     * Partial results returned by teapots during execution of the command.
     * Used to buffer content of multiple subsequent messages before EOT
     * character is received and DeferredResult object associated with request
     * can be fulfilled.
     * Partial results are mapped by request id.
     */
    private Map<String, String> partialResults;

    /**
     * Constructor.
     */
    public TeapotCommandServiceImpl() {
        mapTeapotIdSessionId = new HashMap<>();
        mapSessionIdTeapotId = new HashMap<>();
        results = new HashMap<>();
        partialResults = new HashMap<>();
    }

    /** {@inheritDoc} */
    @Override
    public synchronized void register(String teapotId,
            WebSocketSession session) {
        String sessionId = session.getId();

        sessionRepository.save(session);

        mapTeapotIdSessionId.put(teapotId, sessionId);
        mapSessionIdTeapotId.put(sessionId, teapotId);
    }

    /** {@inheritDoc} */
    @Override
    public synchronized void unregister(WebSocketSession teapotSession) {
        String sessionId = teapotSession.getId();
        String teapotId = mapSessionIdTeapotId.get(sessionId);

        sessionRepository.delete(teapotSession);

        mapTeapotIdSessionId.remove(teapotId);
        mapSessionIdTeapotId.remove(sessionId);
    }

    /** {@inheritDoc} */
    @Override
    public void shutdown(String teapotId) throws IOException,
            TeapotNotConnectedException {
        if (mapTeapotIdSessionId.containsKey(teapotId)) {
            sessionRepository
                .getSession(mapTeapotIdSessionId.get(teapotId))
                .close();
        } else {
            throw new TeapotNotConnectedException(teapotId);
        }
    }

    /** {@inheritDoc} */
    @Override
    public synchronized String[] getRegisteredTeapotsIds() {
        return (String[]) mapTeapotIdSessionId.keySet().toArray();
    }

    /** {@inheritDoc} */
    @Override
    public synchronized boolean isTeapotConnected(String teapotId) {
        return mapTeapotIdSessionId.containsKey(teapotId);
    }

    /** {@inheritDoc} */
    @Override
    public DeferredResult<String> sendMessage(HttpServletRequest req,
            String teapotId, String msg) {

        /* Create unique request id */
        String requestId = new StringBuilder("req-")
            .append(req.getSession().getId())   // HTTP session id
            .append('-')
            .append(new GregorianCalendar().getTimeInMillis())  // timestamp
            .toString();

        /* Create message object */
        TeapotMessage message = new TeapotMessage(requestId, msg);

        /* Get teapot websocket session */
        WebSocketSession teapotSession = sessionRepository.getSession(
                mapTeapotIdSessionId.get(teapotId));

        DeferredResult<String> result = new DeferredResult<>();

        if (teapotSession != null) {
            try {
                String msgJson = jacksonMapper.writeValueAsString(message);
                teapotSession.sendMessage(new TextMessage(msgJson));
                results.put(requestId, result);
                partialResults.put(requestId, "");
            } catch (IOException e) {

                /* Reject deferred result */
                result.setErrorResult(e);

                e.printStackTrace();
            }
        } else {

            /* Reject deferred result */
            result.setErrorResult(new TeapotNotConnectedException(teapotId));
        }

        return result;
    }

    /** {@inheritDoc} */
    @Override
    public void submitResponse(String clientId, String msg) {
        if (REQUEST_ID_PATTERN.matcher(clientId).matches()) {

            /* This is request id */

            String partial = Optional
                    .ofNullable(partialResults.get(clientId))
                    .map(s -> s + "\n")
                    .orElse("") + msg;

            partialResults.remove(clientId);

            results.computeIfPresent(clientId, (id, result) -> {
                result.setResult(partial);
                return null;
            });

        } else {

            /* This is websocket session id */
            // TODO: code this behaiviour

        }
    }

    /** {@inheritDoc} */
    @Override
    public void submitResponse(String clientId, String msg, boolean isLast) {
        if (REQUEST_ID_PATTERN.matcher(clientId).matches()) {

            /* This is request id */

            String partial = Optional
                    .ofNullable(partialResults.get(clientId))
                    .map(s -> s + "\n")
                    .orElse("") + msg;

            if (isLast) {
                results.computeIfPresent(clientId, (id, result) -> {
                    result.setResult(partial);
                    return null;
                });
            } else {
                partialResults.put(clientId, partial);
            }

        } else {

            /* This is websocket session id */
            // TODO: code this behaiviour

        }
    }

    /** {@inheritDoc} */
    @Override
    public void submitError(String clientId, String error) {
        if (REQUEST_ID_PATTERN.matcher(clientId).matches()) {

            /* This is request id */

            // TODO: use Optional of Java8 instead
            String partial = partialResults.get(clientId);
            if (partial == null) {
                partial = "";
            }
            partial += (partial.isEmpty() ? "" : "\n") + error;

            partialResults.remove(clientId);

            // TODO: use Optional maybe
            if (results.containsKey(clientId)) {
                results.get(clientId).setErrorResult(
                        new TeapotInternalErrorException(partial));
                results.remove(clientId);
            }

        } else {

            /* This is websocket session id */
            // TODO: code this behaiviour

        }
    }

}