package com.getcapacitor.plugin;

import com.getcapacitor.Bridge;
import com.getcapacitor.Logger;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.net.CookieStore;
import java.net.HttpCookie;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;

public class CapacitorCookieManager extends CookieManager {

    private final android.webkit.CookieManager webkitCookieManager;

    private final String localUrl;

    private final String serverUrl;

    private final String TAG = "CapacitorCookies";

    /**
     * Create a new cookie manager with the default cookie store and policy
     */
    public CapacitorCookieManager(Bridge bridge) {
        this(null, null, bridge);
    }

    /**
     * Create a new cookie manager with specified cookie store and cookie policy.
     * @param store a {@code CookieStore} to be used by CookieManager. if {@code null}, cookie
     *              manager will use a default one, which is an in-memory CookieStore implementation.
     * @param policy a {@code CookiePolicy} instance to be used by cookie manager as policy
     *               callback. if {@code null}, ACCEPT_ORIGINAL_SERVER will be used.
     */
    public CapacitorCookieManager(CookieStore store, CookiePolicy policy, Bridge bridge) {
        super(store, policy);
        webkitCookieManager = android.webkit.CookieManager.getInstance();
        this.localUrl = bridge.getLocalUrl();
        this.serverUrl = bridge.getServerUrl();
    }

    public void removeSessionCookies() {
        this.webkitCookieManager.removeSessionCookies(null);
    }

    public String getSanitizedDomain(String url) throws URISyntaxException {
        if (this.serverUrl != null && !this.serverUrl.isEmpty() && (url == null || url.isEmpty() || this.serverUrl.contains(url))) {
            url = this.serverUrl;
        } else if (this.localUrl != null && !this.localUrl.isEmpty() && (url == null || url.isEmpty() || this.localUrl.contains(url))) {
            url = this.localUrl;
        } else try {
            URI uri = new URI(url);
            String scheme = uri.getScheme();
            if (scheme == null || scheme.isEmpty()) {
                url = "https://" + url;
            }
        } catch (URISyntaxException e) {
            Logger.error(TAG, "Failed to get scheme from URL.", e);
        }

        try {
            new URI(url);
        } catch (Exception error) {
            Logger.error(TAG, "Failed to get sanitized URL.", error);
            throw error;
        }
        return url;
    }

    private String getDomainFromCookieString(String cookie) throws URISyntaxException {
        String[] domain = cookie.toLowerCase(Locale.ROOT).split("domain=");
        return getSanitizedDomain(domain.length <= 1 ? null : domain[1].split(";")[0].trim());
    }

    /**
     * Gets the cookies for the given URL.
     * @param url the URL for which the cookies are requested
     * @return value the cookies as a string, using the format of the 'Cookie' HTTP request header
     */
    public String getCookieString(String url) {
        try {
            url = getSanitizedDomain(url);
            Logger.info(TAG, "Getting cookies at: '" + url + "'");
            return webkitCookieManager.getCookie(url);
        } catch (Exception error) {
            Logger.error(TAG, "Failed to get cookies at the given URL.", error);
        }

        return null;
    }

    /**
     * Gets a cookie value for the given URL and key.
     * @param url the URL for which the cookies are requested
     * @param key the key of the cookie to search for
     * @return the {@code HttpCookie} value of the cookie at the key,
     *         otherwise it will return a new empty {@code HttpCookie}
     */
    public HttpCookie getCookie(String url, String key) {
        HttpCookie[] cookies = getCookies(url);
        for (HttpCookie cookie : cookies) {
            if (cookie.getName().equals(key)) {
                return cookie;
            }
        }

        return null;
    }

    /**
     * Gets an array of {@code HttpCookie} given a URL.
     * @param url the URL for which the cookies are requested
     * @return an {@code HttpCookie} array of non-expired cookies
     */
    public HttpCookie[] getCookies(String url) {
        try {
            ArrayList<HttpCookie> cookieList = new ArrayList<>();
            String cookieString = getCookieString(url);
            if (cookieString != null) {
                String[] singleCookie = cookieString.split(";");
                for (String c : singleCookie) {
                    HttpCookie parsed = HttpCookie.parse(c).get(0);
                    parsed.setValue(parsed.getValue());
                    cookieList.add(parsed);
                }
            }
            HttpCookie[] cookies = new HttpCookie[cookieList.size()];
            return cookieList.toArray(cookies);
        } catch (Exception ex) {
            return new HttpCookie[0];
        }
    }

    /**
     * Sets a cookie for the given URL. Any existing cookie with the same host, path and name will
     *  be replaced with the new cookie. The cookie being set will be ignored if it is expired.
     * @param url the URL for which the cookie is to be set
     * @param value the cookie as a string, using the format of the 'Set-Cookie' HTTP response header
     */
    public void setCookie(String url, String value) {
        try {
            url = getSanitizedDomain(url);
            Logger.info(TAG, "Setting cookie '" + value + "' at: '" + url + "'");
            webkitCookieManager.setCookie(url, value);
            flush();
        } catch (Exception error) {
            Logger.error(TAG, "Failed to set cookie.", error);
        }
    }

    /**
     * Sets a cookie for the given URL. Any existing cookie with the same host, path and name will
     *  be replaced with the new cookie. The cookie being set will be ignored if it is expired.
     * @param url the URL for which the cookie is to be set
     * @param key the {@code HttpCookie} name to use for lookup
     * @param value the value of the {@code HttpCookie} given a key
     */
    public void setCookie(String url, String key, String value) {
        String cookieValue = key + "=" + value;
        setCookie(url, cookieValue);
    }

    public void setCookie(String url, String key, String value, String expires, String path) {
        String cookieValue = key + "=" + value + "; expires=" + expires + "; path=" + path;
        setCookie(url, cookieValue);
    }

    /**
     * Removes all cookies. This method is asynchronous.
     */
    public void removeAllCookies() {
        webkitCookieManager.removeAllCookies(null);
        flush();
    }

    /**
     * Ensures all cookies currently accessible through the getCookie API are written to persistent
     *  storage. This call will block the caller until it is done and may perform I/O.
     */
    public void flush() {
        webkitCookieManager.flush();
    }

    @Override
    public void put(URI uri, Map<String, List<String>> responseHeaders) {
        // make sure our args are valid
        if ((uri == null) || (responseHeaders == null)) return;

        // go over the headers
        for (String headerKey : responseHeaders.keySet()) {
            // ignore headers which aren't cookie related
            if ((headerKey == null) || !(headerKey.equalsIgnoreCase("Set-Cookie2") || headerKey.equalsIgnoreCase("Set-Cookie"))) continue;

            // process each of the headers
            for (String headerValue : Objects.requireNonNull(responseHeaders.get(headerKey))) {
                try {
                    // Set at the requested server url
                    setCookie(uri.toString(), headerValue);

                    // Set at the defined domain in the response or at default capacitor hosted url
                    setCookie(getDomainFromCookieString(headerValue), headerValue);
                } catch (Exception ignored) {}
            }
        }
    }

    @Override
    public Map<String, List<String>> get(URI uri, Map<String, List<String>> requestHeaders) {
        // make sure our args are valid
        if ((uri == null) || (requestHeaders == null)) throw new IllegalArgumentException("Argument is null");

        // save our url once
        String url = uri.toString();

        // prepare our response
        Map<String, List<String>> res = new HashMap<>();

        // get the cookie
        String cookie = getCookieString(url);

        // return it
        if (cookie != null) res.put("Cookie", Collections.singletonList(cookie));
        return res;
    }

    @Override
    public CookieStore getCookieStore() {
        // we don't want anyone to work with this cookie store directly
        throw new UnsupportedOperationException();
    }
}
