/*
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.io.uri.impl;

import org.slf4j.Logger;

import net.ranides.assira.collection.prototype.GenericMap;
import net.ranides.assira.credentials.UserProperty;
import net.ranides.assira.io.IOHandle;
import net.ranides.assira.io.IOStreams;
import net.ranides.assira.io.uri.URIFlags;
import net.ranides.assira.io.uri.URIHandle;
import net.ranides.assira.io.uri.URIHandler;
import net.ranides.assira.io.uri.URIResolver;
import net.ranides.assira.io.uri.URITime;
import net.ranides.assira.text.Charsets;
import net.ranides.assira.text.FormatBase64;
import net.ranides.assira.text.StringTraits;
import net.ranides.assira.trace.LoggerUtils;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.nio.charset.Charset;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Supported schemes: http https
 *
 * Please note:
 * As every URIHandler, it should not be constructed directly, but connected with some instance of URIResolver.
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
public class CHTTPHandler implements URIHandler {
    
    private static final Logger LOGGER = LoggerUtils.getLogger();

    private static final int CONNECT_TIMEOUT = 1000;
    
    private static final Pattern RE_CHARSET = Pattern.compile(";[ ]*charset=([^; ]+)");

    private final URIResolver resolver;

    /**
     * Default constructor
     */
    public CHTTPHandler() {
        this.resolver = URIResolver.DEFAULT;
    }

    /**
     * Constructor used by ServiceLoader inside static initialization of {@link URIResolver}
     * @param resolver owner
     */
    public CHTTPHandler(URIResolver resolver) {
        this.resolver = resolver;
    }
    
    @Override
    public URIHandle resolve(URI uri) {
        return new CHttpHandle(uri);
    }

    @Override
    public Collection<String> schemes() {
        return Arrays.asList("http","https");
    }
    
    private class CHttpHandle extends CHandle {
        
        private final URI uri;
        private final IOHandle<HttpURLConnection> head;
        private final IOHandle<HttpURLConnection> get;
        private final IOHandle<HttpURLConnection> post;

        public CHttpHandle(URI uri) {
            this.uri = uri;
            head = IOHandle.shared(() -> connect("HEAD"));
            get = IOHandle.shared(() -> connect("GET"));
            post = IOHandle.shared(() -> connect("POST"));
        }

        @Override
        public URIResolver resolver() {
            return resolver;
        }
        
        @Override
        public URI uri() {
            return uri;
        }
        
        @Override
        public Charset charset() throws IOException {
            return getContentCharset(head().getContentType());
        }

        @Override
        public Set<URIFlags> flags() {
            Set<URIFlags> re = URIFlags.collect(URIFlags.FILE);
            try {
                HttpURLConnection c = head();
                if(isSuccess(c)) {
                    re.add(URIFlags.EXISTS);
                    re.add(URIFlags.READABLE);
                }

                if(StringTraits.starts(c.getContentType(), "text/")) {
                    re.add(URIFlags.TEXT);
                } else {
                    re.add(URIFlags.BINARY);
                }
                if(c.getContentLengthLong() >= 0) {
                    re.add(URIFlags.SIZE);
                }
                if(StringTraits.contains(c.getHeaderField("Accept-Ranges"), "bytes")) {
                    re.add(URIFlags.SEEK);
                }
            } catch(IOException cause) {
                LOGGER.warn("URI.flags: HTTP error", cause);
            }
            return re;
        }

        @Override
        public Instant time(URITime ut) throws IOException {
            if(ut == URITime.MODIFIED) {
                HttpURLConnection c = head();
                if(null == c.getHeaderField("Last-Modified")) {
                    return Instant.ofEpochMilli(c.getDate());
                }
                return new Date(c.getHeaderFieldDate("Last-Modified", 0)).toInstant();
            }
            return super.time(ut);
        }

        @Override
        public long size() throws IOException {
            HttpURLConnection c = head();
            if(2 != (c.getResponseCode() / 100)) {
                throw new IOException(c.getResponseMessage());
            }
            return c.getContentLengthLong();
        }

        @Override
        public InputStream istream() throws IOException {
            return get.get().getInputStream();
        }

        @Override
        public InputStream istream(long begin, long end) throws IOException {
            HttpURLConnection c = get.get();
            if(end == Long.MAX_VALUE) {
                c.setRequestProperty("Range", "bytes=" + begin + "-");
            } else {
                c.setRequestProperty("Range", "bytes=" + begin + "-" + end);
            }
            if (c.getResponseCode() != HttpURLConnection.HTTP_PARTIAL) {
                return IOStreams.limit(c.getInputStream(), begin, end);
            } else {
                return c.getInputStream();
            }
        }

        @Override
        public OutputStream ostream() throws IOException {
            HttpURLConnection c = post.get();
            c.connect();
            return c.getOutputStream();
        }

        @Override
        public void create() throws IOException {
            execute("PUT");
        }

        @Override
        public void delete() throws IOException {
            execute("DELETE");
        }
        
        private HttpURLConnection head() throws IOException {
            return get.opened() ? get.get() : head.get();
        }
        
        private void execute(String method) throws IOException {
            HttpURLConnection c = connect(method);
            boolean success = isSuccess(c);
            c.disconnect();
            if(!success) {
                throw new IOException(c.getResponseCode() + ": " + c.getResponseMessage());
            }
        }
        
        private HttpURLConnection connect(String method) throws IOException {
            HttpURLConnection c = (HttpURLConnection)uri.toURL().openConnection();
            c.setConnectTimeout(CONNECT_TIMEOUT);
            c.setRequestMethod(method);

            resolver.users()
                .find(uri)
                .filter(v -> StringTraits.isNotEmpty(v.get(UserProperty.PASSWORD)))
                .first()
                .ifPresent(v -> setAuthorizationHeader(c, v));
            return c;
        }

    }
    
    private static boolean isSuccess(HttpURLConnection c) throws IOException {
        return 2 == (c.getResponseCode() / 100);
    }
    
    private static Charset getContentCharset(String type) throws IOException {
        if(null == type) {
            throw new IOException("Unknown MIME type");
        }
        if(!type.startsWith("text/")) {
            throw new IOException("Unsupported MIME type: " + type);
        }
        Matcher hit = RE_CHARSET.matcher(type);
        if(!hit.find()) {
            return Charsets.UTF8;
        }
        return Charset.forName(hit.group(1));
    }

    private static void setAuthorizationHeader(HttpURLConnection connection, GenericMap<String> user) {
        String passphrase = user.get(UserProperty.LOGIN) + ":" + user.get(UserProperty.PASSWORD);

        // there is no point in checking WWW-Authenticate: ... charset="UTF-8"
        // if we have non-ASCII characters inside passphrase the ONLY option is to use UTF-8
        // if we have only ASCII characters then UTF-8 has no effect anyway
        //
        // https://stackoverflow.com/questions/7242316/what-encoding-should-i-use-for-http-basic-authentication

        String header = "Basic " + FormatBase64.format(passphrase.getBytes(Charsets.UTF8));
        connection.setRequestProperty("Authorization", header);
    }
}
