package leigh.esp.sdk.service;

import jakarta.servlet.MultipartConfigElement;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Part;
import leigh.eo.EO;
import leigh.eo.io.EOInputStream;
import leigh.eo.json.JsonInput;
import leigh.eo.json.JsonOutput;
import leigh.esp.keikai.sdk.KeikaiClient;
import leigh.esp.sdk.ESPClient;
import leigh.esp.sdk.link23.Link23Request;
import leigh.esp.sdk.service.schema.v1.MicroServiceCfgElementDAO;
import leigh.mecha.fabric.HandlerStatus;
import leigh.mecha.fabric.LiteralMessageSubscription;
import leigh.mecha.http.server.PrefixedHandler;
import leigh.mecha.http.server.WebPipeline;
import leigh.mecha.http.server.WebServer;
import leigh.mecha.http.server.WebTransaction;
import leigh.mecha.io.FileUtil;
import leigh.mecha.log.MechaLogger;
import leigh.mecha.log.MechaLoggerFactory;
import leigh.mecha.net.NetworkPortUtils;
import org.eclipse.jetty.server.Request;

import java.io.IOException;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;

/**
 * A basic implementation of an Enhanced Services Platform (ESP) micro-service. A micro-service registers itself
 * with a Kabe instance via KeiKai and presents a service interface to remote callers. This is distinguished from
 * ESP clients, which may consume services through Kabe, but do not present any themselves. If you need a
 * client and not a service, see the {@link ESPClient} class.
 * <p>
 * This implementation will detect the local system's hostname and will find an available TCP/IP port. Therefore,
 * the system hostname must be set properly to something reachable by all the Kabe servers.
 * <p>
 * This implementation only provides support for a single {@link PrefixedHandler} for the sake of simplicity. If
 * multiple handlers are required to implement the service, they can be routed internally from the base handler
 * the caller provides.
 *
 * @author Alex Leigh
 */
public abstract class MicroService extends PrefixedHandler implements Runnable {
    public final static String LOCAL_PREFIX = "/svc";
    private static final MechaLogger logger = MechaLoggerFactory.getLogger(MicroService.class);
    private final static HashSet<String> prefixes = new HashSet<>();

    static {
        prefixes.add(MicroService.LOCAL_PREFIX);
    }

    private final KeikaiClient keikaiClient;
    private final WebServer webserver;
    // TODO: Do this right
    private final MultipartConfigElement MULTI_PART_CONFIG =
            new MultipartConfigElement(String.valueOf(FileUtil.createTmpDirectory("esp", ".tmp")));
    private boolean isRunning = false;

    public MicroService(EO cfg) throws IOException {
        super(prefixes);
        String hostAddress = InetAddress.getLocalHost().getHostName();
        int localPort = NetworkPortUtils.findFreePort();

        String localUrl = "http://" + hostAddress + ":" + localPort + LOCAL_PREFIX;
        logger.info("Initializing. [host: {}] [localUrl: {}]", hostAddress, localUrl);

        keikaiClient = new KeikaiClient(MicroServiceCfgElementDAO.getKabeUrl(cfg),
                MicroServiceCfgElementDAO.getSrcUri(cfg),
                localUrl);

        webserver = new WebServer(localPort, 1000);

        prefixes.add(LOCAL_PREFIX);
        webserver.getWebPipeline().getServicePipeline().getSubscriptionBase().subscribe(
                new LiteralMessageSubscription<>(this, WebPipeline.KEY_SERVICE));
    }

    @Override
    public void run() {
        isRunning = true;
        new Thread(keikaiClient).start();
        webserver.start();
    }

    /**
     * Return an {@link EOInputStream} representing the request. If there was no request, <code>null</code>
     * is returned.
     * <p>
     * If this is a multi-part request, then multi-part processing will be performed. The method will look for
     * a Link23 request located in one of the parts. Clients should send this part first, ideally, to reduce
     * processing latency.
     */
    public EOInputStream getInputStream(WebTransaction txn) throws IOException, ServletException {
        logger.info("Mime-type: {}", txn.httpServletRequest.getContentType());

        if (!txn.isMultipartRequest()) {
            return new JsonInput(txn.httpServletRequest.getInputStream());
        }

        txn.request.setAttribute(Request.__MULTIPART_CONFIG_ELEMENT, MULTI_PART_CONFIG);
        Part p = txn.httpServletRequest.getPart(Link23Request.MULTIPART_REQUEST_KEY);
        if (p == null) return null;

        return new JsonInput(p.getInputStream());
    }

    public EO getInputObject(WebTransaction txn) throws Exception {
        try (EOInputStream is = getInputStream(txn)) {
            return is.readEo();
        }
    }

    @Override
    public String toString() {
        return "MicroService{" +
                "keikaiClient=" + keikaiClient +
                ", webserver=" + webserver +
                ", isRunning=" + isRunning +
                '}';
    }

    /**
     * Send a standardized error to the caller.
     */
    public void sendError(WebTransaction request, int statusCode) throws IOException {
        request.httpServletResponse.setStatus(statusCode);
        request.httpServletResponse.setContentType("text/plain");
        StringBuffer sb = new StringBuffer();
        // TODO: Add more information here, like request id, processing node, software, etc.
        sb.append("ERROR " + statusCode + "\n");
        request.httpServletResponse.getOutputStream().write(sb.toString().getBytes(StandardCharsets.UTF_8));
    }

    public void sendResponse(WebTransaction txn, EO response) throws IOException {
        try (JsonOutput out = new JsonOutput(txn.httpServletResponse.getOutputStream())) {
            out.writeEO(response);
        }
    }

    public abstract void serviceRequest(WebTransaction request) throws Throwable;

    @Override
    public final HandlerStatus handlePrefixedWebRequest(WebTransaction request) throws Throwable {
        serviceRequest(request);
        return HandlerStatus.BREAK;
    }
}
