package org.xbib.io.webdav.client;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xbib.io.webdav.api.DavConstants;
import org.xbib.io.webdav.api.DavResource;
import org.xbib.io.webdav.api.Namespace;
import org.xbib.io.webdav.common.DavException;
import org.xbib.io.webdav.common.DavPropertyName;
import org.xbib.io.webdav.client.methods.Mkcol;
import org.xbib.io.webdav.client.methods.Options;
import org.xbib.io.webdav.client.methods.Propfind;
import org.xbib.io.webdav.common.DavPropertyNameSet;
import org.xbib.io.webdav.common.DavPropertySet;
import org.xbib.io.webdav.common.DomUtil;
import org.xbib.io.webdav.common.EventDiscovery;
import org.xbib.io.webdav.common.MultiStatus;
import org.xbib.io.webdav.common.MultiStatusResponse;
import org.xbib.io.webdav.common.SubscriptionDiscovery;
import org.xml.sax.SAXException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.List;
import java.util.Set;
import javax.xml.parsers.ParserConfigurationException;

public class WebDavClient {

    private static final Namespace DCR_NAMESPACE = Namespace.getNamespace("dcr", "http://www.day.com/jcr/webdav/1.0");

    private HttpClient httpClient;

    private URI uri;

    public void init(URI uri, String username, String password) {
        this.uri = uri;
        this.httpClient = HttpClient.newBuilder()
                .version(HttpClient.Version.HTTP_1_1)
                .build();
    }

    public Set<String> getComplianceClasses(URI uri) throws IOException, InterruptedException {
        Options options = new Options();
        HttpRequest httpRequest = HttpRequest.newBuilder()
                .method(options.method(), null)
                .uri(uri).build();
        HttpResponse<String> response = httpClient.send(httpRequest, options);
        int status = response.statusCode();
        if (status != 200) {
            throw new DavAccessFailedException("OPTIONS failed with code " + status);
        }
        return options.getComplianceClasses();
    }

    public InputStream get(URI uri) throws IOException, InterruptedException {
        HttpRequest httpRequest = HttpRequest.newBuilder()
                .GET().uri(uri).build();
        HttpResponse<InputStream> response = httpClient.send(httpRequest,
                HttpResponse.BodyHandlers.ofInputStream());
        int status = response.statusCode();
        if (status != 200) {
            throw new DavAccessFailedException("GET failed with code " + status);
        }
        return response.body();
    }

    public WebDavLsResult ls(URI uri) throws IOException, InterruptedException {
        WebDavLsResult result = new WebDavLsResult();
        DavPropertyNameSet set = new DavPropertyNameSet();
        set.add(DavPropertyName.create(DavConstants.PROPERTY_DISPLAYNAME));
        set.add(DavPropertyName.create(DavConstants.PROPERTY_RESOURCETYPE));
        set.add(DavPropertyName.create(DavConstants.PROPERTY_SOURCE));
        set.add(DavPropertyName.create(DavConstants.PROPERTY_GETCONTENTLENGTH));
        set.add(DavPropertyName.create(DavConstants.PROPERTY_GETCONTENTTYPE));
        set.add(DavPropertyName.create(DavConstants.PROPERTY_CREATIONDATE));
        set.add(DavPropertyName.create(DavConstants.PROPERTY_GETLASTMODIFIED));
        Propfind propfind = new Propfind(set, 1);
        HttpRequest httpRequest = HttpRequest.newBuilder()
                .method(propfind.method(), propfind.bodyPublisher()).uri(uri).build();
        HttpResponse<InputStream> response = httpClient.send(httpRequest,
                HttpResponse.BodyHandlers.ofInputStream());
        if (response.statusCode() < 200 || response.statusCode() >= 300) {
            throw new DavAccessFailedException(propfind.method() + " failed with code = " + response.statusCode());
        }
        MultiStatus multistatus = getResponseBodyAsMultiStatus(response);
        MultiStatusResponse[] responses = multistatus.getResponses();
        for (MultiStatusResponse respons : responses) {
            DavPropertySet found = respons.getProperties(200);
            DavPropertySet notfound = respons.getProperties(404);
            if (notfound.contains(DavPropertyName.GETCONTENTLENGTH)) {
                result.addDirectory(new WebDavDirectory(uri, found));
            } else {
                result.addFile(new WebDavFile(uri, found));
            }
        }
        return result;
    }

    public WebDavDirectory mkdir(URI resource) throws IOException, InterruptedException {
        Mkcol mkcol = new Mkcol();
        HttpRequest httpRequest = HttpRequest.newBuilder()
                .method(mkcol.method(), mkcol.bodyPublisher()).uri(uri).build();
        HttpResponse<InputStream> response = httpClient.send(httpRequest,
                HttpResponse.BodyHandlers.ofInputStream());
        if (response.statusCode() < 200 || response.statusCode() >= 300) {
            throw new DavAccessFailedException(mkcol.method() + " failed with code = " + response.statusCode());
        }
        WebDavLsResult webDavLsResult = ls(resource);
        List<WebDavDirectory> webDavDirectories = webDavLsResult.getDirectories();
        if (webDavDirectories.isEmpty()) {
            throw new DavAccessFailedException(
                    "Directory " + mkcol.toString() + " will not be found.");
        }
        return webDavDirectories.get(0);
    }

    public WebDavFile put(URI uri, InputStream inputStream) throws IOException, InterruptedException {
        HttpRequest httpRequest = HttpRequest.newBuilder()
                .PUT(HttpRequest.BodyPublishers.ofInputStream(() -> inputStream)).uri(uri).build();
        HttpResponse<InputStream> response = httpClient.send(httpRequest,
                HttpResponse.BodyHandlers.ofInputStream());
        if (response.statusCode() < 200 || response.statusCode() >= 300) {
            throw new DavAccessFailedException("PUT failed with code = " + response.statusCode());
        }
        WebDavLsResult webDavLsResult = ls(uri);
        List<WebDavFile> webDavFileList = webDavLsResult.getFiles();
        if (webDavFileList.isEmpty()) {
            throw new DavAccessFailedException("file not found: " + uri);
        }
        return webDavFileList.get(0);
    }

    /**
     * Gets a {@link Document} representing the response body.
     *
     * @return document or {@code null} for null entity
     * @throws IOException in case of I/O or XMP pasting problems
     */
    public Document getResponseBodyAsDocument(HttpResponse<InputStream> response) throws IOException {
        try (InputStream in = response.body()) {
            return DomUtil.parseDocument(in);
        } catch (ParserConfigurationException | SAXException | IOException ex) {
            throw new IOException(ex);
        }
    }

    /**
     * Return response body as {@link MultiStatus} object.
     *
     * @throws IllegalStateException when response does not represent a {@link MultiStatus}
     * @throws DavException          for failures in obtaining/parsing the response body
     */
    public MultiStatus getResponseBodyAsMultiStatus(HttpResponse<InputStream> response) throws IOException {
        Document doc = getResponseBodyAsDocument(response);
        if (doc == null) {
            throw new DavException(response.statusCode(), "no response body");
        }
        return MultiStatus.createFromXml(doc.getDocumentElement());
    }

    /**
     * Return response body as {@link SubscriptionDiscovery} object.
     *
     * @throws IllegalStateException when response does not represent a {@link SubscriptionDiscovery}
     * @throws DavException          for failures in obtaining/parsing the response body
     */
    public SubscriptionDiscovery getResponseBodyAsSubscriptionDiscovery(HttpResponse<InputStream> response) throws DavException {
        try {
            DavPropertyName SUBSCRIPTIONDISCOVERY = DavPropertyName.create("subscriptiondiscovery", DCR_NAMESPACE);
            Document doc = getResponseBodyAsDocument(response);
            if (doc == null) {
                throw new DavException(response.statusCode(), "no response body");
            }
            Element root = doc.getDocumentElement();

            if (!DomUtil.matches(root, DavConstants.XML_PROP, DavConstants.NAMESPACE)
                    && DomUtil.hasChildElement(root, SUBSCRIPTIONDISCOVERY.getName(),
                    SUBSCRIPTIONDISCOVERY.getNamespace())) {
                throw new DavException(response.statusCode(),
                        "Missing DAV:prop response body in SUBSCRIBE response.");
            }

            Element sde = DomUtil.getChildElement(root, SUBSCRIPTIONDISCOVERY.getName(),
                    SUBSCRIPTIONDISCOVERY.getNamespace());
            SubscriptionDiscovery sd = SubscriptionDiscovery.createFromXml(sde);
            if (sd.getValue().length > 0) {
                return sd;
            } else {
                throw new DavException(response.statusCode(),
                        "Missing 'subscription' elements in SUBSCRIBE response body. At least a single subscription must be present if SUBSCRIBE was successful.");
            }
        } catch (IOException ex) {
            throw new DavException(response.statusCode(), ex);
        }
    }


    /**
     * Return response body as {@link EventDiscovery} object.
     *
     * @throws IllegalStateException when response does not represent a {@link EventDiscovery}
     * @throws DavException          for failures in obtaining/parsing the response body
     */
    public EventDiscovery getResponseBodyAsEventDiscovery(HttpResponse<InputStream> response) throws DavException {
        try {
            Document doc = getResponseBodyAsDocument(response);
            if (doc == null) {
                throw new DavException(response.statusCode(), "no response body");
            }
            return EventDiscovery.createFromXml(doc.getDocumentElement());
        } catch (IOException ex) {
            throw new DavException(response.statusCode(), ex);
        }
    }

    /**
     * Obtain a {@link DavException} representing the response.
     *
     * @throws IllegalStateException when the response is considered to be successful
     */
    public DavException getResponseException(HttpResponse<InputStream> response) {
        if (response.statusCode() >= 200 && response.statusCode() < 300) {
            String msg = "Cannot retrieve exception from successful response.";
            throw new IllegalStateException(msg);
        }
        Element responseRoot = null;
        try {
            responseRoot = getResponseBodyAsDocument(response).getDocumentElement();
        } catch (IOException e) {
            // non-parseable body -> use null element
        }
        return new DavException(response.statusCode(), "", null, responseRoot);
    }

    public boolean exists(String path) {
        return false;
    }

    public void createDirectory(String path) {
    }

    public void delete(String path) {
    }

    public List<DavResource> list(String path) {
        return null;
    }
}
