package ctbrec.recorder.server;
import static javax.servlet.http.HttpServletResponse.*;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Enumeration;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ctbrec.Config;

public class HlsServlet extends AbstractCtbrecServlet {

    private static final transient Logger LOG = LoggerFactory.getLogger(HlsServlet.class);

    private final Config config;

    public HlsServlet(Config config) {
        this.config = config;
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String contextPath = getServletContext().getContextPath();
        String request = req.getRequestURI().substring(contextPath.length() + 5);
        Path recordingsDirPath = Paths.get(config.getSettings().recordingsDir).toAbsolutePath().normalize();
        Path requestedFilePath = recordingsDirPath.resolve(request).toAbsolutePath().normalize();

        boolean isValidRequestedPath = requestedFilePath.startsWith(recordingsDirPath);
        if (isValidRequestedPath) {
            File requestedFile = requestedFilePath.toFile();
            if (requestedFile.getName().equals("playlist.m3u8")) {
                try {
                    boolean isRequestAuthenticated = checkAuthentication(req, req.getRequestURI());
                    if (!isRequestAuthenticated) {
                        writeResponse(resp, SC_UNAUTHORIZED, "{\"status\": \"error\", \"msg\": \"HMAC does not match\"}");
                        return;
                    }
                } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
                    writeResponse(resp, SC_UNAUTHORIZED, "{\"status\": \"error\", \"msg\": \"Authentication failed\"}");
                    return;
                }

                servePlaylist(req, resp, requestedFile);
            } else {
                if (requestedFile.exists()) {
                    Enumeration<String> headerNames = req.getHeaderNames();
                    while(headerNames.hasMoreElements()) {
                        String header = headerNames.nextElement();
                        LOG.trace("{}: {}", header, req.getHeader(header));
                    }
                    serveSegment(req, resp, requestedFile);
                } else {
                    error404(req, resp);
                }
            }
        } else {
            writeResponse(resp, SC_FORBIDDEN, "Stop it!");
        }
    }

    private void writeResponse(HttpServletResponse resp, int code, String body) {
        try {
            resp.setStatus(code);
            resp.getWriter().write(body);
        } catch (IOException e) {
            LOG.error("Couldn't write HTTP response", e);
        }
    }

    private void error404(HttpServletRequest req, HttpServletResponse resp) {
        resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
    }

    private void serveSegment(HttpServletRequest req, HttpServletResponse resp, File requestedFile) throws IOException {
        String mimetype = requestedFile.getName().endsWith(".mp4") ? "video/mp4" : "application/octet-stream";
        serveFile(req, resp, requestedFile, mimetype);
    }

    private void servePlaylist(HttpServletRequest req, HttpServletResponse resp, File requestedFile) throws IOException {
        serveFile(req, resp, requestedFile, "application/x-mpegURL");
    }

    private void serveFile(HttpServletRequest req, HttpServletResponse resp, File file, String contentType) throws IOException {
        ByteRange range = getByteRange(req, file);
        LOG.trace("Serving segment {} range: {}-{}", file.getAbsolutePath(), range.from, range.to);
        resp.setStatus(range.set ? SC_PARTIAL_CONTENT : SC_OK);
        resp.setContentType(contentType);
        try (RandomAccessFile fin = new RandomAccessFile(file, "r")) {
            fin.seek(range.from);
            byte[] buffer = new byte[1024 * 100];
            long bytesLeft = range.to - range.from;
            resp.setContentLengthLong(bytesLeft);
            if (range.set) {
                resp.setHeader("Content-Range", "bytes " + range.from + '-' + range.to + '/' + file.length());
            }
            int bytesToRead = (int) Math.min(bytesLeft, buffer.length);
            int length = -1;
            while ((length = fin.read(buffer, 0, bytesToRead)) >= 0) {
                bytesLeft -= length;
                resp.getOutputStream().write(buffer, 0, length);
            }
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req, resp);
    }

    private ByteRange getByteRange(HttpServletRequest req, File file) {
        ByteRange range = new ByteRange();
        range.to = file.length();
        String rangeHeader = req.getHeader("Range");
        if (rangeHeader != null) {
            range.set = true;
            Matcher m = Pattern.compile("bytes=(\\d+)-(\\d+)*").matcher(rangeHeader);
            if (m.find()) {
                range.from = Long.parseLong(m.group(1));
                String to = m.group(2);
                if (to != null && !to.trim().isEmpty()) {
                    range.to = Long.parseLong(to);
                }
            }
        }
        return range;
    }

    class ByteRange {
        boolean set = false;
        long from = 0;
        long to = Long.MAX_VALUE;
    }
}
