package ctbrec.sites.flirt4free;

import static ctbrec.io.HttpConstants.*;

import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.iheartradio.m3u8.Encoding;
import com.iheartradio.m3u8.Format;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.ParsingMode;
import com.iheartradio.m3u8.PlaylistException;
import com.iheartradio.m3u8.PlaylistParser;
import com.iheartradio.m3u8.data.MasterPlaylist;
import com.iheartradio.m3u8.data.Playlist;
import com.iheartradio.m3u8.data.PlaylistData;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;

import ctbrec.AbstractModel;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.io.HttpException;
import ctbrec.recorder.download.StreamSource;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;

public class Flirt4FreeModel extends AbstractModel {

    private static final transient Logger LOG = LoggerFactory.getLogger(Flirt4FreeModel.class);
    private String id;
    private String chatHost;
    private String chatPort;
    private String chatToken;
    private String streamHost;
    private String streamUrl;
    int[] resolution = new int[2];
    private Object monitor = new Object();
    private boolean online = false;
    private boolean isInteractiveShow = false;
    private String userIdt = "";
    private String userIp = "0.0.0.0";

    private static Semaphore requestThrottle = new Semaphore(2, true);
    private static volatile long lastRequest = 0;
    private long lastOnlineRequest = 0;


    @Override
    public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
        long now = System.currentTimeMillis();
        long timeSinceLastCheck = now - lastOnlineRequest;
        if (ignoreCache && timeSinceLastCheck > TimeUnit.MINUTES.toMillis(1)) {
            String url = "https://ws.vs3.com/rooms/check-model-status.php?model_name=" + getName();
            acquireSlot();
            try {
                Request request = new Request.Builder()
                        .url(url)
                        .header(ACCEPT, "*/*")
                        .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5")
                        .header(REFERER, getUrl())
                        .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
                        .header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
                        .build();
                try (Response response = getSite().getHttpClient().execute(request)) {
                    if (response.isSuccessful()) {
                        String body = response.body().string();
                        if (body.trim().isEmpty()) {
                            return false;
                        }
                        JSONObject json = new JSONObject(body);
                        //LOG.debug("check model status: {}", json.toString(2));
                        online = Objects.equals(json.optString("status"), "online");
                        id = String.valueOf(json.get("model_id"));
                        if (online) {
                            try {
                                loadStreamUrl();
                            } catch (Exception e) {
                                online = false;
                                onlineState = Model.State.OFFLINE;
                            }
                        }
                    } else {
                        throw new HttpException(response.code(), response.message());
                    }
                }
            } finally {
                lastOnlineRequest = System.currentTimeMillis();
                releaseSlot();
            }
        }
        return online;
    }

    private void loadModelInfo() throws IOException, InterruptedException {
        String url = getSite().getBaseUrl() + "/webservices/chat-room-interface.php?a=login_room&model_id=" + id;
        LOG.trace("Loading url {}", url);
        Request request = new Request.Builder()
                .url(url)
                .header(ACCEPT, "*/*")
                .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5")
                .header(REFERER, getUrl())
                .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
                .header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
                .build();
        try (Response response = getSite().getHttpClient().execute(request)) {
            if (response.isSuccessful()) {
                JSONObject json = new JSONObject(response.body().string());
                if (json.optString("status").equals("success")) {
                    // LOG.debug("chat-room-interface {}", json.toString(2));
                    JSONObject config = json.getJSONObject("config");
                    JSONObject performer = config.getJSONObject("performer");
                    setName(performer.optString("name_seo", "n/a"));
                    setDisplayName(performer.optString("name", "n/a"));
                    setUrl(getSite().getBaseUrl() + "/rooms/" + getName() + '/');
                    JSONObject room = config.getJSONObject("room");
                    chatHost = room.getString("host");
                    chatPort = room.getString("port_to_be");
                    chatToken = json.getString("token_enc");
                    JSONObject user = config.getJSONObject("user");
                    userIp = user.getString("ip");
                } else {
                    LOG.trace("Loading model info failed. Assuming model {} is offline", getName());
                    online = false;
                    onlineState = Model.State.OFFLINE;
                }
            } else {
                throw new HttpException(response.code(), response.message());
            }
        }
    }

    @Override
    public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
        return getStreamSources(true);
    }

    private List<StreamSource> getStreamSources(boolean withWebsocket) throws IOException, ExecutionException, ParseException, PlaylistException {
        MasterPlaylist masterPlaylist = null;
        try {
            if (withWebsocket) {
                acquireSlot();
                try {
                    loadStreamUrl();
                } finally {
                    releaseSlot();
                }
            }
            masterPlaylist = getMasterPlaylist();
        } catch (InterruptedException e) {
            throw new ExecutionException(e);
        }
        List<StreamSource> sources = new ArrayList<>();
        for (PlaylistData playlist : masterPlaylist.getPlaylists()) {
            if (playlist.hasStreamInfo()) {
                StreamSource src = new StreamSource();
                src.bandwidth = playlist.getStreamInfo().getBandwidth();
                src.height = playlist.getStreamInfo().getResolution().height;
                src.mediaPlaylistUrl = "https://manifest.vscdns.com/" + playlist.getUri();
                LOG.trace("Media playlist {}", src.mediaPlaylistUrl);
                sources.add(src);
            }
        }
        return sources;
    }

    public MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException, InterruptedException {
        LOG.trace("Loading master playlist {}", streamUrl);
        Request req = new Request.Builder()
                .url(streamUrl)
                .header(ACCEPT, "*/*")
                .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5")
                .header(REFERER, getUrl())
                .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
                .header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
                .build();
        acquireSlot();
        try (Response response = getSite().getHttpClient().execute(req)) {
            if (response.isSuccessful()) {
                InputStream inputStream = response.body().byteStream();
                PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
                Playlist playlist = parser.parse();
                MasterPlaylist master = playlist.getMasterPlaylist();
                return master;
            } else {
                throw new HttpException(response.code(), response.message());
            }
        } finally {
            releaseSlot();
        }
    }

    private void loadStreamUrl() throws IOException, InterruptedException {
        loadModelInfo();
        Objects.requireNonNull(chatHost, "chatHost is null");
        String h = chatHost.replaceAll("chat", "chat-vip");
        String url = "https://" + h + "/chat?token=" + URLEncoder.encode(chatToken, "utf-8") + "&port_to_be=" + chatPort;
        LOG.trace("Opening chat websocket {}", url);
        Request req = new Request.Builder()
                .url(url)
                .header(ACCEPT, "*/*")
                .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5")
                .header(REFERER, getUrl())
                .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
                .header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
                .build();

        getSite().getHttpClient().newWebSocket(req, new WebSocketListener() {
            @Override
            public void onOpen(WebSocket webSocket, Response response) {
                LOG.trace("Chat websocket for {} opened", getName());
            }

            @Override
            public void onMessage(WebSocket webSocket, String text) {
                LOG.trace("Chat wbesocket for {}: {}", getName(), text);
                JSONObject json = new JSONObject(text);
                //LOG.debug("WS {}", text);
                if (json.optString("command").equals("8011")) {
                    JSONObject data = json.getJSONObject("data");
                    streamHost = data.getString("stream_host"); // TODO look, if the stream_host is equal to the one encoded in base64 in some of the ajax requests (parameters)
                    online = true;
                    isInteractiveShow = data.optString("devices").equals("1");
                    if(data.optString("room_state").equals("P")) {
                        onlineState = Model.State.PRIVATE;
                        online = false;
                    }
                    if(data.optString("room_state").equals("0") && data.optString("login_group_id").equals("14")) {
                        onlineState = Model.State.GROUP;
                        online = false;
                    }
                    try {
                        resolution[0] = Integer.parseInt(data.getString("stream_width"));
                        resolution[1] = Integer.parseInt(data.getString("stream_height"));
                    } catch(Exception e) {
                        LOG.warn("Couldn't determine stream resolution", e);
                    }
                    webSocket.close(1000, "");
                }
            }

            @Override
            public void onFailure(WebSocket webSocket, Throwable t, Response response) {
                LOG.error("Chat websocket for {} failed", getName(), t);
                synchronized (monitor) {
                    monitor.notify();
                }
            }

            @Override
            public void onClosed(WebSocket webSocket, int code, String reason) {
                LOG.trace("Chat websocket for {} closed {} {}", getName(), code, reason);
                synchronized (monitor) {
                    monitor.notify();
                }
            }
        });

        synchronized (monitor) {
            monitor.wait(10_000);
            if (streamHost == null) {
                throw new RuntimeException("Couldn't determine streaming server for model " + getName());
            } else {
                streamUrl = "https://manifest.vscdns.com/manifest.m3u8.m3u8?key=nil&provider=level3&secure=true&host=" + streamHost + "&model_id=" + id;
            }
        }
    }

    @Override
    public void invalidateCacheEntries() {
    }

    @Override
    public void receiveTip(Double tokens) throws IOException {
        try {
            //        if(tokens < 50 || tokens > 750000) {
            //            throw new RuntimeException("Tip amount has to be between 50 and 750000");
            //        }

            // make sure we are logged in and all necessary model data is available
            getSite().login();
            acquireSlot();
            try {
                loadStreamUrl();
            } catch (InterruptedException e) {
                throw new IOException("Couldn't send tip", e);
            } finally {
                releaseSlot();
            }

            // send the tip
            int giftId = isInteractiveShow ? 775 : 171;
            int amount = tokens.intValue();
            LOG.debug("Sending tip of {} to {}", amount, getName());
            String url = "https://ws.vs3.com/rooms/send-tip.php?" +
                    "gift_id=" + giftId +
                    "&num_credits=" + amount +
                    "&userId=" + getUserIdt() +
                    "&username=" + Config.getInstance().getSettings().flirt4freeUsername +
                    "&userIP=" + userIp +
                    "&anonymous=N&response_type=json" +
                    "&t=" + System.currentTimeMillis();
            LOG.debug("Trying to send tip: {}", url);
            Request req = new Request.Builder()
                    .url(url)
                    .header(ACCEPT, "*/*")
                    .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5")
                    .header(REFERER, getUrl())
                    .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
                    .header(REFERER, getUrl())
                    .header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
                    .build();
            try (Response response = getSite().getHttpClient().execute(req)) {
                if (response.isSuccessful()) {
                    JSONObject json = new JSONObject(response.body().string());
                    if (json.optInt("success") != 1) {
                        String msg = json.optString("message");
                        if (json.has("error_message")) {
                            msg = json.getString("error_message");
                        }
                        LOG.error("Sending tip failed: {}", msg);
                        LOG.debug("Response: {}", json.toString(2));
                        throw new IOException(msg);
                    }
                } else {
                    throw new HttpException(response.code(), response.message());
                }
            }
        } catch (InterruptedException e) {
            throw new IOException("Couldn't acquire request slot", e);
        }
    }

    private String getUserIdt() throws IOException, InterruptedException {
        if (userIdt.isEmpty()) {
            acquireSlot();
            try {
                Request req = new Request.Builder()
                        .url(getUrl())
                        .header(ACCEPT, "*/*")
                        .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5")
                        .header(REFERER, getUrl())
                        .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
                        .build();
                try (Response response = getSite().getHttpClient().execute(req)) {
                    if (response.isSuccessful()) {
                        String body = response.body().string();
                        Matcher m = Pattern.compile("idt\\s*:\\s*'(.*?)',").matcher(body);
                        if (m.find()) {
                            userIdt = m.group(1);
                        } else {
                            throw new IOException("userIdt not found on HTML page");
                        }
                    } else {
                        throw new HttpException(response.code(), response.message());
                    }
                }
            } finally {
                releaseSlot();
            }
        }
        return userIdt;
    }

    @Override
    public int[] getStreamResolution(boolean failFast) throws ExecutionException {
        if(failFast) {
            return resolution;
        } else {
            if(streamUrl != null) {
                try {
                    List<StreamSource> streamSources = getStreamSources(false);
                    Collections.sort(streamSources);
                    StreamSource best = streamSources.get(streamSources.size()-1);
                    resolution = new int[] {best.width, best.height};
                } catch (IOException | ParseException | PlaylistException e) {
                    throw new ExecutionException("Couldn't determine stream resolution", e);
                }
            }
            return resolution;
        }
    }

    @Override
    public boolean follow() throws IOException {
        try {
            return changeFavoriteStatus(true);
        } catch (InterruptedException e) {
            throw new IOException("Couldn't change follow status for model " + getName(), e);
        }
    }

    @Override
    public boolean unfollow() throws IOException {
        try {
            isOnline(true);
            return changeFavoriteStatus(false);
        } catch (ExecutionException | InterruptedException e) {
            throw new IOException("Couldn't change follow status for model " + getName(), e);
        }
    }

    private boolean changeFavoriteStatus(boolean add) throws IOException, InterruptedException {
        getSite().login();
        acquireSlot();
        try {
            loadModelInfo();
        } finally {
            releaseSlot();
        }
        String url = getSite().getBaseUrl() + "/external.php?a=" +
                (add ? "add_favorite" : "delete_favorite") +
                "&id=" + id +
                "&name=" + getDisplayName() +
                "&t=" + System.currentTimeMillis();
        LOG.debug("Sending follow/unfollow request: {}", url);
        Request req = new Request.Builder()
                .url(url)
                .header(ACCEPT, "*/*")
                .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5")
                .header(REFERER, getUrl())
                .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
                .build();
        try (Response response = getSite().getHttpClient().execute(req)) {
            if (response.isSuccessful()) {
                String body = response.body().string();
                LOG.debug("Follow/Unfollow response: {}", body);
                return Objects.equals(body, "1");
            } else {
                throw new HttpException(response.code(), response.message());
            }
        }
    }

    public void setId(String id) {
        this.id = id;
    }

    @Override
    public void readSiteSpecificData(JsonReader reader) throws IOException {
        reader.nextName();
        id = reader.nextString();
    }

    @Override
    public void writeSiteSpecificData(JsonWriter writer) throws IOException {
        writer.name("id").value(id);
    }

    public void setStreamUrl(String streamUrl) {
        this.streamUrl = streamUrl;
    }

    public void setOnline(boolean b) {
        online = b;
    }

    private void acquireSlot() throws InterruptedException {
        //LOG.debug("Acquire: {}", requestThrottle.availablePermits());
        requestThrottle.acquire();
        long now = System.currentTimeMillis();
        long millisSinceLastRequest = now - lastRequest;
        if(millisSinceLastRequest < 500) {
            //LOG.debug("Sleeping: {}", (500-millisSinceLastRequest));
            Thread.sleep(500 - millisSinceLastRequest);
        }
    }

    private void releaseSlot() {
        lastRequest = System.currentTimeMillis();
        requestThrottle.release();
        //LOG.debug("Release: {}", requestThrottle.availablePermits());
    }
}
