package ca.carleton.gcrc.couch.client.impl;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import net.sf.json.JSONArray;
import net.sf.json.JSONObject;

import ca.carleton.gcrc.couch.client.CouchContext;
import ca.carleton.gcrc.couch.client.CouchDb;
import ca.carleton.gcrc.couch.client.CouchClient;
import ca.carleton.gcrc.couch.client.CouchDesignDocument;

public class CouchDbImpl implements CouchDb {

	private CouchClient client;
	private URL url;
	
	public CouchDbImpl(CouchClient client, URL url) {
		this.client = client;
		this.url = url;
	}

	@Override
	public CouchContext getContext() {
		return client.getContext();
	}
	
	@Override
	public CouchClient getClient() {
		return client;
	}

	@Override
	public URL getUrl() {
		return url;
	}

	@Override
	public JSONObject createDocument(JSONObject doc) throws Exception {
		// Figure out id. Either one is provided, or one must be obtained
		String uuid = null;
		if( doc.has("_id") ) {
			Object id = doc.get("_id");
			if( null != id && id instanceof String ) {
				uuid = (String)id;
			} 
		}
		if( null == uuid ){
			uuid = client.getUuid();
			doc.put("_id", uuid);
		}
		
		// Compute document URL
		URL effectiveUrl = new URL(url, uuid);
		
		// Put document
		JSONObject response = ConnectionUtils.putJsonResource(getContext(), effectiveUrl, doc);
		
		ConnectionUtils.captureReponseErrors(response, "Error while creating document: ");
		
		// Parse response
		String id = null;
		String revision = null;
		try {
			id = response.getString("id");
			revision = response.getString("rev");
		} catch(Exception e) {
			throw new Exception("Error parsing create document response",e);
		}
		
		doc.put("_id", id);
		doc.put("_rev", revision);
		
		return response;
	}

	@Override
	public boolean documentExists(String docId) throws Exception {
		// Compute URL
		List<UrlParameter> parameters = new ArrayList<UrlParameter>(3);
		parameters.add( new UrlParameter("include_docs","false") );
		parameters.add( new UrlParameter("startkey","\""+docId+"\"") );
		parameters.add( new UrlParameter("endkey","\""+docId+"\"") );
		URL effectiveUrl = ConnectionUtils.computeUrlWithParameters(new URL(url, "_all_docs"), parameters);
		
		JSONObject response = ConnectionUtils.getJsonResource(client.getContext(), effectiveUrl);
		
		ConnectionUtils.captureReponseErrors(response, "Error while testing document existence for "+docId+": ");
		
		// Parse response
		boolean exists = false;
		try {
			JSONArray rows = response.getJSONArray("rows");
			if( rows.size() > 0 ) {
				exists = true;
			}
			
		} catch(Exception e) {
			throw new Exception("Error parsing document existence for: "+docId, e);
		}
		
		return exists;
	}

	@Override
	public boolean documentExists(JSONObject doc) throws Exception {
		// Fetch document id
		String docId = null;
		try {
			docId = doc.getString("_id");
		} catch(Exception e) {
			throw new Exception("Unable to find document id to verify existence", e);
		}
		
		return documentExists(docId);
	}

	@Override
	public JSONObject getDocument(String docId) throws Exception {
		URL docUrl = new URL(url, docId);
		JSONObject doc = ConnectionUtils.getJsonResource(getContext(), docUrl);
		
		ConnectionUtils.captureReponseErrors(doc, "Error while fetching document "+docId+": ");

		return doc;
	}

	@Override
	public String getDocumentRevision(String docId) throws Exception {
		// Compute URL
		List<UrlParameter> parameters = new ArrayList<UrlParameter>(3);
		parameters.add( new UrlParameter("include_docs","false") );
		parameters.add( new UrlParameter("startkey","\""+docId+"\"") );
		parameters.add( new UrlParameter("endkey","\""+docId+"\"") );
		URL effectiveUrl = ConnectionUtils.computeUrlWithParameters(new URL(url, "_all_docs"), parameters);
		
		JSONObject response = ConnectionUtils.getJsonResource(getContext(), effectiveUrl);
		
		ConnectionUtils.captureReponseErrors(response, "Error while fetching revision for "+docId+": ");
		
		// Parse response
		String revision = null;
		try {
			JSONArray rows = response.getJSONArray("rows");
			JSONObject row = rows.getJSONObject(0);
			JSONObject doc = row.getJSONObject("value");
			revision = doc.getString("rev");
			
		} catch(Exception e) {
			throw new Exception("Error parsing document revision for: "+docId, e);
		}
		
		return revision;
	}

	@Override
	public String getDocumentRevision(JSONObject doc) throws Exception {
		// Fetch document id
		String docId = null;
		try {
			docId = doc.getString("_id");
		} catch(Exception e) {
			throw new Exception("Unable to find document id to fetch revision", e);
		}
		
		return getDocumentRevision(docId);
	}

	@Override
	public void updateDocument(JSONObject doc) throws Exception {
		// Fetch document id
		String docId = null;
		try {
			docId = doc.getString("_id");
		} catch(Exception e) {
			throw new Exception("Unable to find document id during update", e);
		}
		
		// Compute URL
		URL effectiveUrl = new URL(url, docId);
		
		// Update
		JSONObject response = ConnectionUtils.putJsonResource(getContext(), effectiveUrl, doc);
		
		ConnectionUtils.captureReponseErrors(response, "Error while updating "+docId+": ");
		
		// Parse response
		String revision = null;
		try {
			revision = response.getString("rev");
			
		} catch(Exception e) {
			throw new Exception("Error parsing revision during update: "+docId, e);
		}
		
		doc.put("_rev", revision);
	}

	@Override
	public void deleteDocument(JSONObject doc) throws Exception {
		// Fetch document id
		String docId = null;
		String revision = null;
		try {
			docId = doc.getString("_id");
			revision = doc.getString("_rev");
		} catch(Exception e) {
			throw new Exception("Unable to find document id or revision during delete", e);
		}
		
		// Compute URL
		UrlParameter parameter = new UrlParameter("rev",revision);
		URL effectiveUrl = ConnectionUtils.computeUrlWithParameter(new URL(url, docId), parameter);
		
		// POST
		JSONObject response = ConnectionUtils.deleteJsonResource(getContext(), effectiveUrl);
		
		ConnectionUtils.captureReponseErrors(response, "Error while deleting "+docId+": ");
	}

	@Override
	public CouchDesignDocument getDesignDocument(String ddName) throws Exception {
		URL effectiveUrl = new URL(url, "_design/"+ddName+"/");
		CouchDesignDocumentImpl dd = new CouchDesignDocumentImpl(this,effectiveUrl);
		
		return dd;
	}

	@Override
	public void uploadAttachment(
		JSONObject doc
		,String name
		,File file
		,String contentType
		) throws Exception {
		
		FileInputStream fis = null;
		try {
			long size = file.length();
			fis = new FileInputStream(file);
			uploadAttachment(doc, name, fis, contentType, size);
					
		} catch (Exception e) {
			throw e;
		} finally {
			if( null != fis ) {
				try {
					fis.close();
				} catch (Exception e) {
					// Ignore
				}
			}
		}
	}

	@Override
	public void uploadAttachment(
			JSONObject doc
			,String name
			,InputStream is
			,String contentType
			,long size
			) throws Exception {

		String docId = doc.getString("_id");
		URL attachmentUrl = new URL(url, docId+"/"+name);
		URL effectiveUrl = ConnectionUtils.computeUrlWithParameter(attachmentUrl, new UrlParameter("rev",doc.optString("_rev")));
		
		// PUT
		JSONObject response = ConnectionUtils.putSreamResource(
			getContext()
			,effectiveUrl
			,is
			,contentType
			,size
			);
		
		ConnectionUtils.captureReponseErrors(response, "Error while uploading attachment to "+docId+": ");
		
		// DO NOT UPDATE DOCUMENT REVISION. If the document gets submitted
		// again without being refreshed, then it would not include the new attachment, which
		// would instruct the server to delete it. Better have revision wrong and forcing caller
		// to refresh document.
	}

}
