/*
 * Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v3.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/gpl.html
 */
package at.bitfire.davdroid.resource;

import android.content.Context;
import android.text.TextUtils;
import android.util.Log;

import com.squareup.okhttp.HttpUrl;

import org.xbill.DNS.Lookup;
import org.xbill.DNS.Record;
import org.xbill.DNS.SRVRecord;
import org.xbill.DNS.TXTRecord;
import org.xbill.DNS.TextParseException;
import org.xbill.DNS.Type;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.LinkedList;
import java.util.List;

import at.bitfire.dav4android.DavResource;
import at.bitfire.dav4android.exception.DavException;
import at.bitfire.dav4android.exception.HttpException;
import at.bitfire.dav4android.property.AddressbookDescription;
import at.bitfire.dav4android.property.AddressbookHomeSet;
import at.bitfire.dav4android.property.CalendarColor;
import at.bitfire.dav4android.property.CalendarDescription;
import at.bitfire.dav4android.property.CalendarHomeSet;
import at.bitfire.dav4android.property.CalendarTimezone;
import at.bitfire.dav4android.property.CurrentUserPrincipal;
import at.bitfire.dav4android.property.CurrentUserPrivilegeSet;
import at.bitfire.dav4android.property.DisplayName;
import at.bitfire.dav4android.property.ResourceType;
import at.bitfire.dav4android.property.SupportedCalendarComponentSet;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.DavUtils;
import at.bitfire.davdroid.HttpClient;

public class DavResourceFinder {
	private final static String TAG = "davdroid.ResourceFinder";
	
	final protected Context context;

	
	public DavResourceFinder(Context context) {
		this.context = context;
	}

	public void findResources(final ServerInfo serverInfo) throws URISyntaxException, IOException, HttpException, DavException {
        final HttpClient httpClient = new HttpClient(context, serverInfo.getUserName(), serverInfo.getPassword(), serverInfo.authPreemptive);

        // CardDAV
        Constants.log.info("*** CardDAV resource detection ***");
		HttpUrl principalUrl = getCurrentUserPrincipal(httpClient, serverInfo, "carddav");

        DavResource principal = new DavResource(httpClient, principalUrl);
        principal.propfind(0, AddressbookHomeSet.NAME);
        AddressbookHomeSet addrHomeSet = (AddressbookHomeSet)principal.properties.get(AddressbookHomeSet.NAME);
        if (addrHomeSet != null && !addrHomeSet.hrefs.isEmpty()) {
            Constants.log.info("Found addressbook home set(s): " + addrHomeSet);

            // enumerate address books
            List<ServerInfo.ResourceInfo> addressBooks = new LinkedList<>();
            for (String href : addrHomeSet.hrefs) {
                DavResource homeSet = new DavResource(httpClient, principalUrl.resolve(href));
                homeSet.propfind(1, ResourceType.NAME, CurrentUserPrivilegeSet.NAME, DisplayName.NAME, AddressbookDescription.NAME);
                for (DavResource member : homeSet.members) {
                    ResourceType type = (ResourceType)member.properties.get(ResourceType.NAME);
                    if (type != null && type.types.contains(ResourceType.ADDRESSBOOK)) {
                        Constants.log.info("Found address book: " + member.location);

                        CurrentUserPrivilegeSet privs = (CurrentUserPrivilegeSet)member.properties.get(CurrentUserPrivilegeSet.NAME);
                        if (privs != null && (!privs.mayRead || !privs.mayWriteContent)) {
                            Constants.log.info("Only read/write address books are supported, ignoring this one");
                            continue;
                        }

                        DisplayName displayName = (DisplayName)member.properties.get(DisplayName.NAME);
                        AddressbookDescription description = (AddressbookDescription)member.properties.get(AddressbookDescription.NAME);

                        addressBooks.add(new ServerInfo.ResourceInfo(
                                ServerInfo.ResourceInfo.Type.ADDRESS_BOOK,
                                false,
                                member.location.toString(),
                                displayName != null ? displayName.displayName : null,
                                description != null ? description.description : null,
                                null
                        ));
                    }
                }
            }
            serverInfo.setAddressBooks(addressBooks);
        }

        // CalDAV
        Constants.log.info("*** CalDAV resource detection ***");
        principalUrl = getCurrentUserPrincipal(httpClient, serverInfo, "caldav");

        principal = new DavResource(httpClient, principalUrl);
        principal.propfind(0, CalendarHomeSet.NAME);
        CalendarHomeSet calHomeSet = (CalendarHomeSet)principal.properties.get(CalendarHomeSet.NAME);
        if (calHomeSet != null && !calHomeSet.hrefs.isEmpty()) {
            Constants.log.info("Found calendar home set(s): " + calHomeSet);

            // enumerate address books
            List<ServerInfo.ResourceInfo>
                    calendars = new LinkedList<>(),
                    taskLists = new LinkedList<>();

            for (String href : calHomeSet.hrefs) {
                DavResource homeSet = new DavResource(httpClient, principalUrl.resolve(href));
                homeSet.propfind(1, ResourceType.NAME, CurrentUserPrivilegeSet.NAME, DisplayName.NAME,
                        CalendarDescription.NAME, CalendarColor.NAME, CalendarTimezone.NAME, SupportedCalendarComponentSet.NAME);
                for (DavResource member : homeSet.members) {
                    ResourceType type = (ResourceType)member.properties.get(ResourceType.NAME);
                    if (type != null && type.types.contains(ResourceType.CALENDAR)) {
                        Constants.log.info("Found calendar: " + member.location);

                        DisplayName displayName = (DisplayName)member.properties.get(DisplayName.NAME);
                        CalendarDescription description = (CalendarDescription)member.properties.get(CalendarDescription.NAME);
                        CalendarColor color = (CalendarColor)member.properties.get(CalendarColor.NAME);

                        CurrentUserPrivilegeSet privs = (CurrentUserPrivilegeSet)member.properties.get(CurrentUserPrivilegeSet.NAME);
                        boolean readOnly = false;
                        if (privs != null) {
                            if (!privs.mayRead) {
                                Constants.log.info("Calendar not readable, ignoring this one");
                                continue;
                            }
                            readOnly = !privs.mayWriteContent;
                        }

                        ServerInfo.ResourceInfo collection = new ServerInfo.ResourceInfo(
                                ServerInfo.ResourceInfo.Type.ADDRESS_BOOK,
                                readOnly,
                                member.location.toString(),
                                displayName != null ? displayName.displayName : null,
                                description != null ? description.description : null,
                                color != null ? DavUtils.CalDAVtoARGBColor(color.color) : null
                        );

                        CalendarTimezone tz = (CalendarTimezone)member.properties.get(CalendarTimezone.NAME);
                        if (tz != null)
                            collection.timezone = tz.vTimeZone;

                        boolean isCalendar = true, isTaskList = true;
                        SupportedCalendarComponentSet comp = (SupportedCalendarComponentSet)member.properties.get(SupportedCalendarComponentSet.NAME);
                        if (comp != null) {
                            isCalendar = comp.supportsEvents;
                            isTaskList = comp.supportsTasks;
                        }

                        if (isCalendar)
                            calendars.add(collection);
                        if (isTaskList)
                            taskLists.add(collection);
                    }
                }
            }
            serverInfo.setCalendars(calendars);
            serverInfo.setTaskLists(taskLists);
        }

		/*if (!serverInfo.isCalDAV() && !serverInfo.isCardDAV())
			throw new DavIncapableException(context.getString(R.string.setup_neither_caldav_nor_carddav));*/
	}
	
	
	/**
	 * Finds the initial service URL from a given base URI (HTTP[S] or mailto URI, user name, password)
	 * @param serverInfo	User-given service information (including base URI, i.e. HTTP[S] URL+user name+password or mailto URI and password)
	 * @param serviceName	Service name ("carddav" or "caldav")
	 * @return				Initial service URL (HTTP/HTTPS), without user credentials
	 * @throws URISyntaxException when the user-given URI is invalid
	 */
	public HttpUrl getInitialContextURL(ServerInfo serverInfo, String serviceName) throws URISyntaxException {
		String	scheme,
				domain;
		int		port = -1;
		String	path = "/";
		
		URI baseURI = serverInfo.getBaseURI();
		if ("mailto".equalsIgnoreCase(baseURI.getScheme())) {
			// mailto URIs
			String mailbox = serverInfo.getBaseURI().getSchemeSpecificPart();

			// determine service FQDN
			int pos = mailbox.lastIndexOf("@");
			if (pos == -1)
				throw new URISyntaxException(mailbox, "Missing @ sign");
			
			scheme = "https";
			domain = mailbox.substring(pos + 1);
			if (domain.isEmpty())
				throw new URISyntaxException(mailbox, "Missing domain name");
		} else {
			// HTTP(S) URLs
			scheme = baseURI.getScheme();
			domain = baseURI.getHost();
			port = baseURI.getPort();
			path = baseURI.getPath();
		}

		// try to determine FQDN and port number using SRV records
		try {
			String name = "_" + serviceName + "s._tcp." + domain;
			Constants.log.debug("Looking up SRV records for " + name);
			Record[] records = new Lookup(name, Type.SRV).run();
			if (records != null && records.length >= 1) {
				SRVRecord srv = selectSRVRecord(records);
				
				scheme = "https";
				domain = srv.getTarget().toString(true);
				port = srv.getPort();
				Log.d(TAG, "Found " + serviceName + "s service for " + domain + " -> " + domain + ":" + port);

				// SRV record found, look for TXT record too (for initial context path)
				records = new Lookup(name, Type.TXT).run();
				if (records != null && records.length >= 1) {
					TXTRecord txt = (TXTRecord)records[0];
					for (Object o : txt.getStrings().toArray()) {
						String segment = (String)o;
						if (segment.startsWith("path=")) {
							path = segment.substring(5);
                            Constants.log.debug("Found initial context path for " + serviceName + " at " + domain + " -> " + path);
							break;
						}
					}
				}
			}
		} catch (TextParseException e) {
			throw new URISyntaxException(domain, "Invalid domain name");
		}
		
		HttpUrl.Builder builder = new HttpUrl.Builder().scheme(scheme).host(domain);
        if (port != -1)
            builder.port(port);
        if (TextUtils.isEmpty(path))
            path = "/";
        return builder.encodedPath(path).build();
	}
	
	
	/**
	 * Detects the current-user-principal for a given WebDavResource. At first, /.well-known/ is tried. Only
	 * if no current-user-principal can be detected for the .well-known location, the given location of the resource
	 * is tried.
	 * @param serverInfo	Location that will be queried
	 * @param serviceName	Well-known service name ("carddav", "caldav")
	 * @return	            WebDavResource of current-user-principal for the given service, or initial context URL if it can't be found
	 * 
	 * TODO: If a TXT record is given, always use it instead of trying .well-known first
	 */
	HttpUrl getCurrentUserPrincipal(HttpClient httpClient, ServerInfo serverInfo, String serviceName) throws URISyntaxException {
        HttpUrl initialURL = getInitialContextURL(serverInfo, serviceName);

        if (initialURL != null) {
            Constants.log.info("Looking up principal URL for service " + serviceName + "; initial context: " + initialURL);

            // look for well-known service (RFC 5785)
            try {
                DavResource wellKnown = new DavResource(httpClient, initialURL.resolve("/.well-known/" + serviceName));
                wellKnown.propfind(0, CurrentUserPrincipal.NAME);

                CurrentUserPrincipal principal = (CurrentUserPrincipal)wellKnown.properties.get(CurrentUserPrincipal.NAME);
                if (principal != null) {
                    HttpUrl url = wellKnown.location.resolve(principal.href);
                    Constants.log.info("Found principal URL from well-known URL: " + url);
                    return url;
                }
            } catch (IOException e) {
                Constants.log.warn("Well-known " + serviceName + " service detection failed with I/O error", e);
            } catch (HttpException e) {
                Constants.log.warn("Well-known " + serviceName + " service detection failed with HTTP error", e);
            } catch (DavException e) {
                Constants.log.warn("Well-known " + serviceName + " service detection failed with DAV error", e);
            }
        }

        // fall back to user-given initial context path
        Log.d(TAG, "Well-known service detection failed, trying initial context path " + initialURL);
        try {
            DavResource base = new DavResource(httpClient, initialURL);
            base.propfind(0, CurrentUserPrincipal.NAME);
            CurrentUserPrincipal principal = (CurrentUserPrincipal)base.properties.get(CurrentUserPrincipal.NAME);
            if (principal != null) {
                HttpUrl url = base.location.resolve(principal.href);
                Constants.log.info("Found principal URL from initial context URL: " + url);
                return url;
            }
        } catch (IOException e) {
            Constants.log.warn("Well-known " + serviceName + " service detection failed with I/O error", e);
        } catch (HttpException e) {
            Log.e(TAG, "HTTP error when querying principal", e);
        } catch (DavException e) {
            Log.e(TAG, "DAV error when querying principal", e);
        }

        Log.i(TAG, "Couldn't find current-user-principal for service " + serviceName + ". Assuming principal path is initial context path!");
		return initialURL;
	}

	
	SRVRecord selectSRVRecord(Record[] records) {
		if (records.length > 1)
			Log.w(TAG, "Multiple SRV records not supported yet; using first one");
		return (SRVRecord)records[0];
	}

}
