package moe.kyokobot.music.source;

import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager;
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools;
import com.sedmelluq.discord.lavaplayer.tools.io.HttpConfigurable;
import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface;
import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterfaceManager;
import com.sedmelluq.discord.lavaplayer.track.AudioItem;
import com.sedmelluq.discord.lavaplayer.track.AudioReference;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo;
import io.vertx.core.json.JsonObject;
import org.apache.http.Header;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicNameValuePair;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.parser.Parser;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.COMMON;
import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.SUSPICIOUS;

/**
 * Audio source manager that implements finding NicoNico tracks based on URL.
 */
@SuppressWarnings("WeakerAccess")
public class NicoAudioSourceManager implements AudioSourceManager, HttpConfigurable {
    private static final String TRACK_URL_REGEX = "^(?:http://|https://|)(?:www\\.|)nicovideo\\.jp/watch/([sn]m[0-9]+)(?:\\?.*|)$";

    private static final Pattern trackUrlPattern = Pattern.compile(TRACK_URL_REGEX);

    private final String email;
    private final String password;
    private final HttpInterfaceManager httpInterfaceManager;
    private final AtomicBoolean loggedIn;

    /**
     * @param email    Site account email
     * @param password Site account password
     */
    public NicoAudioSourceManager(String email, String password) {
        this.email = email;
        this.password = password;
        httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager();
        loggedIn = new AtomicBoolean();
    }

    @Override
    public String getSourceName() {
        return "nico";
    }

    @Override
    public AudioItem loadItem(DefaultAudioPlayerManager manager, AudioReference reference) {
        Matcher trackMatcher = trackUrlPattern.matcher(reference.identifier);

        if (trackMatcher.matches()) {
            return loadTrack(trackMatcher.group(1));
        }

        return null;
    }

    private AudioTrack loadTrack(String videoId) {
        checkLoggedIn();

        try (HttpInterface httpInterface = getHttpInterface()) {
            try (CloseableHttpResponse response = httpInterface.execute(new HttpGet("https://www.nicovideo.jp/watch/" + videoId))) {
                int statusCode = response.getStatusLine().getStatusCode();
                if (statusCode != 200) {
                    throw new IOException("Unexpected response code from video info: " + statusCode);
                }

                Document document = Jsoup.parse(response.getEntity().getContent(), StandardCharsets.UTF_8.name(), "", Parser.htmlParser());
                return extractTrackFromHtml(videoId, document);
            }
        } catch (IOException e) {
            throw new FriendlyException("Error occurred when extracting video info.", SUSPICIOUS, e);
        }
    }

    private AudioTrack extractTrackFromHtml(String videoId, Document document) {
        Element element = document.selectFirst("#js-initial-watch-data");
        if (element != null) {
            String data = element.attributes().get("data-api-data");
            if (data == null) {
                return null;
            }

            JsonObject object = new JsonObject(data);
            JsonObject video = object.getJsonObject("video");

            String uploader = object.getJsonObject("owner").getString("nickname");
            String title = video.getString("title");
            long duration = video.getLong("duration") * 1000;

            return new NicoAudioTrack(new AudioTrackInfo(title, uploader, duration, videoId, false, getWatchUrl(videoId)), this);
        }
        return null;
    }

    @Override
    public boolean isTrackEncodable(AudioTrack track) {
        return true;
    }

    @Override
    public void encodeTrack(AudioTrack track, DataOutput output) {
        // No extra information to save
    }

    @Override
    public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) {
        return new NicoAudioTrack(trackInfo, this);
    }

    @Override
    public void shutdown() {
        // Nothing to shut down
    }

    /**
     * @return Get an HTTP interface for a playing track.
     */
    public HttpInterface getHttpInterface() {
        return httpInterfaceManager.getInterface();
    }

    @Override
    public void configureRequests(Function<RequestConfig, RequestConfig> configurator) {
        httpInterfaceManager.configureRequests(configurator);
    }

    @Override
    public void configureBuilder(Consumer<HttpClientBuilder> configurator) {
        httpInterfaceManager.configureBuilder(configurator);
    }

    void checkLoggedIn() {
        synchronized (loggedIn) {
            if (loggedIn.get()) {
                return;
            }

            HttpPost loginRequest = new HttpPost("https://secure.nicovideo.jp/secure/login");

            loginRequest.setEntity(new UrlEncodedFormEntity(Arrays.asList(
                    new BasicNameValuePair("mail", email),
                    new BasicNameValuePair("password", password)
            ), StandardCharsets.UTF_8));

            try (HttpInterface httpInterface = getHttpInterface()) {
                try (CloseableHttpResponse response = httpInterface.execute(loginRequest)) {
                    int statusCode = response.getStatusLine().getStatusCode();

                    if (statusCode != 302) {
                        throw new IOException("Unexpected response code " + statusCode);
                    }

                    Header location = response.getFirstHeader("Location");

                    if (location == null || location.getValue().contains("message=")) {
                        throw new FriendlyException("Login details for NicoNico are invalid.", COMMON, null);
                    }

                    loggedIn.set(true);
                }
            } catch (IOException e) {
                throw new FriendlyException("Exception when trying to log into NicoNico", SUSPICIOUS, e);
            }
        }
    }

    private static String getWatchUrl(String videoId) {
        return "http://www.nicovideo.jp/watch/" + videoId;
    }
}