
import java.util.logging.Logger;
import java.util.logging.Level;

import java.security.Principal;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletOutputStream;

import java.io.OutputStreamWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;

/* for streaming the cutout-file */
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;

import java.util.Arrays;
import java.util.List;
import java.util.LinkedList;
import java.util.Map;
import java.util.HashMap;
import java.util.Properties;

// for Logging/Accounting
import org.json.simple.JSONObject;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;


import java.nio.file.StandardOpenOption;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

import vo.parameter.*;
import vo.error.*;

public class ServletCutout extends HttpServlet
{
   protected static final Logger      LOGGER     = Logger.getLogger(ServletCutout.class.getName());
   protected static final Settings    settings   = Settings.getInstance();
   protected static final Subsurvey[] subsurveys = Subsurvey.loadSubsurveys(settings.fitsPaths.surveysMetadataAbsPathname());

   protected boolean resolveFromId    = true;//FIXME separate setting authz is separate table settings.dbConn.isDbUriEmpty(); 
   protected boolean useEngineOverCli = settings.amqpConn.isHostnameEmpty();

   final String RESPONSE_ENCODING      = "utf-8";
   final String DEFAULT_RESPONSEFORMAT = "application/fits";

   public void init() throws ServletException
   {
      LOGGER.config("FITS : " + settings.fitsPaths.toString());
      if(subsurveys != null)
         LOGGER.config("Subsurveys loaded : " + String.valueOf(subsurveys.length));
      LOGGER.config("DEFAULT_RESPONSEFORMAT : " + DEFAULT_RESPONSEFORMAT);
      LOGGER.config("Resolver : " + (resolveFromId    ? "IVOID" : "DB"));
      LOGGER.config("Engine   : " + (useEngineOverCli ? "CLI"   : "AMQP"));
      if(!useEngineOverCli)
         LOGGER.config("AMQP : " + settings.amqpConn.toString());
   }


   protected void doSodaDescriptor(PrintWriter writer, String requestUrl)
   {
      String theDescriptor =
         "<VOTABLE xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://www.ivoa.net/xml/VOTable/v1.3\" version=\"1.3\">"
         + "<RESOURCE type=\"meta\" utype=\"adhoc:service\" name=\"this\">"
         + "<PARAM name=\"standardID\" datatype=\"char\" arraysize=\"*\" value=\"ivo://ivoa.net/std/SODA#sync-1.0\"/>"
         + "<PARAM name=\"accessURL\" datatype=\"char\" arraysize=\"*\" value=\"" + requestUrl  + "\"/>"
         + "<GROUP name=\"inputParams\">"
         +  "<PARAM name=\"ID\" ucd=\"meta.id;meta.dataset\" datatype=\"char\" arraysize=\"*\" value=\"\"/>"
         +  "<PARAM name=\"POS\" ucd=\"pos.outline;obs\" datatype=\"char\" arraysize=\"*\" value=\"\"/>"
         +  "<PARAM name=\"CIRCLE\" ucd=\"phys.angArea;obs\" unit=\"deg\" datatype=\"double\" arraysize=\"3\" xtype=\"circle\" value=\"\"/>"
         +  "<PARAM name=\"POLYGON\" unit=\"deg\" ucd=\"pos.outline;obs\" datatype=\"double\" arraysize=\"*\" xtype=\"polygon\"  value=\"\"/>"
         +  "<PARAM name=\"BAND\" ucd=\"stat.interval\" unit=\"m\" datatype=\"double\" arraysize=\"2\" xtype=\"interval\" value=\"\"/>"
         +  "<PARAM name=\"TIME\" ucd=\"time.interval;obs.exposure\" unit=\"d\" datatype=\"double\" arraysize=\"2\" xtype=\"interval\" value=\"\"/>"
         +  "<PARAM name=\"POL\" ucd=\"meta.code;phys.polarization\" datatype=\"char\" arraysize=\"*\" value=\"\"/>"
         +  "<PARAM name=\"PIXELS\" ucd=\"instr.pixel;meta.dataset\" datatype=\"char\" arraysize=\"*\" value=\"\"/>"
         +  "<PARAM name=\"RESPONSEFORMAT\" ucd=\"meta.code.mime\" datatype=\"char\" arraysize=\"*\" value=\"application/fits\"/>"

         +  "<PARAM name=\"POSSYS\" ucd=\"pos.frame\" datatype=\"char\" arraysize=\"*\" value=\"\">"
         +   "<DESCRIPTION>Coordinate system for POS values</DESCRIPTION>"
         +   "<VALUES>"
         +     "<OPTION>ICRS</OPTION>"
         +     "<OPTION>GALACTIC</OPTION>"
         +     "<OPTION>GRID</OPTION>"
         +   "</VALUES>"
         +  "</PARAM>"

         +  "<PARAM name=\"BANDSYS\" ucd=\"spect;pos.frame\" datatype=\"char\" arraysize=\"*\" value=\"\">"
         +   "<DESCRIPTION>Coordinate system for BAND vlaues.</DESCRIPTION>"
         +   "<VALUES>"
         +     "<OPTION>WAVE_Barycentric</OPTION>"
         +     "<OPTION>VELO_LSRK</OPTION>"
         +     "<OPTION>GRID</OPTION>"
         +   "</VALUES>"
         +  "</PARAM>"

         + "</GROUP>"
         + "</RESOURCE>"
         + "</VOTABLE>";

      writer.println(theDescriptor);
   }


   protected void doCutoutStream(String id, Pos pos, Band band, Time time, Pol pol, String pixels,
         OutputStream respOutputStream) throws IOException, InterruptedException
   {
      LOGGER.fine("trace " + pos);

      final Resolver resolver = (resolveFromId ?
            new ResolverFromId(subsurveys) :
            new ResolverByObsCore(settings.dbConn, subsurveys));

      final Soda soda = new SodaImpl(settings.fitsPaths);

      resolver.resolve(id);

      soda.doStream(resolver.relPathname(), resolver.hdunum(), pos, band, time, pol, pixels, respOutputStream);
   }


   protected CutResult doCutoutFile(String id, Pos pos, Band band, Time time, Pol pol, String pixels,
         boolean countNullValues)
         throws IOException, InterruptedException
      {
         LOGGER.fine("trace");

         FitsCard[] extraCards = null;

         final Resolver resolver = (resolveFromId ?
               new ResolverFromId(subsurveys)
               : new ResolverByObsCore(settings.dbConn, subsurveys));
         final Vlkb vlkb = (useEngineOverCli ?
               new VlkbCli(settings, subsurveys)
               : new VlkbAmqp(settings, subsurveys));

         resolver.resolve(id);

         String subsurveyId = resolver.obsCollection();
         if(subsurveyId != null)
         {
            extraCards = Subsurvey.subsurveysFindCards(subsurveys, subsurveyId);
         }
         else
         {
            LOGGER.fine("Resolver returns subsurveyId null: no extraCards loaded.");
         }

         return vlkb.doFile(resolver.relPathname(), resolver.hdunum(),
               pos, band, time, pol, pixels, countNullValues, extraCards);
      }



   /* HTTP/J2EE -> SODA */


   /* DALI allows GET and POST for sync services */

   protected void doGet(HttpServletRequest request, HttpServletResponse response)
         throws ServletException, IOException, UnsupportedEncodingException
      {
         final boolean NO_QUERY_STRING = (request.getQueryString() == null);

         if(NO_QUERY_STRING)
         {
            writeSodaDescriptor(request, response);
            LOGGER.fine("normal exit with SODA service descriptor");
            return;
         }
         else
         {
            LOGGER.info(URLDecoder.decode(request.getQueryString(), "UTF-8"));
            execRequest(request, response);
            LOGGER.fine("normal exit");
         }
      }

   protected void doPost(HttpServletRequest request, HttpServletResponse response)
         throws ServletException, IOException, UnsupportedEncodingException
      {
         final boolean NO_QUERY_STRING = (request.getQueryString() == null);

         if(NO_QUERY_STRING)
         {
            writeSodaDescriptor(request, response);
            LOGGER.fine("normal exit with SODA service descriptor");
            return;
         }
         else
         {
            LOGGER.info(URLDecoder.decode(request.getQueryString(), "UTF-8"));
            execRequest(request, response);
            LOGGER.fine("normal exit");
         }
      }



   protected void writeSodaDescriptor(HttpServletRequest request, HttpServletResponse response)
         throws ServletException, IOException, UnsupportedEncodingException
      {
         PrintWriter writer = new PrintWriter(new OutputStreamWriter(response.getOutputStream(), RESPONSE_ENCODING));
         response.setContentType("text/xml");
         doSodaDescriptor(writer, request.getRequestURL().toString());
         writer.close();
      }



   protected void execRequest(HttpServletRequest request, HttpServletResponse response) 
         throws ServletException, IOException, UnsupportedEncodingException

      {
         long    startTime_msec = System.currentTimeMillis();
         long    startTime_nsec = System.nanoTime();

         ServletOutputStream  respOutputStream = response.getOutputStream();

         try
         {
            Map<String, String[]> params = request.getParameterMap();

            String id   = SingleStringParam.parseSingleStringParam(params, "ID");
            Pos    pos  = Pos.parsePos(params);
            Band   band = Band.parseBand(params);
            Time   time = Time.parseTime(params);
            Pol    pol  = Pol.parsePol(params);
            String pixels = SingleStringParam.parseSingleStringParam(params, "PIXELS");

            String respFormat = sodaReq_getResponseFormat(request, DEFAULT_RESPONSEFORMAT);

            LOGGER.finest("responseFormat: " + respFormat);

            if(respFormat.startsWith("application/fits"))
            {
               response.setContentType(respFormat);
               doCutoutStream(id, pos, band, time, pol, pixels, respOutputStream);
            }
            else if(respFormat.startsWith("application/x-vlkb+xml"))
            {
               boolean showDuration    = true;
               boolean countNullValues = vlkbReq_getNullValues(request);
               response.setContentType(respFormat);

               CutResult cutResult = doCutoutFile(id, pos, band, time, pol, pixels, countNullValues);// FIXME wh? , respFormat);


               /* FIXME errors from engine not checked - cut-file might not have been created */

               PrintWriter writer =new PrintWriter(new OutputStreamWriter(respOutputStream, RESPONSE_ENCODING));

               String accessUrl = convertLocalPathnameToRemoteUrl(cutResult.fileName,
                     settings.fitsPaths.cutouts(),
                     settings.fitsPaths.cutoutsUrl());

               XmlSerializer.serializeToLegacyCutResult(writer, RESPONSE_ENCODING,
                     cutResult, accessUrl,
                     id, pos, band, time, pol, pixels, countNullValues,
                     showDuration, startTime_msec);

               writer.close(); /* must close to force flush to complete the xml */
            }
            else
            {
               throw new IllegalArgumentException("Unsupported RESPONSEFORMAT value : " + respFormat);
            }

         }
         catch(MultiValuedParamNotSupported ex)
         {
            LOGGER.warning("MultiValuedParamNotSupported: " + ex.getMessage());

            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            response.setContentType("text/plain");
            PrintWriter writer = new PrintWriter(new OutputStreamWriter(respOutputStream, RESPONSE_ENCODING));

            Lib.doMultiValuedParamNotSupported(ex.getMessage(), writer);

            writer.close();
         }
         catch(IllegalArgumentException ex)
         {
            LOGGER.warning("IllegalArgumentException: " + ex.getMessage());

            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            response.setContentType("text/plain");
            PrintWriter writer = new PrintWriter(new OutputStreamWriter(respOutputStream, RESPONSE_ENCODING));

            Lib.doUsageError(ex.getMessage(), writer);

            writer.close();
         }
         catch(Exception ex)
         {
            LOGGER.severe("Exception: " + ex.getMessage());
            ex.printStackTrace();

            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            response.setContentType("text/plain");
            PrintWriter writer = new PrintWriter(new OutputStreamWriter(respOutputStream, RESPONSE_ENCODING));

            Lib.doError(ex.toString(), writer);

            writer.close();
         }
         finally
         {
            respOutputStream.close();
         }

         LOGGER.fine("RUNTIME[sec]: "+String.valueOf( (System.nanoTime() - startTime_nsec) / 1000000000.0 ));
      }

   private String convertLocalPathnameToRemoteUrl(String localPathname,
         String FITScutpath, String FITSRemoteUrlCutouts)
   {
      LOGGER.fine("trace " + localPathname);
      String fileName = localPathname.replaceAll(FITScutpath + "/", "");
      LOGGER.finest("local filename: " + fileName);
      String remotefname = FITSRemoteUrlCutouts + "/" + fileName;
      LOGGER.finest("remote url    : " + remotefname);
      return remotefname;
   }






   /* SODA */


   /* return null if value not present or the value if present exactly once
    * else throw MultiplicityNotSupoorted SODA_error
    */ 
   private String soda_getSingleValue(HttpServletRequest req, String name)
   {
      String[] valArr = req.getParameterValues(name);

      if(valArr == null)
         return null;
      else
         if(valArr.length == 0)
            return null;
         else if(valArr.length == 1)
            return valArr[0];
         else
            throw new IllegalArgumentException(
                  "MultiValuedParamNotSupported: " + name + " was found " + valArr.length + " times");
   }


   private String sodaReq_getResponseFormat(HttpServletRequest req, String defaultResponseFormat)
   {
      String respFormat = soda_getSingleValue(req, "RESPONSEFORMAT");
      return ((respFormat == null) ? defaultResponseFormat : respFormat);
   }


   private boolean vlkbReq_getNullValues(HttpServletRequest request)
   {
      return (null != soda_getSingleValue(request, "nullvals"));
   }

}



/* from SODA (upon error):
   Error codes are specified in DALI. Error documents should be text
   using the text/plain content-type and the text must begin with one of the
   following strings:

   Error CodeDescription
   ---------------------------------------
Error: General error (not covered below)

AuthenticationError: Not authenticated
AuthorizationError: Not authorized to access the resource

ServiceUnavailable: Transient error (could succeed with retry)
UsageError: Permanent error (retry pointless)

MultiValuedParamNotSupported: request included multiple values for a parameter
but the service only supports a single value
*/


/* from DALI (upon successful request):
   The service should set HTTP headers (Fielding and Gettys et al.,
   1999) that are useful to the correct values where possible. Recommended
   headers to set when possible:
   Content-Type
   Content-Encoding
   Content-Length  -- not in SPDA-stream impossible to know
   Last-Modified   -- not in SODA-stream impossible to know
   */
