package com.lts.cmd;

import com.lts.core.commons.utils.Assert;
import com.lts.core.commons.utils.DateUtils;
import com.lts.core.json.JSON;
import com.lts.core.logger.Logger;
import com.lts.core.logger.LoggerFactory;

import java.io.*;
import java.net.Socket;
import java.net.URLDecoder;
import java.util.*;

/**
 * @author Robert HG (254963746@qq.com)  on 2/17/16.
 */
public class HttpCmdExecutor implements Runnable {

    private static final Logger LOGGER = LoggerFactory.getLogger(HttpCmdExecutor.class);
    private HttpCmdContext context;
    private Socket socket;

    public HttpCmdExecutor(HttpCmdContext context, Socket socket) {
        this.context = context;
        this.socket = socket;
    }

    @Override
    public void run() {

        try {
            // 解析请求
            HttpCmdRequest request = parseRequest();

            Assert.notNull(request, "Request Error");

            Assert.hasText(request.getCommand(), "Command is blank");

            Assert.hasText(request.getNodeIdentity(), "nodeIdentity is blank");

            HttpCmdProc httpCmdProc = context.getCmdProcessor(request.getNodeIdentity(), request.getCommand());

            Assert.notNull(httpCmdProc, "Can not find the command:[" + request.getCommand() + "]");

            sendResponse(HTTP_OK, JSON.toJSONString(httpCmdProc.execute(request)));

        } catch (HttpCMDErrorException ignored) {
            // 忽略
        } catch (IllegalArgumentException e) {
            sendError(HTTP_BADREQUEST, JSON.toJSONString(HttpCmdResponse.newResponse(false, e.getMessage())), false);
        } catch (Throwable t) {
            LOGGER.error("Error When Execute Command", t);
            sendError(HTTP_INTERNALERROR, JSON.toJSONString(HttpCmdResponse.newResponse(false, "Error:" + t.getMessage())), false);
        }
    }

    private HttpCmdRequest parseRequest() throws Exception {

        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));

        StringTokenizer st = new StringTokenizer(in.readLine());
        if (!st.hasMoreTokens())
            sendError(HTTP_BADREQUEST, "BAD REQUEST: Syntax error");

        String method = st.nextToken();

        if (!st.hasMoreTokens())
            sendError(HTTP_BADREQUEST, "BAD REQUEST: Missing URI");

        String uri = st.nextToken();

        Properties params = new Properties();
        assert uri != null;
        int qmi = uri.indexOf('?');
        if (qmi >= 0) {
            decodeParams(uri.substring(qmi + 1), params);
            uri = uri.substring(0, qmi);
        }

        Properties header = new Properties();
        if (st.hasMoreTokens()) {
            String line = in.readLine();
            while (line.trim().length() > 0) {
                int p = line.indexOf(':');
                header.put(line.substring(0, p).trim().toLowerCase(), line.substring(p + 1).trim());
                line = in.readLine();
            }
        }

        if (method.equalsIgnoreCase("POST")) {
            long size = 0x7FFFFFFFFFFFFFFFL;
            String contentLength = header.getProperty("Content-Length");
            if (contentLength == null) {
                contentLength = header.getProperty("content-length");
            }
            if (contentLength != null) {
                size = Integer.parseInt(contentLength);
            }
            String postLine = "";
            char buf[] = new char[512];
            int read = in.read(buf);
            while (read >= 0 && size > 0 && !postLine.endsWith("\r\n")) {
                size -= read;
                postLine += String.valueOf(buf, 0, read);
                if (size > 0)
                    read = in.read(buf);
            }
            postLine = postLine.trim();
            decodeParams(postLine, params);
        }
        return resolveRequest(uri, params);
    }


    protected static HttpCmdRequest resolveRequest(String uri, Properties params) {

        HttpCmdRequest request = new HttpCmdRequest();
        String[] pathNode = uri.substring(1, uri.length()).split("/");
        String nodeIdentity = pathNode[0];
        String command = pathNode[1];
        ;
        request.setCommand(command);
        request.setNodeIdentity(nodeIdentity);

        for (Map.Entry<Object, Object> entry : params.entrySet()) {
            request.addParam(String.valueOf(entry.getKey()), String.valueOf(entry.getValue()));
        }
        return request;
    }

    public static final String HTTP_OK = "200 OK", HTTP_REDIRECT = "301 Moved Permanently",
            HTTP_FORBIDDEN = "403 Forbidden", HTTP_NOTFOUND = "404 Not Found",
            HTTP_BADREQUEST = "400 Bad Request", HTTP_INTERNALERROR = "500 Internal Server Error",
            HTTP_NOTIMPLEMENTED = "501 Not Implemented";

    public static final String MIME_PLAINTEXT = "text/plain", MIME_HTML = "text/html",
            MIME_DEFAULT_BINARY = "application/octet-stream";

    private void sendError(String status, String msg) {
        sendError(status, msg, true);
    }

    private void sendError(String status, String msg, boolean needInterrupt) {
        sendResponse(status, msg);
        if (needInterrupt) {
            throw new HttpCMDErrorException();
        }
    }

    private void sendResponse(String status, String msg) {
        sendResponse(status, MIME_PLAINTEXT, null, new ByteArrayInputStream(msg.getBytes()));
    }

    private void decodeParams(String params, Properties p) throws Exception {
        if (params == null)
            return;

        StringTokenizer st = new StringTokenizer(params, "&");
        while (st.hasMoreTokens()) {
            String e = st.nextToken();
            int sep = e.indexOf('=');
            if (sep >= 0) {
                String key = e.substring(0, sep);
                String value = URLDecoder.decode((e.substring(sep + 1)), "UTF-8");
                p.put(key, value);
            }
        }
    }

    private void sendResponse(String status, String mime, Properties header, InputStream data) {
        try {
            if (status == null)
                throw new Error("sendResponse(): Status can't be null.");
            OutputStream out = socket.getOutputStream();
            PrintWriter pw = new PrintWriter(out);
            pw.print("HTTP/1.0 " + status + " \r\n");

            if (mime != null)
                pw.print("Content-Type: " + mime + "\r\n");

            if (header == null || header.getProperty("Date") == null)
                pw.print("Date: " + DateUtils.formatYMD_HMS(new Date()) + "\r\n");

            if (header != null) {
                Enumeration e = header.keys();
                while (e.hasMoreElements()) {
                    String key = (String) e.nextElement();
                    String value = header.getProperty(key);
                    pw.print(key + ": " + value + "\r\n");
                }
            }

            pw.print("\r\n");
            pw.flush();

            if (data != null) {
                byte[] buff = new byte[2048];
                while (true) {
                    int read = data.read(buff, 0, 2048);
                    if (read <= 0)
                        break;
                    out.write(buff, 0, read);
                }
            }
            out.flush();
            out.close();
            if (data != null)
                data.close();
        } catch (IOException ioe) {
            try {
                socket.close();
            } catch (Throwable ignored) {
            }
        }
    }

    private class HttpCMDErrorException extends RuntimeException {
        public HttpCMDErrorException() {
            super();
        }
    }
}
