package leigh.m3;

import leigh.eo.EO;
import leigh.eo.json.JsonOutput;
import leigh.esp.sdk.service.MicroService;
import leigh.esp.sdk.service.schema.v1.MicroServiceCfgElementDAO;
import leigh.mecha.http.server.WebTransaction;
import leigh.mecha.json.JSONArray;
import leigh.mecha.json.JSONObject;
import leigh.mecha.log.MechaLogger;
import leigh.mecha.log.MechaLoggerFactory;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;

import java.beans.Introspector;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;

/**
 * M3 V2.0 is a re-imagining of Eric Lindvall's M3 service for Continuity. This modern re-write implements
 * the HLS protocol instead of ICYCAST. In general, the other mechanics of M3 are preserved.
 * <p>
 * M3Service is an Enhanced Services Platform (ESP) microservice, intended to be run behind a Kabe server. However,
 * it can also be used stand-alone service by disabling the Keikai client and connecting directly.
 * <p>
 * THe service expects to find a media directory (as set by the <code>BASE_PATH</code> variable) which contains
 * the following files:
 * <p>
 * AAC 256kbit TS media files, incrementing as filename_0.aac, filename_1.aac, filename_2.aac, etc.
 * A M3U8 describing all segments for a given track, as filename.m3u8
 * A JSON file containing the meta values for the track, as filename.json
 * An optional JPG image containing the cover art, as filename.jpg
 * A JPG for missing cover art, named missingcover.jpg
 * <p>
 * Other media formats than AAC/256 will work, but various settings in this class have to be adjusted to produce
 * correct playlists.
 *
 * @author Alex Leigh
 */
public class M3Service extends MicroService {
    private static final MechaLogger logger = MechaLoggerFactory.getLogger(M3Service.class);
    // This must have a trailing slash
    private static final String BASE_PATH = "/opt/music/mp3/";
    private static final String MISSING_COVER_IMG = "/opt/music/mp3/missingcover.jpg";
    private final MediaRepository repo = new MediaRepository(Path.of(BASE_PATH));
    private final MediaStream stream = new MediaStream(repo);


    public M3Service(EO cfg) throws IOException {
        super(cfg);
        repo.scan();
    }

    public static void main(String args[]) throws IOException {
        EO cfg = MicroServiceCfgElementDAO.create();
        MicroServiceCfgElementDAO.setKabeUrl(cfg, "https://leigh-co.com/");
        MicroServiceCfgElementDAO.setSrcUri(cfg, "/radio");
        M3Service svc = new M3Service(cfg);
        svc.run();
    }

    @Override
    public void serviceRequest(WebTransaction request) throws Throwable {
        logger.info("request: {}", request.getPathInfo());

        try {
            switch (request.getPathInfo()) {
                case "/playlist.m3u8":
                    doPlaylist(request);
                    return;
                case "/master.m3u8":
                    doMasterPlaylist(request);
                    return;
                case "/api/v1/current/read":
                    doCurrentSong(request);
                    return;
                case "/api/v1/current/cover":
                    doCoverArt(request);
                    return;
                default:
                    if (request.getPathInfo().endsWith("aac")) {
                        doMediaSegment(request);
                        return;
                    }
                    break;
            }
            request.httpServletResponse.setStatus(404);
            request.httpServletResponse.getOutputStream().write("404 M3Handler".getBytes(StandardCharsets.UTF_8));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void doCurrentSong(WebTransaction txn) throws IOException, InterruptedException {
        StreamWindow current = stream.getCurrentSegments();
        SegmentSchedule currentSegment = current.getWindow().get(0);

        String metaFilename = currentSegment.getChunk().getM3u().getFile().toString().replace(".m3u8", ".json");
        logger.info("Opening metafile: {}", BASE_PATH + metaFilename);

        EO reply = new EO();
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(metaFilename))) {
            JSONArray arr = new JSONArray(IOUtils.toString(bis, Charset.defaultCharset()));
            JSONObject meta = (JSONObject) arr.get(0);
            for (String key : meta.keySet()) {
                reply.setValue(Introspector.decapitalize(key), meta.optString(key));
            }
        }

        reply.remove("appleStoreAccount");
        reply.remove("coverArt");

        try (JsonOutput out = new JsonOutput(txn.httpServletResponse.getOutputStream())) {
            out.writeEO(reply);
        }
    }

    private void doCoverArt(WebTransaction txn) throws IOException, InterruptedException {
        StreamWindow current = stream.getCurrentSegments();
        SegmentSchedule currentSegment = current.getWindow().get(0);

        String imgFilename = currentSegment.getChunk().getM3u().getFile().toString().replace(".m3u8", ".jpg");
        File imgFile = new File(imgFilename);

        logger.info("Deduced image file location: {}", imgFile);

        if (!imgFile.exists()) {
            imgFile = new File(MISSING_COVER_IMG);
        }

        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(imgFile))) {
            txn.httpServletResponse.setHeader("Cache-Control", "no-cache");
            txn.httpServletResponse.setContentType("image/jpeg");
            IOUtils.copy(bis, txn.httpServletResponse.getOutputStream());
        }
    }

    /**
     * Provide a media segment to the remote client. This works based on translating the requested sequence ID
     * with the translation file to find the actual segment on disk.
     */
    private void doMediaSegment(WebTransaction txn) throws IOException {
        String tsFile = FilenameUtils.getName(txn.getPathInfo());
        Path fn = Path.of(BASE_PATH + tsFile);
        logger.info("Streaming chunk to client. [fn: {}]", fn);

        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fn.toFile()))) {
            txn.httpServletResponse.setContentType("video/mp2t");
            IOUtils.copy(bis, txn.httpServletResponse.getOutputStream());
        }
    }

    private void doPlaylist(WebTransaction request) throws IOException, InterruptedException {
        request.httpServletResponse.setContentType("application/vnd.apple.mpegurl");
        StreamWindow window = stream.getCurrentSegments();
        StringBuffer sb = new StringBuffer();

        request.httpServletResponse.setHeader("Cache-Control", "no-cache");

        // https://stackoverflow.com/questions/47047919/how-does-ext-x-discontinuity-sequence-tag-work-in-hls-m3u8-file
        // Target duration must be an int, or Safari breaks
        // Apple warns that no segments should be longer than the target duration
        // Removing TARGETDURATION breaks it on Safari/IOS
        // Files cannot be removed from the playlist if the type is EVENT. It can never change if VOD.
        // Safari/IOS will go through stream boundaries without disco, VLC hangs silent

        sb.append("#EXTM3U\n" +
                "#EXT-X-VERSION:4\n" +
                "#EXT-X-INDEPENDENT-SEGMENTS\n" +
                "#EXT-X-TARGETDURATION:6\n" +
                "#EXT-X-DISCONTINUITY-SEQUENCE:" + window.getPastDiscos() + "\n" +
                "#EXT-X-MEDIA-SEQUENCE:" + window.getLowSequence() + "\n"
        );

        for (SegmentSchedule scjed : window.getWindow()) {
            if (scjed.isDiscontinuous()) {
                sb.append("#EXT-X-DISCONTINUITY\n");
            }
            sb.append("#EXTINF:" + scjed.getChunk().getDuration() + ",\n" + scjed.getChunk().getUrlEncodedFilename() + "\n");
        }

        logger.trace("Playlist: {}", sb);

        request.httpServletResponse.getOutputStream().write(sb.toString().getBytes(StandardCharsets.UTF_8));
    }

    private void doMasterPlaylist(WebTransaction request) throws IOException {
        request.httpServletResponse.setContentType("application/vnd.apple.mpegurl");
        // This is based on the current ffmpeg settings for AAC/256
        request.httpServletResponse.getOutputStream().write("#EXTM3U\n#EXT-X-VERSION:4\n#EXT-X-STREAM-INF:BANDWIDTH=500000,CODECS=\"mp4a.40.2\"\nplaylist.m3u8".getBytes(StandardCharsets.UTF_8));
    }
}
