package ctbrec.sites.chaturbate;

import ctbrec.Config;
import ctbrec.io.FlaresolverrClient;
import ctbrec.io.HtmlParser;
import ctbrec.io.HttpClient;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;

import java.time.*;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import static ctbrec.io.HttpConstants.REFERER;
import static ctbrec.io.HttpConstants.USER_AGENT;

@Slf4j
public class ChaturbateHttpClient extends HttpClient {

    private static final String PATH = "/auth/login/"; // NOSONAR
    protected String token;
    protected final FlaresolverrClient flaresolverr;

    private static final Semaphore requestThrottle = new Semaphore(2, true);
    private static long lastRequest = 0;
    
    // a lock to prevent multiple requests from 
    ReentrantReadWriteLock cookieRefreshLock = new ReentrantReadWriteLock();
    AtomicInteger cookieErrorCounter = new AtomicInteger(0);

    public ChaturbateHttpClient(Config config) {
        super("chaturbate", config);
        
        if (config.getSettings().chaturbateUseFlaresolverr) {
            flaresolverr = new FlaresolverrClient(config.getSettings().flaresolverr.apiUrl, config.getSettings().flaresolverr.timeoutInMillis);
            
            // try {
            //     flaresolverr.createSession("ctbrec").get();
            // } catch (InterruptedException e) {
            //     Thread.currentThread().interrupt();
            // } catch (Exception e) {
            //     log.error("Error starting Flaresolverr session", e);
            // }
        } else {
            flaresolverr = null;
        }
    }
    
    @Override
    public String getEffectiveUserAgent() {
        if (flaresolverr != null) {
            return config.getSettings().flaresolverr.userAgent;
        } else {
            return config.getSettings().httpUserAgent;
        }
    }

    private void extractCsrfToken(Request request) {
        try {
            Cookie csrfToken = cookieJar.getCookie(request.url(), "csrftoken");
            token = csrfToken.value();
        } catch (NoSuchElementException e) {
            log.trace("CSRF token not found in cookies");
        }
    }

    public String getToken() throws IOException {
        if (token == null) {
            login();
        }
        return token;
    }

    @Override
    public boolean login() throws IOException {
        if (loggedIn) {
            return true;
        }
        if (checkLogin()) {
            loggedIn = true;
            log.debug("Logged in with cookies");
            return true;
        }
        Request login = new Request.Builder()
                .url(Chaturbate.baseUrl + PATH)
                .header(USER_AGENT, getEffectiveUserAgent())
                .build();
        try (var initResponse = client.newCall(login).execute()) {
            String content = initResponse.body().string();
            token = HtmlParser.getTag(content, "input[name=csrfmiddlewaretoken]").attr("value");
            log.debug("csrf token is {}", token);

            RequestBody body = new FormBody.Builder()
                    .add("username", Config.getInstance().getSettings().chaturbateUsername)
                    .add("password", Config.getInstance().getSettings().chaturbatePassword)
                    .add("next", "")
                    .add("csrfmiddlewaretoken", token)
                    .build();
            login = new Request.Builder()
                    .url(Chaturbate.baseUrl + PATH)
                    .header(REFERER, Chaturbate.baseUrl + PATH)
                    .header(USER_AGENT, getEffectiveUserAgent())
                    .post(body)
                    .build();

            try (var loginResponse = client.newCall(login).execute()) {
                if (loginResponse.isSuccessful()) {
                    content = loginResponse.body().string();
                    if (content.contains("Login, Chaturbate login")) {
                        loggedIn = false;
                    } else {
                        loggedIn = true;
                        extractCsrfToken(login);
                    }
                }
            }
        } catch (Exception ex) {
            log.debug("Login failed: {}", ex.getMessage());
        }
        return loggedIn;
    }

    public boolean checkLogin() {
        String url = "https://chaturbate.com/api/ts/chatmessages/pm_users/?offset=0";
        Request req = new Request.Builder()
                .url(url)
                .header(USER_AGENT, getEffectiveUserAgent())
                .build();
        try (Response response = execute(req)) {
            boolean result = false;
            if (response.isSuccessful()) {
                String content = response.body().string();
                if (content.startsWith("[")) {
                    result = true;
                }
            }
            log.trace("Chaturbate client login result: {}, {}", result, response.body().string());
            return result;
        } catch (Exception ex) {
            return false;
        }
    }

    @Override
    public Response execute(Request req) throws IOException {
        boolean throttled = req.url().host().contains("chaturbate.com");
        return executeThrottled(req, throttled);
    }

    private Response executeThrottled(Request req, boolean throttle) throws IOException {
        try {
            if (throttle) {
                acquireSlot();
            }
            
            Response resp;
            
            try {
                cookieRefreshLock.readLock().lock();
                resp = super.execute(req);
            } finally {
                cookieRefreshLock.readLock().unlock();
            }
            
            // try to solve the cloudflare challenge if we got one (clearance cookie expired, update it)
            if (resp.code() == 403 && flaresolverr != null) {
                resp = refreshCookiesAndRetry(req, resp);
            }
            
            extractCsrfToken(req);
            return resp;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new InterruptedIOException("Interrupted during request");
        } finally {
            if (throttle) {
                releaseSlot();
            }
        }
    }
    
    private Response refreshCookiesAndRetry(Request req, Response origResp) throws IOException {
        log.debug("403 received from {}. Trying to refresh cookies with Flaresolverr", req.url().host());
        
        try {
            cookieRefreshLock.writeLock().lock();            
            
            // we need to prevent repeated challenge requests from multiple threads, so we check if the clearance cookie needs updating
            // maybe this can be done with some syncronization primitive, or maybe an expiresAt() check is enough
            var cookie = Optional
                .ofNullable(cookieJar.getCookies().get(req.url().topPrivateDomain()))
                .flatMap(x -> cookieJar.getCookieFromCollection(x, "cf_clearance"));
                
            var cookieExpired = cookie.map(c -> 
                Instant.ofEpochMilli(c.expiresAt()).isBefore(Instant.now())  // by time
                || req.headers("Cookie").stream().anyMatch(headerCookie -> headerCookie.contains(c.value()))  // we got 403 with current cookie present
            ).orElse(true);
                
            if (cookieExpired || cookieErrorCounter.incrementAndGet() >= 5) {
                cookieErrorCounter.set(0);
                
                var apiResponse = flaresolverr.getCookies(req.url().toString()).get();
                if (apiResponse.getStatus().equals("ok")) {
                    // update user agent. It should be the same for all sites, assuming we use the same api address every time
                    if (!config.getSettings().flaresolverr.userAgent.equals(apiResponse.getUserAgent())) {
                        config.getSettings().flaresolverr.userAgent = apiResponse.getUserAgent();
                        config.save();
                    }
                    
                    cookieJar.saveFromResponse(req.url(), apiResponse.getCookies());    
                    persistCookies();  
                    log.debug("Cookies successfully refreshed with Flaresolverr in {}", Duration.between(apiResponse.getStartTimestamp(), apiResponse.getEndTimestamp()));
                } else {
                    log.debug("Unsuccessful attempt to refresh cookies. Response from Flaresolverr: {}", apiResponse);
                    return origResp;
                }
            } else {
                log.debug("Looks like the cookies were refreshed already, skipping refreshing");
            }
        } catch (Exception e) {
            log.warn("Error refreshing cookies with Flaresolverr", e);
            return origResp;
        } finally {
            cookieRefreshLock.writeLock().unlock();
        }
        
        origResp.close();
        
        return super.execute(req);
    }

    private static void acquireSlot() throws InterruptedException {
        long pauseBetweenRequests = Config.getInstance().getSettings().chaturbateMsBetweenRequests;
        requestThrottle.acquire();
        long now = System.currentTimeMillis();
        long millisSinceLastRequest = now - lastRequest;
        if (millisSinceLastRequest < pauseBetweenRequests) {
            Thread.sleep(pauseBetweenRequests - millisSinceLastRequest);
        }
    }

    private static void releaseSlot() {
        lastRequest = System.currentTimeMillis();
        requestThrottle.release();
    }
}
